前言
本文中的沙箱,均指一个受控、虚拟化的环境,专门用来自动运行、监控和分析可疑程序的行为。传统的沙箱根据监控、收集恶意软件行为的方式,可以分为两种模式:一种是基于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,整个调用链如下:

本文讲解的这个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就会记录这条系统调用信息,如下:

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

将之作为辅助定位栈上函数地址的Magic Number
继续跟进到EtwpLogKernelEvent中,会发现一些有意思的函数:

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

这里暴露出一个函数指针的调用,这正是我们想要的,查阅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。

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

对照上面的控制流条件以及相关配置文档,需要进行两步操作:
- 调用
ZwSetSystemInformation分配一个PMC Profile Source,将Flags.PmcCounters置位 - 调用
ZwSetSystemInfomration设置HookId为0xF33
参考如下代码:
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, ¶ms[0]);
28 _FillParamPtr(L"StartRoutine", StartRoutine, ¶ms[1]);
29 _FillParamUlong(L"CreateFlags", CreateFlags, ¶ms[2]);
30 _FillParamImageName(L"ImageName", targetPid, ¶ms[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无法捕捉到调用

我仅实现了自动化拉起样本、传递调用数据等功能;更进一步地,可以考虑实现自动化重启虚拟机、分析调用数据并打行为标签、自动提取C2(可以通过网络过滤驱动实现)。
参考文章及项目
[1] [原创]InfinityHook 可兼容最新版windows-软件逆向-看雪论坛-安全社区|非营利性质技术交流社区