// HackTheBox  ·  writeup

ShadowDropLoader

HackTheBox Medium objdumpstringsPython
Malware Analysis

Scenario

ShadowDrop Loader is a reverse engineering lab requiring deep static analysis of a malicious DLL using IDA and a debugger. Students must analyze API hashing routines, XOR-decrypted strings, NT syscall-level file operations, and in-memory payload execution to reconstruct the loader’s behavior.

Executive Summary

The sample is a DLL loader built so that neither its imports nor its strings give anything away. It resolves ntdll and kernel32 functions at runtime by hashing export names with DJB2, and it keeps the name of its companion payload as XOR-encoded UTF-16 in .rdata, decoding it only when needed and appending it to the directory the DLL sits in. The file is opened through the native NT API using an NT-style object path, and a “name not found” status marks the payload as absent. When the file is present, the loader reads it into freshly committed memory, applies a size threshold, flips the region to read-write-execute and runs it from there.

Artifacts

Format note: Q7 wants the hex value followed by the flag names in parentheses, exactly as the mask shows.

Defense Evasion

Q1 — What is the initial seed value used by the malware's hashing algorithm for API and module name resolution?

Find the routine that walks an export name one character at a time and returns a 32-bit value. Before its loop it loads a constant into the accumulator at 0x1000138f. That constant, 0x1505, is the classic DJB2 seed, and the question wants the decimal form.

objdump -d -M intel sample.dll | grep -n -B3 -A12 '1000138f'
Click flag to reveal 5381
Q2 — What is the multiplier constant used in the hash computation for each character iteration?

From Q1 the loop is DJB2, which computes hash * 33 + c per character. The disassembly agrees: inside the loop the accumulator is multiplied with imul by the immediate 0x21.

objdump -d -M intel sample.dll | grep -n 'imul.*0x21'
Click to reveal answer 33
Q10 — What is the DJB2 hash value (in hex) the malware uses to resolve the NtCreateFile API from ntdll.dll?

Reuse the seed and multiplier from Q1 and Q2 and hash the export name NtCreateFile yourself. This value was computed rather than read from a labelled field in the binary, so the Python result is the evidence.

h = 5381
for c in b"NtCreateFile":
    h = (h * 33 + c) & 0xFFFFFFFF
print(hex(h))

Tip: Mask to 32 bits on every step, otherwise Python keeps growing the integer and the result won’t match a DWORD.

Click to reveal answer 0x15A5ECDB
Q3 — What is the primary XOR key (in decimal) used to decrypt the majority of the encoded filename characters?

The encoded strings sit in .rdata as 16-bit words, and the decoder loop XORs each word with a constant immediate, 0x7f. Decoding the block with it and getting readable text confirms the key.

objdump -d -M intel sample.dll | grep -n 'xor.*0x7f'

Gotcha: The question asks for decimal, so convert 0x7f before answering.

Click flag to reveal 127
Q4 — What is the decrypted filename the malware constructs and appends to the DLL's directory path?

With the key from Q3, read the encoded block at .rdata offset 0xa10, XOR each word with 0x7f and print the result. The decoded UTF-16 string is the payload name the loader concatenates to its own directory.

import sys
data = open(sys.argv[1], "rb").read()
off = int(sys.argv[2], 16)  # file offset of .rdata@0xa10
words = [int.from_bytes(data[off+i:off+i+2], "little") for i in range(0, 40, 2)]
print("".join(chr(w ^ 0x7f) for w in words))

Tip: The decode runs over the whole block, so cut the output at the first NUL; anything after it is unrelated data.

Click to reveal answer AVKTray.dat

Execution

Q5 — What NT object path prefix (as a string) does the malware prepend to the full file path before calling NtCreateFile?

NtCreateFile takes an NT object path, not a Win32 path, so the loader prefixes the DLL directory and the Q4 filename before building the UNICODE_STRING. The prefix is a plain UTF-16 string in .rdata at 0xa00, directly before the encoded filename.

strings -el sample.dll | head
Click flag to reveal \??\
Q6 — What is the NTSTATUS symbolic name the malware checks after calling NtCreateFile to detect that the target file does not exist?

After the NtCreateFile call the return value is tested with xor eax, 0xc0000034. That constant is the NTSTATUS for a missing object name, and the loader branches on it to handle the payload being absent.

objdump -d -M intel sample.dll | grep -n 'c0000034'

Hint: The mask has five underscore-separated words; the hex constant maps to a standard ntstatus.h name.

Click to reveal answer STATUS_OBJECT_NAME_NOT_FOUND
Q7 — What memory allocation type flags (as a combined hex value) does the malware pass to VirtualAlloc when allocating space for the payload?

Find the call to the resolved VirtualAlloc and read the arguments pushed just before it. The allocation type is 0x3000, which is MEM_COMMIT (0x1000) combined with MEM_RESERVE (0x2000).

objdump -d -M intel sample.dll | grep -n -B6 'call.*eax' | grep 'push.*0x3000'

Gotcha: The mask spells out the flag names in parentheses, and the bare 0x3000 is rejected. Reproduce the parenthetical exactly.

Click flag to reveal 0x3000 (MEM_COMMIT | MEM_RESERVE)
Q9 — What is the minimum file size (in decimal bytes) the malware requires before it proceeds to change memory protection and execute the payload?

After the payload is read into the Q7 allocation, the loader compares a value in ebx against an immediate: cmp ebx, 0x3e8, which is 1000 decimal. Anything smaller does not reach the protection change.

objdump -d -M intel sample.dll | grep -n 'cmp.*ebx,0x3e8'
Click to reveal answer 1000
Q8 — What is the symbolic name of the memory protection the malware applies via NtProtectVirtualMemory to make the payload region executable?

Once the size check from Q9 passes, the loader calls NtProtectVirtualMemory on the allocation. The new-protection argument is 0x40, read, write and execute in one flag, and it is the step that turns the data buffer into runnable code.

objdump -d -M intel sample.dll | grep -n -B8 'NtProtect\|call' | grep 'push.*0x40'
Click flag to reveal PAGE_EXECUTE_READWRITE

Attack Summary

PhaseAction
Defense EvasionResolves ntdll and kernel32 APIs at runtime with DJB2 (seed 5381, multiplier 33)
Defense EvasionDecodes the payload filename from XOR-encoded UTF-16 in .rdata
ExecutionBuilds an NT path from the \??\ prefix, the DLL directory and the decoded filename
ExecutionCalls NtCreateFile and checks for a missing file
ExecutionAllocates committed, reserved memory with VirtualAlloc and reads the payload into it
ExecutionRequires at least 1000 bytes, then sets the region to read-write-execute and runs it

IOCs

TypeValue
FilenameAVKTray.dat (payload, looked up in the DLL’s own directory)
NT path prefix\??\
API hash0x15A5ECDB (DJB2 of NtCreateFile)
Hash parametersseed 0x1505, multiplier 0x21
String XOR key0x7f

MITRE ATT&CK

TechniqueIDDescription
Obfuscated Files or Information: Dynamic API ResolutionT1027.007DJB2 hashes replace import names
Obfuscated Files or InformationT1027Payload filename stored XOR-encoded
Deobfuscate/Decode Files or InformationT1140String decoded at runtime before use
Native APIT1106NtCreateFile and NtProtectVirtualMemory called directly
Reflective Code LoadingT1620Payload read into allocated memory and executed in place

Defender Takeaways

An empty import table on a DLL is a finding in itself. Hash-based API resolution leaves almost nothing for import-based scanning. Flag DLLs that import only a loader stub but walk the PEB and export tables, and add YARA rules for the DJB2 seed and multiplier pair (0x1505, 0x21) near an export-walking loop.

Hunt the payload by name and location, not just by signature. The loader looks for AVKTray.dat next to its own DLL, and the name mimics a security product tray component. A data-extension file beside an unsigned DLL, in a directory that does not belong to that product, is a cheap file-creation alert.

Watch the allocate, read, protect sequence. Committed memory, a file read into it, then a switch to PAGE_EXECUTE_READWRITE is a recognisable pattern. EDR rules that alert on RWX transitions via NtProtectVirtualMemory from a freshly loaded DLL catch this stage even when the payload content is unknown.

Native API calls bypass Win32 hooks. Using NtCreateFile with \??\ paths avoids userland hooks on CreateFileW. Monitor at the syscall or kernel callback layer where possible.

Conclusion

The loader hides its imports with DJB2 hashing and its payload name behind a one-byte XOR, but each trick is a handful of constants (5381, 33, 0x7f) that fall out of a static read. What it can’t hide is the final move: a file mapped into memory and flipped to RWX, which is where detection should sit.