Forcing Alternate Execution Paths

28 minIn Progress

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:

  1. Navigate to the JNE instruction (Ctrl+G, enter address)
  2. Select the instruction
  3. Right-click > Assemble (or press Space bar)
  4. Type NOP in the text field
  5. Enable the "Fill with NOP's" checkbox
  6. Click OK
  7. 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:

OriginalOpcodeInvertedOpcodeUse Case
JNZ/JNE0x75JZ/JE0x74Invert debugger check
JZ/JE0x74JNZ/JNE0x75Invert equality check
JA0x77JBE0x76Invert unsigned comparison
JB0x72JAE0x73Invert unsigned comparison
JG0x7FJLE0x7EInvert signed comparison
JL0x7CJGE0x7DInvert 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:

  1. Set a breakpoint on the TEST or CMP instruction
  2. When it breaks, double-click the register value (e.g., RAX) in the Registers pane
  3. Set to 0 (simulate "no debugger" for IsDebuggerPresent) or any needed value
  4. 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

  1. In the Listing view, right-click an instruction
  2. Select Patch Instruction (or press Ctrl+Shift+G)
  3. Type the replacement instruction in the dialog
  4. To save: File > Export Program > choose "Original File" format

Common Patch Recipes

ScenarioOriginalPatchBytes
Skip anti-debugJNZ exit_labelNOP; NOP90 90
Force true branchJNZ targetJMP targetEB xx
Skip Sleep callCALL SleepNOP (x5)90 90 90 90 90
Disable check functionFunction startXOR EAX,EAX; RET31 C0 C3
Force zero returnCALL check_fn + TESTNOP call + XOR EAX,EAX90...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:

  1. Set breakpoint on IsDebuggerPresent (at the RET instruction)
  2. Right-click the breakpoint > Edit Breakpoint
  3. In the Command field, enter: eax=0
  4. Set Break Condition to 0 (never actually break)
  5. 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:

  1. Find the command dispatcher: Look for switch/case or if-else chains after recv() or HTTP response parsing
  2. Set a breakpoint before the command comparison
  3. Modify the command variable in memory to trigger each branch
  4. Analyze each code path individually to map all capabilities
  5. 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:

  1. Initial run: Execute sample without patches, observe where it exits or deviates
  2. Identify checks: Use CAPA, import analysis, and string search to catalog all checks
  3. Apply ScyllaHide: Enable all options -- this handles ~90% of common anti-debug
  4. Test again: Run and check if behavior changes
  5. If still evading: Set breakpoints on suspicious APIs, trace to find remaining checks
  6. Patch individually: NOP, invert, or force each remaining check
  7. Document everything: Record all patches with addresses for reproducibility
  8. Save patched binary: File > Patch file > Patch File for future analysis

Practical Exercise

  1. Load getdown.exe in x64dbg
  2. Search for intermodular calls to identify anti-debug APIs
  3. Find the IsDebuggerPresent check and the JNE instruction
  4. Patch the JNE with NOPs using the Assemble dialog
  5. Run the patched sample and verify it now makes network connections
  6. Save the patched binary via File > Patch file
  7. Compare behavior: original (exits immediately) vs. patched (connects to C2)
  8. Document all patches applied with addresses and rationale
Try it in the shell
Practise this lesson's tooling on its sample in an emulated analyst shell. Output is pre-recorded — nothing executes.

Suggested triage steps

  1. 1

    Find the branch worth forcing

    The anti-debug check IS the branch — patch its result and the sample takes the path it hides.

  2. 2

    See what is on the other side

    The injection APIs only run once the checks pass, which is why forcing the branch is worth the effort.

  3. 3

    Know the behaviour you are unlocking

    What you should expect to observe after the patch.

analyst@lab:~emulated · nothing executes

MAA analyst shell — emulated. Nothing executes.

Type 'help', or click a step on the left.

$
Forcing Alternate Execution Paths | Malware Analysis Academy