amiga-bootcamp/05_reversing/patching_techniques.md
Ilia Sharin 21751c0025 docs(amiga): complete AmigaOS 3.1/3.2 developer reference — 172 files across 17 sections
Comprehensive technical documentation covering:
- Hardware: OCS/ECS/AGA custom chip registers, Copper & Blitter deep dives
- Boot sequence: cold boot through startup-sequence
- Binary format: HUNK executable spec, relocation, debug info
- Linking & ABI: .fd files, LVO tables, register calling conventions
- Exec kernel: tasks, interrupts, memory, signals, semaphores
- AmigaDOS: file I/O, FFS/OFS layout, CLI/Shell scripting
- Graphics: planar bitmaps, Copper programming, HAM/EHB modes
- Intuition: screens, windows, IDCMP, BOOPSI
- Devices: trackdisk, SCSI, serial, timer, audio, keyboard
- Libraries: utility, expansion, IFFParse, locale, ARexx
- Networking: bsdsocket API, SANA-II, TCP/IP stack comparison
- Toolchain: GCC, vasm/vlink, SAS/C, NDK, debugging
- Reverse engineering: IDA/Ghidra setup, compiler fingerprints, case studies
- CPU & MMU: 68040/060 emulation libs, PMMU, cache management
- Driver development: SANA-II, Picasso96/RTG, AHI audio

All files include breadcrumb navigation. No local paths or proprietary content.
2026-04-23 12:17:35 -04:00

6 KiB
Raw Blame History

← Home · Reverse Engineering

Patching Techniques for Amiga Binaries

Overview

This document covers the methods used to surgically patch AmigaOS HUNK executables and libraries — without access to source code. All techniques operate on the binary file directly.


Method 1: NOP Patching

Replace one or more instructions with NOP ($4E71) to eliminate a code path:

# Python: NOP out 6 bytes at offset $1234
import struct

with open("target.library", "r+b") as f:
    f.seek(0x1234)
    f.write(b'\x4e\x71' * 3)   # 3× NOP = 6 bytes

Use case: Disable a conditional check:

; Before: jumps to expiry code if timer > limit
CMPI.L  #$12345678, D3
BHI.S   .expired

; After: NOP the BHI (2 bytes)
CMPI.L  #$12345678, D3
NOP
NOP

Method 2: Branch Inversion

Flip the condition of a branch instruction to always take / never take a path:

Original Patched Effect
BEQ $4E43 BRA $4E43 Always branch (was: branch if equal)
BNE $4E43 BRA $4E43 Always branch (was: branch if not equal)
BEQ $4E43 NOP×2 Never branch (was: branch if equal)
BHI $4E43 BLS $4E43 Invert condition

Branch instruction bytes:

67 xx   BEQ.S (offset xx)
66 xx   BNE.S
6E xx   BGT.S
6F xx   BLE.S
60 xx   BRA.S

Change BEQ.S to BRA.S: replace first byte $67 with $60.


Method 3: Return Value Forcing

Force a function to always return a specific value:

; Original: complex check, returns 0 on failure
_CheckTimer:
    LINK    A5, #-4
    ...                   ; timer logic
    UNLK    A5
    RTS

; Patched: always return 1 (success/valid)
_CheckTimer:
    MOVEQ   #1, D0        ; $7001 — MOVEQ #1, D0 (2 bytes)
    RTS                   ; $4E75 (2 bytes)

MOVEQ #1, D0 = $70 $01 (2 bytes) RTS = $4E $75 (2 bytes)

Write 4 bytes at the function entry point.


Method 4: JMP Redirect

Redirect a function call to a completely different address:

; Original call:
JSR     _CheckAuth          ; $4EB9 XXXXXXXX

; Patched to call our stub instead:
JSR     _AlwaysTrue         ; $4EB9 YYYYYYYY

Requires updating the relocation table if the new address is in a different hunk — simpler if both are in the same hunk (no reloc change needed).


Method 5: Constant Replacement

Replace a comparison constant (timer limit, version check value, etc.):

; Original: 10 retry limit
CMPI.L  #$0000000A, D3     ; $0A = 10

; Patched: effectively unlimited retries
CMPI.L  #$7FFFFFFF, D3     ; max positive longword

Find the constant in the binary: xxd target.library | grep "00 00 00 0a" Replace with new value at that offset.


Method 6: Library JMP Table Patch (Runtime)

For patching a library's JMP table at runtime (not file-level):

; Install patch at LVO -30 of DOSBase:
MOVEA.L _DOSBase, A0        ; library base
MOVE.L  #_MyOpen, -28(A0)  ; overwrite address bytes of JMP.L at -30
                             ; JMP.L = 4E F9 [AAAAAAAA]
                             ; address is at offset -30+2 = -28

Note: this does not use SetFunction() — no LIBF_CHANGED flag. Used when you need silent patching.


Updating Relocations After Patching

If a patched longword is in the HUNK_RELOC32 list, the loader will overwrite your patch at load time by adding the hunk base to it. You must either:

  1. Remove the reloc entry for that offset from HUNK_RELOC32
  2. Adjust the stored value so that after relocation it becomes the desired value

Finding reloc entries to remove:

def remove_reloc_entry(data, hunk_offset, target_offset):
    """Remove a specific offset from HUNK_RELOC32 records."""
    # Parse the file, find HUNK_RELOC32, remove the entry
    # This requires a full HUNK parser — see hunk_parser.py
    pass

Automated Patcher Template (Python)

#!/usr/bin/env python3
"""
Amiga HUNK Binary Patcher
"""
import struct
import shutil
import sys

class AmigaPatcher:
    def __init__(self, path):
        with open(path, 'rb') as f:
            self.data = bytearray(f.read())
        self.path = path

    def find_pattern(self, pattern: bytes) -> list:
        """Find all occurrences of a byte pattern."""
        results = []
        idx = 0
        while True:
            idx = self.data.find(pattern, idx)
            if idx == -1:
                break
            results.append(idx)
            idx += 1
        return results

    def patch_bytes(self, offset: int, new_bytes: bytes, comment: str = ""):
        old = self.data[offset:offset+len(new_bytes)]
        print(f"[PATCH] {comment}")
        print(f"  Offset: {offset:#010x}")
        print(f"  Old: {old.hex()}")
        print(f"  New: {new_bytes.hex()}")
        self.data[offset:offset+len(new_bytes)] = new_bytes

    def nop(self, offset: int, count: int, comment: str = ""):
        self.patch_bytes(offset, b'\x4e\x71' * count, f"NOP×{count}: {comment}")

    def save(self, out_path: str):
        with open(out_path, 'wb') as f:
            f.write(self.data)
        print(f"[SAVE] Written to {out_path}")

# Example usage:
if __name__ == "__main__":
    p = AmigaPatcher("target.library")

    # Patch 1: NOP out redundant range check
    p.nop(0x1234, 3, "skip bounds validation BHI branch")

    # Patch 2: Force version check to pass
    p.patch_bytes(0x5678, bytes([0x70, 0x01, 0x4E, 0x75]),
                  "always return 1 from CheckVersion")

    p.save("target_patched.library")

Verification After Patching

  1. Checksum update: Some libraries check SumLibrary() at init — may need to disable that check too
  2. Test on hardware: Use the MiSTer or UAE emulator to verify the patched binary
  3. Regression test: Ensure patched functions that are chained still work correctly

References