-
-
[原创]对照内核结构深入理解动态定位 API
-
2015-8-14 19:45 2817
-
图 / 文 N0viceLive <rectigu@gmail.com>
2015 年 8 月
tl, dr
我们先理清总体的方向。动态定位 API 有如下几个重要步骤。
有了这两个地址,其他的函数(比如说 LoadLibrary)就都好办了。
接下来我们依次详细看看这两个目标是如何实现的。
代码
文中的所有代码均可在下面的仓库中找到。
除了文中的代码,该仓库还包含其他相关的代码与实用工具。
请前往 https://github.com/NoviceLive/shellcoding。
利用 PEB->Ldr 获取 kernel32.dll 的基址
Windows 内核模式使用 ETHREAD / KTHREAD、EPROCESS / KPROCESS
来描述线程、进程的结构,
他们在用户模式的类似结构就是 TEB 与 PEB。
不过 TEB、PEB 与他们的定义都在 ntoskrnl.exe 之中。
我们可以用 WinDbg 查看他们的定义。
上面的片段截取自 Windows XP with SP3。
PEB(进程环境块)中包含加载模块的信息(Ldr 成员),其中就包括了 kernel32.dll。
PEB 本身则可以通过 TEB(线程环境块)中的 ProcessEnvironmentBlock 成员找到,
而 TEB 在 IA32 架构 Windows 位于 fs 段寄存器,在 AMD64 则位于 gs 段寄存器。
我们以 IA32 环境举例。
下面的代码片段将 PEB 的地址放入 ecx 寄存器。
我们对照一下 PEB->Ldr 的结构,其中 InLoadOrderModuleList、InMemoryOrderModuleList 与
InInitializationOrderModuleList 依次为加载顺序模块列表、
内存顺序模块列表与初始化顺序模块列表。
LIST_ENTRY 包含两个指向 LIST_ENTRY 结构的指针,用来将同构或者异构的结构链接成(循环)双向链表。
我们使用初始化顺序模块列表来定位 kernel32.dll。
这时, ecx 寄存器为 PEB->Ldr->InInitializationOrderModuleList,
他的 Flink 成员指向一个 LDR_DATA_TABLE_ENTRY 结构的 LIST_ENTRY 类型成员,
InInitializationOrderLinks。
顺着往前遍历,我们一定会遇见 kernel32.dll 的。
对照 LDR_DATA_TABLE_ENTRY 结构,
ecx + 8 就是 DllBase 了,不过这还不是 kernel32.dll 的基址。
我们可以通过这个结构的 BaseDllName 成员来确定我们遍历到的结点是不是
kernel32.dll 结点。这是一个 UNICODE_STRING,注意一个字符是两个字节。
BaseDllName 在 LDR_DATA_TABLE_ENTRY 中的偏移为 0x2c,
Buffer 在 UNICODE_STRING 中的偏移为 0x4,
那么,0x2c + 0x4 也就是 0x30,减去 0x10(读者想想,为什么?),
所以 edx + 20h 就是 UNICODE_STRING 的 Buffer。
判断 Buffer 的第 7 个字符是不是 '3',第 7 个字符的偏移为 (7 - 1) * 2,
也就是 12,或者 0xc。
如果是,说明我们找到了 kernel32.dll 的结点,如果不是,那就继续向前遍历。
代码片段如下。
搜索 kernel32.dll 导出表获取 GetProcAddress 的地址
接下来,我们详细看看在找到了 kernel32.dll 的基址之后,
该怎样获取 GetProcAddress 的地址。
kernel32.dll 是一个映像文件,也就是通常所说的 PE 文件。所以我们要做的也就是
遍历这个 PE 文件的导出表,找到 GetProcAddress。
我们现在已经获取到了 kernel32.dll 的加载基址,也就是 DOS 头的地址。
DOS 头中的 e_lfanew 是 NT 头的文件偏移地址。e_lfanew 的值加上之前我们定位到的
kernel32.dll 的基址,就是 NT 头在内存中的位置了。
下面的代码片段将 NT 头的内存地址放入 ebp 寄存器。
我们看看 NT 头的结构。
导出表的虚拟地址位于可选头的数据目录中的第一项。看下面,可以知道
导出表距离 NT 头起始位置的偏移,0x18 + 0x60,也就是 0x78。
反映到代码里也就是。
此时,edx 中便是数据目录的第一项了。
VirtualAddress 成员加上 kernel32.dll 的基址就是导出表在内存中的地址。
然而笔者在 WinDbg 里并没有找到导出目录(IMAGE_EXPORT_DIRECTORY)的定义。
翻了翻 PE 规范,发现那里有对导出目录的较为详细的描述。

不过幸运地是,笔者最后在 winnt.h 里找到了导出目录的定义。
导出目录的 AddressOfNames 成员, 偏移 32,也就是 0x20,
为导出名字指针表的相对虚拟地址。
下面的代码片段将导出名字指针表的内存地址放入 eax 寄存器。
接下来我们可以开始搜索函数名字,我们要找的是 GetProcAddress。
还是导出表,AddressOfNameOrdinals 成员,偏移 36,也就是 0x24,
为序号表的相对虚拟地址。
下面的代码片段将序号表的内存地址放入 esi 寄存器。
序号表与导出名字指针表一一对应。
这也就是我们为什么先在名字指针表里找到 GetProcAddress 的索引,
然后用这个索引在序号表里找序号,
最后用序号到导出地址表(相对虚拟地址位于导出目录的 AddressOfFunctions 成员)里找。
如下图(图片来自 PE 规范)。

值得说明的是,序号表是一个 word 数组,所以,一个成员两个字节。
OrdinalBase 通常为 1,所以在 shellcode 里就直接使用了硬编码的 1.
此时,我们要找的函数已经位于 esi 寄存器了。
做点什么呢,
不弹计算器,弹个 Hello World! 吧。
我们可以用已有的 kernel32.dll 的基址和 GetProcAddress 获取
LoadLibrary 的地址,然后加载 user32.dll,再从 user32.dll 中获取
MessageBox 的地址。
弹个窗就可以优雅地 ExitProcess 了。

如果想做其他的各种神奇的事情,自己写吧。
完整代码加注释,还有 shellcode 自动提取脚本(自动到生成测试用的 C 源文件),
在这里 https://github.com/NoviceLive/shellcoding。
结论
本文结合相关结构详细地阐述了利用 PEB->Ldr 获取 kernel32.dll
基址与搜索导出表定位函数的方法。
至于定位 kernel32.dll 的其他方法,就留给伙伴们自己去玩吧。
PS. 我们可不是脚本小子。
推荐阅读
2015 年 8 月
tl, dr
我们先理清总体的方向。动态定位 API 有如下几个重要步骤。
1. 获取 kernel32.dll 的加载基址。 2. 获取 GetProcAddress 的地址。
有了这两个地址,其他的函数(比如说 LoadLibrary)就都好办了。
接下来我们依次详细看看这两个目标是如何实现的。
代码
文中的所有代码均可在下面的仓库中找到。
除了文中的代码,该仓库还包含其他相关的代码与实用工具。
请前往 https://github.com/NoviceLive/shellcoding。
利用 PEB->Ldr 获取 kernel32.dll 的基址
Windows 内核模式使用 ETHREAD / KTHREAD、EPROCESS / KPROCESS
来描述线程、进程的结构,
他们在用户模式的类似结构就是 TEB 与 PEB。
不过 TEB、PEB 与他们的定义都在 ntoskrnl.exe 之中。
我们可以用 WinDbg 查看他们的定义。
lkd> dt nt!_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 /* tl, dr */
lkd> dt nt!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 SpareBool : UChar +0x004 Mutant : Ptr32 Void +0x008 ImageBaseAddress : Ptr32 Void +0x00c Ldr : Ptr32 _PEB_LDR_DATA /* tl, dr */
上面的片段截取自 Windows XP with SP3。
PEB(进程环境块)中包含加载模块的信息(Ldr 成员),其中就包括了 kernel32.dll。
PEB 本身则可以通过 TEB(线程环境块)中的 ProcessEnvironmentBlock 成员找到,
而 TEB 在 IA32 架构 Windows 位于 fs 段寄存器,在 AMD64 则位于 gs 段寄存器。
我们以 IA32 环境举例。
下面的代码片段将 PEB 的地址放入 ecx 寄存器。
xor ecx, ecx mov ecx, dword ptr fs:[ecx + 30h]
我们对照一下 PEB->Ldr 的结构,其中 InLoadOrderModuleList、InMemoryOrderModuleList 与
InInitializationOrderModuleList 依次为加载顺序模块列表、
内存顺序模块列表与初始化顺序模块列表。
lkd> dt nt!_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
LIST_ENTRY 包含两个指向 LIST_ENTRY 结构的指针,用来将同构或者异构的结构链接成(循环)双向链表。
lkd> dt nt!_LIST_ENTRY +0x000 Flink : Ptr32 _LIST_ENTRY +0x004 Blink : Ptr32 _LIST_ENTRY
我们使用初始化顺序模块列表来定位 kernel32.dll。
mov ecx, dword ptr [ecx + 0ch] mov ecx, dword ptr [ecx + 1ch]
这时, ecx 寄存器为 PEB->Ldr->InInitializationOrderModuleList,
他的 Flink 成员指向一个 LDR_DATA_TABLE_ENTRY 结构的 LIST_ENTRY 类型成员,
InInitializationOrderLinks。
顺着往前遍历,我们一定会遇见 kernel32.dll 的。
对照 LDR_DATA_TABLE_ENTRY 结构,
ecx + 8 就是 DllBase 了,不过这还不是 kernel32.dll 的基址。
lkd> dt nt!_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 /* tl, dr */
我们可以通过这个结构的 BaseDllName 成员来确定我们遍历到的结点是不是
kernel32.dll 结点。这是一个 UNICODE_STRING,注意一个字符是两个字节。
lkd> dt nt!_UNICODE_STRING +0x000 Length : Uint2B +0x002 MaximumLength : Uint2B +0x004 Buffer : Ptr32 Uint2B
BaseDllName 在 LDR_DATA_TABLE_ENTRY 中的偏移为 0x2c,
Buffer 在 UNICODE_STRING 中的偏移为 0x4,
那么,0x2c + 0x4 也就是 0x30,减去 0x10(读者想想,为什么?),
所以 edx + 20h 就是 UNICODE_STRING 的 Buffer。
判断 Buffer 的第 7 个字符是不是 '3',第 7 个字符的偏移为 (7 - 1) * 2,
也就是 12,或者 0xc。
如果是,说明我们找到了 kernel32.dll 的结点,如果不是,那就继续向前遍历。
代码片段如下。
find_kernel32_dll_base: mov ebx, dword ptr [ecx + 8] mov eax, dword ptr [ecx + 20h] mov ecx, dword ptr [ecx] cmp byte ptr [eax + 0ch], 33h jne find_kernel32_dll_base
搜索 kernel32.dll 导出表获取 GetProcAddress 的地址
接下来,我们详细看看在找到了 kernel32.dll 的基址之后,
该怎样获取 GetProcAddress 的地址。
kernel32.dll 是一个映像文件,也就是通常所说的 PE 文件。所以我们要做的也就是
遍历这个 PE 文件的导出表,找到 GetProcAddress。
我们现在已经获取到了 kernel32.dll 的加载基址,也就是 DOS 头的地址。
lkd> dt nt!_IMAGE_DOS_HEADER +0x000 e_magic : Uint2B +0x002 e_cblp : Uint2B +0x004 e_cp : Uint2B +0x006 e_crlc : Uint2B +0x008 e_cparhdr : Uint2B +0x00a e_minalloc : Uint2B +0x00c e_maxalloc : Uint2B +0x00e e_ss : Uint2B +0x010 e_sp : Uint2B +0x012 e_csum : Uint2B +0x014 e_ip : Uint2B +0x016 e_cs : Uint2B +0x018 e_lfarlc : Uint2B +0x01a e_ovno : Uint2B +0x01c e_res : [4] Uint2B +0x024 e_oemid : Uint2B +0x026 e_oeminfo : Uint2B +0x028 e_res2 : [10] Uint2B +0x03c e_lfanew : Int4B
DOS 头中的 e_lfanew 是 NT 头的文件偏移地址。e_lfanew 的值加上之前我们定位到的
kernel32.dll 的基址,就是 NT 头在内存中的位置了。
下面的代码片段将 NT 头的内存地址放入 ebp 寄存器。
mov ebp, ebx add ebp, dword ptr [ebp + 3ch]
我们看看 NT 头的结构。
lkd> dt nt!_IMAGE_NT_HEADERS +0x000 Signature : Uint4B +0x004 FileHeader : _IMAGE_FILE_HEADER +0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER
导出表的虚拟地址位于可选头的数据目录中的第一项。看下面,可以知道
导出表距离 NT 头起始位置的偏移,0x18 + 0x60,也就是 0x78。
lkd> dt nt!_IMAGE_OPTIONAL_HEADER +0x000 Magic : Uint2B +0x002 MajorLinkerVersion : UChar +0x003 MinorLinkerVersion : UChar +0x004 SizeOfCode : Uint4B +0x008 SizeOfInitializedData : Uint4B +0x00c SizeOfUninitializedData : Uint4B +0x010 AddressOfEntryPoint : Uint4B +0x014 BaseOfCode : Uint4B +0x018 BaseOfData : Uint4B +0x01c ImageBase : Uint4B +0x020 SectionAlignment : Uint4B +0x024 FileAlignment : Uint4B +0x028 MajorOperatingSystemVersion : Uint2B +0x02a MinorOperatingSystemVersion : Uint2B +0x02c MajorImageVersion : Uint2B +0x02e MinorImageVersion : Uint2B +0x030 MajorSubsystemVersion : Uint2B +0x032 MinorSubsystemVersion : Uint2B +0x034 Win32VersionValue : Uint4B +0x038 SizeOfImage : Uint4B +0x03c SizeOfHeaders : Uint4B +0x040 CheckSum : Uint4B +0x044 Subsystem : Uint2B +0x046 DllCharacteristics : Uint2B +0x048 SizeOfStackReserve : Uint4B +0x04c SizeOfStackCommit : Uint4B +0x050 SizeOfHeapReserve : Uint4B +0x054 SizeOfHeapCommit : Uint4B +0x058 LoaderFlags : Uint4B +0x05c NumberOfRvaAndSizes : Uint4B +0x060 DataDirectory : [16] _IMAGE_DATA_DIRECTORY
反映到代码里也就是。
mov ebp, dword ptr [ebp + 78h]
此时,edx 中便是数据目录的第一项了。
VirtualAddress 成员加上 kernel32.dll 的基址就是导出表在内存中的地址。
lkd> dt nt!_IMAGE_DATA_DIRECTORY +0x000 VirtualAddress : Uint4B +0x004 Size : Uint4B
add ebp, ebx
然而笔者在 WinDbg 里并没有找到导出目录(IMAGE_EXPORT_DIRECTORY)的定义。
翻了翻 PE 规范,发现那里有对导出目录的较为详细的描述。

不过幸运地是,笔者最后在 winnt.h 里找到了导出目录的定义。
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
导出目录的 AddressOfNames 成员, 偏移 32,也就是 0x20,
为导出名字指针表的相对虚拟地址。
下面的代码片段将导出名字指针表的内存地址放入 eax 寄存器。
mov eax, dword ptr [ebp + 20h] add eax, ebx
接下来我们可以开始搜索函数名字,我们要找的是 GetProcAddress。
xor edx, edx find_get_proc_address: mov esi, dword ptr [eax + edx * 4] add esi, ebx inc edx cmp dword ptr [esi], 'PteG' jne find_get_proc_address cmp dword ptr [esi + 4], 'Acor' jne find_get_proc_address
还是导出表,AddressOfNameOrdinals 成员,偏移 36,也就是 0x24,
为序号表的相对虚拟地址。
下面的代码片段将序号表的内存地址放入 esi 寄存器。
mov esi, dword ptr [ebp + 24h] add esi, ebx
序号表与导出名字指针表一一对应。
这也就是我们为什么先在名字指针表里找到 GetProcAddress 的索引,
然后用这个索引在序号表里找序号,
最后用序号到导出地址表(相对虚拟地址位于导出目录的 AddressOfFunctions 成员)里找。
如下图(图片来自 PE 规范)。

值得说明的是,序号表是一个 word 数组,所以,一个成员两个字节。
mov dx, word ptr [esi + edx * 2] mov esi, dword ptr [ebp + 1ch] add esi, ebx
OrdinalBase 通常为 1,所以在 shellcode 里就直接使用了硬编码的 1.
mov esi, dword ptr [esi + edx * 4 - 4] add esi, ebx
此时,我们要找的函数已经位于 esi 寄存器了。
做点什么呢,
不弹计算器,弹个 Hello World! 吧。
我们可以用已有的 kernel32.dll 的基址和 GetProcAddress 获取
LoadLibrary 的地址,然后加载 user32.dll,再从 user32.dll 中获取
MessageBox 的地址。
弹个窗就可以优雅地 ExitProcess 了。

如果想做其他的各种神奇的事情,自己写吧。
完整代码加注释,还有 shellcode 自动提取脚本(自动到生成测试用的 C 源文件),
在这里 https://github.com/NoviceLive/shellcoding。
结论
本文结合相关结构详细地阐述了利用 PEB->Ldr 获取 kernel32.dll
基址与搜索导出表定位函数的方法。
至于定位 kernel32.dll 的其他方法,就留给伙伴们自己去玩吧。
PS. 我们可不是脚本小子。
推荐阅读
1. [dt(Display Type)](https://msdn.microsoft.com/en-us/library/windows/hardware/ff542772%28v=vs.85%29.aspx) 2. [Any Windows Version - Messagebox Shellcode (113 bytes)](https://www.exploit-db.com/exploits/28996/) 3. [Allwin URLDownloadToFile + WinExec + ExitProcess Shellcode](https://www.exploit-db.com/exploits/24318/) 4. Windwos PE 权威指南,第 11 章,动态加载技术 5. Practical Reverse Engineering,第 3 章,The Windows Kernel 6. 0day 安全,第 1 篇,漏洞利用原理(初级)
赞赏
他的文章