# Binary Patching Guide for Malware Analysis ## Overview Binary patching modifies executable files to alter their behavior without recompiling from source. In malware analysis, patching is used to disable anti-analysis checks, redirect control flow, and enable debugging. ## Prerequisites - Hex editor (HxD, 010 Editor, or xxd) - Disassembler (Ghidra, IDA, or radare2) - Debugger (x64dbg, WinDbg, or GDB) - Understanding of x86/x64 instruction encoding ## Core Patching Techniques ### 1. NOP Sled (No Operation) Replace instructions with NOP (0x90) to effectively remove them. **When to use**: Removing function calls, conditional checks, or unwanted behavior. ``` Before: E8 xx xx xx xx ; call anti_debug_func After: 90 90 90 90 90 ; nop nop nop nop nop Before: FF 15 xx xx xx xx ; call [IsDebuggerPresent] After: 90 90 90 90 90 90 ; 6x nop ``` **Important**: The patch must be exactly the same number of bytes as the original instruction(s). ### 2. Force Return Value Replace a function's body to immediately return a specific value. **Return 0 (32-bit)**: ``` 31 C0 ; xor eax, eax C3 ; ret ``` **Return 1 (32-bit)**: ``` B8 01 00 00 00 ; mov eax, 1 C3 ; ret ``` **Return 0 (64-bit)**: ``` 48 31 C0 ; xor rax, rax C3 ; ret ``` ### 3. Invert Conditional Jumps Flip the logic of a conditional branch. | Original | Patched | Original Meaning | Patched Meaning | |----------|---------|------------------|-----------------| | 74 (JE) | 75 (JNE) | Jump if equal | Jump if not equal | | 75 (JNE) | 74 (JE) | Jump if not equal | Jump if equal | | 0F 84 (JE near) | 0F 85 (JNE near) | Jump if equal (near) | Jump if not equal (near) | | 0F 85 (JNE near) | 0F 84 (JE near) | Jump if not equal (near) | Jump if equal (near) | | 7C (JL) | 7D (JGE) | Jump if less | Jump if greater/equal | | 7F (JG) | 7E (JLE) | Jump if greater | Jump if less/equal | **Example**: Anti-debug check followed by conditional exit: ``` Before: call IsDebuggerPresent test eax, eax 75 xx ; JNE to_exit (if debugger detected, exit) After: call IsDebuggerPresent test eax, eax 74 xx ; JE to_exit (inverted: never exits because eax=1 under debugger) ``` ### 4. Unconditional Jump (Force Branch) Force execution to always take a specific path. ``` Before: 74 xx ; JE short (conditional) After: EB xx ; JMP short (unconditional) Before: 0F 84 xx xx xx xx ; JE near (conditional) After: E9 xx xx xx xx 90 ; JMP near + NOP (unconditional) ``` ### 5. Patch Sleep Calls Reduce or eliminate sleep delays used to slow analysis. **NOP the call**: ``` Before: 6A 00 68 E8030000 FF 15 xx xx xx xx ; push 0; push 1000; call [Sleep] After: 90 90 90 90 90 90 90 90 90 90 90 90 ; 12x NOP ``` **Reduce sleep time** (change pushed value): ``` Before: 68 60EA0000 ; push 60000 (60 seconds) After: 68 01000000 ; push 1 (1 millisecond) ``` ## Step-by-Step Patching Workflow ### Using Ghidra 1. Open binary in Ghidra, let auto-analysis complete 2. Navigate to the target instruction (G for Go To) 3. Right-click the instruction and select "Patch Instruction" 4. Enter the new instruction (e.g., `NOP`, `XOR EAX,EAX`) 5. File -> Export Program -> Original File Format to save ### Using radare2 ```bash # Open in write mode r2 -w sample.exe # Seek to address s 0x00401234 # Write NOP bytes wx 9090909090 # Write instruction wa xor eax, eax # Write and pad with NOPs wao nop # NOP current instruction # Save and quit q ``` ### Using x64dbg (Runtime) 1. Load binary in x64dbg 2. Navigate to target instruction (Ctrl+G) 3. Select instruction(s) to patch 4. Right-click -> Assemble (Space) or Binary -> Fill with NOPs 5. Patches -> Patch File to save ### Using Python (programmatic) ```python import struct from pathlib import Path data = bytearray(Path("sample.exe").read_bytes()) # Patch at offset 0x1234: replace 6 bytes with xor eax,eax + 4 NOPs offset = 0x1234 data[offset:offset+6] = b"\x31\xc0\x90\x90\x90\x90" Path("sample_patched.exe").write_bytes(data) ``` ## Common Anti-Debug Patches ### IsDebuggerPresent ``` ; Find this pattern: FF 15 [4 bytes IAT addr] ; call [kernel32.IsDebuggerPresent] 85 C0 ; test eax, eax 75 xx ; jne detected ; Patch option 1: NOP the call, zero eax 31 C0 90 90 90 90 ; xor eax, eax + 4 NOPs 85 C0 ; test eax, eax (will be zero) 75 xx ; jne (won't jump) ; Patch option 2: Invert the jump FF 15 [4 bytes IAT addr] ; call [kernel32.IsDebuggerPresent] 85 C0 ; test eax, eax 74 xx ; je detected (inverted, won't trigger) ``` ### NtQueryInformationProcess (ProcessDebugPort) ``` ; Find push 7 near a call to NtQueryInformationProcess 6A 07 ; push 7 (ProcessDebugPort) ; ... more pushes ... FF 15 [addr] ; call [NtQueryInformationProcess] ; Patch: change query class to something benign 6A 00 ; push 0 (ProcessBasicInformation) ``` ### PEB.BeingDebugged Direct Access ``` ; 32-bit: fs:[30h] PEB access 64 A1 30 00 00 00 ; mov eax, large fs:30h 0F B6 40 02 ; movzx eax, byte ptr [eax+2] (BeingDebugged) ; Patch: zero eax instead 31 C0 90 90 90 90 ; xor eax, eax + NOPs 90 90 90 90 ; NOPs (replacing movzx) ``` ## Safety Guidelines 1. **Always work on copies**: Never patch the original sample 2. **Verify PE integrity**: Ensure the patched file still loads and executes 3. **Check section boundaries**: Don't patch across section boundaries 4. **Maintain instruction alignment**: Patches must not create invalid instruction sequences 5. **Document all patches**: Record offset, original bytes, and new bytes 6. **Watch for self-integrity checks**: Some malware verifies its own hash 7. **Test incrementally**: Apply one patch at a time and verify behavior ## Troubleshooting | Issue | Cause | Solution | |-------|-------|----------| | Patched binary crashes | Invalid instruction created | Verify instruction boundaries; use disassembler | | Patched binary same behavior | Wrong offset patched | Verify file offset vs RVA; check for multiple checks | | "Corrupted executable" error | PE checksum mismatch | Zero out PE checksum field (offset 0x58 in optional header) | | Anti-tamper triggers | Binary self-hash check | Find and patch the hash verification too | | ASLR complications | Addresses change each run | Use RVA-based offsets, not absolute addresses |