0%
毅种循环

模块 54 - IAT 隐藏与混淆 - 自定义 GetModuleHandle

恶意软件开发课程 - IAT 隐藏与混淆 - 自定义 GetModuleHandle


模块 54 - IAT 隐藏与混淆 - 自定义 GetModuleHandle#

[!IMPORTANT] 本知识库声明

本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示实践心得

  1. 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
  2. 版权归属:原始知识产权归原作者/官方所有。
  3. 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程
  4. 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。

IAT 隐藏与混淆 - 自定义 GetModuleHandle#

简介#

GetModuleHandle 函数用于检索指定 DLL 的句柄。如果该 DLL 存在于调用进程中,函数返回其句柄;否则返回 NULL

在本模块中,我们将实现一个名为 GetModuleHandleReplacement 的函数来替代这个系统 API。其函数原型如下:

HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName){}
c

GetModuleHandle 的工作原理#

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. 方法 1:通过 __readfsdword(0x18) 获取 TEB,再访问 PEB。
  2. 方法 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;
c

LIST_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

实现逻辑总结#

  1. 获取 PEB。
  2. 从 PEB 获取 Ldr 成员。
  3. 获取链表中的第一个元素。
  4. 遍历链表,直到找到匹配的 DLL 名称。

💡 初学者提示:链表遍历的“秘密”

链表的遍历逻辑 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] 中。

演示#

运行该函数,它会遍历 Ldr 链表并成功找到指定 DLL 的基地址。

图片
图片


🎯 总结#

在本模块中,我们学习了:

  1. PEB/TEB 访问:掌握了利用寄存器(GS/FS)直接在内存中定位内核结构的方法。
  2. Ldr 链表枚举:理解了 Windows 如何在内存中维护已加载模块的清单。
  3. 高度隐匿:由于我们是直接在大脑(PEB)中查询名单,而不是去问保安(调用 GetModuleHandle),因此实现了极佳的反分析效果。

💡 关键要点

  • 直接内存访问:这是绕过 Hook 的高级技巧。如果杀软 Hook 了 GetModuleHandle API,我们的手动遍历方案依然有效。
  • 跨平台兼容:代码通过宏处理了 32 位和 64 位的差异。

📚 下一步学习

下一个模块将介绍 IAT Hiding & Obfuscation - API Hashing。我们将把前几个模块学到的技巧融会贯通:先手动找 DLL 基址,再在 DLL 里手动找函数,最后用哈希隐匿一切!