0%

分析 CS HTTP 分段 beacon 的 shellcode

文本记录对 CS 4.2 版本的 32 位 HTTP 分段 beacon 的 shellcode 的分析过程,重点分析其定位 DLL 导出函数的代码。前置知识可以参考这篇文章以及《恶意代码分析实战》第 19 章。

从 shellcode 执行处开始动态调试,同时 dump 出 shellcode 用于静态调试备用:

image-20220121233015965

步进这个 call(因为 shellcode 的实现通常不是常规的函数,为了防止跑飞,尽量使用步进而不是步过):

image-20220121233058899

经典 call + pop 操纵控制流,前两个 push 将字符串 wininet 压栈,第三个 push 压栈一个神秘数字,大概会是一个散列过的导出符号名(参考《恶意代码分析实战》)。可以合理推测 call ebp 的功能是根据这个散列值找到 LoadLibrary 函数,然后将 wininet 作为参数,加载 wininet.dll。

在 call ebp 处步进:

image-20220121233318396

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:

image-20220121233437682

可以看到取出了 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 到栈上暂存:

image-20220121233510759

此处 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 双向链表下一个节点,重复上述操作:

image-20220121233540667

RVA 不为零则开始解析导出表:

image-20220121233621746

顺便复习一下导出表的结构和解析导出表的过程:

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 时断下:

image-20220121233732228

取 0x24,即 AddressOfNameOrdinals 的 RVA,此时 ecx 保存函数名 LoadLibraryExA 在
AddressOfNames 中的索引。AddressOfNameOrdinals 中元素的长度为 2 字节,取出值保存在 cx。0x1c 取 AddressOfFunctions,将 ecx 的值作为索引,最终取出 LoadLibraryExA 的地址,然后通过 jmp eax 执行,此时的参数正好是 wininet:

image-20220121233809317

image-20220121233832334

后续的函数调用均采用此方式,在 jmp eax 处下断点可以很方便地进行分析。

---------------感谢您的阅读---------------