# 绕过用户态Hooking

在上个小节，我们讨论了 EDR 在用户态设置 Hook 的原理，那么相应地，我们可以根据这原理寻找间隙，实现对用户态 Hook 的绕过。截至目前，已经有多种方法绕过 Hook。不过，Hook 并非 EDR 的全部检测能力的来源，因此绕过 Hook 的这个过程本身可能就会被检测为恶意。不过无论如何，在这个小节，我们会过一下常见的一些用于绕过 Hook 的方法，以及它们的 IOC。在下个小节，我们会继续探讨绕过用户态 Hook 的方法，虽然会更加复杂一些。

### **检测内联 Hook**

内联 Hook 的实施是在要 Hook 的 NTAPI 的 syscall stub 中的 syscall 指令之前用无条件跳转指令覆盖原有指令。不同的 EDR 可能会覆盖不同的指令，例如 CrowdStrike 覆盖的是 **mov eax, SSN** 这条指令，有的 EDR 覆盖的是 **mov r10, rcx** 这条指令。

因此，代码的逻辑便很直接，逐一检查 syscall stub 的前 4 个字节。在代码里，我们通过 PEB Walking 的方法在不调用 LoadLibray，GetModuleHandle，GetProcAddress 函数的情况下可以获得 ntdll 模块的地址、给定函数的地址。这么做可以避免 LoadLibray，GetModuleHandle，GetProcAddress 这些函数在 IAT 中的显示。

因为涉及对模块的解析，因此我们也会频繁用到 PE 文件相关的结构体。

```c
#include <stdio.h>
#include <windows.h>
#include <winternl.h>
#include <stdint.h>
#include <string.h>


//Get module handle for ntdll and kernel32 at the same time
void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR * funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName,pFunctionName)==0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked\n", funcName);
				return true;
			}
			return false;
		}
	}
	return false;
}

int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	CheckFuncByName(ntdll,"NtAllocateVirtualMemory");
	CheckFuncByName(ntdll, "NtOpenProcess");
	CheckFuncByName(ntdll, "NtReadVirtualMemory");
	CheckFuncByName(ntdll, "NtWriteVirtualMemory");
    return 0;
}


```

编译后，使用 WinDBG 来调试该程序，通过手动修改 NtOpenProcess 的第一条指令来模拟 hook。程序也成功地检测出 NtOpenProcess API 的指令被纂改。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/CzR68AnerRrjyN59-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/CzR68AnerRrjyN59-image.png)

###   


### **替换 .text 节**

我们知道 PE 文件的 .text 节是可执行代码的区域，权限是 **RX**。既然特定 NTAPI 被 hook 了，只要用干净的 ntdll 的 .text 节来覆盖，那么我们就会得到干净的代码，自然可以实现 unhook。

因此，我们首先要获得载入的 ntdll 模块的地址，这个我们已经用代码实现了。然后我们从磁盘读取 ntdll 文件，并存储在缓冲区中。需要注意的是，存储在缓冲区中的 ntdll 的内容是基于磁盘中的形式，即尚未映射到内存中。这样，我们有了 2 份不同的 ntdll 的地址，一份是**被 hook** 的，一份是**干净**的；一份是**映射在内存中**的，一份是**基于磁盘形式**的。因此，在将干净 ntdll 文件中的 .text 节覆盖被 hook 的 ntdll 的 .text 节时，我们需要稍加注意 PointerOfRawData 与 VirtualAddress，SizeOfRawData 与 VirtualSize。

```c
typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
```

以下面截图中的 ntdll 为例，在本地磁盘时，文件偏移为 0x400，尺寸为 0x11920。当被载入至内存时，RVA 是 0x1000，尺寸为 0x1190ce。我们需要注意到这差异。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/1IZsEiMsyPoFIEMk-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/1IZsEiMsyPoFIEMk-image.png)

因为两者 .text 节的尺寸有轻微不同，为了保险起见，我们适用尺寸更大的 .text 节来覆盖。之所以考虑这点，我是担心如果干净的 .text 节尺寸更小，那么没有完全覆盖，载入的 ntdll 的代码区还会有少量代码残留，可能在特定情况下导致意想不到的结果。尽管在本案例中干净的代码区尺寸更大，但在其他的操作系统版本可能是相反的情况，所以我们依旧需要考虑到。

```c
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <string.h>

void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR* funcName)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked\n", funcName);
				return true;
			}
			return false;
		}
	}
	return false;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	CheckFuncByName(ntdll, "NtOpenProcess");

	HANDLE hFile = CreateFileA("C:\\Windows\\System32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		printf("[!] CreateFileA Failed With Error : %d \n\n", GetLastError());
		return -1;
	}
	DWORD dwFileLen = GetFileSize(hFile, NULL);
	DWORD dwNumberOfBytesRead;
	PVOID pNtdllBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileLen);
	if (!ReadFile(hFile, pNtdllBuffer, dwFileLen, &dwNumberOfBytesRead, NULL) || dwFileLen != dwNumberOfBytesRead)
	{
		printf("[!] ReadFile Failed With Error : %d \n\n", GetLastError());
		return -1;
	}
	if (hFile)
	{
		CloseHandle(hFile);
	}


	PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdll;
	PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdll + hookedDosHeader->e_lfanew);
	PIMAGE_DOS_HEADER CleanDosHeader = (PIMAGE_DOS_HEADER)pNtdllBuffer;
	PIMAGE_NT_HEADERS CleanNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)pNtdllBuffer + CleanDosHeader->e_lfanew);

	for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++)
	{
		PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));
		PIMAGE_SECTION_HEADER CleanSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(CleanNtHeader) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

		if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text"))
		{
			LPVOID hookedTextSection = (LPVOID)((DWORD_PTR)ntdll + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
			LPVOID CleanTextSection = (LPVOID)((DWORD_PTR)pNtdllBuffer + (DWORD_PTR)CleanSectionHeader->PointerToRawData);
			size_t size_TextSection = (hookedSectionHeader->Misc.VirtualSize > CleanSectionHeader->SizeOfRawData) ? hookedSectionHeader->Misc.VirtualSize : CleanSectionHeader->SizeOfRawData;
			DWORD oldProtection = 0;
			bool isProtected = VirtualProtect(hookedTextSection, size_TextSection, PAGE_EXECUTE_READWRITE, &oldProtection);
			memcpy(hookedTextSection, CleanTextSection, size_TextSection);
			isProtected = VirtualProtect(hookedTextSection, size_TextSection, oldProtection, &oldProtection);
		}
	}
	CheckFuncByName(ntdll, "NtOpenProcess");
	return 0;
}
```

编译后，我们使用 WinDBG 来调试该程序，为了模拟 hook，我们手动修改 NtOpenProcess API 的第一条指令，并且确认了该修改是成功的。在程序运行结束后，查看该 API，发现代码被恢复成原有的了。因此，通过替换 .text 节，我们可以实现对用户态 Hook 的绕过。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/92kOCjNgWikt13Xz-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/92kOCjNgWikt13Xz-image.png)

值得一提的是，我发现其他利用此方法的代码里，作者们用了 CreateFileMapping 与 MapViewOfFile 来将干净的 ntdll 载入至内存中。

```c
HANDLE CreateFileMappingA(
  [in]           HANDLE                hFile,
  [in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  [in]           DWORD                 flProtect,
  [in]           DWORD                 dwMaximumSizeHigh,
  [in]           DWORD                 dwMaximumSizeLow,
  [in, optional] LPCSTR                lpName
);

LPVOID MapViewOfFile(
  [in] HANDLE hFileMappingObject,
  [in] DWORD  dwDesiredAccess,
  [in] DWORD  dwFileOffsetHigh,
  [in] DWORD  dwFileOffsetLow,
  [in] SIZE_T dwNumberOfBytesToMap
);
```

使用这些 WinAPI 时，因为映像被映射到内存中，因此 Windows 加载器会适用变更的**对齐系数**，导致 .text 的偏移也不同。如果 CreateFileMappingA 的 flProtext 参数没有包含 **SEC\_IMAGE** 或 **SEC\_IMAGE\_NO\_EXECUTE** 标志，则不会适用新的对齐。但SEC\_IMAGE\_NO\_EXECUTE 标志还是会更好一些，因为它不会触发 PsSetLoadImageNotifyRoutine 回调。这意味着当 ntdll.dll 被映射到内存时，使用此标志不会提醒接收**映像载入通知例程**的安全产品。

```c
hSection = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE_NO_EXECUTE, NULL, NULL, NULL);
if (hSection == NULL) {
	printf("[!] CreateFileMappingA Failed With Error : %d \n", GetLastError());
	return -1;
}

// mapping the view of file of ntdll.dll
pNtdllBuffer = MapViewOfFile(hSection, FILE_MAP_READ, NULL, NULL, NULL);
if (pNtdllBuffer == NULL) {
	printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
	return -1;
}
```

好，我们来讨论一下该方法存在的 IOC：

1. 从磁盘中读取 ntdll.dll 对于良性程序来说比较可疑
2. 在 unhook 之前，我们需要用到一些敏感的函数，例如 VirtualProtect，WriteProcessMemory(可用于代替 VirtualProtect 和 memcpy 组合) 等
3. EDR 可以验证载入的 ntdll 的完整性以判断是否遭到了纂改

**拓展**

我们是从磁盘中读取 ntdll，其实我们还可以从 KnownDlls 目录、远程 web 服务器上读取。请查询资料以及所需的 API 的用法，进行实现作为练习。

### **补丁 NTAPI**

相比替换整个 ntdll 模块的 .text 节，我们可以选择只补丁我们所需的且被 hook 的函数，这样，补丁的动作会相对小一些。相比之前的代码，我们可以硬编码或者动态地获得目标 NTAPI 的 syscall stub 指令字节，其实区别只在于 SSN。至于如何动态地获取 SSN，我们会在下一小节进行讲解，因此这里我们就硬编码 NtOpenProcess 的 syscall stub 好了。

需要略加注意的是，对于给定的函数，其地址很大概率不是与**内存页**对齐的，幸运的是，像 VirtualAlloc、VirtualProtect 这类函数会自动帮我们适用向下最近的页的地址。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/ZN27Pa9u53LfJwUC-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/ZN27Pa9u53LfJwUC-image.png)

对于代码，我们只需要增加硬编码的 NTAPI 的 syscall stub，以及对 CheckFuncByName 稍加修改。

```c
#include <stdio.h>
#include <Windows.h>
#include <winternl.h>
#include <string.h>

void GetModule(HMODULE* ntdll, HMODULE* kernel32)
{
	PPEB peb = (PPEB)(__readgsqword(0x60));
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((PBYTE)peb + 0x18); //PPEB_LDR_DATA pLdr = pPeb->Ldr;
	PLIST_ENTRY ntdlllistentry = *(PLIST_ENTRY*)((PBYTE)ldr + 0x30);
	*ntdll = *(HMODULE*)((PBYTE)ntdlllistentry + 0x10);
	PLIST_ENTRY kernelbaselistentry = *(PLIST_ENTRY*)((PBYTE)ntdlllistentry);
	PLIST_ENTRY kernel32listentry = *(PLIST_ENTRY*)((PBYTE)kernelbaselistentry);
	*kernel32 = *(HMODULE*)((PBYTE)kernel32listentry + 0x10);
}

BOOL CheckFuncByName(IN HMODULE hModule, const CHAR* funcName, unsigned char* cleanNTAPI)
{
	PBYTE pBase = (PBYTE)hModule;
	PIMAGE_DOS_HEADER	pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
		return false;
	PIMAGE_NT_HEADERS	pImgNtHdrs = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
		return false;

	IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++)
	{
		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
		PBYTE pFunctionAddress = (PBYTE)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
		if (_stricmp(funcName, pFunctionName) == 0)
		{
			// Check if the first 4 bytes match 0x4C, 0x8B, 0xD1, and 0xB8
			if (pFunctionAddress[0] == 0x4C && pFunctionAddress[1] == 0x8B && pFunctionAddress[2] == 0xD1 && pFunctionAddress[3] == 0xB8)
			{
				printf("NTAPI %s may not be hooked\n", funcName);
			}
			else
			{
				printf("NTAPI %s is hooked, its address is 0x%x\n", funcName, pFunctionAddress);
				DWORD_PTR pageStart = ((DWORD_PTR)pFunctionAddress / 0x1000) * 0x1000;
				printf("Start address of the page is 0x%x\n", pageStart);
				DWORD oldProtection = 0;
				bool isProtected = VirtualProtect((PBYTE)pageStart, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtection);
				memcpy(pFunctionAddress, cleanNTAPI, 0xb);
				isProtected = VirtualProtect((PBYTE)pageStart,0x1000, oldProtection, &oldProtection);
				return true;
			}
			return false;
		}
	}
	return false;
}


int main()
{
	HMODULE ntdll;
	HMODULE kernel32;
	GetModule(&ntdll, &kernel32);
	printf("ntdll base address: %p\n", ntdll);
	printf("kernel32 base address: %p\n", kernel32);
	unsigned char cleanNtOpenProcess[] = "\x4c\x8b\xd1\xb8\x26\x00\x00\x00\x0f\x05\xc3";
	CheckFuncByName(ntdll, "NtOpenProcess",cleanNtOpenProcess);
	CheckFuncByName(ntdll, "NtOpenProcess",cleanNtOpenProcess);
	return 0;
}
```

代码的 syscall stub 中保存的是最精简的指令，因为这足以成功发起 syscall。总之，通过补丁给定被 hook 的函数指令，可以实现对想要的函数进行 unhook。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/cxAdiy2r2nfk4gdO-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/cxAdiy2r2nfk4gdO-image.png)

对于此方法，虽然比替换整个代码区动静要小一些，但因为原理相似，论 IOC 其实是差不多的。

### **从挂起的进程中载入纯净 ntdll 模块**

我们还可以通过读取挂起进程中载入的 ntdll 来获得纯净的副本，并用于 unhook。当进程以**挂起**或者**被调试**的状态被创建，此时只有 ntdll 被载入，EDR 还未来得及注入其检视 API 调用的模块。

[![image.png](https://raven-medicine.com/uploads/images/gallery/2024-03/scaled-1680-/j0SqN4PdAoczUyZe-image.png)](https://raven-medicine.com/uploads/images/gallery/2024-03/j0SqN4PdAoczUyZe-image.png)

在这之后，我们可以通过 ReadProcessMemory 来读取载入的 ntdll 模块的代码区。要能精准地获取干净副本的代码区并实现 unhook，我们可以通过以下的步骤实现：

1. 使用 **NtQueryInformationProcess** API 获得远程挂起进程的 PEB 地址
2. 通过 **PEB Walking** 的方法获得远程进程中载入的纯净 ntdll 地址。因为是远程进程，步骤会不那么直接一些。
3. 因为都是已经载入到内存的 ntdll，解析当前载入的 ntdll 模块(即被 hook 的) 从而获取代码区的 **RVA** 以及**尺寸**
4. 从纯净的 ntdll 模块的代码区开始读取，直到读取字节数达到尺寸
5. 覆盖被 hook 的代码区实现 unhook

***知道了原理与流程后，作为一道练习题，请学员们尝试自行完成完整代码，并分析该方法有哪些 IOC。***