LAB-CAPSTONEExtreme

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.

60 min11 prerequisites10 tools

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

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: strings with 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 through grep for 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 rbp is 1 byte, mov rsi, 0x7b is 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: 0x17 is 23, 0x1bb is 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, 0x20 opens a stack frame with 32 bytes of local space; mov rsp, rbp / pop rbp / ret tears 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 to cleanup — 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] then mov rdx, 0x1bb immediately before call InternetConnectW — the x64 ABI passes the first two arguments in rcx and rdx. This is the same convention lesson W5 covers for every WinAPI call. The pointer rcx loads 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 0x7B is not a section RVA — cross-reference it against the DOS header layout from lesson F1 and lab-01: e_lfanew (offset 0x3C) points to the real PE signature, and everything between 0x40 and e_lfanew is DOS stub padding. 0x7B falls in that range. Unlike the other IOCs, this one is near the front of the file — use Go to offset (G) to jump straight to 0x7B.
  • Read the 23 raw bytes at file offset 0x7B from the hex viewer, XOR each one with 0x5A by 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?