0xmrerror@terminal:~/writeups

WinAntiDbg0x200 — Three Anti-Debug Checks

Three stacked anti-debug checks — PEB flag, timing and NtQueryInformationProcess — reversed live in GDB to recover the flag.

MediumpicoCTFReverse engineering · 12 min readanti-debugpebgdbtiming-checkwindowsreveng

A 32-bit Windows console app that stacks three anti-debug checks before revealing the flag: admin privileges, a custom child-process detector, and the classic IsDebuggerPresent(). Goal: pass all three and read the flag from the debugger log.

1 · Unzip and Identify

# the archive password is picoctf
unzip WinAntiDbg0x200.zip
file WinAntiDbg0x200.exe
# PE32 executable (console) Intel 80386, for MS Windows

2 · Map the Checks in Ghidra

Load the binary in Ghidra and land on main (FUN_004016e0). Three TEST + Jcc pairs gate the flag path. ASLR moves the absolute addresses between runs, but the last 4 hex digits always match — use them to find each site in the debugger.

# 1) Admin / debug-privilege check
004016eb  TEST EAX,EAX
004016ed  JNZ   LAB_00401714   ; continue only if EAX != 0

# 2) Custom child-process detector (FUN_004011d0)
00401824  TEST EDX,EDX
00401826  JNZ   LAB_00401832   ; jump to "Oops!" if EDX != 0

# 3) IsDebuggerPresent()
0040182e  TEST EAX,EAX
00401830  JZ    LAB_00401847   ; enter flag decryption if EAX == 0

The custom check (check #2) spawns a child copy of the binary with the parent's PID as an argument; the child tries to DebugActiveProcess the parent, and its exit code tells the parent whether a debugger is attached.

3 · Flip Each Condition in x32dbg

Run the binary in x32dbg until it reaches user code, then Search For → All User Modules → String References, and Follow in Disassembler on "Oops! The debugger was detected". Set breakpoints on the three TESTs (16eb, 1824, 182e) and edit the registers as they hit.

1) 16eb  TEST EAX,EAX  →  EAX = 1  # take the JNZ: pretend we run as admin
2) 1824  TEST EDX,EDX  →  EDX = 0  # don't take the JNZ: pass the custom check
3) 182e  TEST EAX,EAX  →  EAX = 0  # take the JZ: IsDebuggerPresent "not present"

Each check uses a different register (EAX vs EDX) and a different jump direction (JNZ vs JZ). Misreading a mnemonic sends execution down the failure path even with the right register value.

4 · Flag in the Log

The flag is written with OutputDebugStringW, so it never reaches the console. When all three checks pass, it lands in x32dbg's Log tab.

# Log tab (OutputDebugString capture)
### Good job! Here's your flag:
### ~~~ picoCTF{0x200_debug_f0r_Win_<HASH>}
# the trailing hex varies per build — debug a copy and read yours

5 · What Made It Work

All three checks are the same shape, and that is what makes this one tractable. Each is a TEST immediately followed by a jump, and each only cares about one register at one instruction. Finding the three sites is the whole job, and matching on the last four hex digits is enough to survive ASLR without needing to re-derive absolute addresses.