Forcing Alternate Execution Paths
Overview and Learning Objectives
Malware often contains conditional branches that gate malicious behavior behind environment checks, network connectivity, time triggers, or configuration flags. To fully analyze all capabilities, you need to force execution down specific paths. By the end of this lesson, you will:
- Master binary patching techniques in x64dbg and Ghidra
- Understand conditional breakpoint scripting for automated bypass
- Know how to handle Sleep delays and network failures during analysis
- Be able to force C2 command paths for complete capability mapping
Why this matters: A sample that silently exits due to an anti-debug check may appear benign in sandbox reports. Forcing execution reveals the true payload and allows complete analysis.
Patching with x64dbg
Technique 1: NOP Sled (Removing a Check)
Replace a conditional jump with NOP instructions to skip a check entirely:
; Original code at getdown.exe:
14000102A call qword ptr ds:[<&IsDebuggerPresent>]
14000102A test eax, eax
14000102C jne getdown.140001216 ; 6-byte JNE instruction
; After patching -- select JNE, right-click > Assemble > type NOP
; Enable "Fill with NOP's" checkbox to pad all bytes:
14000102C nop
14000102D nop
14000102E nop
14000102F nop
140001030 nop
140001031 nop
Step-by-step in x64dbg:
- Navigate to the JNE instruction (Ctrl+G, enter address)
- Select the instruction
- Right-click > Assemble (or press Space bar)
- Type
NOPin the text field - Enable the "Fill with NOP's" checkbox
- Click OK
- Verify the patch in the Patches window (Ctrl+P)
Pro Tip: After patching, you can save the modified binary permanently via File > Patch file... > Patch File. The Patches window shows both original and new byte values, allowing you to revert if needed.
Technique 2: Inverting a Jump
Change the direction of a conditional jump so the opposite branch executes:
| Original | Opcode | Inverted | Opcode | Use Case |
|---|---|---|---|---|
| JNZ/JNE | 0x75 | JZ/JE | 0x74 | Invert debugger check |
| JZ/JE | 0x74 | JNZ/JNE | 0x75 | Invert equality check |
| JA | 0x77 | JBE | 0x76 | Invert unsigned comparison |
| JB | 0x72 | JAE | 0x73 | Invert unsigned comparison |
| JG | 0x7F | JLE | 0x7E | Invert signed comparison |
| JL | 0x7C | JGE | 0x7D | Invert signed comparison |
In x64dbg: right-click the instruction > Assemble > type the new instruction (e.g., change jne to je).
When to invert vs. NOP: Inverting is useful when you want the malware to only run in a debugger (for controlled analysis). NOPing is better when you want the check completely eliminated.
Technique 3: Register Manipulation
Before a comparison instruction, modify the register to force the desired branch:
- Set a breakpoint on the
TESTorCMPinstruction - When it breaks, double-click the register value (e.g., RAX) in the Registers pane
- Set to 0 (simulate "no debugger" for IsDebuggerPresent) or any needed value
- Press F9 to continue execution
Advantages: Non-intrusive, does not modify the binary Disadvantages: Must be repeated every time the check executes; impractical for frequent checks
Technique 4: Forced Return Values
Replace an entire function with a forced return:
; Replace the start of an anti-debug function with:
XOR EAX, EAX ; Set return value to 0 (2 bytes: 31 C0)
RET ; Return immediately (1 byte: C3)
; Remaining bytes of original function are now dead code
This is useful when a single function contains multiple anti-debug checks -- replacing the entire function is cleaner than patching each check individually.
Patching with Ghidra
Applying Patches
- In the Listing view, right-click an instruction
- Select Patch Instruction (or press Ctrl+Shift+G)
- Type the replacement instruction in the dialog
- To save: File > Export Program > choose "Original File" format
Common Patch Recipes
| Scenario | Original | Patch | Bytes |
|---|---|---|---|
| Skip anti-debug | JNZ exit_label | NOP; NOP | 90 90 |
| Force true branch | JNZ target | JMP target | EB xx |
| Skip Sleep call | CALL Sleep | NOP (x5) | 90 90 90 90 90 |
| Disable check function | Function start | XOR EAX,EAX; RET | 31 C0 C3 |
| Force zero return | CALL check_fn + TEST | NOP call + XOR EAX,EAX | 90...31 C0 |
Conditional Breakpoint Scripting in x64dbg
Auto-Patching Return Values
Set a breakpoint that automatically modifies state without stopping:
; In the x64dbg command window:
SetBPX IsDebuggerPresent ; Set breakpoint on API
SetBreakpointCondition IsDebuggerPresent, 0 ; Never break (auto-continue)
SetBreakpointCommand IsDebuggerPresent, "eax=0" ; Set return value to 0
GUI method:
- Set breakpoint on
IsDebuggerPresent(at the RET instruction) - Right-click the breakpoint > Edit Breakpoint
- In the Command field, enter:
eax=0 - Set Break Condition to
0(never actually break) - The return value is silently patched every time
Multi-API Auto-Bypass Script
; Create a script that auto-bypasses multiple anti-debug APIs:
SetBPX IsDebuggerPresent
SetBreakpointCommand IsDebuggerPresent, "eax=0"
SetBreakpointCondition IsDebuggerPresent, 0
SetBPX CheckRemoteDebuggerPresent
; Hook at RET, set the output BOOL to FALSE
SetBreakpointCondition CheckRemoteDebuggerPresent, 0
Hardware Breakpoints
For anti-debug malware that scans its own code for 0xCC (INT 3) software breakpoints:
- Use hardware breakpoints instead (DR0-DR3 registers, max 4)
- x64dbg: Right-click address > Breakpoint > Hardware, Execute
- Hardware breakpoints do not modify code bytes, so 0xCC scanning cannot detect them
Handling Sleep and Delays
The Problem
Sleep(300000) means 5 minutes of waiting during analysis. Malware uses this to outlast sandbox timeouts.
Solution 1: Modify Sleep Parameter at Runtime
; Set breakpoint on Sleep API
bp Sleep
; When hit, examine the first parameter (milliseconds)
; In x64 calling convention, RCX = first parameter
; Modify: rcx=1000 (change to 1 second)
; Then press F9 to continue
Solution 2: Auto-Reduce Sleep via Conditional Breakpoint
SetBPX Sleep
SetBreakpointCommand Sleep, "rcx=1" ; Change all sleeps to 1ms
SetBreakpointCondition Sleep, 0 ; Never break, auto-continue
Solution 3: NOP the Sleep Call
; Original:
PUSH 0x493E0 ; 300000 ms = 5 bytes
CALL Sleep ; 5 bytes
; Patched (10 NOPs):
NOP NOP NOP NOP NOP NOP NOP NOP NOP NOP
Solution 4: Use FakeNet-NG Time Manipulation
FakeNet-NG and other tools can intercept and accelerate time-based API calls at the system level.
Handling Network Failures
When malware cannot reach its C2 server during analysis:
FakeNet-NG (Windows FLARE-VM)
# Start FakeNet-NG to intercept and respond to all network traffic
FakeNet.exe
# FakeNet-NG will:
# - Respond to DNS queries with configurable IPs
# - Serve fake HTTP/HTTPS responses
# - Accept TCP connections on any port
# - Log all network activity for analysis
INetSim (REMnux Linux)
# Start INetSim to simulate multiple network services
sudo inetsim
# INetSim simulates: DNS, HTTP, HTTPS, FTP, SMTP, IRC, and more
# Configure in /etc/inetsim/inetsim.conf
# Point your analysis VM's DNS to the REMnux IP
Direct Patching Approach
; If malware checks connect() return value:
; Patch the CMP/TEST after connect to always indicate success
; Or NOP the error-handling branch
Forcing C2 Command Paths
If malware behavior depends on commands received from a C2 server:
- Find the command dispatcher: Look for switch/case or if-else chains after
recv()or HTTP response parsing - Set a breakpoint before the command comparison
- Modify the command variable in memory to trigger each branch
- Analyze each code path individually to map all capabilities
- Document each command and its corresponding action
; Example: Malware checks command byte at [EBP-0x10]
; Set breakpoint at the CMP instruction
; When hit, modify the memory:
; For command 0x01 (keylog): set [EBP-0x10] = 1
; For command 0x02 (screenshot): set [EBP-0x10] = 2
; For command 0x03 (download): set [EBP-0x10] = 3
; Run each path and document the behavior
Complete Bypass Workflow
Follow this systematic approach for any protected sample:
- Initial run: Execute sample without patches, observe where it exits or deviates
- Identify checks: Use CAPA, import analysis, and string search to catalog all checks
- Apply ScyllaHide: Enable all options -- this handles ~90% of common anti-debug
- Test again: Run and check if behavior changes
- If still evading: Set breakpoints on suspicious APIs, trace to find remaining checks
- Patch individually: NOP, invert, or force each remaining check
- Document everything: Record all patches with addresses for reproducibility
- Save patched binary: File > Patch file > Patch File for future analysis
Practical Exercise
- Load getdown.exe in x64dbg
- Search for intermodular calls to identify anti-debug APIs
- Find the IsDebuggerPresent check and the JNE instruction
- Patch the JNE with NOPs using the Assemble dialog
- Run the patched sample and verify it now makes network connections
- Save the patched binary via File > Patch file
- Compare behavior: original (exits immediately) vs. patched (connects to C2)
- Document all patches applied with addresses and rationale
