1. Capability Analysis (capa)
capa was run against the binary with static analysis mode:
capa -f pe <sample>
| ATT&CK Technique | Description |
|---|---|
| T1027 | Obfuscated Files or Information |
| T1083 | File and Directory Discovery |
| T1059 | Command and Scripting Interpreter |
| T1129 | Shared Modules (runtime linking) |
| MBC Behavior | Detail |
|---|---|
| C0027.009 | RC4 encryption |
| C0021.004 | RC4 PRGA (pseudo-random generation) |
| C0021 | Mersenne Twister PRNG |
| C0026.002 | XOR encoding |
| E1027.m02 | Standard encoding algorithm |
The combination of RC4, XOR, Mersenne Twister, and runtime linking is consistent with the SilentNight banking trojan family, which uses layered obfuscation and a custom API hashing scheme to conceal its imports. Quite the collection of encryption primitives for a sample that, presumably, just wants to steal banking credentials.
2. API Resolution Mechanism
My goal for this section was to answer: which APIs does this sample actually use at runtime, and how does the resolver work?
3.1 Resolver Structure (dynamic analysis, x64dbg)
I set a breakpoint at the entry point (0x03091C90) and let the sample run until it paused after completing its API resolution pass. At that point, I disassembled the resolver function at 0x030A3170.
Function signature: resolver(module_index: u32, hash: u32) -> void
The function takes two arguments: a module index (first arg, [ebp+0x08]) and a pre-computed hash value (second arg, [ebp+0x0C]). The hash is computed before the resolver is called; the resolver itself only does the hash table lookup and population.
Hash table layout:
Two parallel arrays, each 4096 bytes (1024 DWORD slots):
| Table | Base Address | Purpose |
|---|---|---|
| Hash table | 0x030BE8A0 |
Stores API name hash values (keys) |
| FuncPtr table | 0x030BF240 |
Stores resolved function VAs (values) |
Slot calculation:
030A317F call 0x030A6170 ; returns table capacity = 616
030A3184 mov ecx, eax ; ecx = 616
030A3186 mov eax, esi ; eax = hash
030A3188 xor edx, edx
030A318A div ecx ; edx = hash % 616 (initial slot)
030A318C mov eax, [edx*4+0x30BE8A0] ; probe hash_table[slot]
Collision resolution:
Open addressing with a fixed step. If hash_table[slot] is occupied by a different hash, the probe advances:
030A31A0 add ebx, 0x8E710A79 ; step = 0x8E710A79
030A31A6 mov eax, [ebx*4+0x30BE8A0] ; probe next slot (no re-modulation)
Critically, the probe index ebx is not taken modulo 1024 after the step. This means collision chains can land in slot indices above 615. The parse_api_table.py output shows 18 "shadow" entries at slots 656-999 whose hash-column values are equal to the resolved function VAs from the primary entries (40-615). These appear to be a secondary caching pass where the resolved VA is re-inserted into the hash table as its own lookup key, forming a reverse index.
Hit and insertion paths:
; Hit: compare stored hash with input hash via 0x030B9A70
030A31B9 call 0x030B9A70
030A31C1 test al, 0x01
030A31C3 jnz 0x030A3266 ; bit 0 set = match found
; Load funcptr on hit
030A3266 mov eax, [edi*4+0x30BF240]
030A326D jmp epilog
; Insert on miss (empty slot found at 0x030A31FD):
030A32EE mov [edi*4+0x030BF240], eax ; store resolved funcptr
030A32F5 mov [ecx], esi ; store hash into hash slot
The resolver also calls itself recursively at 0x030A32BE / 0x030A32D3 with hash 0x0BA94474, an internal helper API needed during DLL loading, resolved on demand. Even the resolver has dependencies it is too embarrassed to declare up front.
Hash function:
The hash is computed externally by a separate routine before 0x030A3170 is called. The Mersenne Twister state block at 0x030BE364 (identified by capa) is used as a PRNG seeding the per-character step. Two confirmed hash/name pairs anchor the algorithm:
| API name (lowercase) | Hash |
|---|---|
loadlibrarya |
0x08ADF2D1 |
getprocaddress |
0x0B1C1FE3 |
The exact hash function disassembly and a standalone emulator script remain as follow-on work.
2.2 Complete Resolved API Table
By combining the 4096-byte hash table and funcptr table dumps (script: parse_api_table.py) with PE export table traversal for each loaded DLL (scripts: parse_pe_headers.py, lookup_remaining.py), I resolved all 33 pre-populated entries.
The 18 "shadow" slots (656-999) are excluded; their funcptr column is zero and they serve as the reverse-lookup cache described above.
| Slot | Hash | FuncPtr | Module | API |
|---|---|---|---|---|
| 40 | 0x0AAF7240 |
0x75B69CC0 |
ws2_32.dll | WSAStartup |
| 92 | 0x08A8238C |
0x76ED4750 |
advapi32.dll | GetSecurityDescriptorSacl |
| 102 | 0x00BD557E |
0x76ECE8C0 |
advapi32.dll | GetTokenInformation |
| 132 | 0x04A8239C |
0x76EE6B50 |
advapi32.dll | SetSecurityDescriptorSacl |
| 145 | 0x08ADF2D1 |
0x76120E70 |
kernel32.dll | LoadLibraryA |
| 170 | 0x0D3A1832 |
0x76881C40 |
combase.dll | StringFromGUID2 |
| 187 | 0x0B1C1FE3 |
0x7611F7F0 |
kernel32.dll | GetProcAddress |
| 204 | 0x08847844 |
0x76ECF2C0 |
advapi32.dll | GetSidSubAuthorityCount |
| 209 | 0x07A1C189 |
0x76ECF160 |
advapi32.dll | GetSidSubAuthority |
| 215 | 0x0FB8D9E7 |
0x76123110 |
kernel32.dll | GetVolumeNameForVolumeMountPointW |
| 231 | 0x055E8477 |
0x760C54C0 |
shlwapi.dll | PathAddBackslashW |
| 239 | 0x0D0682F7 |
0x760C5260 |
shlwapi.dll | PathRemoveBackslashW |
| 242 | 0x0C654D62 |
0x76ECF520 |
advapi32.dll | InitializeSecurityDescriptor |
| 283 | 0x08685DE3 |
0x77455DE0 |
ntdll.dll | RtlAllocateHeap |
| 326 | 0x0F2B7EBE |
0x76ECEC60 |
advapi32.dll | OpenProcessToken |
| 343 | 0x003A5687 |
0x7634DD20 |
shell32.dll | SHGetFolderPathW |
| 348 | 0x0013B274 |
0x76123130 |
kernel32.dll | GetCurrentProcessId |
| 383 | 0x0D513D37 |
0x7688F800 |
combase.dll | CLSIDFromString |
| 415 | 0x0D641D17 |
0x76123200 |
kernel32.dll | CreateEventW |
| 437 | 0x0B8E7DB5 |
0x76123180 |
kernel32.dll | CloseHandle |
| 461 | 0x0B86DE55 |
0x7611E200 |
kernel32.dll | HeapFree |
| 466 | 0x0C702BE2 |
0x7611E2B0 |
kernel32.dll | GetLastError |
| 473 | 0x05B4D601 |
0x73B36410 |
wininet.dll | InternetSetOptionA |
| 476 | 0x02364B34 |
0x76ECED70 |
advapi32.dll | GetLengthSid |
| 519 | 0x0AE63487 |
0x761234C0 |
kernel32.dll | FindFirstFileW |
| 520 | 0x02A85667 |
0x76123510 |
kernel32.dll | FindNextFileW |
| 536 | 0x001E78C0 |
0x7611F620 |
kernel32.dll | GetProcessHeap |
| 543 | 0x042C2F97 |
0x760C4FB0 |
shlwapi.dll | PathRemoveFileSpecW |
| 567 | 0x0ABC78F7 |
0x76EC8900 |
advapi32.dll | ConvertStringSecurityDescriptorToSecurityDescriptorW |
| 583 | 0x044F8007 |
0x76121B40 |
kernel32.dll | GetVersionExW |
| 588 | 0x04A9139C |
0x76ECF280 |
advapi32.dll | SetSecurityDescriptorDacl |
| 593 | 0x044F8011 |
0x76121A00 |
kernel32.dll | GetVersionExA |
| 615 | 0x0FDA8B77 |
0x76120BA0 |
kernel32.dll | GetModuleFileNameW |
Note on shell32!SHGetFolderPathW (slot 343): this function is exported by shell32 as ordinal-only (ordinal 889, no named export). The shell32 stub at 0x7634DD20 is a hot-patch forwarder (mov edi,edi / push ebp / mov ebp,esp / pop ebp / jmp [ptr]) that chains into the actual implementation in windows.storage.dll at RVA 0x0019CA80. The name SHGetFolderPathW was confirmed by tracing into windows.storage.dll's own export table.