前言

本文中的沙箱,均指一个受控、虚拟化的环境,专门用来自动运行、监控和分析可疑程序的行为。传统的沙箱根据监控、收集恶意软件行为的方式,可以分为两种模式:一种是基于API Hook的沙箱,如Cuckoo;一种是基于虚拟化技术(VT-x with EPT)的沙箱,如DrakVuf。

其中,Cuckoo沙箱通过Inject.exe启动样本并挂起,向其中注入monitor.dll对一百多个Native API做了Inline Hook,收集恶意软件的API的调用信息,再解析这些调用信息,打上对应的行为标签。

这样做的缺陷很显然:由于monitor.dll与恶意软件共存,再加上Inline Hook的痕迹过于明显,很容易被探测出监控环境的存在。并且,在Ring-3下的Hook能力是很有限的,像ntdll重载、syscall这些方法都能轻易规避Hook,使得沙箱跑不出任何行为。

因此,本文在Cuckoo沙箱工作原理的基础上进行拓展,从内核Hook的角度出发,设计一个更隐蔽、强大的沙箱(或者说是监控系统),以来减少安全分析员们的工作量。

ETW Hook:核心原理

在64位Windows操作系统上,微软引入了Patch Guard对操作系统中易受攻击的结构、函数进行监控,常规的[SSDT Hook](内核模式 Rootkits,第一部分 | SSDT 钩子 • Adlice 软件 — KernelMode Rootkits, Part 1 | SSDT hooks • Adlice Software)将会引发蓝屏。因此ETW Hook被提出,其核心原理就是利用ETW在遥测syscall时的**“漏洞”**劫持控制流实现Native API Hook。这一漏洞的成因是PG对一些内核函数指针表监控不严(通常在.data段中),我们可以替换某些指针实现Hook而不触发PG,整个调用链如下:

KillChain.drawio

本文讲解的这个Hook点来自[Oxygen]([原创]InfinityHook 可兼容最新版windows-软件逆向-看雪论坛-安全社区|非营利性质技术交流社区)以及[Daax](《Fun with another PatchGuard-compliant Hook - Reverse Engineering》 — Fun with another PatchGuard-compliant Hook - Reverse Engineering),向他们表示感谢!

在Ring-3调用系统API后,最终会通过syscall指令进入内核(一般来说已经不用调用门了)。首先进入的就是内核中的KiSystemCall64函数;在这里,如果开启了 syscall Nt Kernel Logger,ETW-Ti就会记录这条系统调用信息,如下:

image-20250914125557460

在ETW记录系统调用前,会把真实的函数地址暂存在栈上,调用完毕后恢复。这也就给了我们可乘之机,只要在PerfInfoLogSysCallEntry的调用链中寻到一处函数指针替换点,就可以劫持控制流修改栈上暂存的函数地址,从而实现Hook。

PerfInfoLogSysCallEntry中同样保存了EventIDEVENT_DESCRIPTOR在栈上:

F33:501802

image-20250914130238484

将之作为辅助定位栈上函数地址的Magic Number

继续跟进到EtwpLogKernelEvent中,会发现一些有意思的函数:

image-20250914155750957

根据ETW Logger的配置情况,上面的三个函数可能会被执行;最开始的替换点是在EtwpReserveTraceBuffer中的GetCpuClock,但这个函数在高版本Windows已被修复,不适合替换了,因此我们跟进EtwpReserveWithPmcCounters

image-20250914160215355

这里暴露出一个函数指针的调用,这正是我们想要的,查阅HalPrivateDispatchTable的[结构](Vergilius Project | HAL_PRIVATE_DISPATCH),替换掉Table + 0x248处的函数指针即可接管控制流,此时在栈上搜索两个Magic Number去定位到系统调用的函数地址并替换就实现了Hook,如下:

 1//
 2//@brief 代理函数,劫持栈上的函数地址到自己的函数上,同时也是高频执行函数.
 3//
 4void ProxyEtwpReserveWithPmcCounters(PVOID Context, ULONGLONG TraceBuff) {
 5	USHORT Magic = 0xF33;//Magic 1
 6	ULONG Signate = 0x501802;//Magic 2
 7	ULONG Magic2 = 0x601802;
 8#define INFINITYHOOK_MAGIC_501802 ((unsigned long)0x501802) //Win11 23606 以前系统特征码
 9#define INFINITYHOOK_MAGIC_601802 ((unsigned long)0x601802) //Win11 23606 及以后系统的特征码
10#define INFINITYHOOK_MAGIC_F33 ((unsigned short)0xF33)
11	PULONG RspPos = _AddressOfReturnAddress();
12	PULONG RspLimit = __readgsqword(0x1a8);
13	// KPCR->Pcrb.CurrentThread Type:_KTHREAD*
14	ULONG64 currentThread = __readgsqword(0x188);// OFFSET_KPCR_CURRENT_THREAD
15	ULONG systemCallIndex = *(ULONG*)(currentThread + 0x80);// OFFSET_KTHREAD_SYSTEM_CALL_NUMBER
16
17
18	do {
19		if (KeGetCurrentIrql() <= DISPATCH_LEVEL) {
20			// 不接管内核调用
21			if (ExGetPreviousMode() == KernelMode)
22				return g_oriHalCollectPmcCounters(Context, TraceBuff);
23			while (RspPos <= RspLimit)
24			{
25				if (*((PUSHORT)(RspPos)) == INFINITYHOOK_MAGIC_F33)
26				{
27					// Win11 24H2兼容
28					if (RspPos[2] == INFINITYHOOK_MAGIC_501802 || RspPos[2] == INFINITYHOOK_MAGIC_601802)
29					{
30
31						for (; (ULONG64)RspPos <= (ULONG64)RspLimit; ++RspPos)
32						{
33							// 执行到这里则已经确认是SYSCALL的ETW记录,可以开始遍历栈
34                            // 找在SSDT表范围内的地址,找到既是SYSCALL的地址
35
36
37							ULONG64* pllValue = (ULONG64*)RspPos;
38							if ((*pllValue >= PAGE_ALIGN(g_SystemCallTable) &&
39								(*pllValue <= PAGE_ALIGN(g_SystemCallTable + PAGE_SIZE * 2))))
40							{
41
42								HANDLE pid = PsGetCurrentProcessId();
43
44								if (LogManagerIsTargetProcess(pid)) {
45									// 此时IRQL == DISPATCH_LEVEL
46									// 只有目标进程才Hook,其他进程正常放行
47									ProcessSyscall(systemCallIndex, RspPos);
48									return g_oriHalCollectPmcCounters(Context, TraceBuff);
49								}
50								else {
51									return g_oriHalCollectPmcCounters(Context, TraceBuff);
52								}
53
54
55							}
56						}
57					}
58
59				}
60				++RspPos;
61
62			}
63		}
64	} while (FALSE);
65
66	return g_oriHalCollectPmcCounters(Context, TraceBuff);
67}

但此时离ETW Hook还差了一步,由于正常来说控制流不会走到Hook点,所以需要参考一些文档手动配置ETW Logger走到我们替换的地方。

ETW Hook:合理的配置

离实现ETW Hook只剩一步,那就是配置ETW Logger使其正确执行到我们想要劫持的EtwpReserveWithPmcCounter,这分为两部分:

  • 配置ETW NT Kernel Logger以及系统调用事件开启
  • 配置Event Trace类使得控制流走到EtwpReserveWithPmcCounter

第一步不困难,CKCL_TRACE_PROPERTIES是一个相对公开的结构体:

 1//
 2//@brief ֹ开启Nt kernel logger etw
 3//
 4// 
 5NTSTATUS StartOrStopTrace(BOOLEAN control) {
 6	NTSTATUS status = STATUS_UNSUCCESSFUL;
 7	CKCL_TRACE_PROPERTIES* ckclProperty = 0;
 8	ULONG lengthReturned = 0;
 9	do {
10		
11		ckclProperty = ExAllocatePoolWithTag(NonPagedPool, PAGE_SIZE, POOL_TAG);
12		if (!ckclProperty) {
13			DbgPrintEx(77, 0, "Failed to allocate memory for ckcl property.\n");
14			status = STATUS_INSUFFICIENT_RESOURCES;
15			break;
16		}
17		memset(ckclProperty, 0, PAGE_SIZE);
18		UNICODE_STRING tmp = { 0 };
19		RtlInitUnicodeString(&tmp, L"Circular Kernel Context Logger");
20		ckclProperty->Wnode.BufferSize = PAGE_SIZE;
21		ckclProperty->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
22		ckclProperty->ProviderName = tmp;
23		ckclProperty->Wnode.Guid = CkclSessionGuid;
24		ckclProperty->Wnode.ClientContext = 1;
25		ckclProperty->BufferSize = sizeof(ULONG);
26		ckclProperty->MinimumBuffers = ckclProperty->MaximumBuffers = 2;
27		ckclProperty->LogFileMode = EVENT_TRACE_BUFFERING_MODE;
28
29		status = ZwTraceControl(control ? EtwpStartTrace : EtwpStopTrace, ckclProperty, PAGE_SIZE, ckclProperty, PAGE_SIZE, &lengthReturned);
30		// STATUS_OBJECT_NAME_COLLISION
31		if (!NT_SUCCESS(status) && status != STATUS_OBJECT_NAME_COLLISION) {
32			DbgPrintEx(77, 0, "Failed to enable kernel logger etw trace,status=%x", status);
33			break;
34		}
35		if (control)
36		{
37			ckclProperty->EnableFlags = EVENT_TRACE_FLAG_SYSTEMCALL;
38
39			status = ZwTraceControl(EtwpUpdateTrace, ckclProperty, PAGE_SIZE, ckclProperty, PAGE_SIZE, &lengthReturned);
40			if (!NT_SUCCESS(status))
41			{
42				DbgPrintEx(77, 0, "Failed to enable syscall etw, errcode=%x", status);
43				StartOrStopTrace(FALSE);
44				break;
45			}
46		}
47	} while (FALSE);
48	if (ckclProperty)
49		ExFreePool(ckclProperty);
50	return status;
51}

对于第二步,由于没有公开的结构文档,所以需要自己配置相关的内容,从上面的代码中抠出条件:

1Flags & 0x800 && 
2LoggerContext->PmcData->HookIdCount != 0 && 
3PmcData->HookId[index] == HookID

这里的Flags是这样一个Union Struct

 1union
 2    {
 3        ULONG Flags;                                                        //0x330
 4        struct
 5        {
 6            ULONG Persistent:1;                                             //0x330
 7            ULONG AutoLogger:1;                                             //0x330
 8            ULONG FsReady:1;                                                //0x330
 9            ULONG RealTime:1;                                               //0x330
10            ULONG Wow:1;                                                    //0x330
11            ULONG KernelTrace:1;                                            //0x330
12            ULONG NoMoreEnable:1;                                           //0x330
13            ULONG StackTracing:1;                                           //0x330
14            ULONG ErrorLogged:1;                                            //0x330
15            ULONG RealtimeLoggerContextFreed:1;                             //0x330
16            ULONG PebsTracing:1;                                            //0x330
17            ULONG PmcCounters:1;  // 我们关注的位                                     
18            ULONG PageAlignBuffers:1;                                       //0x330
19            ULONG StackLookasideListAllocated:1;                            //0x330
20            ULONG SecurityTrace:1;                                          //0x330
21            ULONG LastBranchTracing:1;                                      //0x330
22            ULONG SystemLoggerIndex:8;                                      //0x330
23            ULONG StackCaching:1;                                           //0x330
24            ULONG ProviderTracking:1;                                       //0x330
25            ULONG ProcessorTrace:1;                                         //0x330
26            ULONG QpcDeltaTracking:1;                                       //0x330
27            ULONG MarkerBufferSaved:1;                                      //0x330
28            ULONG LargeMdlPages:1;                                          //0x330
29            ULONG ExcludeKernelStack:1;                                     //0x330
30            ULONG BootLogger:1;                                             //0x330
31        };
32    };

在[这篇文档中](事件跟踪信息类 — EVENT_TRACE_INFORMATION_CLASS)告诉我们,需要调用NtSetThreadInformation来设置Flags以及HookID。

2f40cfac-8c9f-4517-a76b-b849df52fe12

我们跟进NtSetThreadInformation -> EtwSetPerformanceTraceInformation去找到具体的内容

image-20250914164907726

对照上面的控制流条件以及相关配置文档,需要进行两步操作:

  • 调用 ZwSetSystemInformation分配一个PMC Profile Source,将Flags.PmcCounters置位
  • 调用ZwSetSystemInfomration设置HookId0xF33

参考如下代码:

 1//
 2//@brief 开启PMC计数器 PerformanceCounter
 3//
 4NTSTATUS OpenPmcCounter() {
 5	NTSTATUS status = STATUS_SUCCESS;
 6	PEVENT_TRACE_PROFILE_COUNTER_INFORMATION countInfo = 0;
 7	PEVENT_TRACE_SYSTEM_EVENT_INFORMATION eventInfo = 0;
 8	if (!g_isActive)return STATUS_UNSUCCESSFUL;
 9	do {
10		countInfo = ExAllocatePoolWithTag(NonPagedPool, sizeof(EVENT_TRACE_PROFILE_COUNTER_INFORMATION),POOL_TAG);
11		if (!countInfo) {
12			DbgPrintEx(77, 0, "Failed to allocate memory for PMC count.\n");
13			status = STATUS_INSUFFICIENT_RESOURCES;
14			break;
15		}
16		countInfo->EventTraceInformationClass = EventTraceProfileCounterListInformation;
17		countInfo->TraceHandle = 2;
18		countInfo->ProfileSource[0] = 1;
19		//	STATUS_WMI_ALREADY_ENABLED
20        // 第一步
21		status = ZwSetSystemInformation(SystemPerformanceTraceInformation, countInfo, sizeof(EVENT_TRACE_PROFILE_COUNTER_INFORMATION));
22		if (!NT_SUCCESS(status) && status != STATUS_WMI_ALREADY_ENABLED) {
23			DbgPrintEx(77, 0, "Failed to configure PMC counter.status=%x\n", status);
24			break;
25		}
26		
27		eventInfo = ExAllocatePoolWithTag(NonPagedPool,  sizeof(EVENT_TRACE_SYSTEM_EVENT_INFORMATION),POOL_TAG);
28		if (!eventInfo) {
29			DbgPrintEx(77, 0, "Failed to allocate memory for event info.\n");
30			status = STATUS_INSUFFICIENT_RESOURCES;
31			break;
32		}
33
34		eventInfo->EventTraceInformationClass = EventTraceProfileEventListInformation;
35		eventInfo->TraceHandle = 2;
36		eventInfo->HookId[0] = SyscallHookId;// 0xF33
37		// 第二步
38		status = ZwSetSystemInformation(SystemPerformanceTraceInformation, eventInfo, sizeof(EVENT_TRACE_SYSTEM_EVENT_INFORMATION));
39		if (!NT_SUCCESS(status))
40		{
41			DbgPrintEx(77,0,"failed to configure pmc event, status=%x", status);
42			break;
43		}
44
45	} while (FALSE);
46	if (countInfo)ExFreePool(countInfo);
47	if (eventInfo)ExFreePool(eventInfo);
48	if (status == STATUS_WMI_ALREADY_ENABLED)return STATUS_SUCCESS;
49	return status;
50}

打造一个沙箱

可以说,ETW Hook是一个比较完美的沙箱监控手段。因为其只接管应用态的系统调用,不需要特别过滤内核调用;同时其工作环境皆在内核之中,不会对应用态程序暴露出Hook特征。

因此我们可以参照Cuckoo,Hook一些常见的NT API来收集恶意软件的行为,然后将之传入我们的3环程序解析成易处理的JSON或者BSON格式的数据,再手写一个分析模块来自动化研究其恶意行为。同时由于我们的Hook运行在调用链的最底层,还可以用该Hook去遥测恶意程序的堆栈信息,检测如Hell's Gate及其变种的Ring-3脱钩手段。

以下是一个简单的Demo,首先考虑使用MiniFilter框架将收集的调用信息作为日志传入Ring-3

 1// MiniFilter发送数据到 3 环
 2NTSTATUS SendToUser(PVOID buffer, ULONG bufSize) {
 3	if (!g_ClientPort) return STATUS_INVALID_DEVICE_STATE;
 4
 5	LARGE_INTEGER timeout;
 6	timeout.QuadPart = -10 * 1000 * 1000;  // 1 秒 超时时间
 7
 8	NTSTATUS status = FltSendMessage(
 9		g_FilterHandle,
10		&g_ClientPort,
11		buffer,
12		bufSize,
13		NULL,
14		NULL,
15		&timeout);
16
17	if (!NT_SUCCESS(status)) {
18		DbgPrintEx(77, 0, "[%s] failed: 0x%X\n", __FUNCTION__,status);
19	}
20	return status;
21}

比如想要监控跨进程的线程创建,可以这么写Hook函数:

 1NTSTATUS DetourNtCreateThreadEx(
 2	_Out_ PHANDLE ThreadHandle,
 3	_In_ ACCESS_MASK DesiredAccess,
 4	_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
 5	_In_ HANDLE ProcessHandle,
 6	_In_ PVOID StartRoutine,
 7	_In_opt_ PVOID Argument,
 8	_In_ ULONG CreateFlags,
 9	_In_ ULONG_PTR ZeroBits,
10	_In_opt_ SIZE_T StackSize,
11	_In_opt_ SIZE_T MaximumStackSize,
12	_In_opt_ PVOID AttributeList
13) {
14	
15	NTSTATUS status = STATUS_SUCCESS;
16    RETURN_IF_NOT_PASSIVE(g_NtCreateThreadEx(ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle, StartRoutine, Argument, CreateFlags, ZeroBits, StackSize, MaximumStackSize, AttributeList));
17
18    StackDetect();
19	status = g_NtCreateThreadEx(ThreadHandle, DesiredAccess, ObjectAttributes, ProcessHandle, StartRoutine, Argument, CreateFlags, ZeroBits, StackSize, MaximumStackSize, AttributeList);
20
21    HANDLE targetPid = NULL;
22    // cross-process
23    if (NT_SUCCESS(status) && !_IsCurrentProcessHandle(ProcessHandle) && NT_SUCCESS(_GetPidFromProcessHandle(ProcessHandle, &targetPid)) && targetPid != PsGetCurrentProcessId()) {
24        (VOID)LogManagerSetTargetProcess(targetPid, TRUE);
25        SYSCALL_PARAMETER params[MAX_SYSCALL_PARAMETERS];
26        RtlZeroMemory(params, sizeof(params));
27        _FillParamHandle(L"TargetPid", targetPid, &params[0]);
28        _FillParamPtr(L"StartRoutine", StartRoutine, &params[1]);
29        _FillParamUlong(L"CreateFlags", CreateFlags, &params[2]);
30        _FillParamImageName(L"ImageName", targetPid, &params[3]);
31        
32        LogManagerSendLog(PsGetCurrentProcessId(), PsGetCurrentThreadId(), L"NtCreateThreadEx", L"RemoteThread", status, 4, params);
33    }
34    else {
35        #define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x4
36        if(CreateFlags == THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER)
37            LogManagerSendLog(PsGetCurrentProcessId(), PsGetCurrentThreadId(), L"NtCreateThreadEx", L"DetectDebugger", status, 0, NULL);
38    }
39    InterlockedDecrement(&gHooksActive);
40	return status;
41}

普通的ETW Hook是全局的Hook,即所有的应用态系统调用都会被我们接管。因此还需要维护一个ProcessList记录样本的PID;对于跨进程的操作,比如有的样本注入shellcode至白进程,或者释放了新进程并拉起,则需要将新进程的PID也加入到ProcessList中,避免遗漏行为。上文中的LogManagerSetTargetProcess正是在做这件事。

沙箱拓展:堆栈检测

上文中提到的Hell's Gate指的是一种构造gadget直接调用syscall指令进入内核的手法,能够做到规避3环的挂钩。但这样做导致其堆栈不正常,呈现出malware.exe -> ntoskrnl.exe这样的调用链,因此我们可以在Hook开头加一个堆栈检测,检测其返回地址(存放在TrapFrame中)是否位于正常的模块(如ntdll.dll),否则判为一次恶意调用。

 1// 直接系统调用检测(SYSCALL)
 2for (int i = 0; i < MODULE_NUM; i++) {
 3	if (rip >= g_UserModule[i].BaseAddress
 4		&& rip <= (ULONG64)g_UserModule[i].BaseAddress + g_UserModule[i].Size)
 5	{
 6		abnormalSyscall = FALSE;
 7		// ntdll.dll需特殊判定,对抗间接系统调用 - 检测点1
 8		UNICODE_STRING tmp = { 0 };
 9		RtlInitUnicodeString(&tmp, L"ntdll.dll");
10		if (!RtlCompareUnicodeString(&g_UserModule[i].ModuleName, &tmp, TRUE))
11			abnormalSyscall = SpecialDetectSyscall(rip, systemCallIndex, TRUE);
12		goto log;
13
14	}
15log:
16
17	if (abnormalSyscall) {
18		SYSCALL_PARAMETER params[1];
19		params[0].Type = 2;
20		RtlStringCchCopyW(params[0].Name, ARRAYSIZE(params[0].Name), L"name");
21		RtlStringCchCopyW((WCHAR*)params[0].Data, ARRAYSIZE(params[0].Data) / sizeof(WCHAR), g_SyscallTable[systemCallIndex].Name);
22		params[0].Size = (ULONG)(wcslen(g_SyscallTable[systemCallIndex].Name) * sizeof(WCHAR));
23		LogManagerSendLog(PsGetCurrentProcessId(), PsGetCurrentThreadId(), L"Syscall", L"Abnormal Syscall", 0, 1, params);
24	}

从对抗的层面来讲这种检测是有欠缺的,因为现在武器化的SYSCALL通常是间接系统调用,调用链通常为malware.exe -> ntdll.dll -> ntoskrnl.exe,或者更加合法的malware.exe -> kernel32.dll -> ntdll.dll -> ntoskrnl.exe ;更高级别的检测就是检测跳转处是否为正常的CALL,还是恶意软件自己维护的跳板。

堆栈数据是EDR内存扫描的一个重要指标,但对于沙箱来说,跑出更多的行为才是最重要的,因此这里就不展开讨论了。

演示

演示的样本为某厂攻击队的样本,采用了间接系统调用的API执行手段,普通的3环Hook无法捕捉到调用

image-20250914173155589

我仅实现了自动化拉起样本、传递调用数据等功能;更进一步地,可以考虑实现自动化重启虚拟机、分析调用数据并打行为标签、自动提取C2(可以通过网络过滤驱动实现)。

参考文章及项目

[1] [原创]InfinityHook 可兼容最新版windows-软件逆向-看雪论坛-安全社区|非营利性质技术交流社区

[2] 《Fun with another PatchGuard-compliant Hook - Reverse Engineering》 — Fun with another PatchGuard-compliant Hook - Reverse Engineering

[3] https://github.com/zhutingxf/InfinityHookPro

[4] https://github.com/Oxygen1a1/InfinityHook_latest