Classroom Public page

RE-011 Week 11: Binary Patching

1,087 words

objcopy, Ghidra's patch tool, and the smallest-possible-patch discipline. Modifying a compiled binary to change behavior without source code.


Reading (~30 min)

Read the objcopy man page, specifically the --change-section-address, --pad-to, and byte-manipulation sections. Also read the hexedit documentation (or man hexedit). These are your command-line patching tools for cases where Ghidra is too heavy.

From Yurichev RE4B: read the "Patching" chapter if present (it may be titled "Modifying executables"). Yurichev's examples show the minimal-patch approach in practice.


Lecture outline (~1.5 hr)

Part 1: What binary patching is and when it is appropriate (15 min)

Binary patching means modifying the bytes of a compiled binary to change its behavior, without access to the source code or the build toolchain.

Appropriate uses:

  • Bypassing a check in a CrackMe for educational analysis (this is what Lab 8 is)
  • Applying a workaround for a bug in a binary you cannot recompile (embedded firmware, legacy software)
  • Patching out an anti-debug check so you can debug a binary more easily
  • Security research: verifying that a specific code path causes a vulnerability by patching the condition that triggers it

Inappropriate uses:

  • Bypassing copy protection or license checks in commercial software you do not own
  • Modifying software to cheat in online games (violates Terms of Service and potentially CFAA)
  • Removing security controls from software to enable unauthorized access

The RE-011 scope is Lab 8 (patching a CrackMe) and the conceptual understanding of the technique. The ethical boundaries here are the same as all RE-011 work: your binary, your machine, authorized training targets.

Part 2: The smallest-possible-patch discipline (20 min)

When patching a binary, always make the smallest patch that achieves the goal. This principle matters for three reasons:

  1. Reliability: Large patches have more ways to go wrong. A one-byte NOP has one failure mode; a 20-byte payload has twenty.

  2. Reversibility: A small patch is easier to document and reverse. "Changed byte at offset 0x1234 from 0x74 to 0x75" is unambiguous.

  3. Analysis integrity: When you are using patching to understand a binary (rather than to permanently modify it), the patch should change exactly one thing, so you can attribute any behavioral change to that one change.

Smallest patches by category:

  • Inverting a conditional jump: je (74) to jne (75), or jl (7C) to jge (7D). Single byte change. This inverts the branch condition.
  • Forcing a jump to always-taken: Change a conditional jump je (74 XX) to unconditional jmp (EB XX). Two-byte change (opcode + relative offset unchanged). The branch now always executes regardless of flags.
  • Forcing a jump to never-taken: NOP out the jump. A 2-byte je XX becomes 90 90 (two NOPs). The branch never executes.
  • Patching a function return value: Change mov eax, 0 to mov eax, 1 (or vice versa). The return value is wrong but predictable.

NOP sled: A sequence of 90 bytes (the x86 NOP instruction) used to fill space when you need to remove instructions without changing the size of the binary. If you want to remove a 10-byte instruction sequence, you replace all 10 bytes with NOPs.

Part 3: Patching with Ghidra (25 min)

Ghidra can patch binaries directly in the listing view.

Patching bytes directly:

  1. Navigate to the instruction you want to change.
  2. Right-click > "Patch Instruction" to change the instruction mnemonic/operands (Ghidra re-assembles).
  3. OR: right-click > "Patch Bytes" to change raw bytes.
  4. After patching, export the modified binary: File > Export Program > choose "Binary" format.

Patching an instruction via Patch Instruction:

  1. Navigate to je 0x401180 (an undesirable conditional jump).
  2. Right-click the instruction > "Patch Instruction."
  3. Change JE to JMP in the mnemonic field (the relative offset remains the same).
  4. Ghidra assembles the new instruction in-place.

Exporting the patched binary: File > Export Program > Format: "Binary." This writes all mapped segments back to a file in binary form. Note: Ghidra's "Binary" export writes the memory image, not the original file with patches applied. For ELF files, use the "ELF" export option if Ghidra supports it, or use objcopy to apply the patch to the original file after identifying the byte offset.

Part 4: Patching with objcopy and hexedit (20 min)

For simple byte patches, command-line tools are faster than opening Ghidra.

Finding the file offset:

The Ghidra listing shows virtual addresses. To patch the binary file, you need the file offset. For ELF binaries:

file_offset = virtual_address - load_base + section_file_offset

Or: use objdump -d --file-offsets binary to see file offsets alongside virtual addresses. Or use readelf -l to find the PT_LOAD segment's file offset and virtual address base.

Patching with Python:

# Read, patch byte at offset 0x1234, write back
with open('binary', 'r+b') as f:
    f.seek(0x1234)
    b = f.read(1)
    print(f"Original: {b.hex()}")
    f.seek(0x1234)
    f.write(b'\x75')   # jne instead of je

Patching with hexedit:

hexedit binary   # opens binary in interactive hex editor
# Navigate with arrow keys or Ctrl-G to go to offset
# Type new hex values directly
# Ctrl-X to save and exit

Patching with printf and dd (one-liner for known offset):

printf '\x75' | dd of=binary bs=1 seek=$((0x1234)) count=1 conv=notrunc

Lab exercises (~1.5 hr)

Lab 8: Patch to bypass

See labs/lab-8-patching.md for the full specification.

You are given a CrackMe binary with a key check. Your task: find the check function statically, identify the conditional branch that produces "Wrong" output, and patch it with the smallest possible change (single byte preferred) so the binary produces "Correct" output for any input. You document the patch: original byte, patched byte, file offset, and an explanation of why the patch works.


Independent practice (~3 hr)

  • Tool Journal: Document the patching workflow: how to find file offset from virtual address, how to patch with Python (read/seek/write), how to NOP an instruction. Include a quick-reference table of jump opcode inversions: je <-> jne, jl <-> jge, jle <-> jg, jb <-> jae.
  • Checksum anti-patch: Find a CrackMe that includes a checksum (if one exists in your ladder collection). Identify where the checksum is computed and where it is checked. Can you patch both? What does this exercise reveal about the limits of patching as a bypass technique?
  • CrackMe ladder: Solve at least one more challenge this week. By Week 11 you should have 6+ solved (toward the Lab 6 checkpoint of 4+ with technique narrative; the total of 8+ required is due at Week 14).

Reflection prompts

  1. The smallest-possible-patch discipline says to change one byte when possible. But some binary checks cannot be bypassed with a single byte -- for example, a check that involves multiple comparisons across several functions. How would you approach a multi-check bypass? What does the principle "smallest patch" mean when the minimum viable change is larger?

  2. Binary patching modifies a binary without source code. From a software supply chain security perspective, what makes this dangerous? How do modern systems detect patched or tampered binaries? (Think: code signing, checksums, secure boot.)

  3. You have patched a binary to bypass its anti-debug check (from Week 10). You now run it under gdb without triggering the anti-debug exit. But the binary still behaves unexpectedly. What other anti-debug techniques might remain? How would you systematically find all anti-debug checks in a binary?


Week 11 of 14. Next: Firmware teardown -- guided rehearsal, extraction, identification, salient findings.