Post

Binary Patching & Anti-analysis

Bypassing Conditional Execution in an x86 Malware Sample

Objective

During malware analysis, analysts often encounter binaries that intentionally prevent malicious functionality from executing unless very specific conditions are met. These checks may rely on environment validation, internet connectivity, registry values, mutexes, or cryptographic verification.

Instead of recreating the expected environment, an analyst can patch the binary to force execution down the desired code path.


Understanding the Original Program

The original Nim source code performs the following operations:

  1. Download a file (key.crt) from a remote server.
  2. Compute the SHA-256 hash of the downloaded data.
  3. Compare the hash against a hardcoded value.
  4. Execute the payload only if the hashes match.

Conceptually the program behaves like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Download key.crt
        │
        ▼
Calculate SHA256
        │
        ▼
Compare with hardcoded hash
        │
   ┌────┴─────┐
   │          │
 Match     No Match
   │          │
   ▼          ▼
Payload    Print Error

Since the original server is unavailable, the comparison always fails, preventing the payload from executing.


Entering the Program

Opening the executable inside Cutter initially shows the standard main() entry point.

alt text

The first function is mostly initialization code generated by the Nim compiler. It prepares the runtime before transferring execution into the actual program logic.

Following the execution flow leads into NimMainInner().


NimMainInner()

The decompiler reveals that NimMainInner() performs almost no work itself.

alt text

It simply calls:

1
@NimMainModule@0();

This function contains the real application logic generated from the original source code.


The Real Main Function

Inside NimMainModule() we finally reach the code we care about.

alt text

Even without reading assembly, several important function names immediately stand out because symbols have not been stripped:

  • evaluate_http_body()
  • run_payload()
  • echoBinSafe()

From the decompiled code we can see the following logic:

1
2
3
4
5
6
7
8
9
10
result = evaluate_http_body();

if(result)
{
    run_payload();
}
else
{
    echoBinSafe();
}

This perfectly matches the original source code.


Control Flow Graph

Switching to Graph View makes the program significantly easier to understand.

alt text

The graph clearly shows two execution paths.

Path 1

If the verification succeeds:

1
2
3
4
evaluate_http_body()
        │
        ▼
run_payload()

Path 2

If verification fails:

1
2
3
4
evaluate_http_body()
        │
        ▼
echoBinSafe()

The decision between these two paths is controlled by a single conditional jump instruction.


Following the Assembly

The important instructions are shown below:

call    evaluate_http_body
mov     byte [0x454440], al
movzx   eax, byte [0x454440]
xor     eax, 1
test    al, al
jne     0x43f1c0

Although this looks complicated, each instruction performs one small task.


Step 1 – Execute the Verification Function

The first instruction calls the verification routine.

call evaluate_http_body

The function returns either:

  • TRUE (1)
  • FALSE (0)

Like most x86 functions, the return value is placed into the EAX register.

Since this value is only one byte, the lower portion of EAX (AL) is used.

alt text


Step 2 – Store the Result

Immediately after the function returns, the value inside AL is copied into memory.

mov byte [0x454440], al

This simply preserves the result temporarily.

Nothing has been modified yet.


Step 3 – Restore the Value

Next, the stored byte is copied back into EAX.

movzx eax, byte [0x454440]

The MOVZX instruction means:

Move with Zero Extension

Because only one byte is being loaded, the processor clears the remaining bits of EAX.

Example:

1
2
3
4
5
AL = 01

↓

EAX = 00000001

This produces a clean 32-bit value for later operations.

alt text


Step 4 – XOR with 1

Now comes the instruction that changes everything.

xor eax,1

XOR behaves as follows:

InputXOR 1Result
011
110

Therefore:

If evaluate_http_body() returns:

1
2
3
4
5
6
7
8
9
TRUE (1)

↓

1 XOR 1

↓

0

If it returns:

1
2
3
4
5
6
7
8
9
FALSE (0)

↓

0 XOR 1

↓

1

The result has now been inverted.


Step 5 – TEST Instruction

Next comes:

test al, al

Unlike CMP, the TEST instruction performs a bitwise AND without modifying the register.

1
AL AND AL

Its only purpose here is to update the processor flags.

Most importantly:

  • Zero Flag (ZF)

If AL equals zero:

1
ZF = 1

Otherwise:

1
ZF = 0

Step 6 – The Conditional Jump

Finally:

jne 0x43f1c0

alt text

JNE means:

Jump if Not Equal

More accurately:

Jump when the Zero Flag is 0.

Therefore:

If ZF = 0

Execution jumps into the failure branch.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
evaluate_http_body()

↓

FALSE

↓

xor

↓

ZF = 0

↓

JNE Taken

↓

echoBinSafe()

If ZF = 1

The jump is not taken.

Execution simply continues downward.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
evaluate_http_body()

↓

TRUE

↓

xor

↓

ZF = 1

↓

JNE NOT Taken

↓

run_payload()

This single instruction determines which branch executes.


The Problem

Running the executable normally produces:

1
2
3
[-] Error: No such host is known.

[-] No dice, sorry :(

The remote server no longer exists, meaning:

  • HTTP request fails
  • SHA-256 comparison fails
  • Payload never executes

Without modifying the binary, there is no practical way to satisfy the original condition.


Binary Patching

Since we control the executable, we can modify its instructions.

Instead of trying to recreate the missing server, we simply alter the program’s logic.

The easiest modification is to reverse the conditional jump.

Original instruction:

jne

Patched instruction:

je

This changes the branch logic completely.


Applying the Patch in Cutter

Navigate to the conditional jump instruction.

Right-click the instruction and select:

1
2
3
Edit
    ↓
Reverse Jump

alt text

Cutter automatically replaces:

jne

with

je

without changing instruction size or affecting surrounding code.


Result

Running the original executable:

1
2
3
[-] Error: No such host is known.

[-] No dice, sorry :(

Running the patched executable:

1
2
3
[-] Error: No such host is known.

[!] Boom!

Even though the HTTP request still fails, the payload executes because we modified the program’s decision-making process.


Key Takeaways

  • Malware frequently protects its payload behind conditional checks.
  • Reverse engineering allows analysts to identify the exact instruction controlling execution flow.
  • Conditional jumps such as JNE, JE, JG, and JL often determine whether malicious functionality is executed.
  • Small binary patches can completely alter program behavior without modifying large portions of code.
  • Cutter provides an easy way to patch instructions using Edit → Reverse Jump, making it an effective tool for malware analysis and reverse engineering.

Part 2 – Defeating the IsDebuggerPresent() Anti-Debugging Technique

Introduction

One of the oldest and most common anti-analysis techniques used by malware is detecting whether it is being executed under a debugger.

Windows provides an API named IsDebuggerPresent() that allows a program to determine if a user-mode debugger is attached to its process. Malware frequently uses this API to prevent analysts from observing its malicious behavior. If a debugger is detected, the malware may terminate, display a fake error, or avoid executing its payload entirely.

Although this technique is relatively simple, understanding how it works provides an excellent introduction to malware anti-analysis and demonstrates how reverse engineers can bypass runtime protections.


Locating the Anti-Debugging Logic

Open the executable in Cutter and navigate to the WinMain() function.

This function contains the primary execution logic of the program.

alt text

Immediately, one API call stands out:

call IsDebuggerPresent

Since the binary retains its symbols, identifying the anti-debugging routine is straightforward.


Understanding IsDebuggerPresent()

According to the Microsoft documentation:

alt text

IsDebuggerPresent() determines whether the current process is running under a user-mode debugger.

It returns a Windows BOOL value.

Return ValueMeaning
0No debugger detected
Non-zero (typically 1)Debugger detected

This single return value determines which execution path the malware follows.


Examining the Assembly

The core anti-debugging logic consists of only a few instructions.

alt text

call    IsDebuggerPresent
test    eax,eax
setne   al
test    al,al
je      0x1400015a9

Although short, these instructions completely control the malware’s execution flow.


Step 1 – Calling the API

Execution begins with:

call IsDebuggerPresent

The Windows API performs the debugger check internally before returning to the caller.

Its return value is stored inside EAX.

If a debugger exists:

1
EAX = 1

Otherwise:

1
EAX = 0

This return value is the foundation for the remaining instructions.


Step 2 – Testing the Return Value

Immediately after the API call:

test eax,eax

This instruction performs a bitwise AND of EAX against itself.

1
EAX AND EAX

The register itself is not modified.

Instead, the processor updates its status flags.

The important flag here is:

  • Zero Flag (ZF)

If EAX equals zero:

1
ZF = 1

Otherwise:

1
ZF = 0

Although this is a 64-bit application, EAX is still used because the returned BOOL value easily fits within 32 bits.

alt text


Step 3 – The SETNE Instruction

The next instruction is:

setne al

SETNE means:

Set if Not Equal

More precisely:

Set the destination operand to 1 if the Zero Flag equals 0.

Otherwise:

Set it to 0.

This instruction copies the logical result of the previous TEST into the AL register.

Zero FlagAL
01
10

No jumps occur yet.

The processor is simply preparing for the upcoming comparison.


Step 4 – Testing AL

Next comes:

test al,al

Again, this performs a bitwise AND.

1
AL AND AL

Just like before, this updates only the processor flags.

If AL equals zero:

1
ZF = 1

Otherwise:

1
ZF = 0

The Zero Flag now accurately represents whether a debugger was detected.


Step 5 – The Conditional Jump

Finally, execution reaches:

je 0x1400015a9

alt text

JE means:

Jump if Equal

More specifically:

Jump when the Zero Flag equals 1.

This single instruction determines whether the malware proceeds to execute its payload or follows the anti-debugging path.


Program Logic

When no debugger is attached:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
IsDebuggerPresent()

↓

0

↓

TEST EAX,EAX

↓

ZF = 1

↓

SETNE AL

↓

AL = 0

↓

TEST AL,AL

↓

ZF = 1

↓

JE Taken

↓

Continue execution

When a debugger is attached:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
IsDebuggerPresent()

↓

1

↓

TEST EAX,EAX

↓

ZF = 0

↓

SETNE AL

↓

AL = 1

↓

TEST AL,AL

↓

ZF = 0

↓

JE NOT Taken

↓

Debugger Detected

Executing Without a Debugger

Running the executable normally results in the following message:

alt text

Immediately afterward, the payload executes.

alt text

Since no debugger is attached, the malware follows its intended execution path.


Executing Inside x64dbg

Now launch the executable inside x64dbg.

When execution reaches the anti-debugging logic, the malware detects the debugger and immediately displays:

alt text

Instead of executing the payload, the malware terminates.

The anti-analysis technique has successfully prevented execution.


High-Level Equivalent

The assembly instructions are functionally identical to the following C-style pseudocode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if (IsDebuggerPresent())
{
    MessageBox(
        GetForegroundWindow(),
        "Oh, you think you're slick, huh? I see your debugger over there. No soup for you!",
        "MEGASUSBRO",
        MB_OK
    );

    exit(EXIT_FAILURE);
}
else
{
    MessageBox(
        GetForegroundWindow(),
        "No debugger detected! Cowabunga, dudes!",
        "COAST IS CLEAR",
        MB_OK
    );

    MessageBox(
        GetForegroundWindow(),
        "Boom!",
        "PAYLOAD",
        MB_OK
    );

    exit(EXIT_SUCCESS);
}

Although the assembly appears complicated at first glance, it ultimately performs a simple conditional statement.


Dynamic Patching in x64dbg

Previously, we modified the executable on disk using Cutter.

This time we will perform the modification only in memory.

No bytes inside the executable are changed permanently.

Instead, we alter the running process.

This technique is known as dynamic patching.


Locating the Check

Restart the executable inside x64dbg.

Execute the program until the debugger detection message appears.

alt text

Restart the process (Ctrl + F2) so execution returns to the program entry point.

Inside the CPU window select:

1
2
3
4
5
6
7
8
9
Search For

↓

All Modules

↓

String References

Search for:

1
IsDebuggerPresent

alt text

The string is used by GetProcAddress() to dynamically resolve the API.

This places us close to the debugger detection routine.

Set a breakpoint on this reference using F2.


Following Execution

Resume execution with F9.

Eventually the breakpoint triggers.

alt text

Continue stepping with F8 until the function returns.

alt text

Execution eventually reaches the familiar instruction sequence.

alt text

Notice that it exactly matches the instructions previously analyzed in Cutter.

alt text


Inspecting the Zero Flag

When execution pauses on the JE instruction, observe the processor flags.

With a debugger attached:

1
ZF = 0

alt text

Since JE only jumps when ZF = 1, the malware proceeds into the debugger-detected branch.


Modifying Runtime State

Instead of changing the executable itself, we can modify the processor state.

Inside x64dbg simply double-click the Zero Flag.

1
2
3
4
5
6
7
ZF

0

↓

1

alt text

This changes only the current process memory.

Nothing is written back to disk.

From the malware’s perspective, the debugger check now appears to have succeeded.


Continuing Execution

Continue stepping through the program.

The malware now follows the alternate execution path.

The first message box appears.

alt text

Immediately afterward, the payload executes successfully.

alt text

Despite running inside a debugger, the anti-analysis protection has been completely bypassed.


Key Takeaways

  • IsDebuggerPresent() is one of the most common Windows anti-debugging APIs.
  • The API returns 0 when no debugger is attached and a non-zero value when one is detected.
  • The malware converts this return value into processor flags using TEST and SETNE instructions.
  • The JE instruction determines which execution path the malware follows.
  • Static patching modifies the executable on disk, whereas dynamic patching modifies only the running process in memory.
  • By changing the Zero Flag during execution, we redirected program flow without altering a single byte of the executable.

Conclusion

This lab demonstrated how a simple Windows anti-debugging technique can prevent malware from revealing its malicious functionality during analysis. By understanding the assembly instructions that process the return value of IsDebuggerPresent(), I identified the exact decision point controlling execution. Rather than permanently modifying the executable, I dynamically altered the processor’s Zero Flag at runtime, causing the malware to follow its normal execution path even while being debugged. This technique highlights the importance of understanding CPU flags and conditional branching, as even a single modified flag can completely change a program’s behavior during reverse engineering.

This post is licensed under CC BY 4.0 by the author.