SilentNight Analysis Report

Analysis Task
Goal: Identify and reverse engineer the API hashing function. Emulate it with an appropriate string list …
Difficulty: medium

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.