WinAntiDbg0x100 picoCTF 2024 Solution

Published: April 3, 2024

Description

A Windows console binary detects debuggers via IsDebuggerPresent and refuses to show the flag if a debugger is attached. By intercepting the anti-debug check in x64dbg and skipping the alert path, you can read the embedded flag string.

This is a gentle intro to anti-debug bypass: find the check, break on it, and patch EAX to zero before the TEST instruction so execution continues as if no debugger was detected.

Windows & x64dbg

Download WinAntiDbg0x100.zip (password: picoctf) and extract the executable.

Open WinAntiDbg0x100.exe inside x64dbg (32-bit).

bash
wget https://artifacts.picoctf.net/c_titan/84/WinAntiDbg0x100.zip && \
unzip WinAntiDbg0x100.zip

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
First challenge in the WinAntiDbg series; WinAntiDbg0x200 and 0x300 introduce progressively more advanced anti-debug techniques. If x64dbg is new to you, the Ghidra reverse engineering guide gives a useful static-analysis companion view.
  1. Step 1
    Load in Ghidra to understand the check
    Observation
    I noticed the challenge description called out IsDebuggerPresent by name, which suggested static analysis in Ghidra would immediately surface the call site and the surrounding conditional logic before touching the debugger.
    Open the binary in Ghidra (NSA's free reverse engineering suite). Ghidra shows that the program calls IsDebuggerPresent, then tests the return value: if it is zero (no debugger), execution continues to the flag. The TEST instruction address in the canonical build ends in digits like 1602 or 1604; use those last four digits to locate the equivalent instruction in x32dbg.
    What didn't work first

    Tried: Opening the binary with x64dbg (64-bit) instead of x32dbg (32-bit).

    x64dbg and x32dbg are shipped together but serve different architectures. Opening a 32-bit binary in the 64-bit flavor causes it to load incorrectly or show garbled register names (RAX instead of EAX). The correct tool is x32dbg; check the register prefix - E means 32-bit, R means 64-bit.

    Tried: Skipping Ghidra and trying to find IsDebuggerPresent directly in x32dbg by scrolling through disassembly.

    x32dbg's disassembly view shows raw instructions without function labels for internal code, making it tedious to locate the check by eye. Ghidra's symbol tree and decompiler surface the IsDebuggerPresent call in seconds and show the surrounding logic clearly, making it the right starting point before switching to x32dbg for the actual patch.

    Learn more

    IsDebuggerPresent is a Windows API function (in kernel32.dll) that returns non-zero if the calling process has a debugger attached. It reads the BeingDebugged byte in the Process Environment Block (PEB). The result lands in EAX; if EAX is non-zero, a conditional jump sends execution to the failure message. If EAX is zero, execution falls through to the flag display code.

    ASLR means absolute addresses differ between runs, but the last four hex digits of a code address are consistent in this build because the binary is not compiled as PIE. Compare the four-digit suffix you see in Ghidra to the addresses shown in x32dbg to find the corresponding instruction.

    • x64dbg / x32dbg is a free, open-source Windows debugger. Use x32dbg for 32-bit binaries (registers starting with E indicate 32-bit).
    • Ghidra's decompiler view shows the overall logic; x32dbg is where you actually manipulate register values at runtime.
  2. Step 2
    Set a breakpoint at the TEST instruction and zero EAX
    Observation
    I noticed that IsDebuggerPresent returns its result in EAX and the TEST instruction immediately after it determines which branch to take, so patching EAX to zero before TEST executes would make the program believe no debugger is present.
    Run the binary in x32dbg until user code. Locate the TEST instruction (the one immediately after the call to IsDebuggerPresent) using the last-four-digit address match from Ghidra. Set a breakpoint there. Run the binary; it pauses at TEST with EAX = 1 (debugger detected). In the Registers pane, double-click EAX and change it to 0. Then run to completion.

    Important: set the breakpoint on the TEST instruction itself. When x32dbg hits the breakpoint, it pauses before TEST executes; change EAX to 0 at that point, then continue. TEST will run with EAX=0, set ZF=1, and the conditional jump will take the success path. If instead you break after TEST (e.g., on the JNZ/JE itself), the condition flags are already set from the original EAX=1 value and changing EAX there has no effect.

    What didn't work first

    Tried: Setting the breakpoint on the conditional jump (JNZ or JE) instead of the TEST instruction.

    By the time execution reaches the jump, the CPU has already run TEST EAX, EAX and the Zero Flag (ZF) reflects the original EAX=1 value. Changing EAX at this point does nothing because TEST already consumed it. The fix is to break before TEST runs so EAX can be patched while the flags are still uncommitted.

    Tried: Trying to manually flip the Zero Flag in the Flags pane instead of changing EAX.

    Toggling ZF directly on the jump instruction is technically possible but fragile - x32dbg sometimes re-evaluates flags when you step, and the approach relies on knowing which flag to flip. Patching EAX before TEST is the standard and more reliable method because it manipulates the source value rather than its derived effect.

    Learn more

    EAX holds the return value of the most recently called function. After call IsDebuggerPresent, EAX is 1 if a debugger is attached, 0 otherwise. The conditional jump (JZ / JNZ) that follows TEST EAX, EAX decides which path to take. Changing EAX to 0 before the TEST runs tells the CPU there is no debugger, so the jump follows the success path.

    This technique is called a register patch: instead of permanently modifying the binary, you intercept execution at the right moment and change a register value in memory. It is non-destructive (the file on disk is unchanged) and does not require knowing assembly syntax beyond the breakpoint location.

    The WinAntiDbg series progressively introduces more sophisticated anti-debug techniques. Level 0x100 uses this single API check; 0x200 adds multiple checks including one that requires admin privileges; 0x300 uses an infinite loop and requires a patched binary exported via Ghidra.

  3. Step 3
    Read the flag from the log
    Observation
    I noticed that processes launched inside x32dbg have their stdout captured by the debugger rather than printed to a console, which meant the flag string printed by the success path would appear in the x32dbg log pane.
    After changing EAX to 0 and continuing, the program follows the no-debugger code path and prints the flag to the x32dbg log window. Look in the log pane for the line starting with picoCTF.
    What didn't work first

    Tried: Looking for the flag in a console window or command prompt instead of the x32dbg log pane.

    When a program is launched from inside x32dbg, its stdout output is captured by the debugger and shown in the Log tab rather than a separate console window. No separate terminal appears. Click the Log tab at the bottom of x32dbg to see the program's printed output.

    Tried: Running strings on the binary from the command line hoping to find the flag without using a debugger at all.

    The strings command would reveal any plaintext content in the binary's data section, and in this level the flag IS stored as a plain string. However, strings output includes many unrelated library strings, making it easy to overlook the flag. The intended approach is dynamic analysis via the debugger, and in later levels (0x200, 0x300) the flag is not stored plaintext at all.

    Learn more

    Once the anti-debug check is bypassed, the binary executes the code path that displays the flag. The flag string is stored in the binary's data segment and printed to the debugger's log output rather than a console window. Check the x32dbg log tab to find the output.

    Embedded strings in executables are stored in the .data or .rdata section. Running strings on the executable sometimes reveals flag-like content, though challenges that decrypt the flag at runtime require dynamic analysis (running with a debugger) as the only viable path.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{d3bug_f0r_th3_Win_0x100_e7...}

Skipping the failure branch reveals the stored flag string immediately.

Key takeaway

Anti-debugging checks like IsDebuggerPresent work by reading a flag in the Windows Process Environment Block (PEB) that the OS sets when a debugger attaches. A register patch intercepts execution at the comparison point and overwrites the return value before the conditional jump evaluates it, bypassing the check without modifying the binary on disk. This exact technique is used by malware analysts and reverse engineers every day; real-world malware layers multiple anti-debug checks, timing checks, and checksum verifications to raise the cost of analysis.

Related reading

Want more picoCTF 2024 writeups?

Useful tools for Reverse Engineering

What to try next