Description
This challenge is a little bit invasive. It will try to fight your debugger. With that in mind, debug the binary and get the flag.
Hint: there is an infinite loop that constantly checks for the debugger. Get past the infinite loop. Maybe patch the binary to jump to the appropriate location. If you've done everything correctly the flag should pop up after 5 seconds.
Setup
Download WinAntiDbg0x300.zip (password: picoctf) and extract the executable.
Download DebugView from Microsoft Sysinternals (Dbgview.exe). This lets you see OutputDebugString output without attaching a debugger.
This challenge is known to trigger antivirus. Running it inside a Windows 11 evaluation VM from Microsoft is recommended.
The binary is packed with UPX. Run upx -d on it before loading into Ghidra.
wget https://artifacts.picoctf.net/c_titan/89/WinAntiDbg0x300.zip && \
unzip WinAntiDbg0x300.zipupx -d WinAntiDbg0x300.exeSolution
Walk me through it- Step 1Unpack with UPXThe binary is packed with UPX. Running strings on the packed file shows UPX! markers and section names like UPX0 and UPX1. Run upx -d WinAntiDbg0x300.exe to unpack it in place, giving you the real code for Ghidra to analyze. Load the PDB file supplied with the challenge during Ghidra import to get function names and variable names for free.
Learn more
UPX (Ultimate Packer for eXecutables) compresses a binary and prepends a decompression stub. At runtime the stub decompresses the original code into memory and jumps to it. Malware authors use UPX (or custom packers) to shrink payloads and hinder static analysis. The telltale
UPX!magic bytes visible viastringsmake identification trivial.upx -dreverses the packing in place.Importing the PDB (Program Database) file during Ghidra analysis provides all the original symbol names: function names, variable names, and type information. This makes reverse engineering dramatically easier. To load a PDB in Ghidra: import the executable, then before analyzing, go to File > Add to Program and browse to the PDB file. Ghidra will warn about an inexact match; accept it.
- Step 2Understand why you cannot use a debuggerIn Ghidra, examine WinMain and the challenge_create_thread function. The manage_child_process function creates a mutex; if the mutex already exists (meaning a child has been spawned), the child attempts to attach a debugger to the parent and terminate it. The main loop runs forever and the decrypt_flag code is unreachable because an unconditional JMP bypasses it. DebugView captures OutputDebugString calls, letting you see any debug output the program emits without attaching a debugger.
Learn more
DebugView (Dbgview.exe from Microsoft Sysinternals) is a passive monitor for OutputDebugString output. It captures debug messages printed by any process on the system without attaching a debugger, so the anti-debug child-process logic never fires. Run DebugView (32-bit mode for 32-bit binaries) and then run the program normally as administrator.
The every-5-second output in DebugView ("no debugger was present, exiting successfully") confirms the program is running and its debug output is reachable, but the flag is never printed because the loop never exits to the decrypt_flag call. The solution is to patch the binary so the unconditional JMP is replaced with NOPs, letting execution fall through to the decrypt_flag path.
- Step 3Patch the binary in GhidraIn Ghidra's assembly view, locate the unconditional JMP at the bottom of the loop in challenge_create_thread. Select those bytes, go to Patch Instruction (or use the byte-level editor), and replace them with NOP instructions. Ghidra's keyboard shortcut for patching to NOPs is Ctrl+Shift+G. Then export the patched binary: File > Export Program > Windows PE. Run the patched binary as administrator with DebugView open and wait up to 5 seconds for the flag to appear.
Learn more
A NOP (No Operation, opcode
90) is an instruction that does nothing. Replacing a JMP with five NOPs (a JMP instruction is typically 2 to 5 bytes; fill with enough NOPs to cover the original instruction size) causes execution to continue to the next instruction rather than jumping back to the loop head.Ghidra's Export Program function writes the patched binary with your in-place edits preserved. The output is a valid PE executable that you can run on Windows. This workflow of: unpack, analyze, patch in Ghidra, export, run with DebugView, is the canonical approach to binaries that actively fight debuggers.
The challenge_create_thread function also calls compute_hashes functions that modify a global variable. All of those must complete before the flag is decrypted, which is why the flag appears after a delay (about 5 seconds) rather than immediately after the patched JMP is bypassed.
- Step 4Read the flag from DebugViewAfter running the patched binary as administrator with DebugView open, wait up to 5 seconds. The program prints 'you got the flag' and then the flag string to the DebugView output pane. Copy picoCTF{...} from there.
Learn more
The flag is printed via
OutputDebugString, which writes to the Windows debug message stream rather than stdout. Only DebugView (or a proper debugger, which the binary fights) can capture this output. That is why the challenge hints specifically mention DebugView.Running as administrator is required because the binary checks for admin privileges as one of its conditions. The patched binary still runs through those checks but now reaches the decrypt_flag path regardless of the debugger presence, so the flag is printed after all the hash computations complete.
Flag
picoCTF{d3bug_f0r_th3_Win_0x300_c...}
Patching the infinite-loop JMP to NOPs and running the patched binary with DebugView reveals the flag after a short delay.