基于ASN.1语法对TLS证书进行模糊测试
2020年5月14日 - 作者:Filippo Cremonese
在Doyensec的研究期间,我大部分时间都致力于构建一个灵活的基于ASN.1语法的模糊测试工具,用于测试TLS证书解析器。在这个过程中我学到了很多,但经常苦于找不到关于这些主题的好资源。在这篇博客文章中,我想对问题、我所采取的方法以及一些可能为其他安全研究人员节省时间的提示给出一个高层次的概述。
让我们从一些基础知识开始。
什么是TLS证书?
TLS证书是一个DER编码的对象,符合RFC 5280中定义的ASN.1语法和约束,而RFC 5280基于ITU X.509标准。
这里有很多信息需要拆解,让我们一步一步来。
ASN.1
ASN.1(抽象语法记法一)是一种用于定义抽象对象的语法。你可以把它看作是一个更古老、更复杂版本的Protocol Buffers。然而,ASN.1并不定义编码,编码留给其他标准。这种语言由ITU设计,功能极其强大且通用。
这是一个聊天协议中消息的可能定义方式:
1
2
3
4
5
6
7
|
Message ::= SEQUENCE {
senderId INTEGER,
recipientId INTEGER,
message UTF8String,
sendTime GeneralizedTime,
...
}
|
乍一看,ASN.1甚至可能显得相当简单和直观。但别被欺骗了!ASN.1包含许多遗留和复杂的功能。首先,它有约13种字符串类型。可以对字段施加约束,例如,整数和字符串大小可以限制在可接受的范围内。
然而,真正的复杂性在于信息对象、参数化和表格约束。
信息对象允许定义数据类型的模板以及声明该模板实例的语法(哦,是的……在语法中定义语法!)。
这是如何定义不同消息类型的模板的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
-- Definition of the MESSAGE-CLASS information object class
MESSAGE-CLASS ::= CLASS {
messageTypeId INTEGER UNIQUE
&payload [1] OPTIONAL,
...
}
WITH SYNTAX {
MESSAGE-TYPE-ID &messageTypeId
[PAYLOAD &payload]
}
-- Definition of some message types
TextMessageKinds MESSAGE-CLASS ::= {
-- Text message
{MESSAGE-TYPE-ID 0, PAYLOAD UTF8String}
-- Read ACK (no payload)
| {MESSAGE-TYPE-ID 1, PAYLOAD Sequence { ToMessageId INTEGER } }
}
MediaMessageKinds MESSAGE-CLASS ::= {
-- JPEG
{MESSAGE-TYPE-ID 2, PAYLOAD OctetString}
}
|
参数化允许在类型规范中引入参数:
1
2
3
4
5
6
7
8
|
Message {MESSAGE-CLASS : MessageClass} ::= SEQUENCE {
messageId INTEGER,
senderId INTEGER,
recipientId INTEGER,
sendTime GeneralizedTime,
messageTypeId MESSAGE-CLASS.&messageTypeId ({MessageClass}),
payload MESSAGE-CLASS.&payload ({MessageClass} {@messageTypeId})
}
|
虽然格式的完整概述不在本文的范围内,但我发现的一个非常好的入门级但相当全面的资源是这篇ASN1生存指南。细节可以在ITU标准X.680到X.683中找到。
尽管功能强大,ASN.1存在一个很大的实际问题——缺乏广泛选择的编译器(解析器生成器),尤其是非商业的。大多数编译器不支持高级功能,如信息对象。
这意味着,更多情况下,使用ASN.1定义的数据结构是通过手工编写的代码而不是自动生成的解析器进行序列化和反序列化的。许多处理TLS证书的库也是如此。
DER
DER(可分辨编码规则)是一种用于将ASN.1对象转换为字节的编码。它是一种简单的标签-长度-值格式:每个元素通过附加其类型(标签)、负载长度和负载本身进行编码。其规则确保任何给定对象只有一种有效的表示形式,这是处理必须签名和检查异常的数字证书时的有用属性。
DER如何工作的细节与本文无关。一个好的起点是这里。
RFC 5280和X.509
TLS中使用的数字证书的格式在一些RFC中定义,最重要的是RFC 5280(以及RFC 5912,针对ASN.1 2002进行了更新)。该规范基于ITU X.509标准。
这是TLS证书最外层包含的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
Certificate ::= SEQUENCE {
tbsCertificate TBSCertificate,
signatureAlgorithm AlgorithmIdentifier,
signature BIT STRING
}
TBSCertificate ::= SEQUENCE {
version [0] Version DEFAULT v1,
serialNumber CertificateSerialNumber,
signature AlgorithmIdentifier,
validity Validity,
subject Name,
subjectPublicKeyInfo SubjectPublicKeyInfo,
-- If present, version MUST be v2 or v3
subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
-- If present, version MUST be v2 or v3
extensions [3] Extensions OPTIONAL
-- If present, version MUST be v3 --
}
|
你可能通过浏览器内置的查看器检查证书时认出其中一些字段。
找出TLS证书中究竟应该包含什么以及应该如何解释并不是一件容易的事——规范分散在许多RFC和其他标准中,有时信息不完整甚至相互冲突。最近的一些文档很好地揭示了相互矛盾的解释数量。
如今似乎有更多的趋同,寻找如何处理TLS证书的好地方是RFC以及一些广泛使用的TLS库。
从ASN.1开始模糊测试TLS
先前的工作
所有高调的TLS库在其源代码树中都包含一些模糊测试工具,并且大多数甚至持续进行模糊测试(如LibreSSL,由于我的同事Andrea的努力,现在被包含在oss-fuzz中)。大多数库使用经过试验和测试的模糊测试工具,如AFL或libFuzzer,这些工具不了解编码或语法。这很可能意味着许多周期被浪费在生成和测试被解析器早期拒绝的输入上。
X.509解析器已经使用多种方法进行了模糊测试。例如,Frankencert通过组合现有证书的部分来生成证书,而CertificateFuzzer使用手工编码的语法。一些模糊测试工作更侧重于发现内存损坏类型的错误,而另一些则更侧重于发现逻辑错误,通常通过并排比较多个解析器的行为来检测不一致性。
ASN1Fuzz
我想要一个能够从ASN.1语法生成有效输入的工具,这样我可以稍微破坏它们,并希望找到一些漏洞。
我找不到任何接受ASN.1语法的工具,所以我决定自己构建一个。
经过大量实验和三次完全重写,我有一个生成有效X509证书的管道,看起来像这样:
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
|
+-------+
| ASN.1 |
+---+---+
|
pycrate
|
+-------v--------+ +--------------+
| Python classes | | User Hooks |
+-------+--------+ +-------+------+
| |
+-----------+-------------+
|
Generator
|
|
+---v---+
| |
| AST |
| |
+---+---+
|
Encoder
|
+-----v------+
| |
| Output |
| |
+------------+
|
首先,我使用pycrate编译ASN.1语法,pycrate是少数支持ASN.1大多数高级功能的FOSS编译器之一。
编译器的输出被馈送到生成器。通过大量内省pycrate类,该组件生成符合输入语法的随机AST。
AST可以被馈送到编码器(例如DER)以创建二进制输出,适合与目标应用程序进行测试。
像这样生成的证书不会有效,因为许多约束没有编码在语法中。此外,我想给用户完全的自由来操纵生成器的行为。
为了解决这个问题,我开发了一个方便的钩子系统,允许在生成器的任何点进行覆盖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
from pycrate_asn1dir.X509_2016 import AuthenticationFramework
from generator import Generator
spec = AuthenticationFramework.Certificate
cert_generator = Generator(spec)
@cert_generator.value_hook("Certificate/toBeSigned/validity/notBefore/.*")
def generate_notBefore(generator: Generator, node):
now = int(time.time())
start = now - 10 * 365 * 24 * 60 * 60 # 10 years ago
return random.randint(start, now)
@cert_generator.node_hook("Certificate/toBeSigned/extensions/_item_[^/]*/" \
"extnValue/ExtnType/_cont_ExtnType/keyIdentifier")
def force_akid_generation(generator: Generator, node):
# keyIdentifier should be present unless the certificate is self-signed
return generator.generate_node(node, ignore_hooks=True)
@cert_generator.value_hook("Certificate/signature")
def generate_signature(generator: Generator, node):
# (... compute signature ...)
return (sig, siglen)
|
该管道生成的AST已经可以用于差异测试。例如,如果一个库接受证书而其他库不接受,则可能存在需要手动调查的问题。
此外,AST可以使用AFL++的自定义变异器进行突变,该变异器在树上执行随机操作。
ASN1Fuzz目前是研究质量的代码,但我确实打算在未来的某个时候开源它。由于生成从ASN.1语法开始,该工具不仅限于生成TLS证书,还可以用于模糊测试许多其他协议。
请继续关注下一篇博客文章,我将展示这项研究的结果!