文本记录对 CS 4.2 版本的 32 位 HTTP 分段 beacon 的 shellcode 的分析过程,重点分析其定位 DLL 导出函数的代码。前置知识可以参考这篇文章 以及《恶意代码分析实战》第 19 章。
从 shellcode 执行处开始动态调试,同时 dump 出 shellcode 用于静态调试备用:
步进这个 call(因为 shellcode 的实现通常不是常规的函数,为了防止跑飞,尽量使用步进而不是步过):
经典 call + pop 操纵控制流,前两个 push 将字符串 wininet 压栈,第三个 push 压栈一个神秘数字,大概会是一个散列过的导出符号名(参考《恶意代码分析实战》)。可以合理推测 call ebp 的功能是根据这个散列值找到 LoadLibrary 函数,然后将 wininet 作为参数,加载 wininet.dll。
在 call ebp 处步进:
Windows 32 环境下 fs:[0] 指向 TEB,用 Windbg 看结构体,TEB 偏移 0x30 处指向 PEB:
1 2 3 4 5 6 7 8 9 10 0:000> dt !_TEB ntdll!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : Ptr32 Void +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : Ptr32 Void +0x02c ThreadLocalStoragePointer : Ptr32 Void +0x030 ProcessEnvironmentBlock : Ptr32 _PEB +0x034 LastErrorValue : Uint4B ……
PEB 偏移 0xc 处指向 PEB_LDR_DATA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 0:000> dt !_PEB ntdll!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 BitField : UChar +0x003 ImageUsesLargePages : Pos 0, 1 Bit +0x003 IsProtectedProcess : Pos 1, 1 Bit +0x003 IsImageDynamicallyRelocated : Pos 2, 1 Bit +0x003 SkipPatchingUser32Forwarders : Pos 3, 1 Bit +0x003 IsPackagedProcess : Pos 4, 1 Bit +0x003 IsAppContainer : Pos 5, 1 Bit +0x003 IsProtectedProcessLight : Pos 6, 1 Bit +0x003 IsLongPathAwareProcess : Pos 7, 1 Bit +0x004 Mutant : Ptr32 Void +0x008 ImageBaseAddress : Ptr32 Void +0x00c Ldr : Ptr32 _PEB_LDR_DATA +0x010 ProcessParameters : Ptr32 _RTL_USER_PROCESS_PARAMETERS ……
PEB_LDR_DATA 偏移 0x14 处指向 InMemoryOrderModuleList,其结构类型为 LIST_ENTRY:
1 2 3 4 5 6 7 8 9 10 11 12 0:000> dt !_PEB_LDR_DATA ntdll!_PEB_LDR_DATA +0x000 Length : Uint4B +0x004 Initialized : UChar +0x008 SsHandle : Ptr32 Void +0x00c InLoadOrderModuleList : _LIST_ENTRY +0x014 InMemoryOrderModuleList : _LIST_ENTRY +0x01c InInitializationOrderModuleList : _LIST_ENTRY +0x024 EntryInProgress : Ptr32 Void +0x028 ShutdownInProgress : UChar +0x02c ShutdownThreadId : Ptr32 Void ……
1 2 3 4 0:000> dt !_LIST_ENTRY ntdll!_LIST_ENTRY +0x000 Flink : Ptr32 _LIST_ENTRY +0x004 Blink : Ptr32 _LIST_ENTRY
参考微软的官方文档 ,InMemoryOrderModuleList 表示 The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure. 查看此结构体:
1 2 3 4 5 6 7 8 9 10 11 12 13 0:000> dt !_LDR_DATA_TABLE_ENTRY ntdll!_LDR_DATA_TABLE_ENTRY +0x000 InLoadOrderLinks : _LIST_ENTRY +0x008 InMemoryOrderLinks : _LIST_ENTRY +0x010 InInitializationOrderLinks : _LIST_ENTRY +0x018 DllBase : Ptr32 Void +0x01c EntryPoint : Ptr32 Void +0x020 SizeOfImage : Uint4B +0x024 FullDllName : _UNICODE_STRING +0x02c BaseDllName : _UNICODE_STRING +0x034 FlagGroup : [4] UChar +0x034 Flags : Uint4B ……
注意 InMemoryOrderModuleList 在结构体中的偏移为 0x8,所以取相对 InMemoryOrderModuleList 偏移 0x28 实际上是取相对结构体开始偏移 0x28 + 0x8 = 0x30,而 UNICODE_STRING 结构 的定义如下:
1 2 3 4 5 typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING;
所以相对结构体开始偏移 0x30 取出 Buffer:
可以看到取出了 beacon 的文件名 artifact.exe。0x26 + 0x8 = 0x2e,取出 UNICODE_STRING 的 MaximumLength。然后计算取出的文件名的散列值,采用 32 位旋转向右累加散列算法,示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def ror (value, count, max_bits ): right = (value & (2 **max_bits - 1 )) >> (count % max_bits) left = (value << (max_bits - (count % max_bits))) & (2 **max_bits - 1 ) return left | right plain = b"\x6E\x00\x74\x00\x64\x00\x6C\x00\x6C\x00\x2E\x00\x64\x00\x6C\x00\x6C\x00\x00\x00" hash = 0 for char in plain: if char >= ord ('a' ): char -= 0x20 hash = ror(hash , 13 , 32 ) hash += char print (hex (hash )[2 :].rjust(8 , '0' ))
算完散列先 push 到栈上暂存:
此处 edx 仍然指向 InMemoryOrderModuleList,所以 0x10 + 0x8 指向 DllBase。PE 文件头偏移 0x3c 指向 IMAGE_NT_HEADERS。IMAGE_NT_HEADERS 偏移 0x78 指向 IMAGE_OPTIONAL_HEADER 中的 DataDirectory,而 DataDirectory 的第一项就是 IMAGE_DIRECTORY_ENTRY_EXPORT。因此 ds:[eax+78] 取导出表的 RVA。
然后判断该 RVA 是否为 0,如果是,则表明该模块没有导出函数,跳转到 1E0089 处,取 InMemoryOrderModuleList 双向链表下一个节点,重复上述操作:
RVA 不为零则开始解析导出表:
顺便复习一下导出表的结构和解析导出表的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 0:000> dt IMAGE_EXPORT_DIRECTORY ole32!IMAGE_EXPORT_DIRECTORY +0x000 Characteristics : Uint4B +0x004 TimeDateStamp : Uint4B +0x008 MajorVersion : Uint2B +0x00a MinorVersion : Uint2B +0x00c Name : Uint4B +0x010 Base : Uint4B +0x014 NumberOfFunctions : Uint4B +0x018 NumberOfNames : Uint4B +0x01c AddressOfFunctions : Uint4B +0x020 AddressOfNames : Uint4B +0x024 AddressOfNameOrdinals : Uint4B
先从 0x18 获取 NumberOfNames,然后从 0x20 获取 AddressOfNames,遍历每一个函数名,计算散列值,然后在 1E0063 处与栈上的值相加,这个值就是解析导出表之前计算的该 PE 文件名的散列值。然后紧接着下一条语句与栈上的一个值进行比较,这个值就是 call ebp 之前压栈的散列值。在 1E0066 处的跳转语句下条件断点(EFLAGS & 0x40) == 0x40
,运行,程序在函数名为 LoadLibraryExA 时断下:
取 0x24,即 AddressOfNameOrdinals 的 RVA,此时 ecx 保存函数名 LoadLibraryExA 在 AddressOfNames 中的索引。AddressOfNameOrdinals 中元素的长度为 2 字节,取出值保存在 cx。0x1c 取 AddressOfFunctions,将 ecx 的值作为索引,最终取出 LoadLibraryExA 的地址,然后通过 jmp eax 执行,此时的参数正好是 wininet:
后续的函数调用均采用此方式,在 jmp eax 处下断点可以很方便地进行分析。
---------------感谢您的阅读---------------