HTB: FFModule Challenge

FFModule - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameFFModule
CategoryReversing
DifficultyMedium
Authord3vn0mi

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:

Terminal window
# Match the local artifact's UUID against HTB's challenge catalog
curl -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 match
curl -s -H "Authorization: Bearer $HTB_APP_TOKEN" \
"https://labs.hackthebox.com/api/v4/challenge/download/424" -o ffmodule.zip
# Zip password is the HTB standard
unzip -P hackthebox ffmodule.zip
sha256sum ffmodule.exe # cf10a53c...e3a2 — 96 KB PE32+ console EXE

Step 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:

Terminal window
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.ImageBase
ep = 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:

  1. Calls OutputDebugStringA("Running Firefox (105.0.1) Hooking Module") — a debug breadcrumb confirming the target Firefox build.
  2. XORs 0x5a4 bytes starting at RVA 0x17000 (the .data section) with the single byte key 0x72 — this is the obfuscated stage-2 payload.
  3. Calls an injector routine at 0x140001000, which snapshots running processes, finds firefox.exe, and remote-threads the decoded blob into it via the VirtualAllocEx/WriteProcessMemory/CreateRemoteThread sequence 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 crc32 instruction) 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 constant 0x3300730073006e (n\0s\0).
  • Builds a WSAStartup → socket → sendto → closesocket → WSACleanup chain 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 bytes

Decoding the blob yielded the flag, which was submitted and confirmed via the HTB API.

Tools Used

ToolPurpose
pefile (Python)Parse the PE, resolve RVAs/sections, extract the .data payload
capstone (Python)Headless x86-64 disassembly of main() and the extracted shellcode
stringsQuick ASCII/UTF-16 triage of the binary before disassembly
curl + HTB APIIdentify 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 + capstone when 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

  1. 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_name field reliably re-identifies and re-fetches the correct challenge.
  2. 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.
  3. PR_Write inside nss3.dll is a recurring hook target for real-world Firefox credential/traffic stealers, since it sits immediately before TLS encryption — spotting a PEB walk targeting nss3.dll is a strong early signal of malware intent.

Flag

HTB{REDACTED}