Skip to main content

WinAntiDbg0x100 picoCTF 2024 Solution

Reverse engineer a Windows binary that uses anti-debugging checks to hide the flag, then work around them.

Published: April 3, 2024Updated: August 25, 2026

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 1Load in Ghidra to understand the check
    Observation
    The description names IsDebuggerPresent outright. Ghidra will show the call site and the branch around it before you open a debugger at all.
    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 ship together but target different architectures. Open a 32-bit binary in the 64-bit one and it loads wrong, with register names like RAX instead of EAX. The prefix tells you which you are in: E is 32-bit, R is 64-bit.

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

    The x32dbg disassembly shows raw instructions with no function labels for internal code, so finding the check by eye is slow. Ghidra's symbol tree and decompiler surface the call in seconds. Start there, then switch over to 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.

    Windows ASLR relocates the whole image by a multiple of 64 KB, so the absolute address of an instruction differs between Ghidra's default image base and the address x32dbg shows at runtime, but the low 16 bits (the last four hex digits) are unchanged by the shift. 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 2Set a breakpoint at the TEST instruction and zero EAX
    Observation
    IsDebuggerPresent returns into EAX, and the TEST right after it picks the branch. Zero EAX before that TEST runs and the program concludes nothing is attached.
    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 you reach the jump, TEST has already run and the zero flag reflects the original value. Changing EAX now does nothing, because TEST consumed it. Break before the TEST instead, while the flags are still uncommitted.

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

    Flipping the zero flag at the jump works, but it is fragile: x32dbg can re-evaluate flags as you step, and you have to know which flag matters. Patching EAX before the TEST changes the source value instead of 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 3Read the flag from the log
    Observation
    A process launched inside x32dbg has its output captured by the debugger rather than sent to a console, so the flag appears in the 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.

    A program launched from inside x32dbg has its output captured by the debugger, so no separate console appears. The Log tab at the bottom is where it goes.

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

    strings does show plaintext in the data section, and at this level the flag is stored that way. But its output is full of unrelated library strings, so the flag is easy to miss. The intended path is dynamic analysis, and in the later levels the flag is not 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

A check like IsDebuggerPresent just reads a flag in the Process Environment Block that Windows sets when a debugger attaches. Patching the register at the comparison overwrites the answer before the jump evaluates it, bypassing the check without touching the file on disk. Analysts do this daily, and real malware stacks several anti-debug checks, timing checks, and checksums to make it expensive.

Related reading

Useful tools for Reverse Engineering

Where to go next