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.
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.
work/ and analyse statically onlyobjdump -d -M intel disassembly, used for the hash routine, the string decoder and the NT call sequencestrings -el for UTF-16 literals in .rdatapython3 -I to re-implement the XOR decode and the DJB2 hash and check the values read from the disassemblyFormat note: Q7 wants the hex value followed by the flag names in parentheses, exactly as the mask shows.
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'
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'
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.
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
0x7fbefore answering.
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.
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
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.hname.
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
0x3000is rejected. Reproduce the parenthetical exactly.
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'
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'
| Phase | Action |
|---|---|
| Defense Evasion | Resolves ntdll and kernel32 APIs at runtime with DJB2 (seed 5381, multiplier 33) |
| Defense Evasion | Decodes the payload filename from XOR-encoded UTF-16 in .rdata |
| Execution | Builds an NT path from the \??\ prefix, the DLL directory and the decoded filename |
| Execution | Calls NtCreateFile and checks for a missing file |
| Execution | Allocates committed, reserved memory with VirtualAlloc and reads the payload into it |
| Execution | Requires at least 1000 bytes, then sets the region to read-write-execute and runs it |
| Type | Value |
|---|---|
| Filename | AVKTray.dat (payload, looked up in the DLL’s own directory) |
| NT path prefix | \??\ |
| API hash | 0x15A5ECDB (DJB2 of NtCreateFile) |
| Hash parameters | seed 0x1505, multiplier 0x21 |
| String XOR key | 0x7f |
| Technique | ID | Description |
|---|---|---|
| Obfuscated Files or Information: Dynamic API Resolution | T1027.007 | DJB2 hashes replace import names |
| Obfuscated Files or Information | T1027 | Payload filename stored XOR-encoded |
| Deobfuscate/Decode Files or Information | T1140 | String decoded at runtime before use |
| Native API | T1106 | NtCreateFile and NtProtectVirtualMemory called directly |
| Reflective Code Loading | T1620 | Payload read into allocated memory and executed in place |
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.
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.