Description
If you've solved WinAntiDbg0x100 you'll discover something new in this one. Debug the executable and find the flag.
Setup
Download WinAntiDbg0x200.zip (password: picoctf) and extract the executable. Get the files from the challenge page on CyLab Security Academy (formerly play.picoctf.org).
Load WinAntiDbg0x200.exe in Ghidra first to map the anti-debug checks, then open it in x32dbg (32-bit).
Note: the challenge says it requires admin privileges but you can work around this in the debugger.
unzip WinAntiDbg0x200.zipSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Map all three checks in Ghidra
ObservationThe description promises something new compared to the 0x100 level, which means more than one anti-debug check. Map every conditional branch guarding the flag path in Ghidra before opening a debugger.In Ghidra, open the binary and locate the interesting function (the one containing the flag path). You will see: one check near address ending in 16ED that requires the program to be running with debug privileges (admin), one check near 1826, and one check near 1830. Each tests a different condition; if any of them branch to the failure path, you get the oops message instead of the flag.What didn't work first
Tried: Skipping Ghidra entirely and jumping straight into x32dbg to step through the binary.
Without a map of all three checks you hit the first one, patch it, and never learn two more are coming. From there you step blindly and get the second or third wrong, because you did not know which register to watch or which way the jump goes.
Tried: Searching Ghidra for 'IsDebuggerPresent' and assuming that single import is the only check.
There are three checks and only one is the familiar IsDebuggerPresent call; the others are a custom detection routine and a privilege check. Find the API call alone and you still get the failure message after patching it.
Learn more
Multiple anti-debug checks are common in real malware: if the first check is bypassed, the second or third may still detect the debugger. Mapping all checks in Ghidra before setting breakpoints prevents being surprised mid-run when a second check fires unexpectedly.
The challenge says it requires admin privileges. That requirement is one of the anti-debug conditions (near 16ED): it checks whether the process has debug privileges and if not, prints the admin error. In the debugger you can skip this check by manipulating registers, avoiding the need to actually run as admin.
As with 0x100, ASLR means absolute addresses differ between runs, but the last four hex digits match between Ghidra and x32dbg. Use those last four digits to find each instruction in the debugger.
Step 2Set breakpoints at each check and manipulate registers
ObservationThe decompiler shows three TEST and conditional-jump pairs, at addresses ending 16EB, 1824, and 182E, each looking at EAX or EDX. Break on each TEST and set the register before the jump fires.Run the binary in x32dbg until user code. Set breakpoints at the last-four-digit addresses matching the three Ghidra locations (16EB, 1824, and 182E region). Then run and handle each in turn:- At the first breakpoint (16EB region, admin privileges check): a TEST EAX,EAX followed by JNZ is used. Set EAX to 1 so the conditional jump is taken and the error path is cleared.
- At the second breakpoint (1824 region, custom detection function): a TEST EDX,EDX followed by JNZ is used. Set EDX to 0 so the jump is NOT taken - taking this jump skips the third check entirely and leads to the failure message.
- At the third breakpoint (182E region, IsDebuggerPresent): a TEST EAX,EAX followed by JZ is used. Set EAX to 0 so the jump IS taken and execution continues to the flag decryption path.
What didn't work first
Tried: Setting EAX or EDX to 1 for every check without reading the specific jump mnemonic at each one.
The three checks jump in different directions. The first wants EAX set, the second wants EDX cleared. Set everything to 1 and the second check jumps straight to the failure path, skipping the third and never reaching the decryption code.
Tried: Setting a breakpoint on the CALL instruction for the detection function rather than on the TEST instruction after the return.
The value that matters is the called function's return, which lands in EAX or EDX only after the CALL comes back. Break on the CALL and you have to step in and out before the register holds anything. Break on the TEST after it instead.
Learn more
Each check uses a different register (EAX, EDX) and a different conditional jump direction. Reading the jump mnemonic is critical:
JZjumps when the zero flag is set (result was zero);JNZjumps when it is not zero. Getting the direction backwards causes the wrong branch to be taken even after you change the register.The method is the same as 0x100: pause at the key instruction and double-click the register in the Registers pane to change its value. The difference here is that you must track three separate conditions and handle them correctly in sequence during a single run.
Step 3Read the flag from the log
ObservationWith all three patches applied and no jump landing on the failure branch, execution reaches the decryption path. It emits the flag through OutputDebugString, which lands in the x32dbg Log tab.After all three checks are handled correctly, the program follows the success path and prints the flag to the x32dbg log window. Check the log tab for the line starting with picoCTF.What didn't work first
Tried: Looking for the flag in the x32dbg Console tab or in a popup window instead of the Log tab.
The flag goes out through OutputDebugString, which lands in the debugger's log pane. The Console tab is for typing debugger commands, not for program output. Switch to the Log tab.
Tried: Assuming the flag did not print because nothing appeared and restarting the whole run from scratch.
If one of the three patches was wrong, the binary takes the failure branch and logs its message instead of the flag. Scroll the Log tab before restarting: the message tells you which check fired, so you can fix that one breakpoint rather than the whole session.
Learn more
Once all anti-debug checks are bypassed, the binary executes the code that displays the flag. The log pane in x32dbg captures OutputDebugString calls, which is how the flag is printed in this binary. If the flag does not appear, check whether the third breakpoint caused the wrong jump direction.
The progression from 0x100 (one check, simple EAX patch) to 0x200 (three checks, multiple registers) mirrors how real malware layering works: each layer adds cost to the analyst's time. Level 0x300 goes further by using an infinite loop and packed code that requires binary patching with Ghidra.
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{0x200_debug_f0r_Win_...}
Passing all three anti-debug checks in sequence prints the flag to the debugger log.