// HackTheBox  ·  writeup

Holmes 2025 5: The Payload

HackTheBox Hard stringsobjdumpPython
Malware Analysis

Scenario

Analyze a stealthy malware sample that silently propagates across systems. Your task is to uncover its hidden logic, understand how it maintains persistence, and reveal the secret it’s designed to protect.

Executive Summary

The sample is a native Windows x64 loader that never carries its logic in plain sight. It initializes COM, instantiates a managed .NET class through a hard-coded CLSID, and drives it with a handful of vtable calls: one to start up, one to decrypt, one to run. The embedded payload and the killswitch domain are both Base64 blobs XORed with a key generated by a simple linear formula, so neither shows up in strings. Before detonating, the loader resolves the decrypted killswitch domain, and its propagation routine enumerates shares on remote servers. The decrypted payload is a PowerShell reverse shell. All of it was recovered statically, without running the sample.

Artifacts

Format note: The two opcode-style answers are a three-byte FF 50 xx pattern (call [rax+disp8]), written as uppercase hex pairs separated by spaces.

Execution

Q1 — During execution, the malware initializes the COM library on its main thread. Based on the imported functions, which DLL is responsible for providing this functionality?

Triage the extracted binary with strings and read the import table. CoInitialize, CoCreateInstance and OleRun all hang off the same DLL, which is the COM runtime.

unzip -P '&PD8LhraU1hx' ../evidence/danger.zip -d x
strings -a -n5 x/Danger/*.exe
Click flag to reveal ole32.dll
Q2 — Which GUID is used by the binary to instantiate the object containing the data and code for execution?

Disassemble the binary and find the CoCreateInstance call in main. The CLSID argument is a pointer into .rdata at 0x140005a48. Read the 16 bytes there and unpack them as a mixed-endian GUID (first three fields little-endian, the rest as bytes) to get the string form.

objdump -d -M intel x/Danger/*.exe > disasm.txt
import struct; print(struct.unpack('<IHH8B', raw_16_bytes_at_0x140005a48))

Tip: Resolve the virtual address 0x140005a48 to a file offset through the .rdata section header before slicing the bytes. The answer is the uppercase form of the GUID.

Click to reveal answer DABCD999-1234-4567-89AB-1234567890FF
Q3 — Which .NET framework feature is the attacker using to bridge calls between a managed .NET class and an unmanaged native binary?

A native binary that loads the CLR and calls CoCreateInstance on a fixed CLSID is talking to a COM-visible managed class through a COM Callable Wrapper. That mechanism is COM Interop. This one was reasoned from the Q1 and Q2 evidence rather than read from an artifact: the managed assembly itself is not in the evidence, so there is no type library or class definition to confirm it.

Click flag to reveal COM Interop
Q4 — Which Opcode in the disassembly is responsible for calling the first function from the managed code?

After CoCreateInstance returns the interface pointer, main calls through its vtable. The first managed call is call [rax+0x68] at 0x140001d23, which encodes as FF 50 68. The managed assembly is not available, so the method order was inferred from the sequence of vtable calls in main, not from a method table.

grep -nE 'call +QWORD PTR \[rax\+0x[0-9a-f]+\]' disasm.txt
Click to reveal answer FF 50 68
Q9 — Which Opcode is responsible for running the encrypted payload?

The remaining vtable call, call [rax+0x60] at 0x14000197a, sits inside the routine at 0x140001310, which handles the share-scanning and payload-launch logic. It encodes as FF 50 60. Like Q4, this is inferred from call order and surrounding code, since the managed method names are not visible.

grep -nE 'call +QWORD PTR \[rax\+0x60\]' disasm.txt
Click flag to reveal FF 50 60

Defense Evasion

Q5 — Identify the multiplication and addition constants used by the binary's key generation algorithm for decryption.

The key is built in a loop at 0x140001f03 with imul ecx,eax,7 followed by add cl,0x42, so each key byte is (i*7 + 0x42) & 0xff. The constants come out as 7 and 0x42.

grep -nE 'imul +ecx,eax,0x7|add +cl,0x42' disasm.txt

Gotcha: The format mask wants the addition constant in assembler hex notation. Decimal 66 and the 0x42 prefix form both fail; the suffix form 42h is the one that fits.

Click flag to reveal 7, 42h
Q6 — Which Opcode in the disassembly is responsible for calling the decryption logic from the managed code?

The call at 0x1400020f6 is call [rax+0x58] (FF 50 58). It is made with the Base64 key and a Base64 ciphertext starting KXgm... as arguments, and its output feeds straight into the getaddrinfo call from Q7. That data flow marks it as the decrypt call.

grep -nE 'call +QWORD PTR \[rax\+0x58\]' disasm.txt
Click to reveal answer FF 50 58
Q10 — Identify the killswitch domain name the binary attempts to resolve.

The domain never appears in strings. It is the second Base64 string in .rdata, KXgmYHMADxsV8uHiuPPB3w==. Base64-decode it and XOR byte by byte with the Q5 key stream to recover the plaintext. The same key decrypts the larger embedded blob, which turns out to be a PowerShell reverse shell to 10.0.0.13:80.

import base64; c=base64.b64decode('KXgmYHMADxsV8uHiuPPB3w=='); print(bytes(b ^ ((i*7+0x42)&0xff) for i,b in enumerate(c)))
Click to reveal answer k1v7-echosim.net

Command and Control

Q7 — Which Win32 API is being utilized by the binary to resolve the killswitch domain name?

getaddrinfo is imported from the Winsock library and called at 0x1400021fc with the string returned by the Q6 decrypt call, which is the Q10 domain.

grep -n 'getaddrinfo' disasm.txt
Click flag to reveal getaddrinfo

Discovery

Q8 — Which network-related API does the binary use to gather details about each shared resource on a server?

The import table has NetShareEnum from the Netapi32 family, which returns a SHARE_INFO entry for each share on a server. It backs the propagation routine at 0x140001310.

strings -a -n5 x/Danger/*.exe | grep -i NetShare
Click to reveal answer NetShareEnum

Attack Summary

PhaseAction
ExecutionLoader initializes COM through ole32.dll and instantiates a managed class by CLSID
ExecutionNative code drives the managed object through call [rax+disp8] vtable calls (COM Interop)
Defense EvasionPayload and killswitch domain stored as Base64, XORed with a linear key stream
Command and ControlDecrypted killswitch domain is resolved with getaddrinfo
DiscoveryNetShareEnum lists the shares on reachable servers
ExecutionFinal vtable call runs the decrypted payload, a PowerShell reverse shell

IOCs

TypeValue
Killswitch domaink1v7-echosim[.]net
CLSIDDABCD999-1234-4567-89AB-1234567890FF
Reverse shell target10.0.0.13:80
Key formulakey[i] = (i*7 + 0x42) & 0xff

MITRE ATT&CK

TechniqueIDDescription
Inter-Process Communication: Component Object ModelT1559.001Native loader drives a managed class through COM
Deobfuscate/Decode Files or InformationT1140Base64 plus XOR decryption of payload and domain
Obfuscated Files or InformationT1027Killswitch domain and payload absent from plaintext strings
Network Share DiscoveryT1135NetShareEnum against remote servers
Command and Scripting Interpreter: PowerShellT1059.001Decrypted payload is a PowerShell reverse shell
Dynamic ResolutionT1568Domain resolved at runtime with getaddrinfo before proceeding

Defender Takeaways

Hiding logic in a managed assembly behind COM breaks native-only analysis. The native loader is almost empty; the behavior lives in a .NET class reached by a fixed CLSID. Hunt for unregistered or odd CLSIDs passed to CoCreateInstance from unsigned binaries, and always collect the managed assembly alongside the loader.

Linear XOR keys leave the constants in the code. A key of (i*7 + 0x42) & 0xff is trivial once the imul/add pair is found. Static signatures on the key loop (an imul by a small constant followed by add cl, imm) catch families that reuse the pattern even when the ciphertext changes.

Killswitch lookups are a network detection primitive. The loader resolves a domain that is not in strings before it acts. Alert on newly seen, algorithm-looking domains queried by a process that then calls NetShareEnum, and sinkhole the killswitch domain internally to learn which hosts are executing it.

Share enumeration from a non-admin tool is the propagation signal. NetShareEnum against many servers in a short window, followed by SMB writes, is how this sample spreads. Monitor for it from processes that are not file managers or management agents.

Conclusion

The sample is a thin native shell around a COM-hosted managed payload, with a killswitch and a PowerShell reverse shell kept behind one trivial XOR formula. Once the key loop was found, everything else fell out statically; the lesson is that cheap obfuscation still defeats string-based detection but not a look at the arithmetic.