0x01 背景
最近工作上遇到了关于windows签名的校验机制的一个有意思的技术。
可以在不影响一个已签名文件的签名有效性的情况下,通过修改文件的特殊位置来使其hash变动。
之前我一直以为windows的签名机制是对整个可执行文件(直接或者间接,间接比如对整个文件的hash进行签名,效果都一样,都是为了保证完整性),也就是说如果我们修改了pe文件,那么其签名一定会校验失败,哪怕是一个字节。所以看到这个技术的时候还有有些惊讶的。
当时是有其他师傅分析样本的时候发现的这个问题,师傅在文中提到,攻击者可以通过修改pe文件的pe头中的可选头中的checksum字段或者其签名结构Certificate Table Entry(一般文件最后)的填充内容来实现使文件呈现不同的hash,但是其签名的有效性不会受影响。
eg:
如下是一个存在有效签名的exe文件:
其hash如下:
然后我们修改其上述提到的两个点的内容:
一、测试1
修改checksum处。
其checksum的原值:0xb25A5
随便修改,修改为0xb25A1
此时计算hash,如下图发生改变
此时查看签名有效性,如下图,未影响有效性:
结论
修改文件的pe头的可选头的checksum字段,可以实现修改文件hash但是不影响文件的签名有效性。
二、测试2
修改文件签名部分签名结构Certificate Table Entry的填充。
没修改前,该值如下,可以看到文件的最后这个签名部分,也就是是一个Certificate Table Entry结构,这个结构需要8位对齐,最后补充了6个0x00;
随便修改后:
此时前后hash对比,如下,发生了改变
此时签名有效性,如下,签名任有效:
结论
修改文件的pe头的签名区Certificate Table Entry结构的填充内容,可以实现修改文件hash但是不影响文件的签名有效性。
三、忧虑
当时我做完这两个验证之后,就在想这不完犊子了。
因为防守方有些防御方式是通过hash黑名单来做的,那岂不是都失效了。
比如:LOL Driver(易受攻击的驱动)的对抗,攻击者利用在受害机器上加载一些有漏洞的驱动,来实现对抗edr等一些安全防护产品。其中就会有通过禁止LOL Driver里面提到的所有有问题的驱动在终端加载的防护策略,如何判断是否是其中LOLDriver呢,就是通过hash来做。
在比如:有些企业可能会做软件管控,有些实现比较简单的方式,比如通过对相关软件的hash做黑名单,当发现本地终端上启动的进程对于的pe文件hash和黑名单对上了,就禁止启动。(也的确有些软件管控是这么做的。。。)
0x02 分析
怀揣着对上述思考的忧虑,我提出了如下几个问题
1、为什么会有这种问题
2、攻击者可以从哪些方面利用这些特性开展攻击
3、防护手段又该从哪些方面去对抗呢
4、有没有体系的解法
一、为什么会有这样的问题
之前学习windows pe结构的时候,我记得在可选头中的数据目录表中有一个 证书表,这里我们回顾下。
微软对该表的描述如下,这个表记录的属性证书是在原始文件的最后,并且还有一个8字节对齐的规定。并且每一个属性条目的结构也在下面规定了。
偏移量 | 大小 | 字段 | 描述 |
---|---|---|---|
0 | 4 | dwLength | 指定属性证书条目的长度。 |
4 | 2 | wRevision | 包含证书版本号。 有关详细信息,请参阅以下文本。 |
6 | 2 | wCertificateType | 指定 bCertificate 中的内容类型。 有关详细信息,请参阅以下文本。 |
8 | 请参阅以下资源 | bCertificate | 包含证书,例如验证码签名。 有关详细信息,请参阅以下文本。 |
相关字段的赋值规则如下,第一个字段就是遍历条目的时候加的偏移;第二个就是证书的版本;第三个是指定第四个结构(bCertificate)的类型,一般都是WIN_CERT_TYPE_PKCS_SIGNED_DATA
第四个结构就是bCertificate,这里有一个重点,如果bCertficate的内容不是8字节对其的终止,是需要填充0来对齐的。(也就本文背景中提到的,可以通过修改这里的0填充来更改的文件的hash,但不影响签名的有效性校验)
然后我们看下msdn对(bCertficate_WIN_CERT_TYPE_PKCS_SIGNED_DATA)这个结构的描述,首先其是一个 PKCS#7 SignedData 结构,大致结构如下。
参考:https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx
嗯,其实在看这个结构体以及分析之前,我们不妨自己思考下,如果我们来设计这个bCertificate结构,我们该怎么设计。
1、需要一个记录证书的地方,也就是签名者的公钥以及相关属性信息(比如公司名称地址之类的)
2、需要记录一个签名值的地方,先需要思考我们的私钥签名的对象是什么,是全部文件内容,还是什么呢?
这里我们需要考虑的是,一定要保证是对这个文件内容的签名,那么怎么保证这个文件呢,
一种方法是将全部文件内容确认为签名对象,但是这里有一个问题,就是签名都是使用私钥签名,其实就是使用非对称加密算法进行加密,如果我们需要以文件内容来签名,那么就需要大量的计算,因为非对称加密算法签名就是使用私钥加密要签名的对象,而其加密内容的长度是受密钥长度限制的,也就是一次加密的内容长度是等于密钥长度(拿2048位的rsa举例,一次计算只能对2048位的文件内容签名)所以这里我们如果要通过这种方式去做会有很多问题:1、文件长度特别长的时候需要大量的非对称加密运算,导致效能非常低下;2、签名的值和文件内容一样长,那么记录下来就会让文件大小翻倍,这显然是荒唐的。
另一种方法是先对全部文件内容进行hash计算,拿到hash之后,我们对hash签名即可。
然后我们回到微软对这个结构的设计,如下分为三部分,第一部分contextinfo其实就是我们上面提到的如何确认这个签名的对象就是这个文件,不是其他文件,他这里其实就是通过计算pe的hash来记录的。第二不妨就是签名者的证书相关信息以及时间戳。第三部分就是签名后的信息记录。
这下对这个结构体各部位的作用就理解清晰了。
然后我们再来看一个关键的点,如下,msdn提到,在计算这个pehash的时候其实需要排除pe中的某些字段,比如:可选头中的checksum和可选头中的数据目录中的证书条目。(ps,下面这个截图是中文的msdn,翻译有些问题,建议看英文)
其实不止上面这两个,还有一个就是证书条目的落地结构(就是我们上面分析的这个结构)。如下这个图中的解释比较清晰,就是图中灰色部分不参与hash计算。(ps:其实貌似不看msdn的描述,在了解pe文件结构的情况下,我们就应该想到这些不会被记录进去,为什么这么说呢,我们简单思考了,1、证书目录以及条目是在签名之后才有的,所以我们记录相关内容的时候肯定不能把这个计算进去,不然这就变成了一个先鸡还是先有蛋的问题了。2、可选头里面的checksum的值,这个也不应该放进去,因为checksum本身是对文件所有内容的一个校验值,也就是说如果文件的任意一个字节(除该字段之后)发生变动,其都会变动,用来作为文件的一个校验值;所以这里我们添加证书目录表以及条目落地内容的时候肯定要修改文件,所以这个值肯定要变,因为其作用要变成最后签名之后的文件的一个校验值。这里我们多说一句理解这个checksum的时候就类比tcp的校验值就行,其具备保证完整性、不可篡改性)
那么问题到这里就真相大白了,也就是说不光是我们上面背景中提到的字段可以修改,从而实现修改hash,但不影响签名有效性。还有一个点就是可选头数据目录表中certificatetable的内容修改也不会影响。(但是这里要注意,需要和后面的落地bCertificate一致,因为本身certificatetable就是描述bCertificate,比如起始位置大小之类的)
所以这里我们开展第三种测试:
测试3
修改可选头中的CertificateTable里面的内容,这里其实就是8个字节,4字节是va,4字节是长度。
va是0xaccdo,长度0x28d8;对应的内容如下。
注意这里测试的时候我们修改可选头里面CertificateTable的时候,我们先选择修改长度:修改为0x28e0(需要8字节对齐,所以这里我们+8来测试即可)
修改后我们先来看下签名还能有效不,如下,签名直接没了。。。
所以这里这么修改不行,于是笔者想了下,修改完长度之后,是不是他没识别到对应的结构体,因为最后结构体的长度是没有这里可选头上定义的这么长的,我们可以尝试将最后的bCertificate的长度增加8字节,最后用0填充。
如下,我们增加8字节填充0之后:
此时查看证书情况,证书有效性不受影响。
此时的hash亦发生了变动。
其实这里我们不使用0来填充也不影响,使用其他的相关数据来填充也可以。比如下面我们可以随机的数据填充,还是不会影响签名的有效性。
二、攻击者可以从哪些方面利用这些特性开展攻击
从上面三个测试来看,我们全面分析下攻击者可以利用的其做什么。
(1、修改pe头中可选头的checksum字段;
(2、修改文件签名部分签名结构bCertificate的填充;
(3、修改可选头中的CertificateTable里面的长度字段+在尾部bCertificate结构中添加对应增加的长度内容,并可以自定义内容。
1、利用方式一
首先攻击者通过1、2、3中方式都可以实现修改hash,但是不影响签名有效性以及相关pe的正常用途。
那么哪些防守场景会使用hash呢?
(1、样本黑名单(软件管控、病毒查杀、流量还原文件扫描、驱动管控)
一些防护手段有些是依赖pe的黑名单来做的,比如软件管控-落地手段之一是通过对比pe的hash来判断是否是目标软件、病毒查杀-静态查杀的方式之一会通过内置的黑hash库来做匹配、驱动管控-通过禁止某些有漏洞的驱动加载(业界一般都是通过lol driver里面记录的sys的hash列表来做)。
利用:攻击者利用上述的任意方法即可将一个样本制作出成千上万个hash出来,每次都使用不一样的即可(第一种方法有2的32次方个hash可以生成;第二种根据其填充0的个数有2的(0对应个数x4)次方个hash可选;第三种是无限个,甚至如果在md5强碰撞的基础上,如果有人研究出来了基于已有内容做拼接添加实现的强碰撞技术的话,那就直接可以将黑的变成白的,也就是一个黑的hash的文件,通过第三种方法可以将其变成一个白的hash,难以想象其破坏力)。
(2、文件白名单(只允许符合相关hash的文件\进程落地或者做某些操作)
目前从上述维度,没有较好的利用场景。
(3、分析溯源(一些分析人员,拿到hash去情报查询)
一些分析人员会将获取到的样本hash拿到一些情报社区来判断其是否是恶意的。
这个时候一般分为两种情况:
1、相关社区没有收到过相关检测文件,其hash可以和你提供的hash匹配。
2、相关社区收到过相关检测文件,其hash和你提供的hash匹配上了。
第一种情况,你如果手上只有hash,那么你就从情报平台拿不到任何参考依据了。(注意,笔者觉得其实大部分场景是这样:1、我们很多场景,一些告警分析日志分析的场景,可能通过一些日志上只能看到一个hash,你不可能挨个去用户端拉文件,然后去分析。2、特殊的指向性的排查场景你就算拿到了文件,那么一些其他条件可能也会限制你,比如数据保密,你不能将这些样本上传到某步、某t、某信、某0等相关平台。)
第二种情况就没啥好说的了,平台 从引擎检出率、文件沙箱运行结果、他人对文件标签、文件属性、同源属性等方面给你一些参考。
利用:攻击者利用上述的任意方法即可将一个已知的病毒文件的hash改变,从而导致你看到的hash是一个从没有出现的hash,其99.9999%灰色的hash(即:既不是黑的,也不是白的),那么分析工程师其通过情报平台查询相关hash的时候,就会出现上述我们提到情况1,其拿不到任何辅助研判的信息。从而无法开展有效的分析研判工作,并大大提高的对分析工程师的能力要求。(这里我们从指向性的应急排查场景来看,我们是能拿到样本的,那么就需要分析工程师具备对样本的分析能力,比如行为分析、静态分析、逆向分析(静态分析、动态调试分析)等,先不论能力的深度,据笔者所知目前一线工程师具备上述能力的可以说是凤毛麟角)。总之,攻击者利用上述技术不费吹灰之力,就能给防护方在日常分析、应急溯源场景下带来很多阻力。
2、利用方式二
上文提到的第三种方式可以实现在一个签名pe尾部的bcertificate结构中添加任意长度的任意内容,并且不影响签名的有效性。
也就是说,攻击者可以将黑的内容,比如shellcode(或者加密的shellcode)合理的插入到一个静态的合法文件里面去。后续需要运行的时候就可以通过访问合法的文件或进程来获取恶意载荷。
比如,对于办公终端来说,攻击者为了实现权限提升以及驻留隐藏,往往都会注入到explorer、winlogon进程。如果攻击者的注入手段比较的隐蔽,一般的edr检测不到,那么借助这种技术后续攻击者利用explorer就可以通过看似正常的读取的正常文件或者访问正常进程行为来实现的加载恶意payload的目的。使排查人员的排查难度增大。
三、防护手段又该从哪些方面去对抗呢
1、防护于对抗1
1、对于第一种修改pe头中的可选头的checksum的方法,我们不妨思考这个checksum本身的作用,其本身就是为了校验pe是否被更改。也就是说谁制作出来了一个pe文件,其最后的步骤就是计算出这个pe的checksum的值,然后填充到可选头对应位置。所以如果有人修改了这个值,那么我们重新计算得出来的值一定和修改后的不一样。
在windows中我们可以通过MapFileAndCheckSumA这个api来实现对checksum的校验。
通过如下程序我们便可以判断这个pe是否存在问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <windows.h>
#include <imagehlp.h>
#include <stdio.h>
#pragma comment(lib, "imagehlp.lib")
int main() {
const char* filePath = "C:\\Users\\ga0wei\\Downloads\\mainfree.exe";
DWORD headerSum = 0, computedSum = 0;
DWORD result = MapFileAndCheckSumA(filePath, &headerSum, &computedSum);
if (result == 0) {
printf("File: %s\n", filePath);
printf("Header Checksum : 0x%08X\n", headerSum);
printf("Computed Checksum: 0x%08X\n", computedSum);
if (headerSum == computedSum) {
printf("校验和一致。\n");
}
else {
printf("校验和不一致!\n");
}
}
else {
printf("MapFileAndCheckSum 失败,错误码:%lu\n", result);
}
return 0;
}
所以我们可以通过校验checksum来判断一个具备有效签名的软件是否被使用上述第一种方式来修改过内容。
但是不难看出,这里攻击者也可以通过简单的将这个方法升级来对抗上面这种检测,其实就是和第二种方法(修改bcertificate结构最后的填充)相结合。也就是说,攻击者可以先修改填充,然后根据修改完之后的pe内容,重新计算checksum,将新计算得出的结果覆盖之前的checksum。这样以来其实就规避了checksum校验的防护方式。
2、防护于对抗2
对于第三种方法(修改可选头里面的CertificateTable长度,再填充bcertificate结构)该如何对抗呢?
感觉应该从file的长度的维度来检测是否被添加了,或者从bcertificate的结构的维度看是否存在异常的bcertificate结构。上文我们通过查阅msdn,看到过这个_WIN_CERTIFICATE 结构中的bCertificate是一个可变长度字节数组,所以从bcertificate指向想内容的长度上我们没办法限制。
文件长度呢,pe文件的头部没有对于file格式的文件长度描述的字段(转载到内存倒是有,但是貌似也加载的时候不会做校验),对不上,也不会影响pe的正常运行。
那么我们添加校验可以吗,当然可以,但是问题是校验的标准长度从哪来,除非之前记录过正版没有篡改过的相同版本的pe的长度。显然这样并不能解决问题。
0x03 思考
首先针对这个技术,感觉没有将这个技术在攻击和防护方的使用对抗场景梳理的很清楚,很多场景可能还有遗漏,比如攻击方结合一些已有的合法软件的漏洞是否可以做一些事情。先mark下把,之后遇到具体的场景我们再分析对应的可行性。
从这个技术案例也可以看出来,对于一些我们不知道的了解不清楚的技术,我们需要抱有打破沙锅问到底的精神,为什么是这样的,底层的逻辑是怎样的。没有了解这个pe签名即验证机制之前,要是有人和我说可以修改pe内容,不影响签名有效性,我可能会觉得:怎么可能,他是不是疯了。但是仔细想想,我真的对这个pe签名了解吗?好像也没有,为什么我会觉得人家提出这个想法是有问题的呢,其实就是因为我之前改过一些其他字段,pe的签名都会有影响,所以就误认为pe的文件内容和pe签名的有效性是强关联的,修改内容,hash变了,那么pe的签名有效性就肯定没有了。这里的思考逻辑其实是有问题的,有点管中窥豹的意味了。之后还是要从底层的逻辑持有严谨的考虑分析技术。不是我分析不出来,而是我没去分析,抱着想当然的心态去得出结论。 持有少数的充分条件的案例,就觉得两者是充分必要关系,是有些荒谬了。
参考:
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#the-attribute-certificate-table-image-only
https://download.microsoft.com/download/9/c/5/9c5b2167-8017-4bae-9fde-d599bac8184a/Authenticode_PE.docx