本帖最后由 ANY.LNK 于 2024-10-30 01:19 编辑
原文地址:https://retooling.io/blog/an-une ... ers-signature-world,https://shorturl.at/bPCzW
Windows Defender 自 Windows Vista 以来预安装于各个版本的 Windows 上作为默认安全方案。此软件相当复杂,用于满足EDR和EPP使用需求。
因此,微软出售两种产品。Microsoft Defender for Endpoint为基于云的终端安全解决方案,包含具有云优势的传感器功能。Microsoft Defender Antivirus (MDAV),则是在任何全新的Windows上默认安装并启用的端点保护平台(EPP),本文旨在分析MDAV
由于MDAV的广泛应用,一直是各方人员的研究目标,但介于其复杂性,往往针对的是特定的组件。在本文中,我们继续研究MDAV的签名,包括:
·签名数据库
·签名加载过程
·不同种类签名的类型和布局
·对两种签名类型的细节讨论,以及PEHSTRPEHSTR_EXT
Microsoft Defender框架如下:
首个MDAV加载的组件是ELAM驱动程序WdBoot.sys,早于一切其他驱动程序,其会扫描此后加载的每个驱动映像。
它使用特定的自己的签名集,存储在HKLM\ELAM\Microsoft Antimalware Platform\Measured。当ELAM驱动程序卸载后,该注册表键值将无法访问
Microsoft Defender Antivirus Service主要功能是启动MDAV的主程序,该程序MsMpEng.exe本身大小较小(约300K),但它加载了如下的较大模块:
·MsRtp.dll:用于管理实时保护
·MpSvc.dll:加载并管理主要模块MpEngine.dll
·MpEngine.dll:这是最大的组件,用于实行扫描、模拟执行、模块运作,从VDM文件中加载签名和签名处理等等功能
MpCmdRun.exe是一个外部命令行工具,使用MpClient.dll同主服务进行通信交互。
MpClient.dll是一个辅助库实现一系列的和主服务间的RPC通信。(用于获得状态确认或请求执行扫描等)
此外,还有WdFilter.sys, MDAV主要的内核空间元件,通过注册minifilter监控全局的文件系统访问。注册通知回调(映像回调、进程创建、对象访问)以及等等
分析基于引擎版本1.1.23100,签名版本1.401.1166.0
签名数据库:
4个不同的VDM文件:
·mpavbase.vdm:月度更新,一般伴随平台更新,包含反恶意软件签名
·mpasbase.vdm:月度更新,一般伴随平台更新,包含反间谍软件签名
·mpavdlta.vdm:每日多次更新,只包含新签名,运行时在内存中与mpavbase.vdm合并
·mpasdlta.vdm:每日多次更新,只包含新签名,运行时在内存中同mpasbase.vdm合并
位于C:\ProgramData\Microsoft\Windows Defender\Definition Updates\<RandomGUID>\ 。
mpavbase.vdm,mpasbase.vdm均为PE文件,其.rsrc资源字节段包含压缩的签名。这些资源字节段都头部均为魔数RMDX。
在资源起始偏移量0x18区域的uint32区域包含指向有效载荷的相对偏移量(图中为0x130)。实际上,这指针后8字节,压缩的数据缓冲区就开始了。
在偏移量0x40处,全局头有一个DWORD数组,每个DWORD包含两个部分:签名类型(黄色),签名数量(紫色,字节反方向排列)
有效载荷在0x138处开始,为gzip格式,可用如下python代码解压:
- import zlib
- compressed = open('x.gz', 'rb').read()
- decompressed = zlib.decompress(compressed, -zlib.MAX_WBITS)
复制代码
MpEngine:
MDAV的核心逻辑在MpEngine.dll中,由MpSvc.dll的MpSvc!LoadEngine函数调用MpSvc!InitEngineContext加载。这里MpEngine.dll通过KernelBase!LoadLibraryExW加载,并检索使服务完成引擎初始化的函数之一的地址:导出mpengine!__rsignal。粗略地说,rsignal功能本质上是mpengine!DispatchSignalOnHandle函数的包装器,调用与输入参数signal_id相对应的函数。
以下为函数的原型(伪代码?):
- UINT64 DispatchSignalOnHandle(
- PVOID g_hSignalptr,
- UINT64 signal_id,
- BYTE *pParams,
- SIZE_T cbParams
- )
复制代码
为了初始化MpEngine,MpSvc!InitEngineContext调用如下的rsignal函数:
- (pMpengineCtx->pfn__rsignal)(
- &pMpengineCtx->hMpEngineInstance,
- 0x4036,
- pParam,
- 0x1B8
- );
复制代码
参数如下:
·pMpengineCtx->hMpEngineInstance:输出参数,接收初始化引擎的句柄
·0x4036为初始化请求的signal_id
·pParams指向初始化参数
·cbParams是pParams的大小
MpEngine对应指令0x4036的函数为StartMpEngine,此函数使用如下指令ID递归调用DispatchSignalOnHandle
·0x4019:触发InitializeMpEngine的执行,负责初始化gktab的结构(很大,0x15bb0字节;包含数十个区域)。然而首个(也很可能是最相关的)是指向ksignal功能的指针,该函数实现了signal_id相关的大部分功能(超过50个)
·0x401a:调用ksignal并执行管理Defender排除项的函数
·0x400b:调用ksignal以调用modprobe_init,在此处对模块进行初始化
MpEngine模块初始化
MpEngine包含大量模块(名为AutoInitModules)。部分用于引入特定的文件格式(例如PE、ELF、Mach-O)支持并执行针对性的扫描(在引擎初始化时通过ScanRegister函数注册);另一些执行辅助功能,例如引擎加载器
这些模块被全局数组g_pUniModEntries查询。每个入口点都是unimod_entry_t的结构体:
- struct unimod_entry_t
- {
- PCHAR pModuleName;
- PVOID pfnInit;
- PVOID pfnCleanup;
- __int64 Unk;
- };
复制代码
pModule是人类可读的模块名称,pfnInit是指向初始化函数的指针,pfnCleanup指向清理函数。最后一般为0或1,但这不重要。
如前所述,MpSvc!InitEngineContext使用多个signal_id调用__rsignal,其中之一会触发modprobe_init函数的执行,本节余下部分将着重介绍该函数
该函数分三阶段执行:
1.预加载签名数据库:
此阶段包括解析主标头并初始化必要的数据结构
通过调用mpengine!preload_database实现
2.执行初始化函数:
调用所有的AutoInitModules初始化函数
通过循环遍历g_pUniModEntries中的unimod_entry_t入口点,并为每个入口点调用pfnInit函数
3.完成签名加载过程
最终阶段包括完成签名加载过程
通过调用mpengine!load_database实现
对所有的模块进行描述工程量巨大,在此,我们主要关注cksig和pefile模块。
cksig模块
cksig被分类为签名加载器,在mpengine!cksig_init_module中进行初始化。在此阶段,签名数据库尚未加载。由于每种签名的语义都不尽相同,因此引擎为每种签名都配备了专用的加载器。指定函数会读取签名的正文并加载入内存。每个处理特定签名格式的模块必须向引擎注册回调。
当稍后签名加载过程完成(上述阶段3),回调会调用VDM文件中的每条记录。
目前已知如下两个函数会注册回调:
mpengine!RegisterForDatabaseRecords和mpengine!regcntl
其中mpengine!RegisterForDatabaseRecords函数输入为一个全局变量的地址,该变量接收签名句柄和加载器回调
mpengine!regcntl接收如下定义的hstr_handler输入
- struct __declspec(align(8)) hstr_handler
- {
- UINT64 (__stdcall *pfn_push)(UINT64, UINT16 *, UINT64, UINT64, UINT32);
- UINT64 pHstrSigs;
- UINT8 hstr_type;
- UINT64 (__stdcall *pfn_pushend)(UINT64);
- UINT64 (__stdcall *pfn_unload)(UINT64);
- PVOID pHstrSigs2;
- };
复制代码
pfn_push是指向hstr_type的类型签名处理句柄的函数指针;pHstrSigs和pHstrSigs2指向当前hstr_type的签名记录;pfn_pushend和pfn_unload是另两个处理签名的函数,不过不在此帖的讨论范围内。
cksig使用RegisterForDatabaseRecords和regcntl注册两种类别的签名,我们接下来将重点关注后者,因为它针对的是HSTR信号。它的初始化由pattsearch_init子例程完成(由cksig_init_module调用),此例程为特定类别的名为HSTR的签名设立回调。此类别的签名如下:
ID SIGNATURE TYPE
97 SIGNATURE_TYPE_PEHSTR
120 SIGNATURE_TYPE_PEHSTR_EXT
133 SIGNATURE_TYPE_PEHSTR_EXT2
140 SIGNATURE_TYPE_ELFHSTR_EXT
141 SIGNATURE_TYPE_MACHOHSTR_EXT
142 SIGNATURE_TYPE_DOSHSTR_EXT
143 SIGNATURE_TYPE_MACROHSTR_EXT
190 SIGNATURE_TYPE_DEXHSTR_EXT
191 SIGNATURE_TYPE_JAVAHSTR_EXT
197 SIGNATURE_TYPE_ARHSTR_EXT
209 SIGNATURE_TYPE_SWFHSTR_EXT
211 SIGNATURE_TYPE_AUTOITHSTR_EXT212 SIGNATURE_TYPE_INNOHSTR_EXT
215 SIGNATURE_TYPE_CMDHSTR_EXT
228 SIGNATURE_TYPE_MDBHSTR_EXT
234 SIGNATURE_TYPE_DMGHSTR_EXT
为了给HSTR签名设立新组别句柄,它首先计算记录的数字并分配一段连续的0x14 * hstr_total_cnt内存区域(下方的反汇编代码展示了PEHSTR子类别的示例):
ESTIMATED_RECORDS函数接收输入的签名类别并为其返回VDM中的签名数字(图中为0x61 0x78和0x85)。请记住,每种签名的记录数信息位于压缩数据之前的VDM全局标题中,因此预加载阶段后即可使用。
图中示例使用regcntl注册回调,特定HSTR类别的句柄在输入中传递(hstr_push, hstr_push_ext, hstr_push_ext2)
值得注意的是:所有PEHSTR指向了同个hstr.pHstrSigs,这与其他HSTR签名不同。我们同样也发现了动态HSTR签名的引用,它们很可能与MAPS(云提供的保护)相关,但我们同样不在此探讨它们。
上图展示了记录数和签名句柄是如何存储以gktab数据结构在内存中的。第一部分是DWORD的数组,每个包含给定签名的记录数。从偏移量0x800开始,我们可以看到第二个数组,包含struct hstr_handler的指针。
完成签名加载过程
目前我们已经讨论了modprobe_init函数的预加载和初始化阶段。为了使MDA签名子系统启动并运行,最后一步必须完成。这一步在mpengine!load_database调用mpengine!DispatchRecords的过程中完成。在此,每个VDM的入口点都被处理,并且,依据签名类型,签名的最终载荷被分配给该签名的句柄(在先前的步骤中由CKSIG模块注册)
pefile_module 和 the hstr_internal_search
该pefile_module属于扫描器类型。实际上在初始化过程中最先被调用的操作之一就是调用mpengine!ScanRegister。第一个参数是实现实际扫描的函数指针(PE文件模块为pefile_scan)
不论何时PE文件扫描被触发,为了匹配特征与HSTR签名,hstr_internal_search都将被调用。我们注意到了两个不同的调用栈:其一通过mpengine!scan_vbuff而另一者通过mpengine!scan_vmem。这可能是在模拟执行中可能会多次触发扫描。hstr_internal_search在g_HstrSigs数组中(0或PE文件)找到正确的文件类型偏移量并调用hstr_internal_search_worker进行实际的搜索。
签名统计
在正式进入签名分析前。我们先汇报下VDM中不同签名的统计数据。
为了了解签名的分布情况,我们写了一个简单的python脚本解析了VDM并按类别统计了VDM签名的数量。
我们注意到,SIGNATURE_TYPE_KCRCE是目前结果中最常见的签名,超过33.8万个,而HSTR签名总共加起来也不到1万个,远远少于KCRCE。
然而,查看每个威胁的出现次数,情况则会发生很大的变化。所谓“出现次数”,就是说在统计过程中如果特定签名在特定威胁上出现,则记为1,反之,则记为0。换句话说,我们不考虑签名的实际出现次数,转而计量它们在特定威胁上的分布。
在此情况下PEHSTR_EXT就和KCRCE非常接近了。如果将PEHSTR_*加到一起,那就会超过KCRCE。事实上,如果我们关注KCRCE的分布,会发现大部分的签名集中在20%的威胁类别上,而PEHSTR的分布则均匀的多。
基于此,我们决定在后续的分析重心集中在HSTR,尤其是PEHSTR上。
让我们再深入些看看!
签名主要结构
在mpavbase.vdm和mpasbase.vdm中的签名遵循分层组合结构。该结构使用特定的方式组织签名。每种威胁都是一种特定类型的恶意软件,由一集合一个或多个签名定义,用于识别并检测相应的威胁存在。
文件中威胁和相关签名的组织结构由特定类型的签名划分。威胁签名集合的起始由SIGNATURE_TYPE_THREAT_BEGIN标记,末尾由SIGNATURE_TYPE_THREAT_END标记。这些分隔符夹合了共同定义和识别特定类别威胁的签名集。
下图描述了威胁和它们签名的组织概念结构。该图直观的展示了上文描述的层次结构。
作为示例,下图展示了威胁Plugx.C,使用多个检测签名。在SIGNATURE_TYPE_THREAT_BEGIN和SIGNATURE_TYPE_THREAT_END之间,有三种不同的签名,分别是:
·绿色标记的SIGNATURE_TYPE_STATIC
·蓝色标记的SIGNATURE_TYPE_PEHSTR_EXT
·紫罗兰色标记的SIGNATURE_TYPE_KCRCE
通过上述组合办法,MDAV可以有效管理并维护威胁签名的数据库。这就可以有效检测和抵御各类安全威胁。
Windows Defender签名遵循如下的通用结构:
- typedef struct _STRUCT_COMMON_SIGNATURE_TYPE {
- UINT8 ui8SignatureType;
- UINT8 ui8SizeLow;
- UINT16 ui16SizeHigh;
- BYTE pbRuleContent[];
- } STRUCT_COMMON_SIGNATURE_TYPE, *PSTRUCT_COMMON_SIGNATURE_TYPE;
复制代码
在此结构中:
·ui8SignatureType指定签名的类型
·ui8SizeLow表示签名的低字节大小
·ui16SizeHigh表示签名的高字节大小
·pbRuleContent[]包含规则内容,总大小计算公式:ui8SizeLow | (ui16SizeHigh << 8).
SIGNATURE_TYPE_THREAT_BEGIN 和 THREAT_END
出现在此内容中的威胁行为体作为不同类别的用于检测其的签名序列存在。这些签名被包含在上述类别之中。
此二者不只是简单的标记,而且还包含了不同的信息。
SIGNATURE_TYPE_THREAT_BEGIN拥有如下结构
- typedef struct _STRUCT_SIG_TYPE_THREAT_BEGIN {
- UINT8 ui8SignatureType;
- UINT8 ui8SizeLow;
- UINT16 ui16SizeHigh;
- UINT32 ui32SignatureId;
- BYTE unknownBytes1[6];
- UINT8 ui8SizeThreatName;
- BYTE unknownBytes2[2];
- CHAR lpszThreatName[ui8SizeThreatName];
- BYTE unknownBytes3[9];
- } STRUCT_SIG_TYPE_THREAT_BEGIN,* PSTRUCT_SIG_TYPE_THREAT_BEGIN;
复制代码
·ui8SignatureType:用于定义签名类别的十六进制数(0x05c)
·ui8SizeLow:整个签名大小的低位部分
·ui16SizeHigh:整个签名大小的高位部分
·ui32SignatureId:签名的标识,由mpengine.dll识别
·unknownBytes1:六位未知字节
·ui8SizeThreatName:以字节表示威胁名称的大小
·unknownBytes2:两位未知字节
·lpszThreatName:表示威胁名称的字符串
·unknownBytes3:九位未知字节
下图展示了一个示例:
STRUCT_SIG_TYPE_THREAT_END拥有通用的签名格式
- typedef struct _STRUCT_SIG_TYPE_THREAT_END {
- UINT8 ui8SignatureType;
- UINT8 ui8SizeLow;
- UINT16 ui16SizeHigh;
- BYTE pbRuleContent[];
- } STRUCT_SIG_TYPE_THREAT_END,* PSTRUCT_SIG_TYPE_THREAT_END;
复制代码
ui8SignatureType拥有值0x5D,pbRuleContent值和对应的SIGNATURE_TYPE_THREAT_BEGIN中的ui32SignatureId一致。
SIGNATURE_TYPE_PEHSTR 对比 SIGNATURE_TYPE_PEHSTR_EXT
二者均基于字节和字符串匹配检测PE文件。
所有此签名类型都包含
·头部
·一条或多条子规则
每条子规则都有特定的权重,为了触发检测,所有触发的子规则总权重必须大于等于规则设定的阈值。
为实现此机制,二者均共享同种头部结构:
- typedef struct _STRUCT_PEHSTR_HEADER {
- UINT16 ui16Unknown;
- UINT8 ui8ThresholdRequiredLow;
- UINT8 ui8ThresholdRequiredHigh;
- UINT8 ui8SubRulesNumberLow;
- UINT8 ui8SubRulesNumberHigh;
- BYTE bEmpty;
- BYTE pbRuleData[];
- } STRUCT_PEHSTR_HEADER, * PSTRUCT_PEHSTR_HEADER;
复制代码
·ui16Unknown:目的未知
·ui8ThresholdRequiredLow:触发检测阈值的低位部分
·ui8ThresholdRequiredHigh:触发检测阈值的高位部分
·ui8SubRulesNumberLow:该签名子规则数的低位部分
·ui8SubRulesNumberHigh:该签名子规则数的高位部分
·pbRuleData[]:包含用于检测的所有子规则
SIGNATURE_TYPE_PEHSTR_EXT的子规则拥有如下结构:
- typedef struct _STRUCT_RULE_PEHSTR_EXT {
- UINT8 ui8SubRuleWeightLow;
- UINT8 ui8SubRuleWeightHigh;
- UINT8 ui8SubRuleSize;
- UINT8 ui8CodeUnknown;
- BYTE pbSubRuleBytesToMatch[];
- } STRUCT_RULE_PEHSTR_EXT, *PSTRUCT_RULE_PEHSTR_EXT;
复制代码
·ui8SubRuleWeightLow:检测过程中子规则权重的低位部分
·ui8SubRuleWeightHigh:检测过程中子规则权重的高位部分
·ui8SubRuleSize:指定给目标PE的字节字符串大小
·pbSubRuleBytesToMatch[]:触发检测必需的字节
SIGNATURE_TYPE_PEHSTR拥有除了ui8CodeUnknown之外的同结构
此外,SIGNATURE_TYPE_PEHSTR可以包含拥有可读字符串的子规则,SIGNATURE_TYPE_PEHSTR_EXT则可以包含字节序。
下图为SIGNATURE_TYPE_PEHSTR的示例:
为触发Darby.A的检测,至少要达到0x33的阈值。前五条子规则拥有0x0A的权重(绿色),后四条拥有0x01的权重。
任何包含前五条子规则中的字节和至少一条后四条子规则的PE将达到阈值并触发检测。
使用MpCmdRun.exe,可以验证检测。
即使是一个字节和子规则对不上,检测就不会被触发。在下图中,更改的字节用蓝色表示。
然而,值得注意的是,此类规则依赖于字节和字符串匹配,因而极易绕过。因此,大量的通配符被引入,以使规则更难被绕过且更灵活。
实现通配符(通用规则)的方式
所有通用规则起始于字节0x90,随后的第二字节标记它的“类型”,此后更多的字节用于定义它。
从90 01到90 20的模式用于实现在SIGNATURE_TYPE_PEHSTR_EXT解析算法中的通配符。
下一部分,将讲述如下的模式:
90 01 XX
90 02 XX
90 03 XX YY
90 04 XX YY
90 05 XX YY
90 01 XX
此模式用于匹配拥有特定XX长度的字节序列
下图展示了Plugx.A签名相关的子规则:
检测示例:
子规则的Yara规则可写成如下:
- rule Pattern_90_01_example
- {
- strings:
- $sub_rule_3_hex = { 45 78 69 74 C7 85 ?? FF FF FF 54 68 72 65 66 C7 85 ?? 04 FF FF FF 61 64 }
- condition:
- $sub_rule_3_hex
- }
复制代码
90 02 XX
此模式用于占位符,在特定位置匹配最多XX字节。
拥有90 02 XX的Plugx.A子规则示例如下。在此处为90 02 10,最多可匹配该位置上的16字节
Yara规则:
- rule Pattern_90_02_example
- {
- strings:
- $sub_rule_2_hex = { 75 61 6C 41 C7 [0-16] 6C 6C 6F 63 }
-
- condition:
- $sub_rule_2_hex
- }
复制代码
90 03 XX YY
此模式之后是两个连续的字节序列,长度由XX YY定义。找到的字节需与其中之一匹配。
示例:
此为Banker.YB样本,两个字符串分别为CIPA和VIPA。
此模式同样可以处理通用字节序列,如下图。
对应Yara:
- rule Pattern_90_03_example
- {
- strings:
- $sub_rule_1_hex = { 50 6f 6c 69 63 69 65 73 5c 45 78 70 6c 6f 72 65 72 5c 52 75 6e 22 20 2f 76 20 22 (43 49 50 41|56 49 50 41) 22 20 2f 64 20 43 3a 5c 55 6e 6e 69 73 74 74 61 6c 6c 2e 65 78 65 20 2f 74 20 22 52 45 47 5f 53 5a 22 20 2f 66 00 90 00 }
- condition:
- $sub_rule_1_hex
- }
复制代码
下列PE会触发Banker.YB检测:
90 04 XX YY
此模式为类正则表达式占位符,XX表示确切的必需查找的字节数,YY表示正则表达式模块长度
更复杂的模块可以包含确切的匹配字符
上2图中,模块本身用紫罗兰色标记,正则表达(匹配)式大小用葡萄紫标记,正则表达式字节用蓝色标记。
Yara规则:
- rule Pattern_90_04_example
- {
- strings:
- $example_90_04_first_rule = { 68 74 74 70 3a 2f 2f 61 72 70 2e 31 38 31 38 [30-39] [30-39] 2e 63 6e 2f 61 72 70 2e 68 74 6d 90 00 }
- $example_90_04_second_rule = { 5c 48 61 70 70 79 [30-39] [30-39] 68 79 74 2e 65 78 65 90 00 }
-
- condition:
- $example_90_04_first_rule and $example_90_04_second_rule
- }
复制代码
90 05 XX YY
这是另一类类正则表达式占位符,其中XX表示必须找到的字节数上限,YY表示正则表达式模块的长度。同90 04 XX YY最大的不同在于90 05 XX YY在处理90 05 XX 03 61 2D 7A这类的模块时不区分大小写。
标记为蓝色的模块在Stration.CC的SIGNATURE_TYPE_PEHSTR_EXT签名之中。
下图展示了包含触发检测字节的PE
可以看到,块成功复现了检测,即使41大写为了5A
对应Yara:
- rule Pattern_90_05_example
- {
- strings:
- $example_90_05 = "http://[a-zA-Z]{0,64}\\.com/dfrg32\\.exe"
-
- condition:
- $example_90_05
- }
复制代码
尽管加入了通配符和复杂模块用于加强灵活性和检测强度,但仅基于字节和字符串匹配的规则仍然相对容易被绕过。通配符被引入,通过允许规则匹配的字节序列发生变化在一定程度上缓解了这种局限。但是,积极的威胁行为者仍可利用修改或混淆技术修改模块,进而规避规则,逃脱检测。因此,此方法虽然加强了签名的有效性,但仍不是对各类规避技术的万无一失的解决方案。
总结
在此分析中我们探讨了MDAV如何管理其签名,重点关注了PEHSTR和PEHSTR_EXT。拥有这些知识后,我们就可以编写触发特定检测的工具,或者让已被检测的文件规避检测。当然,基于模拟执行的检测方法的能力超过了基于特征码的检测,但这个研究揭示了了解安全解决方案内部结构的重要性。
文档下载
译制版:
https://pan.huang1111.cn/s/dk2xOsV
https://pan.baidu.com/s/1zqJy8jbOJI51QR57YYfTMA?pwd=hj1y
英文原文:
https://pan.huang1111.cn/s/zMgdBCM
https://pan.baidu.com/s/1jHcjSt8n7frVtMZxYO-Abw?pwd=746p
感谢
@Disa-PDB 提供的文章
@神代すみれ 提供的翻译指导
|