大家好,你们可以叫我凌,是个16岁的网络安全学习者。
今天,延续上篇文章内容,正式讲解本栏目内容。今天讲的是如何不使用WindowsAPI来定位kernel32.dll文件。
不使用WindowsAPI定位的缘由
如果要调用WindowsAPI,注入器时需要调用 GetModuleHandleA("kernel32.dll") 来获取基址,但有条注意事项:杀软/EDR的Hook,本质上就是在 GetModuleHandle 的代码入口处插了双眼睛。
只要程序调用了 GetModuleHandle ,EDR会立刻记录:
➢ 我们访问了哪个DLL?
➢ 我们的调用栈是怎样的?
➢ 我们紧接着用它做了什么?
显而易见,这样子在之后的操作中,那报毒是说来就来,极为容易被发现,大大降低了隐蔽性。
解决办法就是绕过这个函数,直接去内存里把它要返回的那个值手工抄出来。 而这个“值”(kernel32.dll 的基址),就记录在Windows内核为每个进程维护的一张“户口本” 里——这个户口本的名字,叫 PEB(进程环境块)。
以下是我利用WindowsAPI编写的简易进程注入程序,可以看出没有丝毫隐私:
>>> 详细代码见 这里
涉及的核心结构体/宏
那在开始之前,我们得先理解其中涉及到的核心结构体以及相关宏,那我们直接来看下吧。
UNICODE_STRING(Windows 内核通用字符串结构)
typedef struct _UNICODE_STRING { USHORT Length; // 字符串当前长度(字节数,不含结尾 '\0') USHORT MaximumLength; // 缓冲区总容量(字节数) PWSTR Buffer; // 指向宽字符数组的指针 } UNICODE_STRING;> 注意:Length是字节数,转换成字符数需Length / 2(宽字符wchar_t占2字节)。
LIST_ENTRY(双向链表节点)
typedef struct _LIST_ENTRY { struct _LIST_ENTRY Flink; // 指向下一个节点(Forward Link) struct _LIST_ENTRY Blink; // 指向上一个节点(Backward Link) } LIST_ENTRY;> Flink:前进方向(从头部向后遍历时使用)
> Blink:后退方向
PEB 关键字段(进程环境块)
typedef struct _PEB { BYTE Reserved1[0x18]; // 0x00~0x17 保留/填充 PPEB_LDR_DATA Ldr; // 0x18 ← 指向加载器数据结构的指针 // ... 后续字段省略 } PEB;PEB_LDR_DATA 关键字段(加载器数据)
typedef struct _PEB_LDR_DATA { ULONG Length; // 0x00 BOOLEAN Initialized; // 0x04 PVOID SsHandle; // 0x08 LIST_ENTRY InLoadOrderModuleList; // 0x10 LIST_ENTRY InMemoryOrderModuleList; // 0x20 ← 我们遍历的链表头 LIST_ENTRY InInitializationOrderModuleList; // 0x30 } PEB_LDR_DATA;LDR_DATA_TABLE_ENTRY 关键字段
struct MY_LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; // 0x00 LIST_ENTRY InMemoryOrderLinks; // 0x10 ← 链表节点(指向前后模块) LIST_ENTRY InInitializationOrderLinks;// 0x20 PVOID DllBase; // 0x30 ← 模块基址(核心输出) PVOID EntryPoint; // 0x38 ULONG SizeOfImage; // 0x40 UNICODE_STRING FullDllName; // 0x48 完整路径 UNICODE_STRING BaseDllName; // 0x58 文件名(如 L"kernel32.dll") // 后续字段省略(不影响遍历) };关键字段说明:
> DllBase:模块在进程内存中的起始地址。
> BaseDllName:模块名称(不含路径),用于匹配目标 DLL。
CONTAINING_RECORD(核心宏,反推结构体首地址)
CONTAINING_RECORD(pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)作用:通过结构体成员的地址反推整个结构体的起始地址。
数学原理:结构体首地址 = 成员地址 - 成员在结构体内的偏移量
实现原理(宏展开)
((LDR_DATA_TABLE_ENTRY)((PCHAR)(pEntry) - (ULONG_PTR)(&((LDR_DATA_TABLE_ENTRY)0)->InMemoryOrderLinks)))__readgsqword(编译器内联函数)
uintptr_t pPeb = __readgsqword(0x60);作用:读取gs段寄存器偏移0x60处的8字节(QWORD)数据。
为什么是 gs:x64下,gs段寄存器固定指向当前线程的TEB(线程环境块)。
为什么是0x60:TEB 偏移0x60处存储着当前进程的PEB指针。
具体的结构体和宏我就不过多赘述了,因为这本身网络上内容就丰富,可以自行查找。
所以,我们就先直接跳转到完整利用链来看看。
完整的利用链(内存寻址链路图)
硬件层
│
├── gs 段寄存器(x64)
│ │
│ └── 指向当前线程的 TEB(线程环境块)
│ │
│ └── [TEB + 0x60] → PEB(进程环境块)
│ │
│ └── [PEB + 0x18] → PEB_LDR_DATA(加载器数据)
│ │
│ └── [Ldr + 0x20] → InMemoryOrderModuleList 链表头(LIST_ENTRY)
│ │
│ └── 遍历头节点的 Flink
│ │
│ ▼
│ 第 1 个 LDR_DATA_TABLE_ENTRY
│ ├── InMemoryOrderLinks → LIST_ENTRY 节点
│ ├── [偏移 0x30] DllBase → 模块基址
│ └── [偏移 0x58] BaseDllName → 模块名
│ │
│ ▼
│ 第 2 个 LDR_DATA_TABLE_ENTRY
│ └── ...(以此类推)
│
└── 遍历直至 Flink 重新指向链表头(循环结束)
关键步骤文字总结:
➢ gs:[0x60] → 拿到PEB指针。
➢ PEB + 0x18 → 拿到Ldr(PEB_LDR_DATA)。
➢ Ldr + 0x20 → 拿到InMemoryOrderModuleList的头部(这是个LIST_ENTRY结构)。
➢ ListHead->Flink → 取出第一个实际模块的 LIST_ENTRY 节点。
➢ CONTAINING_RECORD → 通过该 LIST_ENTRY 节点,反推出完整的 LDR_DATA_TABLE_ENTRY。
➢ 读取 Entry->DllBase(偏移 0x30)获取基址;读取 Entry->BaseDllName.Buffer(偏 移 0x58)获取模块名。
➢ 继续 Flink 遍历,直到回到链表头。
核心偏移量速查表(Win10/11 x64)
> 提醒:这些偏移量在 Windows 10 22H2 / Windows 11 22H2 x64 上长期稳定。但不同 Windows 版本(如 Win7、 Win8)可能存在细微差异(偏移 ± 0x8)。
代码演示
那看了这么多,我们直接来看现成的CPP代码都是如何写的吧
代码示例 >>> 基于偏移量
#include <windows.h> #include <iostream> #include <cwchar> int main() { uintptr_t pPeb = __readgsqword(0x60); uintptr_t pLdr = *(uintptr_t*)(pPeb + 0x18); uintptr_t pHead = pLdr + 0x20; // PEB_LDR_DATA.InMemoryOrderModuleList uintptr_t pEntry = *(uintptr_t*)pHead; while (pEntry != pHead) { uintptr_t pModule = pEntry - 0x10; // LDR_DATA_TABLE_ENTRY.InMemoryOrderLinks uintptr_t dllBase = *(uintptr_t*)(pModule + 0x30); if (dllBase == 0) { pEntry = *(uintptr_t*)pEntry; continue; } USHORT nameLen = *(USHORT*)(pModule + 0x58); // BaseDllName.Length wchar_t* namePtr = *(wchar_t**)(pModule + 0x60); // BaseDllName.Buffer if (namePtr && nameLen > 0) { std::wstring dllName(namePtr, nameLen / 2); if (_wcsicmp(dllName.c_str(), L"kernel32.dll") == 0) { std::wcout << L"[+] kernel32.dll: 0x" << std::hex << dllBase << std::dec << std::endl; } else if (_wcsicmp(dllName.c_str(), L"ntdll.dll") == 0) { std::wcout << L"[+] ntdll.dll: 0x" << std::hex << dllBase << std::dec << std::endl; } } pEntry = *(uintptr_t*)pEntry; } }代码示例 >>> 基于结构体
#include <windows.h> #include <iostream> #include <iomanip> #include <string> // 重定义UNICODE_STRING:是Windows内核中表示Unicode字符串的结构体 struct MY_UNICODE_STRING{ USHORT Length; // 字符串长度(字节数) USHORT MaximumLength; // 缓冲区大小 PWSTR Buffer; // 宽字符串指针 }; // 重定义LDR_DATA_TABLE_ENTRY:是PEB中Ldr模块链表的节点结构体 struct MY_LDR_DATA_TABLE_ENTRY{ LIST_ENTRY InLoadOrderLinks; // 0x00 LIST_ENTRY InMemoryOrderLinks; // 0x10 ← 遍历的链表节点 LIST_ENTRY InInitializationOrderLinks;// 0x20 PVOID DllBase; // 0x30 模块基址 PVOID EntryPoint; // 0x38 ULONG SizeOfImage; // 0x40 MY_UNICODE_STRING FullDllName; // 0x48 完整路径 MY_UNICODE_STRING BaseDllName; // 0x58 文件名(不含路径) // 后续字段省略... }; int main(){ // 获取PEB地址 uintptr_t pPeb = __readgsqword(0x60); // 获取Ldr结构指针 uintptr_t pLdr = *(uintptr_t*)(pPeb + 0x18); // 获取InMemoryOrderModuleList链表头 LIST_ENTRY* pListHead = (LIST_ENTRY*)(pLdr + 0x20); LIST_ENTRY* pEntry = pListHead->Flink; // 首个节点 while(pEntry != pListHead){ // 使用 CONTAINING_RECORD 反推结构体首地址 MY_LDR_DATA_TABLE_ENTRY* pModule = CONTAINING_RECORD(pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); // 读取模块基址 uintptr_t dllBase = (uintptr_t)pModule->DllBase; if(dllBase == 0){ pEntry = pEntry->Flink; continue; } // 读取模块文件名 MY_UNICODE_STRING* pName = &pModule->BaseDllName; if(pName->Buffer && pName->Length > 0){ // 将宽字符串转换为std::wstring std::wstring dllName(pName->Buffer, pName->Length / 2); if(_wcsicmp(dllName.c_str(), L"kernel32.dll") == 0){ std::cout << "[+] kernel32.dll 基址: 0x" << std::hex << dllBase << std::dec << std::endl;} else if(_wcsicmp(dllName.c_str(), L"ntdll.dll") == 0){ std::cout << "[+] ntdll.dll 基址: 0x" << std::hex << dllBase << std::dec << std::endl; } pEntry = pEntry->Flink; } } }运行出来应该是以下内容:
[+] ntdll.dll 基址: 0x7fface5a0000
[+] kernel32.dll 基址: 0x7ffacdb00000
以下为代码里的“间接调用链”映射到硬件的过程:
CPU 寄存器 (gs)
│
├── 读取 gs:[0x60] → 得到 PEB 虚拟地址 (例如 0x00007FFE0000)
│
├── 读取 PEB+0x18 → 得到 Ldr 虚拟地址 (例如 0x00007FFE1000)
│
├── 读取 Ldr+0x20 → 得到链表头 ListHead 地址
│
├── 读取 ListHead->Flink → 获取第一个节点指针
│
├── 通过 CONTAINING_RECORD 得到 LDR_DATA_TABLE_ENTRY 起始地址
│
├── 读取 +0x30 得到 DllBase
└── 读取 +0x58 得到 BaseDllName.Buffer
好了,今天就先到这里了,其余的我就不进行过多赘述了。具体内容可以自行查找了解或看Github内容,因为我本身也是将本栏目作为笔记存放,所以就没有那么详细。
>>> 具体内容见这里(Github仓库)