写在前面
- 本文首发于 Debugwar.com, 授权转载到看雪论坛,其他转载请注明出处。
- 看雪的这个MarkDown的系统与我的写作系统有点冲突,排班可能会有问题,可以点上面的连接去看原文获得更好地排版体验。
- Linux的输入法你懂的,可能会有错字别字,大家多包涵~
本文是 《一文读懂对称加密、非对称加密、哈希值、签名、证书、https之间的关系》 的姊妹篇(下文简称“姊妹篇”),在阅读本文之前,最好先阅读一下上一篇文章。
本文对日常工作中应用最广泛,也是大家最容易接触到的PE文件(也就是大家常说的exe/dll/sys文件)的签名进行了深入讲解,并一步一步讲解如何手工验证一个PE文件签名的有效性。
背景
近期工作中,有个自研驱动需要防止未授权R3程序调用,因此需要设计一个方案出来。笔者第一想到的是利用Windows的签名机制,固化签名的CommonName和受信任的父证书公钥到驱动程序中,然后分别校验驱动、调用发起者线程所在模块、调用发起者二进制程序的签名,来防止非法调用。
查了很多签名相关的资料发现都是R3下WinTrust API函数实现的,然而问题是R0下并没有这些API可以使用,看来要自己写代码实现一遍签名校验的逻辑了。
后来又经过一段时间探索, 发现现成的代码都是重度和某个平台或库绑定——例如Linux下的签名验证代码和linux的数据结构绑定、再比如其他几个开源的代码和openssl库绑定,而openssl库在R0下也是无法调用的,因为依赖了很多R0下没有的函数(例如_open等POSIX C函数)。
看来只能自己动手了,所以继续转向研究签名校验的底层原理。然而网上大部分关于签名的资料,全部都偏重于理论介绍,至少笔者没有搜到只使用哈希和RSA算法实操验证签名的例子——那没办法了,只好完全自己从头来验证一遍了……
几个概念
ASN.1
ASN.1本身只定义了表示信息的抽象句法,但是没有限定其编码的方法。 摘自维基百科
DER编码
由于ASN.1并没有规定编码方式,因此在实际组织数据的时候,还需要一些编码方式,DER就是最常使用的一种,这种编码方式是大字节序的。
PKCS#7/PKCS#1/PKCS#9
这些为多种加密格式的预定义标准。
AuthentiCode签名
微软定义的(使用了PKCS#7等标准的)一种嵌入式签名格式,另外一种常见的为CAT签名(.cat文件)。
通俗一点理解上面的几个概念:ASN.1可以理解为单个的汉字,DER编码则是规定了使用哪些汉字组成一些常用的词组,PKCS#7等标准进一步规定了用哪些词组可以组成一些固定的句式,最后AuthentiCode则综使用这些固定句式写了一篇文章出来。
再谈证书
通过姊妹篇,我们已经大概了解了证书、签名的概念和作用。但是在姊妹篇中,只给出了一张证书的大概结构(下图),并没有对其进行详细解释。
本文由于要进行手工验证,因此有必要给出证书的详细结构:
1 2 3 4 5 | Certificate :: = SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signatureValue BIT STRING
}
|
上述定义使用的是ASN1语法, 此处我们需要先了解如下信息:
- tbsCertificate为上图中的蓝色部分
- signatureValue为上图中的红色部分
- signatureAlogrithm没有体现在上图中,此字段表示使用的是哪个哈希函数对tbsCertificate的数据进行的哈希计算
知道了上述结构,我们就可以搞清楚证书是如何验证的了:
- 提取该证书的父证书(签发者或者叫Issuer)公钥
- 使用公钥解密子证书中的signatureValue部分,然后根据PKCS#9的结构,提取出哈希值1,此处记为Hash1
- 使用signatureAlgorithm中相同哈希算法对tbsCertificate数据做哈希,得到Hash2
- 如果Hash1与Hash2相等,则证书有效
手工验证证书有效性
好了,枯燥的理论暂时告一段落,我们来看一个实际的例子。
我们以Windows版本的Python.exe文件为例, 依次查看其数字签名与证书信息如下:
可以发现Python.exe的数字签名使用的是sha256算法,证书链为:
1 | DigiCert - >DigiCert SHA2 Assured ID Code Singing CA - >Python Software Foundation
|
通过姊妹篇我们知道,整个PKI体系,是建立在信任一些随操作系统或浏览器分发的证书体系之上的。为了方便本文描述,假设我们信任的是上述体系链中的 DigiCert SHA2 Assured ID Code Singing CA 这一张证书,我们选择“详细信息”下的“复制到文件”就可以导出该证书用于研究(记得导出的时候选择DER格式):
首先我们需要获得父证书的公钥,可以使用openssl命令获取,下面的命令将Python.exe的父证书(PythonParent.cer)公钥输出到了PythonParentPublicKey.pem文件中:
1 2 3 4 5 6 7 8 9 10 11 | >> openssl x509 - inform DER - in PythonParent.cer - pubkey - noout > PythonParentPublicKey.pem
>> cat PythonParentPublicKey.pem
- - - - - BEGIN PUBLIC KEY - - - - -
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA + NOzHH8OEa9ndwfTCzFJ
Gc / Q + 0WZsTrbRPV / 5aid2zLXcep2nQUut4 / 6kkPApfmJ1DcZ17aq8JyGpdglrA55
KDp + 6dFn08b7KSfH03sjlOSRI5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF
1C2ho + mILCCVrhxKhwjfDPXiTWAYvqrEsq5wMWYzcT6scKKrzn / pfMuSoeU7MRzP
6vIK5Fe7SrXpdOYr / mzLfnQ5Ng2Q7 + S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ +
UpbB8g8S9MWOD8Gi6CxR93O8vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT
0QIDAQAB
- - - - - END PUBLIC KEY - - - - -
|
现在父证书公钥有了,接下来我们需要分离Python.exe证书的tbsCertificate和signatureValue部分。那么我们如何知道这两部分在证书中的偏移和长度呢?答案是: 继续使用万能的openssl命令:
首先来直观感受一下Pythone.exe的证书信息:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | >> openssl x509 - inform DER - in . / Python.exe.cer - noout - text
Certificate:
Data:
Version: 3 ( 0x2 )
Serial Number:
03 : 3e :d5:ed:a0: 65 :d1:b8:c9: 1d :fc:f9: 2a : 6c : 9b :d8
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = DigiCert Inc, OU = www.digicert.com, CN = DigiCert SHA2 Assured ID Code Signing CA
Validity
Not Before: Dec 18 00 : 00 : 00 2018 GMT
Not After : Dec 22 12 : 00 : 00 2021 GMT
Subject: C = US, ST = New Hampshire, L = Wolfeboro, O = Python Software Foundation, CN = Python Software Foundation
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public - Key: ( 4096 bit)
Modulus:
00 :aa:bd:a4: 4b :b2: 75 :b9: 6e :e8: 25 : 1c : 65 :b1:da:
c4: 4c : 08 :ca: 6a :b7:e8: 1f : 94 :c8:f0:f5: 92 : 4f :a6:
1b :db: 13 :e8:a0:fe:ef: 1d : 3e : 22 : 69 : 55 :f2: 2f : 92 :
f3: 7b : 57 :f9:dc: 9c : 3a :ed: 2a : 7e :bb: 9d :b8: 7c : 95 :
df:df: 4a : 87 : 56 : 21 : 16 :c6:e9:b2:d9: 15 : 86 :d2: 77 :
22 : 53 : 67 : 7e : 98 :ca:b3: 8e : 56 : 80 : 59 : 26 : 4d : 17 : 4b :
b8: 45 :cb:f2: 0c : 9a : 24 : 11 : 5d : 11 : 50 :ea: 88 :e4: 21 :
b9:cc:f2: 37 : 5b :db: 90 :e8:b8: 94 : 93 : 71 :c2: 61 : 6e :
a4:a4: 7d : 7b :ec: 0e : 53 :de: 9c : 3f : 3e : 8f : 0e :f0:a1:
2b : 24 : 69 :f5: 6a : 76 :aa:b4: 82 : 02 :ab:df: 72 : 4b : 1a :
cc: 69 :df:f6: 84 :f3: 01 : 45 :fe: 8d : 75 :a8: 7b : 7f :b1:
cf: 9f : 58 : 24 : 49 : 24 :c0:a1:e8:f2:ba:a1: 79 : 87 :e0:
74 :a8: 8e : 3e : 24 :ae: 7e : 54 :bb:f3:eb: 9f : 55 : 4d :b0:
16 : 26 :c6: 1a : 92 : 4c : 59 :c5: 55 : 98 :a4: 5b :f8: 29 :e4:
12 : 4b : 0a : 28 :d0: 3c :cc:be: 61 : 11 :b1: 3c :cd:bd: 50 :
4c : 5a : 1b :bd: 3a :b8: 89 : 36 : 0f : 90 : 7c : 59 : 9f :f7:ac:
d5: 4e :ef: 77 : 71 : 9f :ab:ef: 13 : 29 : 6d : 7c : 9f : 20 :e1:
8a : 84 : 73 : 1a : 46 :e6: 7c : 8a : 1b : 96 : 23 : 1d :e0: 23 :d5:
87 : 0c : 55 :fa: 7c : 12 : 91 :f3:e1:e5: 85 :d9: 1a : 88 : 11 :
16 : 22 :c5:d1:a3: 2f : 84 : 41 : 4c : 8a :ef: 35 : 2c :f8: 5a :
8e :a3: 6b : 11 : 62 :db: 5b :b3:c3: 13 : 17 :d6: 03 : 28 : 56 :
70 :c8:f8:e7:f5: 69 :fe: 80 :b1: 9d :e4:d5: 04 : 57 : 23 :
6f : 0f :d4: 15 : 18 : 11 : 2d : 37 :bb:f1:f3:b6:dd:b8: 95 :
01 :f0: 5e : 03 :ca: 51 : 2c : 32 :d6: 53 : 7e : 3c : 3f : 6a :ee:
80 : 98 :e9:e6: 9d :e2:b9: 51 :ca: 92 : 26 :ec: 11 :c9: 96 :
86 : 36 : 4e :f2:de:a8:f4:ea:eb: 71 :f8: 74 :d3:a8: 78 :
22 :f7:be: 54 :a7: 17 :f2:af: 00 : 2a : 92 : 8b :e8: 64 : 45 :
81 : 55 : 2a : 6f : 92 :ef: 0f : 56 : 19 : 01 : 5d :c2:e6: 35 :ee:
8c : 10 : 79 : 45 : 89 :a3: 28 : 88 : 00 :c0: 78 :a9: 97 :e5: 11 :
51 : 90 :df: 95 :ae: 66 : 06 : 4e : 0e : 33 : 6a : 3c : 5f : 74 : 77 :
88 : 63 :c4:ef: 2d :fe: 3c :b5: 37 :e6: 9d : 02 : 5d :f7:c8:
1e : 25 : 0b :ff:d3: 53 : 54 :cb:f1: 71 :bb: 0a : 80 :b9: 39 :
1a : 7b : 3e : 4d : 97 : 52 :f1: 3f : 40 :a9: 4c : 78 : 60 : 87 : 33 :
b8: 15 : 10 :f8: 8a :d4:f6:c2:a4:e1:e2: 3a : 68 : 8f : 0f :
50 : 66 : 7b
Exponent: 65537 ( 0x10001 )
X509v3 extensions:
X509v3 Authority Key Identifier:
keyid: 5A :C4:B9: 7B : 2A : 0A :A3:A5:EA: 71 : 03 :C0: 60 :F9: 2D :F6: 65 : 75 : 0E : 58
X509v3 Subject Key Identifier:
FC: 2A :BF: 7E :D4:BE:AC:F3: 82 : 9C :A4:CF: 7B : 22 : 01 : 3B :B8: 8F : 07 :F2
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
Code Signing
X509v3 CRL Distribution Points:
Full Name:
URI:http: / / crl3.digicert.com / sha2 - assured - cs - g1.crl
Full Name:
URI:http: / / crl4.digicert.com / sha2 - assured - cs - g1.crl
X509v3 Certificate Policies:
Policy: 2.16 . 840.1 . 114412.3 . 1
CPS: https: / / www.digicert.com / CPS
Policy: 2.23 . 140.1 . 4.1
Authority Information Access:
OCSP - URI:http: / / ocsp.digicert.com
CA Issuers - URI:http: / / cacerts.digicert.com / DigiCertSHA2AssuredIDCodeSigningCA.crt
X509v3 Basic Constraints: critical
CA:FALSE
Signature Algorithm: sha256WithRSAEncryption
4b : 75 :a1: 2d :b5: 5f : 46 :b1: 89 :a7:cf: 8f : 26 : 3e :be: 56 : 2a : 8d :
62 :ae: 52 :ef:d8: 16 :e6: 16 : 20 : 4a :ba: 89 : 14 : 5a : 15 :a6:cd: 0e :
18 :fd: 44 : 11 : 50 : 17 :f6: 89 : 88 : 4e : 66 :b2:b4: 04 : 39 :f0: 03 :eb:
fe:a0: 55 : 21 :fd:d6: 56 : 56 : 06 :a8: 3a : 34 : 47 : 86 :f5: 3f : 52 : 5d :
b8: 80 : 3e :f2: 7d : 08 : 45 : 85 :b1:d9: 17 : 52 :b8:db: 5a : 1b :c2: 9e :
e6: 7b : 92 :a5: 2e : 53 : 90 : 40 :ba: 62 : 35 : 41 : 16 :e9: 62 : 06 : 4b :dd:
40 : 3e : 89 : 95 :e4: 9a : 36 :c6: 87 : 59 : 67 :f2:b5: 21 : 58 : 8c : 5b : 05 :
82 :f8: 4a : 0d :a7:aa: 90 : 78 :e2: 9e :c2: 50 : 56 : 24 : 3e : 3f :cc: 6f :
05 : 36 : 82 :f0: 55 :da: 95 :e3: 8f : 95 : 9a :b2: 4a : 19 :b6:c0: 02 : 32 :
fe: 60 :e3: 60 : 4d :a1: 52 :e8: 44 : 08 :be: 7a :fe:ac:d3:b3:ce:b7:
e2: 6d : 1d : 12 : 26 :f3:b9: 53 :ca:e7: 3c :c2: 1d : 2c :c1: 33 :d4:c2:
4b : 0a : 6c :b5: 35 : 65 :d8:fd: 0a : 9a :ad:cd: 79 :ee: 54 : 4d : 30 :e7:
47 :b1: 26 :f4: 52 : 2b : 75 : 6d :e2: 0f :b4:be: 28 : 29 : 23 : 7a : 8f : 98 :
37 : 69 : 91 :ff: 7e :e0:cb:f2:d1: 73 : 0d : 72 :aa:ed: 87 : 0d :b4: 47 :
e3:e4: 22 : 53
|
上述 Signature Algorithm: sha256WithRSAEncryption 下面的即为 signatureValue 部分,把他复制到任意的二进制编辑器中,然后另存为 PythonExeSignatureValue.bin, 最后使用openssl解密这部分数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | >> hexdump - C PythonExeSignatureValue. bin
00000000 4b 75 a1 2d b5 5f 46 b1 89 a7 cf 8f 26 3e be 56 |Ku. - ._F.....&>.V|
00000010 2a 8d 62 ae 52 ef d8 16 e6 16 20 4a ba 89 14 5a | * .b.R..... J...Z|
00000020 15 a6 cd 0e 18 fd 44 11 50 17 f6 89 88 4e 66 b2 |......D.P....Nf.|
00000030 b4 04 39 f0 03 eb fe a0 55 21 fd d6 56 56 06 a8 |.. 9. ....U!..VV..|
00000040 3a 34 47 86 f5 3f 52 5d b8 80 3e f2 7d 08 45 85 |: 4G ..?R]..>.}.E.|
00000050 b1 d9 17 52 b8 db 5a 1b c2 9e e6 7b 92 a5 2e 53 |...R..Z....{...S|
00000060 90 40 ba 62 35 41 16 e9 62 06 4b dd 40 3e 89 95 |.@.b5A..b.K.@>..|
00000070 e4 9a 36 c6 87 59 67 f2 b5 21 58 8c 5b 05 82 f8 |.. 6. .Yg..!X.[...|
00000080 4a 0d a7 aa 90 78 e2 9e c2 50 56 24 3e 3f cc 6f |J....x...PV$>?.o|
00000090 05 36 82 f0 55 da 95 e3 8f 95 9a b2 4a 19 b6 c0 |. 6. .U.......J...|
000000a0 02 32 fe 60 e3 60 4d a1 52 e8 44 08 be 7a fe ac |. 2. `.`M.R.D..z..|
000000b0 d3 b3 ce b7 e2 6d 1d 12 26 f3 b9 53 ca e7 3c c2 |.....m..&..S..<.|
000000c0 1d 2c c1 33 d4 c2 4b 0a 6c b5 35 65 d8 fd 0a 9a |.,. 3. .K.l. 5e ....|
000000d0 ad cd 79 ee 54 4d 30 e7 47 b1 26 f4 52 2b 75 6d |..y.TM0.G.&.R + um|
000000e0 e2 0f b4 be 28 29 23 7a 8f 98 37 69 91 ff 7e e0 |....()
000000f0 cb f2 d1 73 0d 72 aa ed 87 0d b4 47 e3 e4 22 53 |...s.r.....G.."S|
00000100
|
上面是提取的签名数据, 使用下面的命令解密, 解密后的文件保存到 PythonExeSignatureValueDecrypted.bin
1 2 3 4 5 6 7 | >> openssl rsautl - inkey PythonParentPublicKey.pem - pubin - in PythonExeSignatureValue. bin >PythonExeSignatureValueDecrypted. bin
>> hexdump - CPythonExeSignatureValueDecrypted. bin
00000000 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 | 010. ..`.H.e.....|
00000010 00 04 20 ca 1c 82 f3 fd 76 74 30 54 39 09 8d 87 |.. .....vt0T9...|
00000020 95 4f e2 af b4 a0 64 24 bf 49 2f 27 34 61 46 2e |.O....d$.I / ' 4aF .|
00000030 ea d7 33 |.. 3 |
00000033
|
上面解密后的“神秘数据”其实是DER编码的PKCS#9消息, 其依然使用ASN1语法规范, 因此可以继续使用openssl解析:
1 2 3 4 5 6 | >> openssl asn1parse - i - inform DER - in PythonExeSignatureDecrypted. bin
0 :d = 0 hl = 2 l = 49 cons: SEQUENCE
2 :d = 1 hl = 2 l = 13 cons: SEQUENCE
4 :d = 2 hl = 2 l = 9 prim: OBJECT :sha256
15 :d = 2 hl = 2 l = 0 prim: NULL
17 :d = 1 hl = 2 l = 32 prim: OCTET STRING [ HEX DUMP]:CA1C82F3FD7674305439098D87954FE2AFB4A06424BF492F273461462EEAD733
|
至此,我们得到了第一个哈希值, 该哈希值是一个SHA256值:
CA1C82F3FD7674305439098D87954FE2AFB4A06424BF492F273461462EEAD733
接下来我们需要提取Python.exe证书的tbsCertificate部分, 可以使用如下openssl命令先查看Python.exe证书的结构(为了描述直观,输出内容做了删减):
1 2 3 4 5 6 7 8 | >> openssl asn1parse - i - inform DER - in Python.exe.cer
0 :d = 0 hl = 4 l = 1607 cons: SEQUENCE
4 :d = 1 hl = 4 l = 1327 cons: SEQUENCE
.......
1335 :d = 1 hl = 2 l = 13 cons: SEQUENCE
1337 :d = 2 hl = 2 l = 9 prim: OBJECT :sha256WithRSAEncryption
1348 :d = 2 hl = 2 l = 0 prim: NULL
1350 :d = 1 hl = 4 l = 257 prim: BIT STRING
|
上述结果中,每一行冒号开始前的数字代表在数据中的偏移, hl代表头长度, l代表实际的数据长度。根据上文中证书的ASN1结构定义, 我们需要从offset 4开始提取4+1327=1331长度的数据, 此部分数据即为Python.exe证书的tbsCertificate部分, 使用如下命令提取:
1 2 3 4 | >> dd if = . / Python.exe.cer of = . / PythonExetbsCertificate. bin skip = 4 bs = 1 count = 1331
输入了 1331 + 0 块记录
输出了 1331 + 0 块记录
1331 字节 ( 1.3 kB, 1.3 KiB) 已复制, 0.018058 s, 73.7 kB / s
|
然后, 我们对提取的这部分tbsCertificate数据做sha256:
1 2 | >> sha256sum PythonExetbsCertificate. bin
ca1c82f3fd7674305439098d87954fe2afb4a06424bf492f273461462eead733 PythonExetbsCertificate. bin
|
可以看到, 得到的第二个sha2256值“恰好”与上面的第一个sha256值相等。
证书验证小结
回顾一下上面我们干了什么。
- 从信任证书 DigiCert SHA2 Assured ID Code Singing CA 中提取了此证书的公钥
- 使用这个公钥解密了Python.exe证书中的signatureValue部分,得到第一个sha256值
- 提取Python.exe证书的tbsCertificate部分数据,并对提取的数据做sha256运算,得到第二个sha256值
- 第一个sha256值与第二个sha256相等
因此,得出结论,Python.exe使用的证书是合法有效且没有经过篡改的,因此这张证书中记录的数据,确切的说是这张证书tbsCertificate部分记录的数据,可以完全信任。那么tbsCertificate记录着什么重要数据需要这么“大费周章”的保护呢?答案是:Python.exe开发者的公钥!
众所周知,RSA作为非对称加密,公钥加密的数据只能由私钥解密(不知道的请去读姊妹篇),反之依然。也就是说,现在我们用Python.exe开发者的公钥去解密一段数据,只要解密成功了,这段数据就一定是开发者“发送”的,因为理论上, Python.exe开发者的私钥, 只有可能被开发者所持有。
那么,我们应该拿着Python.exe开发者的公钥去解密哪块数据呢?
让我们继续探索~
PE文件AuthentiCode
在继续之前,我们又需要补充一部分理论知识了。
一点PE文件结构的知识
估计各位对PE文件结构已经不陌生了,但是为了本文的完整性这里还是要简单说一下,熟悉的可以直接跳过。
这里借用一张PE文件结构的图 [参考5]:
我们可以由DOS Header入手, 找到PE Header, 然后是Optional Header中的IMAGE_DATA_DIRECTORY结构。PE签名涉及到的数据,在IMAGE_DATA_DIRECTORY的第5项(下标为4),这一项记录着SecurityDirectory的偏移和长度,使用PE文件结构分析工具,可以轻松定位这块数据:
从上图中可知,此段数据位于RawOffset 0x16E00处,大小为0x1A10,这段数据定义如下:
1 2 3 4 5 6 7 | typedef struct _WIN_CERTIFICATE
{
DWORD dwLength;
WORD wRevision;
WORD wCertificateType;
BYTE bCertificate[ANYSIZE_ARRAY];
} WIN_CERTIFICATE, * LPWIN_CERTIFICATE;
|
可见,从0x16E00处往后偏移2个DWORD才是真正的PKCS#7格式的数据。可以使用如下命令抽取SecurityDirectory中存储的PKCS#7数据:
1 2 3 4 | >> dd if = . / python.exe of = . / PythonPkcs7Data. bin skip = 93704 bs = 1 count = 6664
输入了 6664 + 0 块记录
输出了 6664 + 0 块记录
6664 字节 ( 6.7 kB, 6.5 KiB) 已复制, 0.0351038 s, 190 kB / s
|
下文中分析的数据,均为此处提取的数据。
有了上面PE结构的基本知识,让我们来看一下更加有深度的内容,首先借用微软文档中的一张图:
上图中左半部分,基本为上文中提到的PE结构。右半部分,微软对PKCS#7格式的数字签名数据部分做了一个大概的结构介绍。实际上,这部分的数据结构要远比上图复杂,不过目前为止大概只需要知道个基本结构即可。
上图中的右半部分,实际的结构对应SignedData,展开看一下该结构。
微软对SignedData的定义
SignedData的定义
上面提到最终提取的数据,是SignedData格式,其实也是符合PKCS#7标准的数据,满足以下ASN.1描述的格式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | SignedData :: = SEQUENCE {
version Version,
digestAlgorithms DigestAlgorithmIdentifiers,
contentInfo ContentInfo,
certificates
[ 0 ] IMPLICIT ExtendedCertificatesAndCertificates
OPTIONAL,
Crls
[ 1 ] IMPLICIT CertificateRevocationLists OPTIONAL,
signerInfos SignerInfos
}
DigestAlgorithmIdentifiers :: =
SET OF DigestAlgorithmIdentifier
ContentInfo :: = SEQUENCE {
contentType ContentType,
content
[ 0 ] EXPLICIT ANY DEFINED BY contentType OPTIONAL
}
ContentType :: = OBJECT IDENTIFIER
SignerInfos :: = SET OF SignerInfo
|
其中contentInfo中会存放PE文件的AuthentiCode信息,这个结构中,我们需要关注content字段中存储的数据,详情请见下文。
certificates中会存放PE文件开发者的证书等信息,我们可以从这里面提取开发者的公钥。上文中已经对证书部分做过详细介绍了,此处不再赘述。
signerInfos中存放着使用PE文件开发者私钥加密的数据和一些用于验证其他信息用的数据(可选,不一定有)。
SignerInfo的定义
其ASN.1语法的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | SignerInfo :: = SEQUENCE {
version Version,
issuerAndSerialNumber IssuerAndSerialNumber,
digestAlgorithm DigestAlgorithmIdentifier,
authenticatedAttributes
[ 0 ] IMPLICIT Attributes OPTIONAL,
digestEncryptionAlgorithm
DigestEncryptionAlgorithmIdentifier,
encryptedDigest EncryptedDigest,
unauthenticatedAttributes
[ 1 ] IMPLICIT Attributes OPTIONAL
}
IssuerAndSerialNumber :: = SEQUENCE {
issuer Name,
serialNumber CertificateSerialNumber
}
EncryptedDigest :: = OCTET STRING
|
这个定义中,我们最为关注的是 encryptedDigest 字段,根据微软文档的定义,此字段记录了一段被软件签发者私钥加密的数据:
通过分析导出的PKCS#7二进制数据,最终定位到此数据的偏移为:0x15f6(需要跳过4字节头),大小为0x0200:
将此部分二进制数据复制出来之后,使用Python.exe证书中提取出来的公钥解密:
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | / / 提取Python.exe公钥到PythonExePublicKey.pem文件中
>> openssl x509 - in Python.exe.cer - inform DER - noout - pubkey > PythonExePublicKey.pem
/ / Dump出来的SignedData.singerInfo.encryptDigest数据
>> hexdump - C PythonPkcs7Encrypted. bin
00000000 12 37 05 82 c9 7f 73 e9 fc ff d0 71 78 f8 50 04 |. 7. ...s....qx.P.|
00000010 18 c4 2d ae 0b 4f da 5f 6a db 88 79 44 bc 80 cd |.. - ..O._j..yD...|
00000020 9d 8d 86 51 48 5d 83 38 d2 1f 5d 60 ed a6 09 6e |...QH]. 8. .]`...n|
00000030 61 1d 55 e6 0b df ab 34 01 21 e1 c3 17 5d 2f d7 |a.U.... 4. !...] / .|
00000040 f6 e0 cd 18 1b 88 bb c9 f9 0f 2e b9 3b ea 34 28 |............;. 4 (|
00000050 0e 4d 99 01 1b 33 66 59 d4 dd fa e0 4a 81 36 35 |.M... 3fY ....J. 65 |
00000060 8f 6d b1 9e 76 f7 eb c9 5a 31 a4 0f 4f 32 28 be |.m..v...Z1..O2(.|
00000070 a7 d9 7a 01 c9 d7 fa 22 06 10 76 06 55 57 42 ce |..z...."..v.UWB.|
00000080 f5 9c 7e 36 ff 24 cc 0f 5d d1 b2 00 d2 e6 da 47 |..~ 6. $..]......G|
00000090 03 c0 06 f1 41 cb 2a f3 7f cc 69 1c f1 ea 53 de |....A. * ...i...S.|
000000a0 ab ca 25 a3 db 53 6e 0e 06 ff 37 b6 71 a9 5b 6b |.. % ..Sn... 7.q .[k|
000000b0 85 d8 d7 b0 19 4e 1f 56 a2 5f b9 4c c4 4a 1b 18 |.....N.V._.L.J..|
000000c0 4f 30 86 95 90 e6 b9 23 6d 74 9e 76 d3 7e 5f ad |O0.....
000000d0 20 8e ae 02 d6 32 f3 a7 be ba 00 6a 30 90 8d e4 | .... 2. ....j0...|
000000e0 83 d5 02 bb 19 e6 eb 62 a0 55 c9 4b 93 59 3b 12 |.......b.U.K.Y;.|
000000f0 51 ef 7e 50 b3 f3 0b 50 3c e5 01 9a ef 6a 9a 9b |Q.~P...P<....j..|
00000100 1a 2b a4 ef 79 47 09 1a d8 48 28 e8 2c 52 3e 29 |. + ..yG...H(.,R>)|
00000110 ad 31 05 2b 24 e6 11 e1 c0 bb 11 1d d0 0f e0 78 |. 1. + $..........x|
00000120 89 cf dc 85 e5 52 21 1c 43 69 b5 40 17 dc 08 98 |.....R!.Ci.@....|
00000130 8e fe d6 36 d4 6b ce ef 24 06 34 09 eb 7f 1b 9d |... 6.k ..$. 4. ....|
00000140 24 04 e2 e6 cb de b9 6d 14 70 3a b2 50 82 f9 83 |$......m.p:.P...|
00000150 37 b8 b8 ee 70 08 ce 6e 94 53 0e c4 0a 8b 5b d9 | 7. ..p..n.S....[.|
00000160 98 b5 54 2f 94 bd 46 20 9f a7 38 02 86 ef d8 b5 |..T / ..F .. 8. ....|
00000170 64 a2 ea 2f 0b 79 fb d3 e0 5c a3 83 8f 94 54 56 |d.. / .y...\....TV|
00000180 51 16 06 f7 fe ba 93 e2 b2 7d 08 74 08 a8 55 84 |Q........}.t..U.|
00000190 11 09 7d 74 4b 6b 48 2f 4f 98 4b dd 19 5d f2 db |..}tKkH / O.K..]..|
000001a0 18 40 d9 d1 a7 52 c3 35 7e 9a 5e d5 72 62 f2 64 |.@...R. 5 ~.^.rb.d|
000001b0 f4 cd 3f 70 d6 de e8 27 a8 cd 06 30 40 58 80 31 |..?p...'... 0 @X. 1 |
000001c0 6a 44 7b 22 bd 4f 1b d0 1d 9e a5 b1 26 60 d5 e4 |jD{".O......&`..|
000001d0 10 8e c5 67 4d 1d d2 51 b0 29 bb f5 f2 0f 24 e6 |...gM..Q.)....$.|
000001e0 96 49 36 99 01 e2 56 aa 32 5f 13 c9 bd a0 2c dd |.I6...V. 2_ ....,.|
000001f0 06 db dc ae ee ca 28 9e 0c e8 98 68 2d e8 d0 a8 |......(....h - ...|
00000200
/ / 使用PythonExePulbicKey.pem公钥解密PythonPkcs7Encrypted. bin 数据到PythonPkcs7Decrypted. bin 文件中
>> openssl rsautl - inkey PythonExePublicKey.pem - pubin - in PythonPkcs7Encrypted. bin > PythonPkcs7Decrypted. bin
/ / 解析解密后的PKCS
>> openssl asn1parse - i - inform DER - in PythonPkcs7Decrypted. bin
0 :d = 0 hl = 2 l = 49 cons: SEQUENCE
2 :d = 1 hl = 2 l = 13 cons: SEQUENCE
4 :d = 2 hl = 2 l = 9 prim: OBJECT :sha256
15 :d = 2 hl = 2 l = 0 prim: NULL
17 :d = 1 hl = 2 l = 32 prim: OCTET STRING [ HEX DUMP]:B300E437486CB53B647A9C1A84B2986502254681C688020158504646776C31E1
|
到这里,我们再次得到了一串神秘的sha256哈希值: B300E437486CB53B647A9C1A84B2986502254681C688020158504646776C31E1。那么问题来了,这段哈希值,是哪段数据的哈希值呢?
这里分两种情况: 不存在SingedData.signerInfos.authenticatedAttributes字段和存在前述字段。其中,第二种情况还有一个巨大的坑,至于是什么坑,下文会讲到。
不存在authenticatedAttributes字段
此种情况下,上述用公钥解密后的哈希值,应该等于ContentInfo字段用相同哈希算法计算后的值。
我们来实际操作一下。
首先是确定需要提取的二进制数据,偏移:0x3d(注意跳过2字节的头) 长度: 0x4c,上图:
验证一下提取数据的哈希值:
1 2 3 4 5 6 7 8 9 | >> hexdump - C PythonExeContentInfo. bin
00000000 30 17 06 0a 2b 06 01 04 01 82 37 02 01 0f 30 09 | 0. .. + ..... 7. .. 0. |
00000010 03 01 00 a0 04 a2 02 80 00 30 31 30 0d 06 09 60 |......... 010. ..`|
00000020 86 48 01 65 03 04 02 01 05 00 04 20 5a 3a d4 3d |.H.e....... Z:. = |
00000030 81 d5 6a 86 d9 7b 24 b3 74 da 49 44 aa ee 94 c7 |..j..{$.t. ID ....|
00000040 10 e3 77 25 4c fe 8e cc 72 04 06 6a |..w % L...r..j|
0000004c
>> sha256sum PythonExeContentInfo. bin
b5e4b32d6ebae47869646ccf93416b320a113b2d35d948e7e2a9122146cb9634 PythonContentInfo. bin
|
貌似……哈希值和上面解密出来的不一样啊……难道搞错了?其实不是的,这里的哈希值不正确是因为本例是第二种情况——其存在authenticatedAttributes字段,这里写出来只是为了作为一个例子,让各位在遇到无authenticatedAttributes字段时知道如何操作。
不过这个sha256值还是有用的, 需要大家留意一下,下文要用到。
存在authenticatedAttributes字段
看图, authenticatedAttributes字段位于偏移: 0x1548处(这次不需要跳过头部两字节,坑1),长度:0x9a,把此部分数据dump出来,保存为PythonExeAttribute.bin。
注意: 坑2来了,根据RFC规定, 需要将此部分数据的第一个字节,改成0x31, 即0xA1需要替换成0x31!这点一定要注意,不然会导致计算出来的哈希值不对!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | / / 注意第一个字节已经被替换成 0x31
>> hexdump - C PythonExeAttrubite. bin
00000000 31 81 98 30 19 06 09 2a 86 48 86 f7 0d 01 09 03 | 1. . 0. .. * .H......|
00000010 31 0c 06 0a 2b 06 01 04 01 82 37 02 01 04 30 1c | 1. .. + ..... 7. .. 0. |
00000020 06 0a 2b 06 01 04 01 82 37 02 01 0b 31 0e 30 0c |.. + ..... 7. .. 1.0 .|
00000030 06 0a 2b 06 01 04 01 82 37 02 01 15 30 2c 06 0a |.. + ..... 7. .. 0 ,..|
00000040 2b 06 01 04 01 82 37 02 01 0c 31 1e 30 1c a0 1a | + ..... 7. .. 1.0 ...|
00000050 80 18 00 50 00 79 00 74 00 68 00 6f 00 6e 00 20 |...P.y.t.h.o.n. |
00000060 00 33 00 2e 00 38 00 2e 00 36 30 2f 06 09 2a 86 |. 3. .. 8. .. 60 / .. * .|
00000070 48 86 f7 0d 01 09 04 31 22 04 20 b5 e4 b3 2d 6e |H...... 1 ". ... - n|
00000080 ba e4 78 69 64 6c cf 93 41 6b 32 0a 11 3b 2d 35 |..xidl..Ak2..; - 5 |
00000090 d9 48 e7 e2 a9 12 21 46 cb 96 34 |.H....!F.. 4 |
0000009b
hacksign@XSignLaptop [ 11 : 02 : 59 ] : ~ / Work / 虚拟机共享目录 / Certificate / python
>> sha256sum PythonExeAttrubite. bin
b300e437486cb53b647a9c1a84b2986502254681c688020158504646776c31e1 PythonExeAttrubite. bin
|
现在计算出来的SHA256就可以和上面公钥解密的数据对上了, 说明Attribute这块数据的确是Python.exe的开发者“发送”的数据,里面的内容我们可以信任。那么……里面的内容又是什么呢?请看上图的红框部分数据,这部分数据是不是“正好”是 PythonExeContentInfo.bin 的SHA256值(请参考“不存在authenticAttributes”一节的最后)?
因此根据哈希值的唯一性,我们进而可以确认ContentInfo部分的数据,也是可信的。那呃ContentInfo部分又双叒存放什么数据呢? 请注意下图红框中的内容:
我们得到了一个SHA256值: 5A3AD43D81D56A86D97B24B374DA4944AAEE94C710E377254CFE8ECC7204066A,由上面的分析可知,此哈希值是可信的。
大家坚持一下,寻宝游戏快要结束了,因为我们即将迎来最重要的部分——AuthentiCode的计算。
AuthentiCode哈希
根据微软的文档,AuthenticCode的计算需要跳过CheckSum字段、SecurityDirectory字段以及最后的PKCS#7格式的证书数据部分。
然而本节并不打算过分深入的追究计算过程。因为——感谢伟大的开源精神——Github上有开源的计算工具,而且还很贴心的提供了编译好的二进制程序:
上图就是本文Python.exe程序的AuthentiCode计算结果。这个工具大家可以到 这个Git Repository 中下载。
注意上图的SHA256部分,是不是再次“恰好”和ContentInfo中存放的SHA256数据相等?
验证PE文件是否经过篡改
PKI体系的最基本思路:
- 验证数据真实有效
- 提取数据中的内容
- 回到第一步,直到验证结束
使用这个思路,梳理一下本文中做的事情和涉及到的哈希值:
首先用Python.exe父证书的公钥解密了Python.exe证书的signatureValue部分,得到了第一个哈希值: CA1C82F3FD7674305439098D87954FE2AFB4A06424BF492F273461462EEAD733,该值和Python.exe证书的tbsCertificate部分数据计算的sha256相等,因此tbsCertificate部分的消息未经篡改、可以信任。
tbsCertificate部分的数据, 包含Python.exe开发者的公钥,然后我们使用Python.exe开发者公钥解密了SingedData.SingerInfo.encryptedDigest部分数据,此时得到了第二个哈希值:B300E437486CB53B647A9C1A84B2986502254681C688020158504646776C31E1
使用相同的哈喜算法,计算SignedData.SingerInfo.authenticatedAttributes字段的哈希值,发现此哈希值和第二步中得到的哈希值二相等。经过进一步查看authenticatedAttributes中的数据发现第三个哈希值:b5e4b32d6ebae47869646ccf93416b320a113b2d35d948e7e2a9122146cb9634
使用相同的算法计算SignedData.contentInfo.content哈希值,发现其和第三步之中的哈希三相等。查看SignedData.contentInfo.content的内容,发现第四个哈希值:5A3AD43D81D56A86D97B24B374DA4944AAEE94C710E377254CFE8ECC7204066A
使用微软描述的方法计算Python.exe的AuthentiCode,发现此值和上面第四步中的哈希相等。
查看上面5步的链路,发现其实是环环相扣的,都是用一个已经确认未经篡改的消息,去验证另外一个消息,如此循环往复。从信任的Python.exe父证书开始,到后面的每一步得出的数据都应该认为是受信任的,最终通过AuthentiCode,得出PE文件未经过篡改的结论。
伸手党福利
如果你只想白嫖现成的PE验证代码,那么可以参考下面这个Repo的代码,这个工程提供了c/c++/python的接口可以操作PE/ELF/MachO等格式的文件,且依赖很少可移植性较强:
lief-project / LIEF
同时,提供一个可移植性很强的ASN.1解析库,此库使用纯C语言编写:
TinyASN1
最后,上文中提到过的AuthentiCode计算器,开源且提供GUI和命令行版本的二进制文件:
hfiref0x/AuthHashCalc
相信通过上面三个工程以及本文的实操,大家应该可以撸一套自己的完整性校验代码出来了。
最后愿大家的代码永远都不会被Patch~咱们下一篇文章再见 ;)
参考:
- ASN.1格式学习
- ASN.1维基百科
- ASN.1 DER 格式編碼與解碼
- PKCS#7
- PE文件结构
- Windows Authenticode Portable Executable Signature Format
- PE文件中的数字签名信息
- PE文件逆向之数字签名详细解析
- 开源的AuthentiCode计算器
- 手工验证一张数字证书的有效性
- Authenticode签名对未签名代码的应用
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界