Lesson 8 of 9 · 22m
Patching: Changing Behavior Without Source
The constraint that shapes everything: you can't insert bytes, only overwrite them. Classic patches, code caves, and the integrity checks that punish careless edits.
Patching is where reverse engineering stops being read-only. The constraint that shapes everything: you cannot insert bytes — the binary is a fixed-size address space where every offset is load-bearing. You overwrite in place, or you build a trampoline out to space nobody is using.
The classic byte-level patches
; kill a check — overwrite the call with NOPs (0x90), matching length exactly
call check_license ; E8 xx xx xx xx (5 bytes) -> 90 90 90 90 90
; invert a decision — flip the conditional jump opcode
jz loc_401050 ; 74 xx -> 75 xx (jz becomes jnz)
; or force it unconditionally:
je loc_401050 ; 0F 84 .. .. .. .. -> E9 .. .. .. .. 90
; (near jmp is one byte shorter — NOP the rest,
; and recompute rel32: it's relative to the next instruction)
; compare against a different constant
cmp eax, 3 ; 83 F8 03 -> 83 F8 00The beginner trap: instruction encodings have fixed lengths. Replacing a 2-byte jz with a 6-byte je overflows into the next instruction and corrupts the stream. Replace same-size-or-smaller and pad the remainder with 0x90.
When there's no room: code caves
Functions and sections end in alignment padding — int3/nop sleds nobody executes. That's a code cave: jmp there, run your extra instructions, jmp back. For anything larger, tools append a whole section and retarget the entry path — that's how real loaders and crack-style keygens inject actual logic rather than one-byte flips.
The check you didn't patch
Programs that care verify themselves: CRC regions over .text, Authenticode signatures, anti-tamper self-hashes. The classic failure — you patched the comparison perfectly and now the binary crashes on startup — means you changed bytes a self-checksum covers. Find the integrity check first (watchpoint the crash path, or look for code that reads its own .text), patch that, then patch the decision. Verify under the debugger: break on the patched block and watch it execute.
Doing it in the tools
In Ghidra: right-click an instruction → Patch Instruction (it assembles for you), then File → Export Program with format "Original File" to write a patched copy. In x64dbg: patch in place, Ctrl+P reviews every pending change, then write the file. Whatever the tool, keep a changelog — file offset: old bytes -> new bytes — because you will need to answer "what did I change" three sessions from now.
Field notes
- Never delete bytes. Always NOP over them. Deleting shifts every subsequent address and silently breaks every absolute reference in the file.
- Mind file offsets versus virtual addresses: the listing shows VAs; your hex editor wants file offsets. Check the offset column or compute through the section headers — editing at the VA directly is the second-most-common patch bug after rebasing.
- Immediates are little-endian:
cmp eax, 0x100encodes as3D 00 01 00 00. A byte editor carries none of the assembler's abstractions. - Your patch is versioned to this exact binary. The next auto-update lands and your one-byte fix is archaeology — keep the changelog so it's ten minutes, not a redo.