大叫好,你们可以叫我凌,是个16岁的网络安全学习者。
今天,延续上篇文章内容,正式讲解本栏目内容。今天讲的是如何不使用WindowsAPI来定位kernel32.dll文件。
不使用WindowsAPI定位的缘由
如果要调用WindowsAPI,注入器时需要调用 GetModuleHandleA("kernel32.dll") 来获取基址,但有条注意事项:杀软/EDR的Hook,本质上就是在 GetModuleHandle 的代码入口处插了双眼睛。
只要程序调用了 GetModuleHandle ,EDR会立刻记录:
➢ 我们访问了哪个DLL?
➢ 我们的调用栈是怎样的?
➢ 我们紧接着用它做了什么?
显而易见,这样子在之后的操作中,那报毒是说来就来,极为容易被发现,大大降低了隐蔽性。
解决办法就是绕过这个函数,直接去内存里把它要返回的那个值手工抄出来。 而这个"值"(kernel32.dll 的基址),就记录在Windows内核为每个进程维护的一张"户口本" 里------这个户口本的名字,叫 PEB(进程环境块)。
以下是我利用WindowsAPI编写的简易进程注入程序,可以看出没有丝毫隐私:

>>> 详细代码见 这里
涉及的核心结构体/宏
那在开始之前,我们得先理解其中涉及到的核心结构体以及相关宏,那我们直接来看下吧。
UNICODE_STRING(Windows 内核通用字符串结构)
cpp
typedef struct _UNICODE_STRING {
USHORT Length; // 字符串当前长度(字节数,不含结尾 '\0')
USHORT MaximumLength; // 缓冲区总容量(字节数)
PWSTR Buffer; // 指向宽字符数组的指针
} UNICODE_STRING;
> 注意:Length是字节数,转换成字符数需Length / 2(宽字符wchar_t占2字节)。
LIST_ENTRY(双向链表节点)
cpp
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY Flink; // 指向下一个节点(Forward Link)
struct _LIST_ENTRY Blink; // 指向上一个节点(Backward Link)
} LIST_ENTRY;
> Flink:前进方向(从头部向后遍历时使用)
> Blink:后退方向
PEB 关键字段(进程环境块)
cpp
typedef struct _PEB {
BYTE Reserved1[0x18]; // 0x00~0x17 保留/填充
PPEB_LDR_DATA Ldr; // 0x18 ← 指向加载器数据结构的指针
// ... 后续字段省略
} PEB;
PEB_LDR_DATA 关键字段(加载器数据)
cpp
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 关键字段
cpp
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(核心宏,反推结构体首地址)
cpp
CONTAINING_RECORD(pEntry, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)
作用:通过结构体成员的地址反推整个结构体的起始地址。
数学原理:结构体首地址 = 成员地址 - 成员在结构体内的偏移量
实现原理(宏展开)
cpp
((LDR_DATA_TABLE_ENTRY)((PCHAR)(pEntry) - (ULONG_PTR)(&((LDR_DATA_TABLE_ENTRY)0)->InMemoryOrderLinks)))
__readgsqword(编译器内联函数)
cpp
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代码都是如何写的吧
代码示例 >>> 基于偏移量
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;
}
}
代码示例 >>> 基于结构体
cpp
#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内容,因为我本身也是将本栏目作为笔记存放,所以就没有那么详细。