// HackTheBox  ·  writeup

Operation Blackout 2025: Ghost Thread

HackTheBox Easy API MonitorIDAPythonunzip
DFIR

Scenario

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.

Executive Summary

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.

Artifacts

Tool note: API Monitor is Windows-only. On Linux, unzip the capture and parse the data blob with Python regexes and struct.

Defense Evasion

Q1 — What process injection technique did the attacker use?

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.

Click flag to reveal Thread Local Storage
Q7 — The injection method used by the attacker executes before the main() function is called. Which Win32 API is responsible for terminating the program before main() runs?

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.

Click flag to reveal ExitProcess

Discovery

Q2 — Which Win32 API was used to take snapshots of all processes and threads on the system?

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())))"
Click to reveal answer CreateToolhelp32Snapshot
Q3 — Which process is the attacker's binary attempting to locate for payload injection?

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'
Click flag to reveal Notepad.exe
Q4 — What is the process ID of the identified process?

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.

Click to reveal answer 16224

Execution

Q5 — What is the size of the shellcode?

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 sizeof counts. Report the program’s size, not the blob length.

Click flag to reveal 511
Q6 — Which Win32 API was used to execute the injected payload in the identified process?

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
Click to reveal answer CreateRemoteThread

Attack Summary

PhaseAction
Defense EvasionInjection logic placed in a TLS callback so it runs before main()
DiscoveryCreateToolhelp32Snapshot plus a process walk to find Notepad
ExecutionOpens Notepad and writes the shellcode into it
ExecutionCreateRemoteThread starts the payload in the Notepad process
Defense EvasionExitProcess ends the injector before main() runs

IOCs

TypeValue
Sampleinject.exe
Target processNotepad.exe (PID 16224 in the capture)
PayloadMetasploit-style shellcode, 510-byte blob in a 511-byte array

MITRE ATT&CK

TechniqueIDDescription
Process Injection: Thread Local StorageT1055.005Injection logic runs from a TLS callback before main()
Process DiscoveryT1057Toolhelp snapshot and process walk to find Notepad
Process InjectionT1055Shellcode written into Notepad and started with a remote thread

Defender Takeaways

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.

Conclusion

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.