Skip to content
Index / $05

Can You Inject New Behaviors into a Binary Without Source Code?

Adding a new secret character to a 1998 arcade game through ROM patching, 68000 assembly injection, palette engineering, and input sequence state machines

Date
Jan 24, 2026 · upd Jul 4, 2026
Runtime
16 min · 3,587 words
Tags
systems-engineering, binary-analysis, state-machines
Slot
$05
Contents
tip // Evidence Strip

Goal: Add Armored Spider-Man (character ID 0x2E) to MvC1 through direct ROM patching -- new palette, new input code, new load function -- no source code required.

Constraints: CPS-2 program code is hardware-encrypted (using Phoenix Edition to bypass). Character system spans 5+ subsystems with no unified API. Secret characters use 3 different table formats. Armor implementation varies (flag-based vs animation-based).

Approach: Reverse engineer existing secret character implementations. Copy the simplest pattern (Gold War Machine: 12 bytes). Design RGB444 palette. Add entries to all 3 table types. Patch position check assembly in expanded ROM space.

Result: ROM expanded to 148MB (CPS-2 max). All 4 existing secret load functions decompiled. Position check blocker bypassed via Autovector 2 interrupt hook -- 116 bytes of 68000 assembly at $0F1FA0 intercept VBlank to detect Wolverine (ID $08) + Down → write Spider-Man (ID $0E) to $FF3053. Safety: 60-frame char-change window (char select only) + input edge detection. ROM built and verified.

Proof / Validation: Assembled hook verified via Capstone disassembly: CMPI.B #$08, D0BTST #2, D1MOVE.B #$0E, $FF3053. Original handler at $054A preserved. ULTRA SETTINGS: FN REDVENOM: OK, FN ORNGHULK: OK, FN GOLDW.M.: OK, FN SHDWLADY: OK. P PALETTE: OK, K PALETTE: OK. Build: 145MB ROM set output.

Repo / Artifacts: CHARACTER_SELECT/scripts/patch_rom_check_complete.py (unified build with VBlank hook), CHARACTER_SELECT/scripts/armored_spiderman.lua (Lua prototype), DECOMP_MVC/src/character/ (decompiled functions).

note // Status update (July 2026)

The binary analysis and injection techniques below stand as documented. The plan for the $2E slot has changed: the Armored Spider-Man / Iron Spider line was retired, and the slot's character work pivoted to a Dark Sakura port from MSHvSF (char-id $2A in that game). Because MSHvSF and MvC1 are same-generation engines, her animation/frame tables carry over byte-identical -- the port is table porting plus tile bank remapping, not new art or new movesets.

Injecting new runtime behaviors into legacy binaries requires reverse engineering their finite state machines, memory layouts, and entity management systems. This deep dive documents the process of adding "Armored Spider-Man" (character ID 0x2E) to Marvel vs. Capcom 1 through direct ROM patching -- no source code, no emulator cheats, pure binary modification.

Key Engineering Concepts:

  • Binary state machine analysis and modification
  • Memory-mapped I/O and entity ID systems
  • 68000 assembly-level patching for behavior injection
  • CPS-2 palette format (RGB555) and resource management
  • ROM expansion to CPS-2 theoretical maximums (128MB GFX, 16MB Audio)

Character Select Screen Layout

Visual Grid Layout

Loading diagram...

Grid Navigation:

  • 18 selectable characters in a 6×3 grid
  • 2 random select slots
  • 6 secret characters accessible via directional input codes
  • P1 starts left side, P2 starts right side

Character ID Table (Internal)

CharacterID (Hex)Type
War Machine0x02Normal
Captain America0x04Normal
Hulk0x06Normal
Wolverine0x08Normal
Gambit0x0ANormal
Venom0x0CNormal
Spider-Man0x0ENormal
Roll0x10Secret (unique)
Ryu0x12Normal
Capt Commando0x14Normal
Chun-Li0x16Normal
Jin0x18Normal
Zangief0x1ANormal
Strider0x1CNormal
Morrigan0x1ENormal
Megaman0x20Normal
Red Venom0x24Secret
Orange Hulk0x26Secret
Gold War Machine0x28Secret
Shadow Lady0x2ASecret
Lilith0x2CSecret
Armored Spider-Man0x2ETarget

Secret Character Codes Reference

Existing Secret Character Codes (All Arrow-Only Pattern)

CharacterStarting PositionInput Sequence
RollZangiefLeft, Left, Down, Down, Right, Right, Down, Down, Left, Left, Up, Right, Up, Up, Right, Right
LilithZangiefLeft, Left, Down, Down, Right, Right, Down, Down, Left, Left, Up, Up, Right, Up, Up, Right, Right
Gold War MachineZangiefLeft, Left, Down, Down, Right, Right, Down, Down, Left, Left, Up, Up, Up, Right, Right
Orange HulkZangiefLeft, Down, Down, Right, Right, Down, Down, Left, Left, Up, Up, Up, Up, Right
Shadow LadyZangiefLeft, Left, Down, Down, Right, Right, Down, Down, Left, Left, Up, Up, Up, Up, Right, Down
Red VenomZangiefLeft, Down, Down, Right, Right, Down, Down, Left, Left, Up, Up, Up, Right, Right

Proposed Armored Spider-Man Code

Starting Position: Spider-Man (directly on Spider-Man's portrait)

Input Sequence (Arrow-Only, Similar to Roll):

Down, Down, Left, Left, Up, Up, Right, Right, Down, Down

This is a 10-input sequence (shorter than Roll's 16-input sequence) that forms a "box" pattern, easy to remember.

Alternative (Longer, More Secure):

Down, Down, Left, Left, Up, Up, Right, Right, Down, Left, Up, Right

Character ID Mapping

Complete Character ID Table

Loading diagram...

Palette Swap Relationships

Secret Character IDBase Character IDBase Character Name
0x240x0CVenom
0x260x06Hulk
0x280x02War Machine
0x2A0x16Chun-Li (!)
0x2C0x1EMorrigan
0x2E0x0ESpider-Man

Phase 1: ROM Structure and Decryption

CPS2 ROM File Structure

Loading diagram...

Decryption/Encryption Process

# Step 1: Extract ROM files from ZIP
unzip mvscj.zip -d mvscj_extracted/
 
# Step 2: Decrypt program ROM using cps2crypt
# (cps2crypt is part of MAME tools)
./cps2crypt -d mvscj_extracted/mvscj.03 mvscj.03.dec mvsc.key
 
# Step 3: Make modifications to mvscj.03.dec
 
# Step 4: Re-encrypt the modified ROM
./cps2crypt -e mvscj.03.dec mvscj.03.mod mvsc.key
 
# Step 5: Replace original file
cp mvscj.03.mod mvscj_extracted/mvscj.03

Phase 2: Character Table Analysis

Character Entry Structure (from Decompilation)

The character data table lives at $065CCA in the program ROM with 32-byte entries. The player runtime structure (in Work RAM) has been partially mapped:

Loading diagram...

Memory Address Map (Confirmed via Decompilation)

=== CPS2 Memory Map (68000 CPU) - CONFIRMED ===
 
ROM Space:
    0x000000 - 0x0FFFFF : Program ROM (1MB used, 4MB max)
    
Hardware Registers:
    0x400000 - 0x40000F : CPS-B registers (layer ctrl, priority, palette)
    0x660000 - 0x66BFFF : Video RAM (48KB, scroll layers)
    0x700000 - 0x701FFF : Object RAM bank 1 (8KB, 1024 sprites)
    0x708000 - 0x709FFF : Object RAM bank 2 (8KB, double-buffered)
    0x800000 - 0x800007 : Input ports (P1/P2 joy+buttons)
    0x900000 - 0x92FFFF : Palette/GFX RAM (192KB)
    
RAM Space:
    0xFF0000 - 0xFFFFFF : Work RAM (64KB)
    
Player Structure Bases:
    0xFF3000 : P1 Character (char ID at +0x53 = $FF3053)
    0xFF3100 : P2 Character (char ID at +0x53 = $FF3153)
 
Unlock System (base = A5 = $FF8000):
    0xFF803C : Red Venom unlock flag
    0xFF8040 : Orange Hulk unlock flag
    0xFF8044 : Gold War Machine unlock flag
    0xFF8048 : Shadow Lady unlock flag
    0xFF804C : Lilith unlock flag
 
Cursor Position:
    0xFF2000 : P1 cursor grid position
    0xFF2002 : P2 cursor grid position
 
=== ROM Data Tables (CONFIRMED) ===
    
    0x065CCA : Character data table (32 bytes/entry)
    0x00BD00 : Animation/sprite data (~512 bytes)
    0x014D6E : Secret table Type A (Shadow Lady, 16-byte entries)
    0x04A3F2 : Secret table Type B (Lilith/Gold WM, 2-byte pairs)
    0x024D3A : Secret table Type C (Orange Hulk/Red Venom, 2-byte pairs)
    
=== Secret Character Load Functions (CONFIRMED) ===
    
    0x022B2E : load_red_venom (66 bytes)
    0x05D044 : load_orange_hulk (60 bytes)
    0x07EF90 : load_gold_war_machine (12 bytes)
    0x0A26C8 : load_shadow_lady (20 bytes)
    0x0FF9B4 : character_load_main (98 bytes)
 
=== Spider-Man Palette Offsets (in mvc.06a) ===
    
    0x48642 : P button palette (32 bytes)
    0x486A2 : K button palette (32 bytes)
    0x48682 : Web attack P palette (32 bytes)
    0x486E2 : Web attack K palette (32 bytes)

Phase 3: Input Code Implementation

Input Detection State Machine

Loading diagram...

68000 Assembly Pseudo-Code

; ============================================
; Armored Spider-Man Selection Code Handler
; Insert into character select input routine
; ============================================
 
ARMORED_CODE_CHECK:
    ; Check if cursor is on Spider-Man (position 0x0F)
    CMP.B   #$0F, D0         ; D0 = current cursor position
    BNE     .skip_armored     ; Not on Spider-Man, skip
    
    ; Check input sequence buffer
    LEA     input_buffer, A0  ; A0 = input history buffer
    LEA     armored_code, A1  ; A1 = expected sequence
    MOVEQ   #9, D1            ; 10 inputs to check (0-9)
    
.check_loop:
    MOVE.B  (A0)+, D2         ; Get buffered input
    CMP.B   (A1)+, D2         ; Compare with expected
    BNE     .skip_armored     ; Mismatch, skip
    DBRA    D1, .check_loop   ; Continue checking
    
    ; Code matched! Select Armored Spider-Man
    MOVE.B  #$2E, D0          ; Set character ID to 0x2E
    MOVE.B  #$01, armored_flag ; Set flag for palette load
    BRA     .select_character
    
.skip_armored:
    ; Continue with normal selection
    RTS
 
; Expected input sequence (Down=2, Left=4, Up=8, Right=6)
armored_code:
    DC.B    $02, $02          ; Down, Down
    DC.B    $04, $04          ; Left, Left 
    DC.B    $08, $08          ; Up, Up
    DC.B    $06, $06          ; Right, Right
    DC.B    $02, $02          ; Down, Down

Phase 4: Palette Creation

CPS2 Palette Format (RGB444)

CPS-2 palettes use RGB444 big-endian format, NOT RGB555 as commonly assumed:

Loading diagram...

Spider-Man has 4 separate palettes in mvc.06a: P button, K button, Web P, and Web K -- each 32 bytes (16 colors).

Armored Spider-Man Color Scheme

IndexOriginal (Spider-Man)Armored VersionRGB444 Value
0TransparentTransparent$0000
1Red (Main)Silver$0999
2Red (Light)Light Silver$0BBB
3Red (Dark)Dark Silver$0666
4Blue (Main)Gunmetal$0444
5Blue (Light)Light Gunmetal$0777
6Blue (Dark)Dark Gunmetal$0333
7Black (Outline)Black$0000
8White (Eyes)Yellow (Visor)$0FF0
9SkinSkin$0DA8
10Web (Gray)Chrome$0DDD

Palette Binary Data

# Armored Spider-Man Palette (32 bytes, RGB444 big-endian)
ARMORED_PALETTE_P = bytes([
    0x00, 0x00,  # 0: Transparent
    0x09, 0x99,  # 1: Silver (main body)
    0x0B, 0xBB,  # 2: Light Silver (highlights)
    0x06, 0x66,  # 3: Dark Silver (shadows)
    0x04, 0x44,  # 4: Gunmetal (secondary)
    0x07, 0x77,  # 5: Light Gunmetal
    0x03, 0x33,  # 6: Dark Gunmetal
    0x00, 0x00,  # 7: Black (outline)
    0x0F, 0xF0,  # 8: Yellow (visor glow)
    0x0D, 0xA8,  # 9: Skin tone
    0x0D, 0xDD,  # 10: Chrome (web)
    0x05, 0x55,  # 11: Mid-gray
    0x02, 0x22,  # 12: Dark accent
    0x08, 0x88,  # 13: Medium gray
    0x0A, 0xAA,  # 14: Light gray
    0x0F, 0xFF,  # 15: White
])

Phase 5: Complete Implementation Workflow

Loading diagram...

Python ROM Patcher Script

#!/usr/bin/env python3
"""
MvC1 Armored Spider-Man ROM Patcher v2.0
Adds Armored Spider-Man as character ID 0x2E
 
Selection Code: Down, Down, Left, Left, Up, Up, Right, Right, Down, Down
Starting from Spider-Man's position
"""
 
import struct
import sys
import os
 
# Configuration - TO BE DISCOVERED VIA MAME DEBUGGER
CONFIG = {
    "char_table_offset": 0x000000,      # TODO: Find via debugger
    "char_entry_size": 0x20,            # Estimated 32 bytes per entry
    "char_count": 24,                   # Number of character entries
    "palette_table_offset": 0x000000,   # TODO: Find via debugger
    "palette_entry_size": 0x40,         # 64 bytes per palette
    "input_handler_offset": 0x000000,   # TODO: Find via debugger
    "spider_man_id": 0x0E,
    "armored_spider_man_id": 0x2E,
    "spider_man_position": 0x0F,        # Grid position
}
 
# Armored Spider-Man Palette (RGB555 Format)
ARMORED_PALETTE = bytes([
    0x00, 0x00,  # 0: Transparent
    0x18, 0x63,  # 1: Silver (main)
    0x9C, 0x73,  # 2: Light Silver
    0x52, 0x4A,  # 3: Dark Silver
    0x4A, 0x29,  # 4: Gunmetal Blue
    0xEF, 0x3D,  # 5: Light Gunmetal
    0xC6, 0x18,  # 6: Dark Gunmetal
    0x00, 0x00,  # 7: Black
    0xE0, 0x7F,  # 8: Yellow (visor)
    0xCB, 0x5A,  # 9: Skin tone
    0xDE, 0x7B,  # 10: Chrome
    0x10, 0x42,  # 11: Shadow
    0x8C, 0x31,  # 12: Mid-tone
    0x00, 0x00,  # 13: Reserved
    0x00, 0x00,  # 14: Reserved
    0xFF, 0x7F,  # 15: White
    # Colors 16-31: Copy from original Spider-Man
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
])
 
# Input sequence (Down=2, Left=4, Up=8, Right=6)
INPUT_SEQUENCE = bytes([
    0x02, 0x02,  # Down, Down
    0x04, 0x04,  # Left, Left
    0x08, 0x08,  # Up, Up
    0x06, 0x06,  # Right, Right
    0x02, 0x02,  # Down, Down
])
 
def read_rom(filepath):
    """Read ROM file into bytearray"""
    with open(filepath, 'rb') as f:
        return bytearray(f.read())
 
def write_rom(filepath, data):
    """Write bytearray to ROM file"""
    with open(filepath, 'wb') as f:
        f.write(data)
 
def find_pattern(data, pattern, start=0):
    """Find byte pattern in data"""
    pattern_len = len(pattern)
    for i in range(start, len(data) - pattern_len):
        if data[i:i+pattern_len] == pattern:
            return i
    return -1
 
def hex_dump(data, offset, length=64):
    """Print hex dump of data region"""
    print(f"\nHex dump at 0x{offset:06X}:")
    for i in range(0, length, 16):
        hex_str = ' '.join(f'{data[offset+i+j]:02X}' 
                          for j in range(min(16, length-i)))
        print(f"  {offset+i:06X}: {hex_str}")
 
if __name__ == '__main__':
    print("MvC1 Armored Spider-Man ROM Patcher")
    print("=" * 40)
    print("NOTE: This script requires ROM offsets to be")
    print("discovered via MAME debugger first.")
    print()
    print("See documentation for MAME debugging steps.")

Current Project Status

The Phoenix Edition project has reached a major milestone: the Armored Spider-Man activation logic is built and assembled into ROM. The position check blocker was bypassed entirely using a VBlank interrupt hook instead of modifying the game's complex state machine.

PhaseTaskStatus
1ROM DecryptionComplete -- Using Phoenix Edition (decrypted mvscud) ROMs
2Character Table AnalysisComplete -- Table at $065CCA (32 bytes/entry), roster at $AC7A, stage table at $BB04C
3ROM ExpansionComplete -- 128MB GFX + 16MB Audio (CPS-2 MAX)
4In-Game VerificationComplete -- ULTRA SETTINGS submenu in F2 Test Menu
5Secret Function DecompilationComplete -- All 4 load functions decompiled to C, patterns documented
6Palette LocationComplete -- Spider-Man P: $48642, K: $486A2, Web P: $48682, Web K: $486E2 (in mvc.06a)
7ID 0x2E AnalysisComplete -- 85 code references confirmed real, 4 conditional load points found
8Secret Table ForensicsComplete -- 3 table types documented, corrected $100000+ base for mvc.05a
9Player Structure MappingComplete -- Char ID at +$53, data ptr at +$54, health at +$60, unlock flags at $FF803C-$FF804C
10Position Check AnalysisComplete -- 13-state machine at $01DFA0 disassembled; hardcoded positions 1,6,11,12,13
11VBlank Interrupt HookComplete -- 116 bytes at $0F1FA0, Autovector 2 redirected from $054A
12Lua PrototypeComplete -- armored_spiderman.lua validated logic in MAME before ROM patch
13ROM BuildComplete -- patch_rom_check_complete.py outputs 145MB ROM set with hook
14Armored Palette CreationPlanned -- RGB444 silver/gunmetal design ready
15Sprite Copy to Expanded GFXPlanned -- Copy Spider-Man tiles to 96MB free GFX space (optional)
16Integration TestingNext -- Boot patched ROM in MAME

What's Been Built

  • 148MB ROM set at CPS-2 theoretical maximum capacity
  • ULTRA SETTINGS diagnostic screen verifying 22 metrics in real time
  • Autovector 2 interrupt hook -- 116 bytes of 68000 assembly at $0F1FA0:
    • Wolverine (ID $08) + Down → Spider-Man (ID $0E) character swap
    • 60-frame char-change window ensures hook only fires on character select screen
    • Input edge detection prevents repeated triggers from held buttons
    • Original handler at $054A preserved via JMP tail call
  • Lua prototype (armored_spiderman.lua) -- validated all memory addresses and logic in MAME before ROM commit
  • Python build pipeline automating expansion + patching + assembly + packaging
  • 4 secret character load functions decompiled and verified:
    • Red Venom: $022B2E (66 bytes, complex with shared subroutine call)
    • Orange Hulk: $05D044 (60 bytes, property writes to player structure)
    • Gold War Machine: $07EF90 (12 bytes, simplest -- movep.l to structure)
    • Shadow Lady: $0A26C8 (20 bytes, flag setup via ori.b)
  • Character data structure partially mapped (32-byte entries at $065CCA)
  • Unlock flag system documented (base $FF8000, offsets $3C-$4C)
  • 4 conditional load points for ID 0x2E found (3/4 near Shadow Lady code)

Key Insight: Bypass vs. Modify

The original plan called for finding and patching the hardcoded position check assembly to add position 5 (Wolverine). After disassembling the 13-state character select state machine at $01DFA0, we realized modifying the tightly-coupled system was unnecessarily risky. The interrupt hook approach operates completely outside the game's state machine, with zero coupling to existing logic.

Remaining Work

  1. Integration testing -- boot patched ROM in MAME, verify Wolverine + Down activates Spider-Man
  2. Create armored palette in RGB444 format (silver/gunmetal scheme designed, values ready)
  3. Wire ID 0x2E load function -- connect to palette swap for visual differentiation
  4. Copy Spider-Man sprite data to expanded GFX space for armor modifications (optional)
EOF · $05 · 3,587 words · Daniel Plas Rivera
Share[X][LinkedIn]