In Ghost Thread, players join Talion “Byte Doctor” Reyes as he investigates a stealthy post-breach attack where malicious code was injected into a legitimate process, leaving almost no trace on disk. Using API Monitor logs, players must uncover the process injection technique used and identify which trusted process was injected.
A small injector binary ran its payload logic before main() through a loader-level callback, which keeps the interesting code away from the usual entry point. It enumerated the running processes with a toolhelp snapshot, picked out a trusted Windows editor by name, opened it, and wrote a Metasploit-style shellcode blob into it. A remote thread then started the shellcode inside the victim process, so the malicious activity ran under a legitimate image. Everything was recovered statically from an API Monitor capture and an IDA database; the sample was never run.
.apmx64 file is a zip. Unzip it and read log/monitoring.txt plus process/0/data, which holds the raw call parameters and returned structuresinject.exe.i64 → open in IDA for imports, symbols and the shellcode arrayTool note: API Monitor is Windows-only. On Linux, unzip the capture and parse the
datablob with Python regexes andstruct.
The scenario hints that the code runs before the normal entry point, and the IDA database confirms it: besides main the binary registers a callback in a loader-run table, and the injection logic sits in that callback. The technique is named after the PE feature that table belongs to.
strings -a inject.exe.i64 | grep -i callback
Gotcha: The format mask is 6-5-7 letters, so a name ending in “Injection” does not fit. Name the PE feature itself.
This one was reasoned rather than read from a single artifact, and HTB accepted it. The injector does its work in the pre-main() callback from Q1, so the process has to be ended there or the normal main() body would run afterwards. The 11-letter mask fits the standard Win32 process-termination call.
Unzip the capture and search the call records in process/0/data for toolhelp functions. One snapshot call appears repeatedly, each time before the process walk begins.
unzip -o ghost.apmx64 -d apm
python3 -I -c "import re,sys;print(set(re.findall(rb'[A-Za-z0-9]+Snapshot',open('apm/process/0/data','rb').read())))"
After the snapshot, the binary walks the process list with Process32First and Process32Next and compares each name. The PROCESSENTRY32W structures in the capture hold the enumerated names, and the one the walk is looking for is also the target of the following OpenProcess. The IDA strings agree.
strings -a -el apm/process/0/data | grep -i '\.exe'
strings -a inject.exe.i64 | grep -i '\.exe'
From Q3 we know the target. Find its PROCESSENTRY32W record in the captured data: the struct starts with dwSize (304 bytes) followed by the usage count and th32ProcessID. Decoding that field for the Notepad record gives the PID, and it matches the PID argument of the OpenProcess call.
python3 -I parse_pe32.py apm/process/0/data Notepad.exe
Tip: The value is a little-endian DWORD, so the hex form reads backwards. Convert to decimal before submitting.
The shellcode is a Metasploit-style blob in the IDA database that ends in the usual ff d5 call stub. Counting its bytes gives one less than the value HTB accepted. The accepted number is the array size as the program uses it, which includes the trailing terminator byte.
python3 -I count_blob.py inject.exe.i64
Gotcha: The raw blob counts to 510 bytes, but the accepted answer is one higher because the C array carries a terminating null that
sizeofcounts. Report the program’s size, not the blob length.
After VirtualAllocEx and WriteProcessMemory place the shellcode in the target, the capture shows a call that starts a thread inside the other process at the written address. It appears in both the API Monitor log and the IDA import table.
grep -i 'Thread' apm/log/monitoring.txt
| Phase | Action |
|---|---|
| Defense Evasion | Injection logic placed in a TLS callback so it runs before main() |
| Discovery | CreateToolhelp32Snapshot plus a process walk to find Notepad |
| Execution | Opens Notepad and writes the shellcode into it |
| Execution | CreateRemoteThread starts the payload in the Notepad process |
| Defense Evasion | ExitProcess ends the injector before main() runs |
| Type | Value |
|---|---|
| Sample | inject.exe |
| Target process | Notepad.exe (PID 16224 in the capture) |
| Payload | Metasploit-style shellcode, 510-byte blob in a 511-byte array |
| Technique | ID | Description |
|---|---|---|
| Process Injection: Thread Local Storage | T1055.005 | Injection logic runs from a TLS callback before main() |
| Process Discovery | T1057 | Toolhelp snapshot and process walk to find Notepad |
| Process Injection | T1055 | Shellcode written into Notepad and started with a remote thread |
Entry-point-only analysis misses TLS callbacks. Debuggers that break on main() or the PE entry point never see code that runs from a TLS callback. Triage tooling should enumerate the TLS directory and treat any callback in a small unsigned binary as suspicious.
The cross-process write sequence is the detection. OpenProcess, VirtualAllocEx, WriteProcessMemory and CreateRemoteThread against a process the caller does not own is a stable pattern. EDR rules that correlate these calls to one target catch it regardless of the shellcode.
Process enumeration by name followed by an open is worth scoring. A toolhelp snapshot, a walk for one specific image name and an OpenProcess on the match, all from an unknown binary, is a cheap signal when combined.
A text editor with a remote thread is abnormal on its own. Notepad receiving a thread from another process, or making network connections, should alert even when the injector itself is missed.
The injector hid its work in a TLS callback and delivered shellcode into Notepad, so the malicious thread ran under a trusted image with nothing dropped on disk. The behaviour was visible in the API call sequence, which is where detection needs to look.