HTB: Debugme Challenge

Debugme - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameDebugme
CategoryReversing
DifficultyMedium
Authord3vn0mi

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.

Terminal window
# 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 password
unzip -P hackthebox debugme.zip

This 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.

Terminal window
# Enumerate sections and symbols
objdump -h debugme.exe
nm 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, l

Disassembling 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.

Terminal window
objdump -d --start-address=0x401620 --stop-address=0x401793 -M intel debugme.exe

Step 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, esp

That’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 place
with open("debugme.exe", "rb") as f:
data = bytearray(f.read())
start, end = 0x220, 0x393 # file offsets corresponding to 0x401620-0x401793
for i in range(start, end):
data[i] ^= 0x5c
with open("debugme_decoded.exe", "wb") as f:
f.write(data)
Terminal window
# Re-disassemble the decoded region
objdump -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 flag
mov eax, fs:[0x30] ; eax = PEB
movzx eax, byte [eax+2] ; PEB->BeingDebugged
test eax, eax
jnz bail_out
; Gate 2 - PEB NtGlobalFlag (set by debuggers via heap flags)
mov eax, fs:[0x30]
mov eax, [eax+0x68] ; PEB->NtGlobalFlag
cmp eax, 0x70 ; debugger heap flags mask
jnz bail_out
; Gate 3 - rdtsc timing check
rdtsc ; timestamp before
; ... junk add/sub instructions as filler/decoy ...
rdtsc ; timestamp after
sub eax, <saved_low>
cmp eax, 0x3e8 ; ~1000 cycles threshold — trips under a debugger's single-stepping
jg bail_out

Passing 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.

Terminal window
# Recover flag bytes via the constants used in the flag-build routine
python3 - <<'EOF'
M = 1 << 32
B = 0x6a253e2d
vals = [
B,
(B - 0x560c29fc) % M,
(B - 0x49fd1bf4) % M,
(B - 0x2b1124ff) % M,
]
# ... reconstruct flag bytes from the recovered dword sequence
EOF
Terminal window
# Submit the recovered flag via the HTB API
python3 - <<'EOF'
from htb_api import HTBClient
c = HTBClient()
c.submit_challenge(72, "HTB{REDACTED}")
EOF

HTB responded with “Congratulations!” — flag accepted.

Tools Used

ToolPurpose
unzipRecover the real binary from the password-protected archive
objdumpDisassemble sections, verify the XOR-decoded instruction stream
nmEnumerate symbols and locate the obfuscated function island
file / md5sumIdentify binary format and verify artifact integrity
Python 3XOR 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, PEB NtGlobalFlag, and rdtsc-based timing checks are cheap, well-known, and easy to recognize by their fs-segment PEB accesses and paired rdtsc instructions — 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}