Skip to main content

WinAntiDbg0x300 picoCTF 2024 Solution

A Windows reverse engineering challenge featuring advanced techniques designed to detect and obstruct debuggers.

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

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.

Windows & DebugView

Download WinAntiDbg0x300.zip (password: picoctf) and extract the executable. Get the files from the challenge page on CyLab Security Academy (formerly play.picoctf.org).

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.

bash
unzip WinAntiDbg0x300.zip
bash
upx -d WinAntiDbg0x300.exe

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
This is the hardest challenge in the WinAntiDbg series. Instead of using a debugger (which the binary actively kills), the solution uses DebugView to passively observe the program's output and Ghidra to patch the binary so the flag branch becomes reachable.
  1. Step 1Unpack with UPX
    Observation
    strings on the executable shows UPX! markers and sections named UPX0 and UPX1. It is packed, so unpack it before Ghidra can see the real code.
    The 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.
    What didn't work first

    Tried: Loading the packed binary directly into Ghidra and trying to analyze the decompiled output.

    Ghidra sees the UPX stub, not the program. The decompilation looks like a tiny routine copying memory and jumping to a computed address, with none of the real functions in sight. Unpack first.

    Tried: Skipping the PDB import and trying to rename functions manually from context clues.

    Without the PDB, every function is FUN_00401234 and every global is DAT_00403000, and the anti-debug logic disappears among dozens of unlabeled routines. The challenge ships the PDB precisely so you can import it and get the original names back.

    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 via strings make identification trivial. upx -d reverses 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.

  2. Step 2Understand why you cannot use a debugger
    Observation
    The description warns that the binary fights back and loops forever checking for a debugger. Understand that mechanism before trying to bypass it, and observe with DebugView rather than attaching.
    In 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.
    What didn't work first

    Tried: Attaching x64dbg or OllyDbg to the unpacked binary and trying to step through the anti-debug checks.

    The binary spawns a child that detects the debugger and kills the parent within seconds, so every attempt to pause or step ends the process before you get anywhere. The answer is not to attach at all: DebugView captures the output passively.

    Tried: Running the binary in a terminal and looking for flag output in stdout or a console window.

    OutputDebugString writes to the Windows debug message stream, which never reaches a console or a file, so redirecting stdout captures nothing. DebugView reads that stream, and so would a kernel debugger, which this binary fights.

    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.

  3. Step 3Patch the binary in Ghidra
    Observation
    In the assembly, challenge_create_thread holds an unconditional JMP that loops forever, so decrypt_flag is never reached. Replace that jump with NOPs and execution falls through to it.
    In 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 binds Patch Instruction to Ctrl+Shift+G; type NOP in the in-place editor once for each byte of the original JMP. 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.
    What didn't work first

    Tried: Patching the IsDebuggerPresent check or the child-process mutex logic instead of the unconditional JMP in the main loop.

    Neutralizing the anti-debug checks does not help because the loop never exits regardless of whether a debugger is detected - the JMP is unconditional, not conditional. You must patch the JMP that keeps the loop running, not the IsDebuggerPresent comparisons above it.

    Tried: Exporting the patched binary with File > Export Program using the default 'Original File' format instead of 'Windows PE'.

    The Original File export writes back the bytes from the original structure and may leave your patches out. Exporting as Windows PE rebuilds a valid executable with the patched bytes applied, so the NOPs are actually there.

    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.

  4. Step 4Read the flag from DebugView
    Observation
    The flag goes out through OutputDebugString rather than stdout, and DebugView is already capturing that stream. Run the patched binary with it open.
    After 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.

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{Wind0ws_antid3bg_0x300_...}

Patching the infinite-loop JMP to NOPs and running the patched binary with DebugView reveals the flag after a short delay.

Key takeaway

Self-monitoring child processes, IsDebuggerPresent checks, and loops that block the real branch make dynamic analysis painful rather than impossible. When you cannot attach, static analysis plus a binary patch sidesteps all of it by rewriting the control flow before the program runs. The same move works on license checks, sandbox evasion, and any protection resting on runtime behavior rather than cryptographic hardness.

Related reading

Useful tools for Reverse Engineering

Where to go next