HTB: Shadow of the Undead Challenge

Shadow of the Undead - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameShadow of the Undead
CategoryMisc (Forensics / Reversing hybrid)
DifficultyHard
Authord3vn0mi

Description

As head of Defense during the battle of Hackster University, a Task Force group is inbound to remove and destroy biohazard waste from the premises. A Guest account was already provisioned for them on a workstation ahead of their briefing — but before the task force even arrives, strange activity starts showing up on the network. The scenario hands over two artifacts for investigation: a full packet capture (capture.pcap) and a process memory minidump (st.dmp) pulled from the compromised workstation, and asks the player to work out what happened and recover the flag.

Solution Overview

The challenge archive unpacks into a network capture and a Windows minidump, which turn out to be two views of the same incident: a dropped executable (st.exe) beaconing out over a custom binary C2 protocol layered on TCP, with its unpacked/decrypted payload still resident in the process’s memory at the time the dump was taken. Solving it required correlating both artifacts — reconstructing the C2 protocol from the pcap to recover the session’s obfuscation key material, then using that (plus static/memory analysis of the dumped process) to unpack, disassemble, and extract the flag from shellcode embedded in the malware’s payload.

Key Steps

Step 1: Unpack the challenge archive

The provided archive is password-protected with the classic hackthebox password.

Terminal window
# Extract the challenge bundle
unzip -o -P hackthebox a12c737f-14f9-47f5-8395-4485ab607089.zip
# Identify what we're working with
file capture.pcap st.dmp

This yields a capture.pcap network trace and an st.dmp process minidump — the two primary artifacts for the challenge.

Step 2: Triage the packet capture

Start broad with protocol hierarchy and conversation stats, then pull out anything served over HTTP.

Terminal window
# Protocol hierarchy + conversation summary
tshark -r capture.pcap -q -z io,phs
tshark -r capture.pcap -q -z conv,tcp
# Pull HTTP requests specifically
tshark -r capture.pcap -Y http -T fields \
-e frame.number -e ip.src -e ip.dst \
-e http.request.method -e http.request.full_uri
# Auto-extract any HTTP-transferred objects (dropped payloads, scripts, etc.)
mkdir -p ex
tshark -r capture.pcap --export-objects http,ex -q

The HTTP export recovers a runner.js loader script and a Windows PE dropper, st.exe — the same binary whose crash/process state ends up in st.dmp. One TCP stream (stream 3) stood apart from the HTTP traffic: a binary, non-HTTP protocol used for ongoing C2 communication.

Step 3: Reconstruct the C2 protocol from TCP stream 3

Following that stream in both hex and ASCII made it clear this was a length/type/code-framed binary protocol rather than plaintext.

Terminal window
# Follow the odd-looking TCP stream in hex and ASCII
tshark -r capture.pcap -q -z follow,tcp,ascii,3
tshark -r capture.pcap -q -z follow,tcp,hex,3 > s3.hex
# Split client->server and server->client directions for separate analysis
tshark -r capture.pcap -Y "ip.src==10.1.1.3" -T fields -e data > c2s.bin
tshark -r capture.pcap -Y "ip.src==10.1.1.1" -T fields -e data > s2c.bin

A small Python parser was written to walk the byte stream and split it into discrete C2 messages, each carrying a sequence number, a message type, and a command/opcode field:

# parse2.py — minimal framing parser for the custom C2 protocol
import struct
def parse(fn):
d = open(fn, 'rb').read()
off = 0
msgs = []
while off + 28 <= len(d):
# header fields recovered by trial-and-error against known byte offsets
seq, mtype, code, length = struct.unpack_from('<IIII', d, off)
payload = d[off+16 : off+16+length]
msgs.append((seq, mtype, code, payload))
off += 16 + length
return msgs

Initial bytes of each stream weren’t random — XORing against a short repeating key (c9cb3f70, recovered by inspecting the first bytes of known/expected traffic) revealed structured content, confirming the protocol uses a lightweight XOR obfuscation layer wrapping AES-encrypted payloads underneath.

Step 4: Pivot into the memory dump for key material

With the wire protocol mapped, the AES key had to come from somewhere — the compromised process’s own memory.

# Load and enumerate the minidump with the `minidump` library
from minidump.minidumpfile import MinidumpFile
mf = MinidumpFile.parse('st.dmp')
print(mf.sysinfo)
for m in mf.modules.modules:
print(hex(m.baseaddress), m.name)
Terminal window
# Static triage of the dropped PE and pull embedded strings
python3 -c "
import pefile
pe = pefile.PE('ex/st.exe')
for s in pe.sections:
print(s.Name, hex(s.VirtualAddress), hex(s.SizeOfRawData))
"
strings -n 6 st.dmp | grep -iE "microsoftcloudservices|st\.exe" | sort -u

Scanning the raw dump for DER-encoded structures (RSA key blobs, certificate headers) surfaced candidate key material, and a brute-force AES key search — trying every plausible 16-byte window against known ciphertext from the captured traffic — was run in the background to cut down the search space:

# keyscan2.py — brute-force candidate AES keys pulled from the dump
# against known-ciphertext from the captured C2 traffic
import struct
from multiprocessing import Pool
from Crypto.Cipher import AES
def try_key(offset):
key = dump[offset:offset+16]
cipher = AES.new(key, AES.MODE_ECB)
if cipher.decrypt(known_ct)[:4] == expected_pt_prefix:
return offset
return None
with Pool() as p:
hits = [h for h in p.map(try_key, range(len(dump) - 16)) if h]

Step 5: Decode the full C2 session and recover the packed payload

With a validated key, every framed message across both directions of stream 3 was decrypted and re-parsed by type/opcode, which revealed the C2 shipping a payload down to the implant in chunks:

# Decrypt every message and dump payload bytes to disk, keyed by
# stream direction / message index / type / opcode for later triage
import full, struct, os
for fn in ['s2c.bin', 'c2s.bin']:
for i, seq, pl in full.msgs_of(fn):
out = f'out/{fn.split(".")[0]}_m{i}_t{pl.mtype}_c{pl.code}.bin'
open(out, 'wb').write(pl.data)

One extracted blob (s2c_m30_t4_c2004.bin) stood out from the rest by size and entropy — raw x86-64 machine code rather than protocol/control data.

Step 6: Disassemble the recovered shellcode

from capstone import *
data = open('out/s2c_m30_t4_c2004.bin', 'rb').read()
md = Cs(CS_ARCH_X86, CS_MODE_64)
for insn in md.disasm(data, 0x0):
print(f'{insn.address:#06x}: {insn.mnemonic} {insn.op_str}')

Walking the disassembly showed classic shellcode-style construction: a run of mov byte [rsp+off], imm8 instructions building a string on the stack one byte at a time — a common technique to avoid embedding a plaintext string in the binary. Two such stack-string regions overlapped in structure, and XORing them against each other cancelled out a shared keystream, revealing the underlying data cleanly:

# Two obfuscated stack-string blocks XORed against each other
# to strip a shared keystream and recover the plaintext beneath
data = open('out/s2c_m30_t4_c2004.bin', 'rb').read()
block1 = data[0x68e:0x68e+0x78]
block2 = data[0x721:0x721+0x78]
recovered = bytes(x ^ y for x, y in zip(block1, block2))
print(recovered)

That recovered plaintext contained the challenge flag.

Tools Used

ToolPurpose
tshark / WiresharkPCAP triage — protocol hierarchy, conversations, TCP stream follow/export
minidump (Python)Parse the Windows minidump — modules, memory regions, string/key recovery
pefileStatic analysis of the dropped st.exe PE binary
pycryptodomeAES decryption of the custom C2 protocol payloads
capstonex86-64 disassembly of the recovered shellcode payload
Python (struct, multiprocessing)Custom C2 protocol framing parser and brute-force AES key search
strings / xxdRaw binary triage across the dump, PCAP objects, and extracted payloads

Key Learnings

Techniques Practiced

  • Correlating a network capture with a memory dump as two views of the same incident
  • Reverse-engineering an undocumented, length-prefixed binary C2 protocol from raw TCP bytes
  • Recovering symmetric key material by brute-forcing candidate keys pulled directly from process memory against known ciphertext
  • Disassembling and manually deobfuscating hand-rolled stack-string shellcode with Capstone
  • Using XOR-of-two-related-ciphertexts to strip a shared keystream when a direct key wasn’t recoverable

Lessons Learned

  1. When a challenge ships both a pcap and a memory dump, treat them as a single dataset — key material unrecoverable from one artifact is often sitting in cleartext (or near-cleartext) in the other.
  2. Don’t assume a “weird” TCP stream is malformed traffic; framing conventions (sequence/type/code/length headers) are usually discoverable by inspecting raw hex before reaching for parsing libraries.
  3. Stack-string shellcode obfuscation is trivially defeated once you spot the repeated mov byte [rsp+N], imm8 pattern in a disassembly — it’s worth scanning for that pattern automatically rather than reading instruction-by-instruction.
  4. Brute-forcing key material against a large memory dump is tractable if you parallelize and validate against a short known-plaintext/ciphertext pair rather than trying to fully decrypt on every attempt.

Flag

HTB{REDACTED}