基于ASN.1语法的TLS证书模糊测试技术解析

本文详细介绍了如何从ASN.1语法构建TLS证书模糊测试工具,包括ASN.1语法解析、DER编码规则、RFC 5280规范解读,以及自主研发的ASN1Fuzz工具架构和钩子系统实现原理。

基于ASN.1语法的TLS证书模糊测试

2020年5月14日 - 作者:Filippo Cremonese

我在Doyensec的大部分研究时间都致力于构建一个灵活的基于ASN.1语法的模糊测试工具,用于测试TLS证书解析器。在这个过程中我学到了很多,但经常苦于找不到关于这些主题的优秀资源。在这篇博文中,我想对问题、我所采用的方法以及一些可能为其他安全研究人员节省时间的指引进行高层级概述。

让我们从一些基础知识开始。

什么是TLS证书?

TLS证书是一种符合RFC 5280中定义的ASN.1语法和约束的DER编码对象,该标准基于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
-- MESSAGE-CLASS信息对象类的定义
MESSAGE-CLASS ::= CLASS {
    messageTypeId INTEGER UNIQUE
    &payload      [1] OPTIONAL,
    ...
}
WITH SYNTAX {
    MESSAGE-TYPE-ID  &messageTypeId
    [PAYLOAD    &payload]
}

-- 一些消息类型的定义
TextMessageKinds MESSAGE-CLASS ::= {
    -- 文本消息
    {MESSAGE-TYPE-ID 0, PAYLOAD UTF8String}
    -- 读取确认(无有效载荷)
  | {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,

                         -- 如果存在,版本必须为v2或v3
    subjectUniqueID [2]  IMPLICIT UniqueIdentifier OPTIONAL,
                         -- 如果存在,版本必须为v2或v3
    extensions      [3]  Extensions OPTIONAL
                         -- 如果存在,版本必须为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语法,这是少数支持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年前
    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应该存在,除非证书是自签名的
    return generator.generate_node(node, ignore_hooks=True)

@cert_generator.value_hook("Certificate/signature")
def generate_signature(generator: Generator, node):
    # (...计算签名...)
    return (sig, siglen)

该流水线生成的AST已经可以用于差异测试。例如,如果一个库接受证书而其他库不接受,则可能存在需要手动调查的问题。

此外,AST可以使用AFL++的自定义变异器进行变异,该变异器对树执行随机操作。

ASN1Fuzz目前是研究质量的代码,但我确实计划在未来的某个时候开源它。由于生成从ASN.1语法开始,该工具不仅限于生成TLS证书,还可以用于模糊测试许多其他协议。

请继续关注下一篇博文,我将在其中展示这项研究的结果!

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计