你将学到什么:
-
WinAPI函数手册位置及汇编代码
-
PEB 结构和 PEB_LDR_DATA
-
PE文件结构
-
相对虚拟地址计算
-
导出地址表 (EAT)
-
Windows x64 调用约定实践
-
像真正的 Giga-Chad 一样用汇编语言写作……
shellcode 有什么限制?
Shellcode 必须与位置无关。它不能假设任何固定地址。因此,shellcode 无法访问我们通常用一行代码在 C 中执行的函数。Shellcode 必须在任何地方都能正常工作,没有任何依赖关系!
上述陈述使我们得出一个显而易见的结论:在 shellcode 中我们不能简单地使用GetProcAddress()和获取任何 WinAPI 函数的地址……因为我们不知道GetProcAddress()函数本身的地址。
WinExec()在这篇文章中,我们将研究手动查找函数地址kernel32.dll并执行calc.exe程序(Windows 内置计算器)以确认一切正常。
概述:如何手动查找 WinAPI 函数?
所有基本的 WinAPI 函数都可以在这个kernel32.dll文件中找到。这个模块会自动加载到 Windows 中每个新创建的进程内存中。但是,模块在内存中的基地址kernel32.dll可能是随机的。
Windows 上的每个进程还在其内存中包含一个PEB(进程环境块)结构。该结构的地址是已知的,一切都从它开始。该结构包含有关进程的大量信息,包括有关所有已加载模块(包括kernel32.dll)的数据。我们可以在其中找到kernel32.dll模块的基内存地址等信息。
然后,通过读取 PE 文件的结构(kernel32.dll),我们得到导出地址表,其中包含文件导出的所有函数的名称和地址。
这样我们就找到了 WinAPI 函数的地址(WinExec)。然后我们就可以根据 x64 调用约定和 Microsoft 的文档来执行它。
简而言之,这就是整个过程:跳过内存结构和指针来寻找我们的函数。让我们看看它的具体内容…
获取PEB结构地址
第一步是找到 PEB 结构的地址。PEB (进程环境块)包含有关当前进程的大量信息。PEB 的关键属性包括:
-
已加载模块(LDR字段:我们想要这个!)
-
环境变量
-
命令行参数
-
有关该过程的其他信息
PEB 结构存储在进程的用户空间中。这意味着,无需任何系统调用即可手动读取它。对于 x64 架构,PEB 地址存储在gs寄存器 + 0x60 偏移量中。fs和gs段寄存器没有硬件定义的特定用途,因此在这种情况下,Windows 内部使用它们来保存重要地址。
mov rbx, gs:[0x60] ; Get address of PEB struct
获取PEB_LDR_DATA的地址
PEB_LDR_DATA(PEB 加载器数据)包含有关进程已加载模块的信息。我们需要访问此结构来获取地址kernel32.dll。
typedef struct _PEB {
BYTE Reserved1[2]; // 2 bytes
BYTE BeingDebugged; // 1 byte
BYTE Reserved2[1]; // 1 byte
PVOID Reserved3[2]; // 16 bytes
PPEB_LDR_DATA Ldr; // <-- We want this
// ...
}
从 PEB 结构的开头到该LDR字段有 20 个字节。然而,事实并非如此!这一切都是因为一种称为数据结构对齐的编译现象。在 64 位 Windows 上,内存结构的对齐通常为 16 个字节。在这种情况下这并不重要。但重要的是 64 位指针与 8 字节边界对齐。这意味着,内存中指针的地址不能不同于的乘积0x8。让我们数一下PVOID Reserved3字段前的字节数:4 个字节!Reserved4指针必须与 4 个字节对齐才能将其地址四舍五入为0x8字节。阅读有关数据结构对齐的更多信息。
最终的 PEB 结构如下所示(包含填充):
struct _PEB {
BYTE Reserved1[2]; // 2 bytes
BYTE BeingDebugged; // 1 byte
BYTE Reserved2[1]; // 1 byte
BYTE Padding[4]; // 4 bytes
PVOID Reserved3[2]; // 16 bytes
PPEB_LDR_DATA Ldr; // <-- We want this
// ...
};
现在我们可以清楚地看到,我们需要 24 个字节(0x18)来获取 LDR 字段。我们通过取消引用(方括号)来提取该字段的值:
mov rbx, [rbx+0x18] ; Get PEB_LDR_DATA address
获取已加载模块的地址
现在我们有了 PEB_LDR_DATA 结构,我们需要该字段的地址InMemoryOrderModuleList:
struct _PEB_LDR_DATA {
BYTE Reserved1[8]; // 8 bytes
PVOID Reserved2[3]; // 24 bytes
LIST_ENTRY InMemoryOrderModuleList;
};
add rbx, 0x20 ; Get address of InMemoryOrderModuleList
LIST_ENTRY实际上是双链表。第一个字段(我们刚刚提取的字段)是指向下一个列表条目的指针。通过取消引用地址,我们可以获取列表中的各个项目。沿着双链表向下走:
mov rbx, [rbx] ; 1st entry in InMemoryOrderModuleList (ntdll.dll)
mov rbx, [rbx] ; 2st entry in InMemoryOrderModuleList (kernelbase.dll)
mov rbx, [rbx] ; 3st entry in InMemoryOrderModuleList (kernel32.dll)
第三个条目是kernel32.dll。我不确定这是否有保证,但几个世纪以来人们一直都是这样做的。我有什么资格质疑这一点……
struct _LDR_DATA_TABLE_ENTRY {
..
LIST_ENTRY InMemoryOrderLinks; // 16 bytes
PVOID Reserved2[2]; // 16 bytes
PVOID DllBase;
...
};
当我们沿着双向链表向下移动时(LIST_ENTRY),我们已经处于结构开头的偏移量。现在我们必须获取指针DllBase。DllBase是内存中 DLL 的地址!。偏移量为 32 字节(0x20):
mov r8, [rbx+0x20] ; Get the kernel32.dll address
现在我们有了kernel32.dll基地址。
获取ExportTable(kernel32.dll)的地址
我们需要进入ExportTable模块kernel32.dll来获取有关其导出的 WinAPI 函数的信息。
PE文件结构(简化):
-
IMAGE_DOS_HEADER(我们需要获取e_lfanew RVA)
-
DOS Stub(跳过此部分)
-
PE 头(kernel32.dll基地址 + e_lfanew RVA)
-
ExportTable(PE Headers addr 的偏移量 = 0x70)
这是我们需要走的路:
让我们看一下IMAGE_DOS_HEADER。它是任何 PE 文件的第一个结构:
typedef struct _IMAGE_DOS_HEADER { // DOS Header
WORD e_magic; // Magic number (2)
WORD e_cblp; // Bytes on last page of file (2)
WORD e_cp; // Pages in file (2)
WORD e_crlc; // Relocations (2)
WORD e_cparhdr; // Size of header in paragraphs (2)
WORD e_minalloc; // Minimum extra paragraphs needed (2)
WORD e_maxalloc; // Maximum extra paragraphs needed (2)
WORD e_ss; // Initial (relative) SS value (2)
WORD e_sp; // Initial SP value (2)
WORD e_csum; // Checksum (2)
WORD e_ip; // Initial IP value (2)
WORD e_cs; // Initial (relative) CS value (2)
WORD e_lfarlc; // File address of relocation table (2)
WORD e_ovno; // Overlay number (2)
WORD e_res[4]; // Reserved words (8)
WORD e_oemid; // OEM identifier (for e_oeminfo) (2)
WORD e_oeminfo; // OEM information; e_oemid specific (2)
WORD e_res2[10]; // Reserved words (20)
LONG e_lfanew; // File address of new exe header (4)
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
我们需要获取字段的值。它包含 PE Headers(或新 EXE Headers e_lfanew )的 RVA 。
相对虚拟地址:PE 文件结构中的许多地址都以相对虚拟地址(RVA)的形式书写。这意味着它们相对于内存中文件的开头(基地址)。要计算(绝对)虚拟地址,我们需要将 RVA 地址添加到基地址kernel32.dll。
mov ebx, [r8+0x3c] ; RBX = kernel32.IMAGE_DOS_HEADER.e_lfanew (PE hdrs offset)
add rbx, r8 ; RBX = PeHeaders offset + &kernel32.dll = &PeHeaders
现在rbx存储 PE Headers 的地址。在0x88 PE Headers 的偏移处ExportTable RVA放置了。它是一个常量值。使用 ExportTable RVA 和kernel32.dll基址,我们就可以访问了ExportTable。
xor rcx, rcx
add cx, 0x88 ; RCX = 0x88 (offset of ExportTable RVA)
add rbx, [rbx+rcx] ; RBX = &PeHeaders + offset of ExportTable RVA = ExportTable RVA
add rbx, r8 ; RBX = ExportTable RVA + &kernel32.dll = &ExportTable
mov r9, rbx ; R9 = &ExportTable
从EAT获取WinAPI函数地址
现在我们有了 EAT 结构的地址。此结构包含有关导出函数的所有信息。我们想使用此结构找到 WinAPI 函数的地址WinExec()。
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA
DWORD AddressOfNames; // RVA
DWORD AddressOfNameOrdinals; // RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
在开始搜索之前,我们需要保存包含我们要查找的 WinAPI 函数名称的字符串。我们无法按照传统方式在节read-only data或类似的地方执行此操作,因为我们只有节.text。我们必须将所有内容放在堆栈上!
堆栈向下增长,地址向上读取,所以我们必须将字符串倒置(WinExec ->