The Unknown Binary
Investigate a full-size, ~1 MB unknown binary end to end: static triage, import capability mapping, a real anti-debug branch, stack-frame and calling-convention reading, and a hand-decoded second-stage flag hidden the way lab-01 taught you to look. The disassembly is genuine — real x86-64 machine code, independently assembled and disassembled, not hand-written pseudocode — so its hex addresses, hex immediates, and symbol-annotated branches read exactly like real objdump/Ghidra output. Nothing here is visible on the first screen — you'll need the hex viewer's Find and Go-to-offset tools, not eyeballing. Every skill from the course applies.
Investigate
Disassembly
_start:
00: push rbp
01: mov rbp, rsp
04: sub rsp, 0x20
08: call 0x56 <IsDebuggerPresent>
0d: test rax, rax
10: jne 0x4a <detected>
16: mov rsi, 0x7b
1d: mov rcx, 0x17
loop_decode:
24: mov al, byte ptr [rsi]
26: xor al, 0x5a
28: mov byte ptr [rsi], al
2a: inc rsi
2d: dec rcx
30: jne 0x24 <loop_decode>
32: lea rcx, [rip + 0x1f] ; -> host_arg
39: mov rdx, 0x1bb
40: call 0x57 <InternetConnectW>
45: jmp 0x51 <cleanup>
detected:
4a: mov rax, 0
cleanup:
51: mov rsp, rbp
54: pop rbp
55: ret
IsDebuggerPresent:
56: ret
InternetConnectW:
57: ret Hints
Hint 1
This file is over a megabyte. Don't scroll — parse the headers first, then use the hex viewer's Find (F) to jump straight to what you're looking for. That's what a real analyst does with strings/grep on a sample this size, too.
Hint 2
The four IOC strings (domain, mutex, registry path, dropped-file path) are buried far apart from each other and from the headers — none of them are anywhere near the start of the file.
Hint 3
The imports cluster into four capabilities: network, injection, persistence, and anti-analysis.
Hint 4
The routine has a normal function shape: prologue (push rbp / mov rbp,rsp / sub rsp,X) at the top, epilogue (mov rsp,rbp / pop rbp / ret) at the bottom — everything in between is the actual work.
Hint 5
This is a real disassembler's output, so immediates print in hex, not decimal — `0x17` is 23, `0x1bb` is 443. Converting hex to decimal by hand is the same skill lesson A6 covers.
Hint 6
Right after the prologue, the very first real work the function does is call IsDebuggerPresent and branch on the result. Trace both paths: what happens if it detects a debugger, and what happens if it doesn't?
Hint 7
mov rcx, ... then mov rdx, ... immediately before a call is the x64 calling convention in action — the first two integer/pointer arguments always land in rcx and rdx, in that order. The `lea rcx, [rip+0x1f] ; -> host_arg` line loads a local placeholder pointer for this demo — the live C2 domain string itself is a separate IOC, buried in `.data` like the others.
Hint 8
The disassembly's starting address for [rsi], 0x7B, isn't inside any section — it's before the PE header even begins, in the first few hundred bytes of the file. Where else in the file did an earlier lab teach you to look?
Hint 9
cmp/test isn't the only way to check a value: xor-ing a byte with the same key twice returns the original byte. That's exactly what encode and decode are here — the same operation.
Hint 10
Network + injection + persistence + anti-debugging + a self-decoding second flag together describe a fully-featured implant, not a single-purpose tool.
Submit Findings
Solution
The Unknown Binary — Verdict
The full profile, assembled with the investigation loop, from a ~1 MB sample where none of this sits near the start of the file:
Strings (.data, found via Find — not visible on the first screen)
- C2 domain:
deep-space-c2.net— file offset0x6E240 - Mutex:
Global\capstone_mutex_7— file offset0x97A50 - Persistence:
Software\Microsoft\Windows\CurrentVersion\Run— file offset0xC3970 - Dropped file:
%APPDATA%\winlogon.exe— file offset0xED180
Imports (capabilities)
- Network:
InternetOpenW/InternetConnectW/HttpSendRequestW(HTTP C2) - Injection:
VirtualAllocEx/CreateRemoteThread(run code in other processes) - Persistence:
RegSetValueExW(Run key) - Anti-analysis:
IsDebuggerPresent(checks for a debugger before acting)
The full routine
The disassembly is real, genuine x86-64 machine code — not a fragment, not pseudocode: a standard prologue (push rbp / mov rbp,rsp / sub rsp,0x20 — 32 bytes of locals) at address 0x0, then call IsDebuggerPresent / test rax,rax / jne detected at 0x8. If a debugger is attached, execution jumps straight to cleanup (0x51) and the function returns having done nothing observable — a real, working anti-debug gate, not just an unused import. If no debugger is detected, it falls into the XOR decode loop (loop_decode: at 0x24), then loads rcx with a pointer (lea rcx, [rip+0x1f], at 0x32) and rdx with 0x1bb (443 decimal) before call InternetConnectW at 0x40 — textbook x64 calling convention, first two arguments in rcx then rdx — before joining the same cleanup epilogue. The rcx pointer here is a local placeholder used to demonstrate the calling convention; the live C2 domain is the separate string found in .data.
The hidden second flag
The loop reads 23 (0x17) bytes starting at rsi = 0x7B, XOR-ing each with 0x5A, and writing it back in place. 0x7B sits in the DOS stub — the same hiding spot lab-01 taught, except this time the payload isn't plaintext, it's encoded, so a plain ASCII scan of the hex dump won't reveal it. Reading the 23 raw bytes at file offset 0x7B and XOR-ing each with 0x5A by hand decodes to:
WEAPON{full_kill_chain}
Narrative: drops winlogon.exe (masquerading as the legitimate process), writes a Run key, guards itself with a mutex, checks for a debugger and goes dormant if one is found, phones home over HTTP when it isn't, and carries a second-stage payload hidden with a technique straight out of lesson one of the course.
Verdict: a remote access trojan (RAT).
IOCs: deep-space-c2.net, Global\capstone_mutex_7, the Run-key value, %APPDATA%\winlogon.exe, the IsDebuggerPresent check with its real conditional branch, and the sample hash.
Prerequisites
- All prior modules (start through anti-re)
- Full investigation workflow: strings → imports → PE structure → behavior → verdict
- DOS stub steganography (lab-01)
- Stack frame layout (push/mov/sub rsp — e1-stack-frames)
- x64 calling convention (rcx/rdx argument registers)
- Bitwise XOR encoding/decoding (a6-shift-rotate)
- Instruction and branch tracing (mov/xor/test/jne/call/ret)
- Reading hex immediates in real disassembler output
- YARA rule writing
- IOC extraction and reporting
- MITRE ATT&CK mapping
Methodology
Investigation Methodology — The RE Loop
This sample is roughly 1 MB — around 65,000 hex-viewer rows. That's the point: nothing here rewards scrolling. Every real reverse engineer's first move on an unfamiliar sample this size is the same one you have available here — parse the headers to know what to look for, then jump straight to it with strings/grep (or, in the built-in viewer, Find — press F) instead of reading the file top to bottom.
One more thing before you start: the disassembly below is real — it's genuine x86-64 machine code, assembled with a real toolchain and disassembled with a real disassembler (Capstone). The bytes at the entry point of .text are exactly what's shown; if you downloaded this file and ran objdump -d or loaded it in Ghidra yourself, you'd get the same instructions at the same addresses. That's why the addresses below are real hex file offsets (not a friendly 0, 1, 2, ...) and why the immediates print in hex the way a real disassembler shows them.
Phase 1: Static Triage (5 min)
- File info: Hash (MD5/SHA256), size (~1 MB — large enough that manual scanning isn't viable), type, architecture
- PE headers: Sections, entry point, timestamps, characteristics — the header only occupies the first ~1 KB, the rest is section content
- Strings:
strings -a -el— extract ASCII + UTF-16 from every section, and the DOS stub (lab-01's lesson:stringswith default settings skips it, and it's not a mapped section, so ASCII scanners often miss what's hidden there). On a real sample you'd pipe this throughgrepfor anything domain/registry/path-shaped; here, the hex viewer's Find does the same job against known substrings once you've decided what to search for. - Imports/Exports: Capability fingerprint
- Resources: Icons, manifests, embedded files
Phase 2: Capability Mapping (10 min)
Cluster imports into ATT&CK techniques:
| Cluster | APIs | Technique |
|---|---|---|
| Network | InternetOpenW, InternetConnectW, HttpSendRequestW |
T1071.001 |
| Injection | VirtualAllocEx, CreateRemoteThread |
T1055 |
| Persistence | RegSetValueExW, Run key path |
T1547.001 |
| Defense Evasion | IsDebuggerPresent |
T1622 |
A sample with all four clusters present is a fully-featured implant, not a proof-of-concept.
Phase 3: The Disassembly — Real Machine Code, Real Disassembler (20 min)
This is the exact output of disassembling the binary's own entry point — not a hand-written illustration:
_start:
0: push rbp
1: mov rbp, rsp
4: sub rsp, 0x20 ; reserve 32 bytes of locals
8: call 0x56 <IsDebuggerPresent>
d: test rax, rax
10: jne 0x4a <detected> ; debugger attached -> bail
16: mov rsi, 0x7b
1d: mov rcx, 0x17 ; 0x17 = 23 (decimal)
loop_decode:
24: mov al, byte ptr [rsi]
26: xor al, 0x5a
28: mov byte ptr [rsi], al
2a: inc rsi
2d: dec rcx
30: jne 0x24 <loop_decode>
32: lea rcx, [rip + 0x1f] ; -> host_arg
39: mov rdx, 0x1bb ; 0x1bb = 443 (decimal)
40: call 0x57 <InternetConnectW>
45: jmp 0x51 <cleanup>
detected:
4a: mov rax, 0
cleanup:
51: mov rsp, rbp
54: pop rbp
55: ret
IsDebuggerPresent:
56: ret
InternetConnectW:
57: ret
- The addresses are real file-relative offsets into the routine, not a simplified index — real instructions have variable byte lengths (
push rbpis 1 byte,mov rsi, 0x7bis 7 bytes), so real addresses aren't evenly spaced. That's a genuine tell you'll see in every real disassembly. - Immediates print in hex:
0x17is 23,0x1bbis 443. Real disassemblers don't guess which base is friendliest for you — converting is on the analyst. - Prologue/epilogue:
push rbp/mov rbp, rsp/sub rsp, 0x20opens a stack frame with 32 bytes of local space;mov rsp, rbp/pop rbp/rettears it back down. Same shape as lesson E1's stack frame diagrams — this is what every non-trivial function looks like underneath. - The anti-debug branch is real, not decorative:
call IsDebuggerPresent→test rax, rax→jne detected. If a debugger is attached, execution jumps straight past the network call and the decode-then-beacon logic tocleanup— the malware simply does nothing observable. This is exactly why dynamic analysis of this sample under a naively-attached debugger would look inert. - The decode loop is self-inverse:
(x XOR k) XOR k == x. Whatever bytes live at[rsi]before this loop runs are the encoded form; after it runs, they're plaintext. You can do the same transform by hand, byte by byte, without a debugger. - The calling convention is on display:
lea rcx, [rip+0x1f]thenmov rdx, 0x1bbimmediately beforecall InternetConnectW— the x64 ABI passes the first two arguments inrcxandrdx. This is the same convention lesson W5 covers for every WinAPI call. The pointerrcxloads here (host_arg) is a small local placeholder used purely to demonstrate the calling convention — the live C2 domain string is a separate IOC, buried in.data(Phase 4), not this one. - Address
0x7Bis not a section RVA — cross-reference it against the DOS header layout from lesson F1 and lab-01:e_lfanew(offset0x3C) points to the real PE signature, and everything between0x40ande_lfanewis DOS stub padding.0x7Bfalls in that range. Unlike the other IOCs, this one is near the front of the file — use Go to offset (G) to jump straight to0x7B. - Read the 23 raw bytes at file offset
0x7Bfrom the hex viewer, XOR each one with0x5Aby hand (or in a scratch script), and you have the second flag.
Phase 4: IOC Extraction (10 min)
Each of these strings is planted deep inside .data, spread apart from each other and from anything else recognizable — search for each one individually with Find:
| Type | Value | Context |
|---|---|---|
| Network | deep-space-c2.net:443/beacon.php |
C2 endpoint |
| Host | Global\capstone_mutex_7 |
Mutex |
| Persistence | HKCU\Software\Microsoft\Windows\CurrentVersion\Run |
Run key |
| File | %APPDATA%\winlogon.exe |
Dropped payload |
| Process | winlogon.exe (masqueraded) |
Process name |
| Anti-Analysis | IsDebuggerPresent check with a real conditional branch |
Defense evasion |
Phase 5: Attribution & Reporting (10 min)
- Malware Family: Compare IOCs to known families (VT, Malpedia)
- ATT&CK Mapping: Tag each capability, including the anti-debug check and the DOS-stub steganography technique itself (T1027, obfuscated files/information)
- Verdict: Classify (RAT, loader, ransomware, wiper, etc.)
- Report: Executive summary + technical details + IOCs + mitigations
Common Pitfalls
- Trying to scroll through a ~1 MB file byte by byte instead of parsing the headers first and using Find/Go-to-offset
- Stopping at strings — missing dynamically resolved imports
- Not checking for packers — UPX, custom stubs hide imports/code
- Missing the DOS stub entirely — most string scanners default to mapped sections only
- Treating `[rsi]` as a section-relative RVA instead of the flat file offset it actually is here
- Misreading hex immediates as decimal — 0x17 is 23, not seventeen; 0x1bb is 443, not one-eighty-something
- Applying the XOR key to the wrong byte range (it's exactly 23 bytes, not the whole stub)
- Assuming the anti-debug check always falls through — trace the `detected` branch too, it's the whole reason dynamic analysis of this sample can look inert under a debugger
- Mixing up which register holds which argument — rcx is always argument 1, rdx is always argument 2, in x64
- Mistaking the routine's local `host_arg` placeholder for the actual C2 domain — the live IOC is a separate string in `.data`
- Not verifying dropped file — hash it, analyze it separately
- Rushing to verdict without the full capability picture (network + injection + persistence + anti-debug + hidden payload)
Tools
- Ghidra (full suite: disasm, decompiler, graphs, scripts)
- PE-bear / CFF Explorer (header analysis)
- FLOSS (obfuscated strings)
- YARA (rule writing, scanning)
- x64dbg (dynamic verification)
- objdump / Capstone (to verify the disassembly independently)
- A scratch Python/CyberChef XOR script (for the hand-decode step)
- VirusTotal / MalwareBazaar (intel pivot)
- MITRE ATT&CK Navigator (mapping)
- CAPA (Mandiant) — automated capability detection
References
- All course modules
- Lab 01: Hidden String Hunt — DOS stub steganography
- Lesson E1: Stack Frames — prologue/epilogue shape
- Lesson W5: WinAPI & Native — x64 calling convention
- Lesson A6: Bit Tricks — XOR, self-inverse operations, hex/decimal conversion
- MITRE ATT&CK Enterprise Matrix
- Mandiant CAPA documentation
- Malware Analysis: Investigation Loop methodology
- SANS FOR610: Reverse Engineering Malware
Extensions & Challenges
- Write a comprehensive YARA rule family for this malware, including a rule that matches the XOR-encoded second stage
- Analyze the dropped `winlogon.exe` — is it the same or different?
- Map to MITRE ATT&CK and create a Navigator layer
- Research: How would you detect this in memory (Volatility)?
- Build a Sigma rule for the persistence registry write
- Write a script that automatically finds and XOR-brute-forces short encoded regions in a DOS stub
- Patch the binary (or the disassembly logic) so it runs its full payload even with a debugger attached — what's the minimal change to the `jne detected` branch?
- Simulate: How would this behave in a sandbox? Would `IsDebuggerPresent` returning true change its behavior, and how would you patch around that?