HTB: FFModule Challenge
FFModule - HackTheBox Challenge Writeup
Challenge Information
| Field | Value |
|---|---|
| Challenge Name | FFModule |
| Category | Reversing |
| Difficulty | Medium |
| Author | d3vn0mi |
Description
Following recent hits attributed to the “Jupiter” banking malware family, a sample of one of its modules was recovered. The module is suspected of targeting Firefox users to steal secrets — the goal is to reverse-engineer the binary and recover the flag.
Solution Overview
ffmodule.exe turned out to be a two-stage Windows PE: a small dropper/injector that decodes an embedded payload and injects it into a running firefox.exe, and a position-independent shellcode stage that resolves Win32 APIs by hash, locates Firefox’s NSS library in memory, and installs a trampoline on PR_Write — the exact function NSS uses to hand off plaintext before TLS encryption. Recovering the flag meant statically decoding the XOR-obfuscated stage-2 payload out of the dropper, disassembling it headlessly with pefile + capstone, and replicating its custom decode routine in Python to unpack the final secret.
Key Steps
Step 1: Fetch and verify the correct artifact
The initial working copy of ffmodule.exe was 0 bytes. The run directory name matched the UUID HackTheBox embeds in its challenge zip filenames, so the challenge could be positively identified via the HTB API before re-downloading:
# Match the local artifact's UUID against HTB's challenge catalogcurl -s -H "Authorization: Bearer $HTB_APP_TOKEN" \ "https://labs.hackthebox.com/api/v4/challenge/list" \ | python3 -c "import json,sys; d=json.load(sys.stdin); \ print([c for c in d['challenges'] if c['name']=='FFModule'])"
# challenge/info/424 reported file_name == a12c737e-...zip, an exact matchcurl -s -H "Authorization: Bearer $HTB_APP_TOKEN" \ "https://labs.hackthebox.com/api/v4/challenge/download/424" -o ffmodule.zip
# Zip password is the HTB standardunzip -P hackthebox ffmodule.zipsha256sum ffmodule.exe # cf10a53c...e3a2 — 96 KB PE32+ console EXEStep 2: Triage the PE and spot the injection chain
strings and the import table were enough to guess the shape of the sample before disassembling anything:
strings -n 5 ffmodule.exe | grep -iE 'fire|fox|nss|inject|dll'The import set was the textbook process-injection chain: CreateToolhelp32Snapshot → OpenProcess → VirtualAllocEx → WriteProcessMemory → VirtualProtectEx → CreateRemoteThread. r2/objdump/gdb weren’t present in the environment, so the reversing had to be done headlessly with pefile + capstone.
Step 3: Disassemble main and recover the XOR key
import pefile, capstone
pe = pefile.PE('ffmodule.exe')ib = pe.OPTIONAL_HEADER.ImageBaseep = pe.OPTIONAL_HEADER.AddressOfEntryPoint
md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)data = pe.get_memory_mapped_image()
# main() at 0x140001180 (ImageBase-relative)for insn in md.disasm(data[0x1180:0x1400], ib + 0x1180): print(f"{insn.address:#x}: {insn.mnemonic} {insn.op_str}")main does three things:
- Calls
OutputDebugStringA("Running Firefox (105.0.1) Hooking Module")— a debug breadcrumb confirming the target Firefox build. - XORs
0x5a4bytes starting at RVA0x17000(the.datasection) with the single byte key0x72— this is the obfuscated stage-2 payload. - Calls an injector routine at
0x140001000, which snapshots running processes, findsfirefox.exe, and remote-threads the decoded blob into it via theVirtualAllocEx/WriteProcessMemory/CreateRemoteThreadsequence seen in the imports.
Step 4: Decode the embedded payload
import pefile
pe = pefile.PE('ffmodule.exe')data = pe.get_memory_mapped_image()
encoded = data[0x17000:0x17000 + 0x5a4]shellcode = bytes(b ^ 0x72 for b in encoded)
open('shellcode.bin', 'wb').write(shellcode)Step 5: Disassemble the shellcode stage
import capstone
sc = open('shellcode.bin', 'rb').read()md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64)
for insn in md.disasm(sc, 0): print(f"{insn.address:#06x}: {insn.mnemonic} {insn.op_str}")The decoded blob is position-independent shellcode that:
- Resolves Win32/NSS APIs by computing the CRC32 of each DLL export name (via the
crc32instruction) and comparing against hardcoded constants — notably not the usual djb2/ROR13 hash seen in most shellcode loaders, which is why disassembling the hash routine itself (rather than assuming a known algorithm) was necessary. - Walks the PEB’s loaded-module list looking for
nss3.dll, matched via a wide-string compare against the packed constant0x3300730073006e(n\0s\0). - Builds a
WSAStartup→socket→sendto→closesocket→WSACleanupchain for exfiltration. - Installs a trampoline on
PR_Write, NSS’s write primitive — Firefox routes outbound TLS plaintext through this function before encryption, making it the ideal hook point for a credential/traffic-stealing module.
Step 6: Recover and decode the flag material
The flag material recovered from the shellcode was packed with a rotate-based transform mirroring the routine identified during disassembly. Replicating that transform in Python against the extracted bytes unpacked the final plaintext flag:
blob = bytes.fromhex('54d5133a4199d4b69374194197615ab6546161388198b7b6f4e1d8b97719f7fa')
def rol(v, n, bits=8): n %= bits return ((v << n) | (v >> (bits - n))) & (2**bits - 1)
# Apply the recovered rotate/XOR transform to unpack the flag bytesDecoding the blob yielded the flag, which was submitted and confirmed via the HTB API.
Tools Used
| Tool | Purpose |
|---|---|
pefile (Python) | Parse the PE, resolve RVAs/sections, extract the .data payload |
capstone (Python) | Headless x86-64 disassembly of main() and the extracted shellcode |
strings | Quick ASCII/UTF-16 triage of the binary before disassembly |
curl + HTB API | Identify the correct challenge from a corrupted artifact and re-download it |
| Custom Python (XOR/ROL) | Decode the obfuscated stage-2 payload and unpack the flag blob |
Key Learnings
Techniques Practiced
- Headless PE triage and disassembly with
pefile+capstonewhen no interactive disassembler (r2/objdump/gdb/Ghidra) is available. - Recognizing classic process-injection import chains (
CreateToolhelp32Snapshot/VirtualAllocEx/WriteProcessMemory/CreateRemoteThread). - Decoding single-byte XOR-obfuscated payload sections directly from section RVAs.
- Identifying custom (non-standard) API-hashing schemes in shellcode by disassembling the hash routine rather than assuming djb2/ROR13.
- PEB-walking and wide-string module matching inside position-independent shellcode.
Lessons Learned
- A 0-byte or corrupted challenge artifact doesn’t mean the challenge is broken — matching the local directory’s UUID against the HTB API’s
file_namefield reliably re-identifies and re-fetches the correct challenge. - Shellcode API resolution isn’t always djb2/ROR13 — this sample used raw CRC32 of export names, which only became clear from reading the hash routine itself.
PR_Writeinsidenss3.dllis a recurring hook target for real-world Firefox credential/traffic stealers, since it sits immediately before TLS encryption — spotting a PEB walk targetingnss3.dllis a strong early signal of malware intent.
Flag
HTB{REDACTED}