第一章:PEB结构解析与内存定位

大叫好,你们可以叫我凌,是个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内容,因为我本身也是将本栏目作为笔记存放,所以就没有那么详细。

>>> 具体内容见这里(Github仓库)

相关推荐
古养摩登原始人1 小时前
脑机接口合规最新发展:从政策到实践,构建安全可控的技术生态
人工智能·安全
Multipath7122 小时前
乾元通基于原生UDP时隙感知的多链路聚合方案
网络·网络协议·5g·安全·udp·智能路由器·实时音视频
liuyunshengsir3 小时前
Codex Harness 安全沙箱机制原理:AI 编程代理如何安全地执行命令
人工智能·安全
江湖人称菠萝包5 小时前
【Windows】《深入浅出Windows API程序设计:编程基础篇》笔记-Chapter1-基础知识
windows·笔记
LorryJovens5 小时前
【LAAP论文】Chat GPT6来临:当一个强认知框架下的harness工程遇到强模型的安全对齐应对的调查研究
安全
白山编程大哥5 小时前
Java 迭代器接口详解:从 Iterator 到 ListIterator 的完整指南
java·开发语言·windows
一叶龙洲5 小时前
Ubuntu从图标到卸载应用
linux·windows·ubuntu
江湖人称菠萝包5 小时前
【Windows】《深入浅出Windows API程序设计:编程基础篇》笔记-Chapter2-Windows窗口程序
windows·笔记
Flynt5 小时前
Chrome 修了个零日漏洞,你的 VS Code 和 Cursor 可能还在裸奔
安全·electron·visual studio code