主要内容
针对进程行为的监控需求,以往很多安全软件都是采用的 Hook 技术拦截关键的系统调用,来实现对恶意软件进程创建的拦截。但在 x64 架构下,系统内核做了很多安全检测措施,特别是类似于 KDP 这样的技术,使得 Hook 方法不再有效。为此 OS 推出了基于回调实现的行为监控方案。本文借助 IDA 逆向分析该技术的实现原理并给出了关键数据结构及调用链,通过双机内核调试验证了该数据结构以及调用链的正确性。
涉及到的内容如下:
1、内核对象及内核对象管理;
2、进程回调;
3、内核调试;
4、Windbg 双击调试;
引言
近年来,各种恶意软件新变种层出不穷,攻击方法、手段多种多样,造成了巨大的经济损失。作为防守的第一个环节就是能够识别出恶意进程创建的动作,而进程创建监控技术是为了能够让安全软件有机会拦截到此动作的技术。安全软件根据匹配算法判断是否准许该进程创建,以此达到保护用户数据安全的目的。
【点此领取逆向文档】
本文基于逆向工程及内核调试技术,分析了该技术的具体实现及系统额外增加的数据检测机制。借助逆向工具 IDA 静态逆向分析了系统关键 API 的内部动作及具体的实现,相关的数据结构,得到该技术实际触发的调用源以及整个调用链。借助 VMWare 搭建双机调试环境,利用 Windbg 动态调试系统内核,查看系统中所涉及到的关键数据,并与 PCHunter 给出的数据做对比分析,验证了分析结论的正确性。此外还通过对调用链中的关键函数下断点,通过栈回溯技术,动态观察了整个调用链及触发时间。分析得到的关键数据结构和系统对数据做的检测校验算法可用于检测病毒木马等软件恶意构造的表项。【领取逆向工程】
1 进程回调原理分析
1.1 安装与卸载逆向分析
根据微软官方技术文档 MSDN 上的说明,通过 PsSetCreateProcessNotifyRoutine、PsSetCreateProcessNotifyRoutineEx 和 PsSetCreateProcessNotifyRoutineEx2 这三 API 来安装一个进程创建、退出通知回调例程,当有进程创建或者退出时,系统会回调参数中指定的函数。以 PsSetCreateProcessNotifyRoutine 为例子,基于 IDA 逆向分析该 API 的具体实现。如图 1 所示,由图可知,该 API 内部仅仅是简单的调用另一个函数,其自身仅仅是一个 stub,具体的实现在 PspSetCreateProcessNotifyRoutine 中,此函数的安装回调例程的关键实现如图所示。
调用 ExAllocateCallBack,创建出了一个回调对象,并将 pNotifyRoutine 和 bRemovel 作为参数传入,以初始化该回调对象,代码如图所示;其中 pNotifyRoutine 即是需要被回调的函数例程,此处的 bRemovel 为 false,表示当前是安装回调例程。
紧接着调用 ExCompareExchangeCallBack 将初始化好的 CallBack 对象添加到 PspCreateProcessNotifyRoutine 所维护的全局数组中。值得注意的是,ExCompareExchangeCallBack 中在安装回调例程时,对回调例程有一个特殊的操作如图所示。
与 0x0F 做了或操作,等价于将低 4 位全部置 1;若 ExCompareExchangeCallBack 执行失败,则接着下一轮循环继续执行。由图 2 中第 66 行代码可知,循环的最大次数是 0x40 次。如果一直失败,可调用 ExFreePoolWithTag 释放掉 pCallBack 所占用的内存,且返回 0xC000000D 错误码。【领取逆向文档】
然后根据 v3 的值判断是通过上述三个 API 中的哪个安装的回调,来更新相应的全局变量。其中 PspCreateProcessNotifyRoutineExCount 和 PspCreateProcessNotifyRoutineCount 分别记录当前通过 PsSetCreateProcessNotifyRoutineEx 和 PsSetCreateProcessNotifyRoutine 安装回调例程的个数。
PspNotifyEnableMask 用以表征当前数组中是否安装了回调例程,该值在系统遍历回调数组执行回调例程时,用以判断数组是否为空,加快程序的执行效率。
除了能够安装回调例程,这三个 API 也能卸载指定的回调例程。以 PsSetCreateProcessNotifyRoutine 为例,分析其实现的关键部分,如图所示。
通过一个 while 循环遍历 PspCreateProcessNotifyRoutine 数组,调用 ExReferenceCallBackBlock 取出数组中的每一项,该 API 内部会做一些检验动作且对返回的数据也做了特殊处理,如图所示。图 6 中*pCallBackObj 即是取出回调对象中的回调例程的函数地址,通过判断其低 4 位是否为 1 来做一些数据的校验,如 17 行所示。
系统做这个处理也是起到保护作用,防止恶意构造数据填入表中,劫持正常的系统调用流程。此外,图中第 33 行处的代码,在将回调例程返回给父调用时,也将回调例程的低 4 位全部清零,否则返回的地址是错误的,调用立马触发 CPU 异常。
ExReferenceCallBackBlock 成功返回后,调用 ExGetCallBackBlockRoutine 从返回的回调对象中取出回调例程,并判断取出的是否为当前指定需要卸载的项,如果是则调用 ExDereferenceCallBackBlock 递减引用计数,接着调用 ExFreePoolWithTag 释放掉 Callback 所占用的内存。期间也会更新 PspCreateProcessNotifyRoutineExCount 或 PspCreateProcessNotifyRoutineCount 的值。根据源码还可以得知,该数组总计 64 项,也即只能安装 64 个回调例程。如果遍历完数组的 64 项依旧没有找到,则返回 0xC000007A 错误码。
1.2 OS 执行回调例程分析
回调例程安装完之后,如果有新的进程创建或退出,内核则便会遍历该数组来执行其中安装的每一项回调例程。通过 IDA 的交叉引用功能,可分析出内核其他地方对 PspCreateProcessNotifyRoutine 的交叉引用,如图所示
共计 5 个地方涉及到此变量。其中 PspCallProcessNotifyRoutines 是直接调用回调例程的函数,该函数的关键部分如图所示。
通过 while 循环,遍历 PspCreateProcessNotifyRoutine 数组中安装的所有回调例程,依次执行。PspNotifyEnableMask & 2 的操作即为判断当前数组中是否安装有回调例程,加快程序的执行效率,这个变量的值在 PsSetCreateProcessNotifyRoutine 中安装回调例程时设置。bRemove & 2 这个 if 分支,是用来判断当前的回调例程是通过 PsSetCreateProcessNotifyRoutine 还是 PsSetCreateProcessNotifyRoutineEx 安装,因为这两个 API 安装的回调例程的原型不同,在实际调用时传入的参数也不同。两者的回调例程原分别为:
void PcreateProcessNotifyRoutine(HANDLE ParentId,HANDLE ProcessId,BOOLEAN Create)和void PcreateProcessNotifyRoutineEx(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)。
复制代码
此外,图 8 中 IDA 给出的伪 C 代码 RoutineFun((unsigned __int64)RoutineFun)明显不对,因为回调例程的参数个数是 3 个,而 IDA 分析出的参数只有 1 个,显然有问题。直接看下反汇编代码即可得知,如图所示
根据 x64 下的调用约定可知,函数的前 4 个参数是通过 rcx、rdx、r8 和 r9 这四个寄存器传递,图给出的正是回调例程的前三个参数,_guard_dispatch_icall 内部会直接取 rax 的值调用过去,而 rax 的值正是 ExGetCallBackBlockRoutine 调用返回的回调例程函数地址。
上图中的第二个涉及到 PspCreateProcessNotifyRoutine 数组的是 PspEnumerateCallback 函数,该函数是系统内部函数,没有导出,其具体实现如图所示。
该函数根据 dwEnumType 来判断想要枚举的是哪个数组,由代码分析可知,系统内核维护了三个回调相关的数组,分别为镜像加载回调数组,进程创建退出回调数组,线程创建退出数组。类似之前的函数校验,这里也检测了索引是否超过 0x40,超过了则返回 0,以示失败。
1.3 触发调用的调用链分析
上节分析了回调例程的直接调用上级函数,本节分析整个调用链,主要分析调用源及调用过程中涉及到的关键函数。根据 IDA 给出的交叉引用图如图所示。
涉及到的函数调用非常多,很多不相关的也被包含进来,不便于分析。经手动分析整理后的调用链,其链路中的关键 API 如图所示。
虚线以上部分为用户态程序,虚线以下为内核态程序,红色标注的都是标准导出的 API。
根据图 12 可知,当用户态进程调用 RtlCreateUserProcess、RtlCreateUserProcesersEx 或 RtlExitUserProcess 时,内核都会去遍历 PspCreateProcessNotifyRoutine 数组,依次执行回调例程,通知给驱动程序做相应的处理。
驱动接管之后,可以做安全校验处理,分析进程的父进程或者进一步分析进程链,此外还可以对即将被拉起的子进程做特征码匹配,PE 指纹识别,导入表检测等防御手段。
这种方式不需要去 Hook 任何 API,也无需做特征码定位等重复繁琐的工作,完全基于系统提供的回调机制,且在 Windows 系统中都可以无缝衔接。且各个安全厂家之间也不存在相互竞争,大大缩小了系统蓝屏的风险。
图 12 中 NtCreateUserProcess 调用 PspInsertThread 的原因是创建进程的 API 内部会创建该进程的主线程。将遍历回调例程数组的工作统一到 PspInsertThread 中,由其去调用下层的 PspCallProcessNotifyRoutines 更为合理。
2 实验
2.1 观察系统中已安装的回调例程
实验环境如表 1 所示,借助于 VMWare 进行双机调试。
Guest OS Build 10.0.16299.125
Host OS Build 10.0.17134.885
Windbg版本 10.0.17134.1
VMWare 14.1.1 build-7528167
PCHunter V1.56
复制代码
在 Windbg 中观察 PspCreateProcessNotifyRoutine 数组,共计 14 项有效数据,如下所示;
1: kd> dd PspCreateProcessNotifyRoutineCount l1
fffff802`151f4e78 00000009
1: kd> dd PspCreateProcessNotifyRoutineExCount l1
fffff802`151f4e7c 00000005
1: kd> dq PspCreateProcessNotifyRoutine l40
fffff802`14da2a80 ffffcc8b`d884b9bf ffffcc8b`d8d9c96f
fffff802`14da2a90 ffffcc8b`d939975f ffffcc8b`da00044f
fffff802`14da2aa0 ffffcc8b`d9bd382f ffffcc8b`da41e8df
fffff802`14da2ab0 ffffcc8b`da53815f ffffcc8b`da5ca8bf
fffff802`14da2ac0 ffffcc8b`dac5178f ffffcc8b`dbef624f
fffff802`14da2ad0 ffffcc8b`dce333af ffffcc8b`dcec67df
fffff802`14da2ae0 ffffcc8b`dc735def ffffcc8b`dcabd32f
复制代码
拆解第一项,寻找其所对应的回调例程,如下:
1: kd> dq ffffcc8b`d884b9b0 l3
ffffcc8b`d884b9b0 00000000`00000020 fffff802`13fd6268
ffffcc8b`d884b9c0 00000000`00000000
复制代码
由此可知,安装的回调例程起始地址为 fffff802`13fd6268,且还可知道 Remove 为 0,即这个是已经安装的。寻找该回调例程对应的驱动模块,如下:
1: kd> u fffff802`13fd6268
360qpesv64+0x26268:
fffff802`13fd6268 mov qword ptr [rsp+08h],rbx
fffff802`13fd626d mov qword ptr [rsp+10h],rbp
fffff802`13fd6272 mov qword ptr [rsp+18h],rsi
fffff802`13fd6277 push rdi
1: kd> lmvm 360qpesv64
start end module name
fffff802`13fb0000 fffff802`14002000 360qpesv64
Loaded symbol image file: 360qpesv64.sys
Image path: 360qpesv64.sys
Image name: 360qpesv64.sys
Timestamp: Wed May 27 20:13:22 2020 (5ECF2C52)
CheckSum: 00054A2A
ImageSize: 00052000
复制代码
可知该回调例程是 360 官方提供。借助 PCHunter 来对比下,其给出的数据如图所示
2.2 动态调试回调例程以表项的第 14 项为例,内容如下,
1: kd> dq ffffcc8b`dcabd320 l3
ffffcc8b`dcabd320 00000000`00000020 fffff802`13d795b4
ffffcc8b`dcabd330 00000000`00000006
1: kd> bp fffff802`13d795b4
1: kd> g
复制代码
断点命中,查看父进程相关信息,如下,
Breakpoint 0 hit
fffff802`13d795b4 48895c2408 mov qword ptr [rsp+8],rbx
1: kd> dt _EPROCESS @$proc -yn ImageFileName
nt!_EPROCESS
+0x450 ImageFileName : [15] "svchost.exe"
复制代码
由此可知,是 svchost.exe 这个父进程创建或者销毁了一个子进程,更具体的信息如下分析;查看下当前的上下文环境;
1: kd> r
rax=fffff80213d795b4 rbx=ffffcb8050526c80 rcx=ffffcc8bdd67e080
rdx=0000000000001f28 rsi=000000000000000d rdi=ffffcc8bdd67e080
rip=fffff80213d795b4 rsp=ffffcb8050526c38 rbp=ffffcb8050526ca9
r8=ffffcb8050526c80 r9=ffffcc8bdc735de0 r10=ffff9401cdcc2760
r11=0000000000000000 r12=0000000000000001 r13=0000000000000000
r14=ffffcc8bdcabd320 r15=fffff80214da2ae8
iopl=0 nv up ei pl zr na po nc
cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000246
根据x64的调用约定可知,rcx寄存器中存储的是EPROCESS对象指针,该对象存储的是即将被创建的子进程的相关信息,可以获取到的作为身份识别或者安全检测的关键信息如下:
1: kd> dt _EPROCESS ffffcc8bdd67e080 -yn ImageFile
ntdll!_EPROCESS
+0x448 ImageFilePointer : 0xffffcc8b`dc97c5c0 _FILE_OBJECT
+0x450 ImageFileName : [15] "UpdateAssistan"
1: kd> dt 0xffffcc8b`dc97c5c0 _FILE_OBJECT -yn FileName
ntdll!_FILE_OBJECT
+0x058 FileName : _UNICODE_STRING "\Windows\UpdateAssistant\UpdateAssistant.exe"
1: kd> .process /p ffffcc8bdd67e080; !peb 186ef07000
Implicit process is now ffffcc8b`dd67e080
.cache forcedecodeuser done
PEB at 000000186ef07000
CurrentDirectory: 'C:\Windows\system32\'
WindowTitle: 'C:\Windows\UpdateAssistant\UpdateAssistant.exe'
ImageFile: 'C:\Windows\UpdateAssistant\UpdateAssistant.exe'
CommandLine: 'C:\Windows\UpdateAssistant\UpdateAssistant.exe /ClientID Win10Upgrade:VNL:NHV19:{} /CalendarRun'
复制代码
可以获取到该进程的 EXE 路径,创建时的命令行参数,父进程的 PID 等信息,这些足以用于安全软件的检测。父进程的完整调用栈如下,
1: kd> k
# Child-SP RetAddr Call Site
00 ffffcb80`50526c38 fffff802`14ef4ae5 0xfffff802`13d795b4
01 ffffcb80`50526c40 fffff802`14ef752c nt!PspCallProcessNotifyRoutines+0x249
02 ffffcb80`50526d10 fffff802`14f2797b nt!PspInsertThread+0x5a4
03 ffffcb80`50526dd0 fffff802`14b79553 nt!NtCreateUserProcess+0x9c7
04 ffffcb80`50527a10 00007ffe`547d1654 nt!KiSystemServiceCopyEnd+0x13
05 0000002f`4b67d258 00007ffe`50b406df ntdll!NtCreateUserProcess+0x14
06 0000002f`4b67d260 00007ffe`50b3d013 KERNELBASE!CreateProcessInternalW+0x1b3f
07 0000002f`4b67dec0 00007ffe`5216ee0f KERNELBASE!CreateProcessAsUserW+0x63
08 0000002f`4b67df30 00007ffe`4ce0a136 KERNEL32!CreateProcessAsUserWStub+0x5f
09 0000002f`4b67dfa0 00007ffe`4ce0bdd9 UBPM!UbpmpLaunchAction+0xb36
0a 0000002f`4b67e280 00007ffe`4ce08ee0 UBPM!UbpmLaunchTaskExe+0x279
0b 0000002f`4b67e490 00007ffe`4ce10a86 UBPM!UbpmpLaunchOneTask+0x6c0
0c 0000002f`4b67e8f0 00007ffe`4ce0b8bc UBPM!UbpmpHandleGroupSid+0x236
0d 0000002f`4b67ea10 00007ffe`4ce0b78b UBPM!UbpmpLaunchExeAction+0xec
0e 0000002f`4b67eaf0 00007ffe`4ce0b5a3 UBPM!UbpmpTakeAction+0xeb
0f 0000002f`4b67eb50 00007ffe`4ce0b193 UBPM!UbpmpPerformTriggerActions+0x293
10 0000002f`4b67eca0 00007ffe`4ce1316c UBPM!UbpmpHandleTriggerArrived+0x563
11 0000002f`4b67ef50 00007ffe`508c32d0 UBPM!UbpmpRepetitionArrived+0x1c
12 0000002f`4b67ef90 00007ffe`508c3033 EventAggregation!EaiSignalAggregateEvent+0x16c
13 0000002f`4b67f060 00007ffe`508c27aa EventAggregation!EaiSignalCallback+0xe7
14 0000002f`4b67f140 00007ffe`508c253e EventAggregation!EaiProcessNotification+0x1aa
15 0000002f`4b67f270 00007ffe`508caef8 EventAggregation!WnfEventCallback+0x506
16 0000002f`4b67f3a0 00007ffe`5476769f EventAggregation!AggregateEventWnfCallback+0x38
17 0000002f`4b67f3f0 00007ffe`54767a51 ntdll!RtlpWnfWalkUserSubscriptionList+0x29b
18 0000002f`4b67f4e0 00007ffe`5476b510 ntdll!RtlpWnfProcessCurrentDescriptor+0x105
19 0000002f`4b67f560 00007ffe`54766b59 ntdll!RtlpWnfNotificationThread+0x80
1a 0000002f`4b67f5c0 00007ffe`54764b70 ntdll!TppExecuteWaitCallback+0xe1
1b 0000002f`4b67f600 00007ffe`52171fe4 ntdll!TppWorkerThread+0x8d0
1c 0000002f`4b67f990 00007ffe`5479ef91 KERNEL32!BaseThreadInitThunk+0x14
1d 0000002f`4b67f9c0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
复制代码
由于前四个参数是通过的寄存器传递的,所以无法直接通过栈来回溯到参数,但可以通过手动方式分析得到。分析 ntdll!NtCreateUserProcess 的调用父函数,其返回地址处的汇编代码如下所示:
1: kd> ub 00007ffe`50b406df
KERNELBASE!CreateProcessInternalW+0x1b11:
00007ffe`50b406b1 488b842440040000 mov rax,qword ptr [rsp+440h]
00007ffe`50b406b9 4889442420 mov qword ptr [rsp+20h],rax
00007ffe`50b406be b800000002 mov eax,2000000h
00007ffe`50b406c3 448bc8 mov r9d,eax
00007ffe`50b406c6 448bc0 mov r8d,eax
00007ffe`50b406c9 488d942448010000 lea rdx,[rsp+148h]
00007ffe`50b406d1 488d8c24e0000000 lea rcx,[rsp+0E0h]
00007ffe`50b406d9 ff1521901600 call qword ptr [KERNELBASE!_imp_NtCreateUserProcess (00007ffe`50ca9700)]
可知,NtCreateUserProcess第一个参数和第二个参数再rsp+0xE0和rsp+0x148处;查看该处的数据如下:
1: kd> dpu 0000002f`4b67d260+E0 0000002f`4b67d260+148
0000002f`4b67d340 00000000`00000000
0000002f`4b67d348 00000000`00000004
0000002f`4b67d350 00000100`00000000
0000002f`4b67d358 00000000`00000020
0000002f`4b67d360 000001f2`d9b87cc0 "C:\Windows\UpdateAssistant\UpdateAssistant.exe"
0000002f`4b67d368 00000000`00000000
0000002f`4b67d370 00000000`00000000
0000002f`4b67d378 0000002f`00000000
0000002f`4b67d380 000001f2`d8d43580 "C:\Windows\UpdateAssistant\UpdateAssistant.exe /ClientI"
0000002f`4b67d388 00000000`00000000
0000002f`4b67d390 00000000`00008664
0000002f`4b67d398 000001f2`d9d73c40 "ALLUSERSPROFILE=C:\ProgramData"
0000002f`4b67d3a0 00000000`00000000
0000002f`4b67d3a8 00000000`00000000
复制代码
由此可知,svchost 拉起的子进程为 UpdateAssistant.exe,与之前分析得到的参数也相吻合。从调用栈可知,是在 svchost 创建子进程 UpdateAssistant.exe 时遍历的回调例程,通知给驱动软件做相应的处理。
我是渗透测试工作者,热爱渗透,用心渗透,为了更好方便大家学习与阅读,我把文中文档整理一遍,另外还包括渗透测试、攻防方面的文档,需要文档的点我领取!!!
3 结束语
本文详细地分析了系统实现进程回调安全机制的内部原理,借助 IDA 工具逆向系统镜像文件,分析了实现的关键代码部分,得到了关键数据结构及系统额外做的数据检测校验算法。对关键全局变量的作用也做了详细解释。
此外,通过逆向分析,给出了整个机制的调用源与调用链。最后基于双机调试环境,动态查看内核中维护的进程回调例程表,并且下断点实际动态调试了整个过程。
对于驱动开发,内核安全相关方面的研究工作者提供了该技术实现原理与机制。基于得到的关键数据结构和系统数据检验保护算法,可以解密关键字段后检测表项中的恶意代码,也可以用于安全厂商在对抗过程中,完全脱离系统提供的 API 手工构建表项,达到监控系统行为的目的。
评论