模块 54 - IAT 隐藏与混淆 - 自定义 GetModuleHandle
恶意软件开发课程 - IAT 隐藏与混淆 - 自定义 GetModuleHandle
模块 54 - IAT 隐藏与混淆 - 自定义 GetModuleHandle#
[!IMPORTANT] 本知识库声明
本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示与实践心得。
- 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
- 版权归属:原始知识产权归原作者/官方所有。
- 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程。
- 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。
IAT 隐藏与混淆 - 自定义 GetModuleHandle#
简介#
GetModuleHandle 函数用于检索指定 DLL 的句柄。如果该 DLL 存在于调用进程中,函数返回其句柄;否则返回 NULL。
在本模块中,我们将实现一个名为 GetModuleHandleReplacement 的函数来替代这个系统 API。其函数原型如下:
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName){}cGetModuleHandle 的工作原理#
HMODULE 数据类型本质上是加载的 DLL 的 基地址 (Base Address)。因此,我们替代函数的目标就是找到指定 DLL 在进程地址空间中的起始位置。
进程环境块 (Process Environment Block, PEB) 包含了关于已加载 DLL 的信息,特别是 PEB 结构中的 PEB_LDR_DATA Ldr 成员。因此,第一步就是通过 PEB 结构访问这个成员。
64 位系统中的 PEB#
回想一下,指向 PEB 结构的指针位于 线程环境块 (Thread Environment Block, TEB) 结构中。

在 64 位系统中,指向 TEB 结构的指针偏移量存储在 GS 寄存器中。

方法 1:获取 PEB (64 位)#
第一种方法是先获取 TEB 结构,然后再获取指向 PEB 的指针。在 Visual Studio 中可以使用 __readgsqword(0x30) 宏,它从 GS 寄存器读取 0x30 字节以到达 TEB 指针。
// 方法 1
PTEB pTeb = (PTEB)__readgsqword(0x30);
PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;c方法 2:直接获取 PEB (64 位)#
这种方法直接跳过 TEB 结构,使用 __readgsqword(0x60) 宏读取 0x60 字节。这是因为在 TEB 结构中,ProcessEnvironmentBlock 成员正好位于偏移量 0x60 (96 字节) 处。
// 方法 2
PPEB pPeb2 = (PPEB)(__readgsqword(0x60));c
32 位系统中的 PEB#
在 32 位系统中,TEB 的指针偏移量存储在 FS 寄存器中。

获取 PEB (32 位)#
同样有两种方法:
- 方法 1:通过
__readfsdword(0x18)获取 TEB,再访问 PEB。 - 方法 2:通过
__readfsdword(0x30)直接获取 PEB(偏移量为0x30或 48 字节)。
枚举 DLL#
获取 PEB 结构后,下一步是访问 PEB_LDR_DATA Ldr 成员。这个成员包含了进程中所有已加载 DLL 的详细信息。
PEB_LDR_DATA 结构#
这个结构体中最核心的成员是 LIST_ENTRY InMemoryOrderModuleList。
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList; // 内存加载顺序的模块列表
} PEB_LDR_DATA, *PPEB_LDR_DATA;cLIST_ENTRY 结构#
LIST_ENTRY 是一个 双向链表。它包含 Flink (Forward Link,指向下一个节点) 和 Blink (Backward Link,指向前一个节点)。要开始遍历,我们从 InMemoryOrderModuleList.Flink 开始。
根据微软的定义,列表中的每个项目都是指向 LDR_DATA_TABLE_ENTRY 结构的指针。
LDR_DATA_TABLE_ENTRY 结构#
每个 LDR_DATA_TABLE_ENTRY 都代表一个唯一的已加载 DLL。
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks; // 内存加载顺序链接
PVOID Reserved2[2];
PVOID DllBase; // DLL 的基地址 (我们要找的东西!)
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName; // 包含加载模块全路径的文件名
BYTE Reserved4[8];
PVOID Reserved5[3];
// ...
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;c实现逻辑总结#
- 获取 PEB。
- 从 PEB 获取 Ldr 成员。
- 获取链表中的第一个元素。
- 遍历链表,直到找到匹配的 DLL 名称。
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {
// 1. 根据系统位数获取 PEB
#ifdef _WIN64
PPEB pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
PPEB pPeb = (PEB*)(__readfsdword(0x30));
#endif
// 2. 获取 Ldr
PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)(pPeb->Ldr);
// 3. 获取链表第一个元素 (DTE)
PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
// 4. 开始遍历
while (pDte) {
// ... 检查名称逻辑 ...
// 移动到链表的下一个节点
// 这里需要通过解引用获取下一个 LIST_ENTRY 的地址
pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}
} c💡 初学者提示:链表遍历的“秘密”
链表的遍历逻辑
pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte)看起来很玄学。 其实很简单:LDR_DATA_TABLE_ENTRY结构体的第一个成员(在某些定义中)或InMemoryOrderLinks成员本质上就是LIST_ENTRY。 我们把当前节点的内容解释为一个新指针,就相当于顺着“下一个”这个箭头跳到了下一个节点。
处理 DLL 名称的大小写#
在 Windows 中,有些 DLL 名称是大写的(如 KERNEL32.DLL),有些是小写的。如果我们直接使用 wcscmp 比较,可能会因为大小写不匹配而失败。
我们需要一个辅助函数 IsStringEqual,将两个字符串都转为小写后再进行比较。
BOOL IsStringEqual (IN LPCWSTR Str1, IN LPCWSTR Str2) {
WCHAR lStr1[MAX_PATH], lStr2[MAX_PATH];
int len1 = lstrlenW(Str1), len2 = lstrlenW(Str2);
if (len1 >= MAX_PATH || len2 >= MAX_PATH) return FALSE;
// 转小写比较
for (int i = 0; i < len1; i++) lStr1[i] = (WCHAR)tolower(Str1[i]);
lStr1[len1] = L'\0';
for (int j = 0; j < len2; j++) lStr2[j] = (WCHAR)tolower(Str2[j]);
lStr2[len2] = L'\0';
return (lstrcmpiW(lStr1, lStr2) == 0);
}c获取 DLL 基地址#
根据不同的文档定义,LDR_DATA_TABLE_ENTRY 中基地址的位置可能被称为 DllBase 或存储在 Reserved2[0] 中。
HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {
// ... 获取 PEB 和 Ldr ...
PLDR_DATA_TABLE_ENTRY pDte = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
while (pDte) {
if (pDte->FullDllName.Length != NULL) {
if (IsStringEqual(pDte->FullDllName.Buffer, szModuleName)) {
// 对应不同的结构体定义返回基址
#ifdef STRUCTS
return (HMODULE)(pDte->InInitializationOrderLinks.Flink);
#else
return (HMODULE)pDte->Reserved2[0];
#endif
}
} else {
break;
}
pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
}
return NULL;
}c演示#
运行该函数,它会遍历 Ldr 链表并成功找到指定 DLL 的基地址。

🎯 总结#
在本模块中,我们学习了:
- PEB/TEB 访问:掌握了利用寄存器(GS/FS)直接在内存中定位内核结构的方法。
- Ldr 链表枚举:理解了 Windows 如何在内存中维护已加载模块的清单。
- 高度隐匿:由于我们是直接在大脑(PEB)中查询名单,而不是去问保安(调用
GetModuleHandle),因此实现了极佳的反分析效果。
💡 关键要点
- 直接内存访问:这是绕过 Hook 的高级技巧。如果杀软 Hook 了
GetModuleHandleAPI,我们的手动遍历方案依然有效。- 跨平台兼容:代码通过宏处理了 32 位和 64 位的差异。
📚 下一步学习
下一个模块将介绍 IAT Hiding & Obfuscation - API Hashing。我们将把前几个模块学到的技巧融会贯通:先手动找 DLL 基址,再在 DLL 里手动找函数,最后用哈希隐匿一切!