大家好!这里是219攻防实验室!
0x00 关于内存扫描
内存扫描也称运行时分析(Run-time Analysis)与传统的文件静态扫描类似,一般依赖一组已知的特征码对恶意代码进行识别,与静态扫描不同的是内存扫描分析的对象是内存(RAM),而不是磁盘上的文件。因为依赖特征码,内存扫描一般无法检测未知的恶意代码,但是误报率接近于零并且可以识别出恶意代码家族。
相较于静态扫描,内存扫描对攻击者更具挑战,因为在内存中混淆代码的难度更大。攻击者可以通过各类无文件攻击(Fileless Attack)、代码加密混淆、加壳等技术避免静态扫描(包括基于文件的机器学习检测)或者修改文件特征,但运行时内存特征一般不变。如各种Loader免杀方案一般是通过使用不同编程语言与编码或者加密算法组合修改恶意代码的文件特征,但执行后始终会将原始镜像在内存中解密执行。
0x01 分析目标及思路
-
确定内存扫描功能具体由哪个模块实现以及实现方式; -
定位内存中具体哪段内存数据是恶意代码特征码; -
内存扫描如何触发的,流程是怎样的。
-
先静态看模块描述,根据模块描述对功能进行猜测; -
对模块功能进行求证,微软的软件一般都有pdb,静态反编译后看下函数名,找下是否有内存扫描相关函数; -
动态分析,对相关函数下断点,根据函数调用堆栈进行分析,分析具体的流程及实现。
0x02 分析过程
1. 模块分析
-
MpRtp.dll: 运行时监控,一般通过和驱动通信获取监控信息,触发其他扫描,内存扫描如何触发应该和这个模块相关。 -
MpClient.dll/MpSvc.dll: 信息较少,应该是给其他组件提供的接口,可能有内存扫描相关接口和实现。 -
mpengine.dll: Defender反病毒核心引擎,特征库匹配、恶意代码判定相关,之前的研究基本都是针对这个模块进行的分析的,还包含本地脱壳、本地沙箱模拟等功能,内存扫描特征码匹配应该在这。
-
MpRtp.dll
//通过微文件端口与WdFilter.sys进行通信获取驱动回调监控信息,可能与触发内存扫描相关
RealtimeProtection::CThreadPoolIoFilterRequest::DoOverlappedOperation
RealtimeProtection::CFilterCommunicatorBase::CommunicatorMainFunction
RealtimeProtection::CFilterCommunicatorBase::ParkFilterRequestToFilter
FilterGetMessage
-
MpClient.dll
//通过内存扫描关键字可以找到一些内存扫描相关函数
MpFastMemoryScanOpen MpClient::CMpMemoryScan::HandleScanEvents
-
MpSvc.dll
OnDemandStartScan
MpService::NewScanContext
MpService::CMpSvcScanWorkItem::OnAction(void)
MpService::CMpSvcScanWorkItem::Run(void)
MpService::CGlobalEventsJob::OnAction
MpService::CMpMemScanEngineVfz::vfz_Read
-
mpengine.dll
CResmgrems::Scan
CEMSContext::EmsScan
CSMSProcess::ScanCompleted
ProcessMemoryScanCache::ProcessMemoryScanCach
eSMSMaps::ShouldSendMemoryScanReport
2. 动态分析环境准备
动态分析前需要先去除Defender的自保护,详细如下:
-
ObProcess(PreCall): 进程对象保护回调,原理是通过注册进程对象回调对OpenProcess这类API的打开进程获取进程句柄行为进行过滤,在WdFilter.sys驱动中实现。可以使用Windows-Kernel-Explorer移除掉WdFilter.sys注册的ObProcess(PreCall)回调保护。 -
PPL(Protected Process Light):在PPL的保护下未经合法签名的程序不能对PPL保护的进程进行任意访问,只有非常受限的权限,可以使用PPLKiller或者mimikatz去除MsMpEng.exe进程的PPL保护。
0: kd> !process 0 0 MsMpEng.exe
PROCESS ffff8009e37ca080
SessionId: 0 Cid: 107c Peb: e79fb62000 ParentCid: 029c
DirBase: 12dc55000 ObjectTable: ffffcc06b8867700 HandleCount: 749.
Image: MsMpEng.exe
0: kd> .process /i /p ffff8009e37ca080
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
0: kd> g
Break instruction exception - code 80000003 (first chance)
nt!DbgBreakPointWithStatus:
fffff805`05400fc0 cc int 3
0: kd> .reload /f /user
Loading User Symbols
........
3. 内存扫描实现模块定位及内存扫描功能实现分析
1: kd> bu KernelBase!ReadProcessMemory
1: kd> k
# Child-SP RetAddr Call Site
00 000000e7`a017d988 00007fff`64e7da0f KERNELBASE!ReadProcessMemory
01 000000e7`a017d990 00007fff`64e7d613 mpengine!CSMSProcess::InvokeScanner+0xb7
02 000000e7`a017da00 00007fff`64e7d384 mpengine!CSMSProcess::ScanRange+0xa7
03 000000e7`a017daa0 00007fff`64e7cb9a mpengine!CSMSProcess::Scan1Worker+0x244
04 000000e7`a017db40 00007fff`64cecca7 mpengine!CSMSProcess::Scan+0x45e
05 000000e7`a017dc30 00007fff`654f19c0 mpengine!CEMSContext::EmsScan+0x34b
06 000000e7`a017dd00 00007fff`654f0130 mpengine!EmsEnumProcesses+0x140
07 000000e7`a017ddd0 00007fff`654f0725 mpengine!RunEMS+0x2dc
08 000000e7`a017df10 00007fff`654f01c5 mpengine!CResmgrems::ScanImpl+0x21d
...
16 000000e7`a017f9b0 00007fff`7dcd2260 mpclient!MpGetASRPerRuleExclusions+0x5f043
17 000000e7`a017fa00 00007fff`7dcc31aa ntdll!TppWorkpExecuteCallback+0x130
18 000000e7`a017fa50 00007fff`7da67034 ntdll!TppWorkerThread+0x68a
19 000000e7`a017fd50 00007fff`7dcc26a1 KERNEL32!BaseThreadInitThunk+0x14
1a 000000e7`a017fd80 00000000`00000000 ntdll!RtlUserThreadStart+0x21
1: kd> !handle @rcx
PROCESS ffff8009e37ca080
SessionId: 0 Cid: 107c Peb: e79fb62000 ParentCid: 029c
DirBase: 12dc55000 ObjectTable: ffffcc06b8867700 HandleCount: 967.
Image: MsMpEng.exe
Handle table at ffffcc06b8867700 with 967 entries in use
0a14: Object: ffff8009e6914340 GrantedAccess: 00001410 (Protected) Entry: ffffcc06aeaf8850
Object: ffff8009e6914340 Type: (ffff8009dcccaf00) Process
ObjectHeader: ffff8009e6914310 (new version)
HandleCount: 10 PointerCount: 294928
1: kd> !process ffff8009e6914340 0
PROCESS ffff8009e6914340
SessionId: 1 Cid: 10ec Peb: f18504000 ParentCid: 1aa8
DirBase: 45344000 ObjectTable: ffffcc06b1110880 HandleCount: 331.
Image: payload.exe
# Child-SP RetAddr Call Site
00 000000e7`a017d548 00007fff`64b4ed9b mpengine!BMMatchEx2
01 000000e7`a017d550 00007fff`64b4e022 mpengine!hstr_internal_search_worker+0x35b
02 000000e7`a017d880 00007fff`64eaf6e1 mpengine!hstr_internal_search+0x7e
03 000000e7`a017d910 00007fff`64eaf64e mpengine!CSMSScanner::ScanWorker+0x59
04 000000e7`a017d960 00007fff`64e7dae1 mpengine!CSMSScanner::Scan+0x3e
05 000000e7`a017d990 00007fff`64e7d613 mpengine!CSMSProcess::InvokeScanner+0x189
06 000000e7`a017da00 00007fff`64e7d384 mpengine!CSMSProcess::ScanRange+0xa7
07 000000e7`a017daa0 00007fff`64e7cb9a mpengine!CSMSProcess::Scan1Worker+0x244
08 000000e7`a017db40 00007fff`64cecca7 mpengine!CSMSProcess::Scan+0x45e
09 000000e7`a017dc30 00007fff`654f19c0 mpengine!CEMSContext::EmsScan+0x34b
...
4. 侧信道定位内存扫描特征码
猜测Defender内存扫描匹配成功和匹配失败ReadProcessMemory读取内存大小应该会不一样,可以根据这种现象判断是否匹配成功,提取特征码,类似爆破密码时可以根据爆破成功和失败返回的内容不同判断是否爆破成功。与预想一致Defender内存匹配真有这个问题,大部分情况下ReadProcessMemory读取的内存大小为0x1000,一旦ReadProcessMemory读取的内存大小是一个不常见的小数字时候就会弹出拦截信息,根据这种现象可以设置Windbg条件断点,断下后打印的栈回溯如下:
00 000000e7`a00fed80 00007fff`65592d69 mpengine!CEMSTele::Matched+0x318
01 000000e7`a00fee50 00007fff`653cf858 mpengine!CSMSScanner::EnumHSTR+0x1f9
02 000000e7`a00feee0 00007fff`653cfbb4 mpengine!CSMSProcess::Report+0x178
03 000000e7`a00ff010 00007fff`64e7ccfc mpengine!CSMSProcess::ScanCompleted+0xd0
04 000000e7`a00ff040 00007fff`65595667 mpengine!CSMSProcess::Scan+0x5c0
05 000000e7`a00ff130 00007fff`65594a58 mpengine!CSMSContext::ScanProcess+0xc3
...
1: kd> bp mpengine!CEMSTele::Matched+0x318 ".printf "Matched:n"; db rdi+1ch L@r15"
1: kd> g
Matched: 00000284`ea182166 4c 63 c2 4d 03 c0 42 0f-10 04 c0 48 8b c1 f3 0f Lc.M..B....H....
00000284`ea182176 7f 01 c3
5. 曲折的分析内存扫描触发逻辑
//mpclient.dll 无pdb符号文件,需要通过类似bindiff这样的工具找到CommonUtil::CMpSimpleThreadPool::Submit函数在当前版本的偏移
0: kd> bp mpclient.dll+a7420 ".echo "mpclient::CommonUtil::CMpSimpleThreadPool::Submit";k;g"
0: kd> bp mpengine!CommonUtil::CMpSimpleThreadPool::Submit ".echo "mpengine!CommonUtil::CMpSimpleThreadPool::Submit";k;g"
-
触发源头是WdFilter.sys驱动捕获到模块加载事件后通过微文件端口将信息发送给mprtp.dll,然后mprtp.dll向线程池中提交了一个ModuleLoad事件待处理。
//由于没有pdb,这里调用栈中的函数名是通过bindiff对比出来的
# Child-SP RetAddr Call Site
00 000000e7`9fcff718 00007fff`73db3ec8 mpclient!CommonUtil::CMpSimpleThreadPool::Submit mpclient!MpGetASRPerRuleExclusions+0x5e9d0
01 000000e7`9fcff720 00007fff`73e1adb0 mprtp!RealtimeProtection::CMpPluginWorkItemBase::PrioritizedDispatchJob_75E833E40 mprtp!MpPluginDeviceControlValidateDataDuplicationRemoteLocationConfiguration+0x1d2b8
02 000000e7`9fcff750 00007fff`73e0cde2 mprtp!RealtimeProtection::CProcessAgent::HandleModuleLoad_75E89ACF0 mprtp!MpPluginDeviceControlValidateDataDuplicationRemoteLocationConfiguration+0x841a0
03 000000e7`9fcff7c0 00007fff`73e2465e mprtp!RealtimeProtection::CProcessWatcher::HandleAsynchronousRequest_75E88CCD0 mprtp!MpPluginDeviceControlValidateDataDuplicationRemoteLocationConfiguration+0x761d2
04 000000e7`9fcff810 00000000`00000000 mprtp!RealtimeProtection::CAsynchronousWatcherBase::HandleRequest_75E8A45B0bl mprtp!MpPluginDeviceControlValidateDataDuplicationRemoteLocationConfiguration+0x8da4e
05 000000e7`a097f188 00007fff`73e08b3c mprtp!RealtimeProtection::CFileSystemWatcher::HandleRequest_75E8886C0 mprtp!MpPluginDeviceControlValidateDataDuplicationRemoteLocationConfiguration+0x71f2c
06 000000e7`a097f768 00007fff`73e52233 mprtp!RealtimeProtection::CFilterCommunicatorBase::CommunicatorMainFunction_75E8D1884 mprtp!MpPluginDeviceControlValidateDataDuplicationRemoteLocationConfiguration+0xbb623
-
然后会对加载的Module进行一次文件检测,检测文件是否可信,不可信的模块会被添加到MOAC中。
00 00000047`a287e338 00007ffa`1e8d2717 mpengine!MOACManager::AddUntrustedToMoac
01 00000047`a287e340 00007ffa`1e3a49d9 mpengine!CacheMgr::AddUntrustedToMoac+0x37
02 00000047`a287e370 00007ffa`1e95041d mpengine!IsFriendlyFile+0x3fd
03 00000047`a287e4a0 00007ffa`1e6dd5bc mpengine!VerifyIsFriendlyFile+0xe1
04 00000047`a287e580 00007ffa`1e724a81 mpengine!ProcessContext::IsFriendlyImageFile+0x13c
05 00000047`a287e640 00007ffa`1e71dec1 mpengine!SignatureHandler::ReportDetection+0x5f1
06 00000047`a287ec50 00007ffa`1e725a37 mpengine!SignatureHandler::HandleDetection+0x421
07 00000047`a287ee30 00007ffa`1e725c43 mpengine!SignatureHandler::TestForDetection+0x2a3
08 00000047`a287f2a0 00007ffa`1e7261f8 mpengine!SignatureHandler::TestForDetectionWithTokenizedPath+0x1db
09 00000047`a287f380 00007ffa`1e721185 mpengine!SignatureHandler::TestForModuleLoad+0x7c
0a 00000047`a287f3f0 00007ffa`1e70dede mpengine!SignatureHandler::HandleNotification+0xa95
...
10 00000047`a287f9d0 00007ffa`1e82b982 mpengine!NotificationItem::OnAction+0x42
11 00000047`a287fa20 00007ffa`3c132260 mpengine!CommonUtil::CMpSimpleThreadPool::AsyncDequeue+0xde
12 00000047`a287fa60 00007ffa`3c1231aa ntdll!TppWorkpExecuteCallback+0x130
13 00000047`a287fab0 00007ffa`3a277034 ntdll!TppWorkerThread+0x68a
14 00000047`a287fdb0 00007ffa`3c1226a1 KERNEL32!BaseThreadInitThunk+0x14
15 00000047`a287fde0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
-
因为Module非可信,同时还会被提交到一个事件到检测队列QueueDetection。
00 00000047`a287e468 00007ffa`1e72c191 mpengine!CommonUtil::CMpSimpleThreadPool::Submit
01 00000047`a287e470 00007ffa`1e6ea00d mpengine!DetectionQueue::QueueDetection+0x369
02 00000047`a287e5c0 00007ffa`1e731116 mpengine!DetectionController::QueueDetection+0x4d
03 00000047`a287e5f0 00007ffa`1e72529d mpengine!ScanHandlerBase::ReportDetection+0xda
04 00000047`a287e640 00007ffa`1e71dec1 mpengine!SignatureHandler::ReportDetection+0xe0d
05 00000047`a287ec50 00007ffa`1e725a37 mpengine!SignatureHandler::HandleDetection+0x421
06 00000047`a287ee30 00007ffa`1e725c43 mpengine!SignatureHandler::TestForDetection+0x2a3
07 00000047`a287f2a0 00007ffa`1e7261f8 mpengine!SignatureHandler::TestForDetectionWithTokenizedPath+0x1db
08 00000047`a287f380 00007ffa`1e721185 mpengine!SignatureHandler::TestForModuleLoad+0x7c
...
-
DetectionQueue::OnAction消费检测队列中的事件,然后TriggerEmsScan会提交一个内存扫描任务到线程池。
00 00000047`a287f118 00007ffa`1e70a05e mpengine!CommonUtil::CMpSimpleThreadPool::Submit
01 00000047`a287f120 00007ffa`1e709b8f mpengine!TriggerScan+0x6a
02 00000047`a287f170 00007ffa`1e6c6b09 mpengine!TriggerEmsScan+0x23b
03 00000047`a287f2a0 00007ffa`1e6cb266 mpengine!DoTriggeredActions+0x2e9
04 00000047`a287f350 00007ffa`1e6cd156 mpengine!PerformDetectionActions+0x2d6
05 00000047`a287f4d0 00007ffa`1e6cb643 mpengine!DetectionItem::UpdateCharacteristics+0x8c6
06 00000047`a287f6b0 00007ffa`1e72ba54 mpengine!DetectionItem::Send+0xf7
07 00000047`a287f820 00007ffa`1e72bd53 mpengine!DetectionQueue::DispatchDetections+0x3f4
08 00000047`a287f9a0 00007ffa`1e82b982 mpengine!DetectionQueue::OnAction+0xa3
09 00000047`a287fa20 00007ffa`3c132260 mpengine!CommonUtil::CMpSimpleThreadPool::AsyncDequeue+0xde
0a 00000047`a287fa60 00007ffa`3c1231aa ntdll!TppWorkpExecuteCallback+0x130
0b 00000047`a287fab0 00007ffa`3a277034 ntdll!TppWorkerThread+0x68a
0c 00000047`a287fdb0 00007ffa`3c1226a1 KERNEL32!BaseThreadInitThunk+0x14
0d 00000047`a287fde0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
-
线程池会执行内存扫描的任务,执行内存扫描的栈回溯。
//bp mpengine!CEMSContext::EmsScan+0x34 ".printf "CEMSContext::EmsScan:%mu", rax;g"
CEMSContext::EmsScan:DeviceHarddiskVolume3payload.exe
# Child-SP RetAddr Call Site
00 00000047`a337d6b0 00007ffa`1ebc19c0 mpengine!CEMSContext::EmsScan+0x34
01 00000047`a337d780 00007ffa`1ebc0130 mpengine!EmsEnumProcesses+0x140
02 00000047`a337d850 00007ffa`1ebc0725 mpengine!RunEMS+0x2dc
03 00000047`a337d990 00007ffa`1ebc01c5 mpengine!CResmgrems::ScanImpl+0x21d
04 00000047`a337da50 00007ffa`1e366a1f mpengine!CResmgrems::Scan+0x15
05 00000047`a337db80 00007ffa`1e791082 mpengine!ResmgrProcessResource+0x1c7
06 00000047`a337ddb0 00007ffa`1e77656d mpengine!ResScan+0xa36
07 00000047`a337e1f0 00007ffa`1e7797d8 mpengine!ScanOpenWithContext+0x1911
08 00000047`a337eaf0 00007ffa`1e759248 mpengine!UberScanOpen+0xa64
09 00000047`a337ec10 00007ffa`1e519c13 mpengine!ksignal+0x6a8
0a 00000047`a337ed90 00007ffa`1e518fcb mpengine!DispatchSignalHelper+0x6f
0b 00000047`a337edf0 00007ffa`2a8070f3 mpengine!DispatchSignalOnHandle+0x9b
0c 00000047`a337f260 00000000`00000000 mpsvc!ServiceCrtMain+0x1f6e3
这里可以采用排除法验证结果是否正确,流程如下:
-
使用Windows-Kernel-Explorer移除WdFilter.sys除模块加载回调以外的其他监控回调(进程/线程/注册表),检查是否触发最终的内存扫描。这里测试结果是能触发内存扫描 -
只移除WdFilter.sys模块加载回调,检查是否触发最终内存扫描。这里测试结果是不能。
-
ArNotification
-
BootChangeNotification
-
DesktopNotification
-
EtwNotification
-
FileNotification
-
InternalNotification
-
NetworkNotification2
-
ProcessNotification
-
RegistryNotification
-
RemoteThreadCreateNotification
-
VolumeMountNotification
限于时间和篇幅这里未对其他事件进行分析,不排除上面的其他事件也可能会导致触发内存扫描逻辑。
0x03 总结
本文对Defender的内存扫描功能进行了一个简单分析,最后总结回答下分析目标中提出的三个问题:
-
确定内存扫描功能具体由哪个模块实现以及实现方式
内存扫描功能具体实现在mpengine.dll中实现,实现方式是通过ReadProcessMemory读取目标进程内存数据,然后通过BM字符串匹配算法对目标内存数据与特征码数据进行匹配,与文件静态检测类似,只是数据源不同。 -
定位内存中具体哪段内存数据是恶意代码特征码
可以通过侧信道分析方法,利用ReadProcessMemory读取数据大小不同定位到特征码,对mpengine!CEMSTele::Matched+0x318下断点可直接打印当次检测出的恶意内存特征码。 -
内存扫描如何触发的,流程是怎样的
本次分析的样本是模块加载事件触发的内存扫描,不排除其他事件也会触发内存扫描。流程比较复杂,中间会经过不同的异步检测过程最终才到内存扫描逻辑。
0x04 参考链接
-
https://labs.withsecure.com/publications/bypassing-windows-defender-runtime-scanning -
https://learn.microsoft.com/zh-cn/microsoft-365/security/intelligence/fileless-threats -
https://github.com/AxtMueller/Windows-Kernel-Explorer
原文始发于微信公众号(219攻防实验室):Windows Defender内存扫描功能分析