HTB: Debugme Challenge
Debugme - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Challenge Name | Debugme |
| Category | Reversing |
| Difficulty | Medium |
| Author | d3vn0mi |
Description
A developer sent in a Windows binary they claimed was “super secure and really hard to debug.” The task: debug it and recover the flag. The binary bundles multiple layers of anti-analysis — a password-protected archive, an obfuscated code island inside an otherwise ordinary MinGW executable, and a trio of runtime anti-debugging checks guarding the flag-generation routine.
Solution Overview
The challenge archive unzips to a Windows PE32 executable built with MinGW. Static disassembly of the binary looks completely normal everywhere except a small ~370-byte cluster of functions with junk names (main, G, d, n, lol, yaya, lala, etc.) — this island disassembles as garbage because it’s XOR-obfuscated. A single-byte XOR key recovered from the known x86 function prologue (push ebp; mov ebp, esp) decodes the region into valid, readable x86. The decoded routine runs three sequential anti-debugging gates — a PEB BeingDebugged check, a PEB NtGlobalFlag check, and an rdtsc-based timing check — before it will construct and print the flag.
Key Steps
Step 1: Recover the real binary from the password-protected archive
The staged artifact debugme.exe initially appeared as a 0-byte file. The source archive, however, was intact — just password protected.
# Confirm the archive actually holds content (0-byte staged file was a red herring)unzip -l debugme.zip# Archive: debugme.zip# Length Date Time Name# --------- ---------- ----- ----# 81377 ... debugme.exe
# Recover the real PE32 with the known CTF-standard passwordunzip -P hackthebox debugme.zipThis yielded a genuine MinGW-compiled Windows PE32 binary.
Step 2: Locate the obfuscated code island
Standard static analysis (sections, symbols, DWARF) mostly looked normal — 14 sections, DWARF debug info present but attributable only to libgcc, with no useful user-source line info.
# Enumerate sections and symbolsobjdump -h debugme.exenm debugme.exe | grep -E " T "The symbol table exposed a cluster of junk-named functions packed together at 0x401620–0x401793:
main, G, d, n, lol, yaya, lala, dsfghtgf, ertrwe, kjnjk, qsacb, tftrtftc, sup, lDisassembling everything else in the binary (printf, libgcc routines) produced clean, sane x86. Disassembling this island produced nonsense — a strong signal of a custom obfuscation layer wrapping the real logic.
objdump -d --start-address=0x401620 --stop-address=0x401793 -M intel debugme.exeStep 3: Break the XOR layer
The obfuscated blob was padded with long runs of the byte 0x5c. Padding bytes are usually the obfuscation key itself (or reveal it), so a single-byte XOR against 0x5c was tried against the start of main:
# main began with the bytes: 09 d5 b9# XOR each byte against the padding value 0x5c:bytes(b ^ 0x5c for b in bytes.fromhex("09d5b9"))# -> 55 89 e5 == push ebp; mov ebp, espThat’s a textbook x86 function prologue, confirming the key. Applying single-byte XOR 0x5c across the whole island decoded it into valid, disassemblable x86 instructions.
# Decode the obfuscated island in placewith open("debugme.exe", "rb") as f: data = bytearray(f.read())
start, end = 0x220, 0x393 # file offsets corresponding to 0x401620-0x401793for i in range(start, end): data[i] ^= 0x5c
with open("debugme_decoded.exe", "wb") as f: f.write(data)# Re-disassemble the decoded regionobjdump -D -b binary -m i386 -M intel --adjust-vma=0x401620 \ <(dd if=debugme_decoded.exe bs=1 skip=0x220 count=0x173 2>/dev/null)Step 4: Analyze the anti-debugging gates
The decoded routine at debugme.exe:0x401620 runs three checks in sequence, each of which — if tripped — prints "Looks like your doing something naughty" and bails out before the flag is ever built:
; Gate 1 - PEB BeingDebugged flagmov eax, fs:[0x30] ; eax = PEBmovzx eax, byte [eax+2] ; PEB->BeingDebuggedtest eax, eaxjnz bail_out
; Gate 2 - PEB NtGlobalFlag (set by debuggers via heap flags)mov eax, fs:[0x30]mov eax, [eax+0x68] ; PEB->NtGlobalFlagcmp eax, 0x70 ; debugger heap flags maskjnz bail_out
; Gate 3 - rdtsc timing checkrdtsc ; timestamp before; ... junk add/sub instructions as filler/decoy ...rdtsc ; timestamp aftersub eax, <saved_low>cmp eax, 0x3e8 ; ~1000 cycles threshold — trips under a debugger's single-steppingjg bail_outPassing all three gates prints two taunting strings and then falls through into the flag-construction logic, which builds the flag on the stack and prints it.
Step 5: Defeat the checks and extract the flag
Rather than fighting a live debugger against the PEB/NtGlobalFlag/timing triad, the checks were satisfied statically by reasoning about the flag-construction routine directly from the decoded disassembly and the constants it referenced in .data — reconstructing the transformed flag bytes and reversing the arithmetic used to build them, then submitting the recovered string.
# Recover flag bytes via the constants used in the flag-build routinepython3 - <<'EOF'M = 1 << 32B = 0x6a253e2dvals = [ B, (B - 0x560c29fc) % M, (B - 0x49fd1bf4) % M, (B - 0x2b1124ff) % M,]# ... reconstruct flag bytes from the recovered dword sequenceEOF# Submit the recovered flag via the HTB APIpython3 - <<'EOF'from htb_api import HTBClientc = HTBClient()c.submit_challenge(72, "HTB{REDACTED}")EOFHTB responded with “Congratulations!” — flag accepted.
Tools Used
| Tool | Purpose |
|---|---|
unzip | Recover the real binary from the password-protected archive |
objdump | Disassemble sections, verify the XOR-decoded instruction stream |
nm | Enumerate symbols and locate the obfuscated function island |
file / md5sum | Identify binary format and verify artifact integrity |
| Python 3 | XOR key derivation, blob decoding, flag byte reconstruction |
HTB API (htb_api.py) | Programmatic flag submission |
Key Learnings
- 0-byte staged artifacts aren’t always broken — check the original archive/zip listing before assuming the challenge file is corrupted; a password-protected zip can legitimately stage as empty until extracted.
- Padding bytes in an obfuscated blob are a free XOR-key oracle — a repeated byte used as filler around a function is very likely the same byte used to encode it; testing it against a known instruction prologue (
push ebp; mov ebp, esp) confirms the key almost instantly. - Symbol tables leak structure even when names are junked — a tight cluster of oddly-named functions sitting between normal, cleanly-disassembling library code is a strong signal of a hand-obfuscated “island,” worth isolating and attacking independently of the rest of the binary.
- Classic Windows anti-debug triad: PEB
BeingDebugged, PEBNtGlobalFlag, andrdtsc-based timing checks are cheap, well-known, and easy to recognize by their fs-segment PEB accesses and pairedrdtscinstructions — recognizing the pattern immediately tells you what a “hard to debug” claim actually means in practice. - Static reconstruction can beat live evasion — when checks are specifically designed to detect a debugger, reversing the flag-construction arithmetic directly from decoded disassembly avoids the anti-debug gates entirely rather than trying to defeat them live.
Flag
HTB{REDACTED}