Golang TLS双向身份认证DoS漏洞分析(CVE-2018-16875)

简介:

一、前言
如果程序源代码使用Go语言编写,并且用到了单向或者双向TLS认证,那么就容易受到CPU拒绝服务(DoS)攻击。Go语言的crypto/x509标准库中的校验算法存在逻辑缺陷,攻击者可以精心构造输入数据,使校验算法在尝试验证客户端提供的TLS证书链时占用所有可用的CPU资源。

为了保护正常服务,大家应立即升级到G0 v1.10.6、v1.11.3或者更新版本。

二、研究背景
42Crunch的API Security平台后端采用的是微服务架构,而微服务使用Go语言编写。微服务之间通过gRPC相互通信,并且部署了REST API网关用于外部调用。为了确保安全性,我们遵循了“TLS everywhere”(处处部署TLS)原则,广泛采用了TLS双向认证机制。

Go的标准库原生支持SSL/TLS认证,也支持大量与连接处理、验证、身份认证等方面有关的x509和TLS原语。这种原生支持可以避免外部依赖,使用标准化的、经过精心维护和审核的TLS库也能降低安全风险。

因此42Crunch很有可能受此TLS漏洞影响,需要理解漏洞原理,保证42Crunch平台的安全性。

42Crunch安全团队针细致分析了该CVE,如下文所示。

三、问题描述
这个DoS问题最早由Netflixx发现,Golang在issue跟踪日志中提到:

crypto/x509包负责解析并验证X.509编码的密钥和证书,正常情况下会占用一定的资源来处理攻击者提供的证书链。

crypto/x509包并没有限制验证每个证书链时所分配的工作量,攻击者有可能构造恶意输入,导致CPU拒绝服务。Go TLS服务器在接受客户端证书或者TLS客户端在验证证书时会受此漏洞影响。

该漏洞具体位于crypto/x509 Certificate.Verify()函数的调用路径中,该函数负责证书认证及验证。

四、漏洞分析
背景知识
为了便于漏洞分析,我们举个简单的例子:TLS客户端连接至TLS服务器,服务器验证客户端证书。

TLS服务器在8080端口监听TLS客户端请求,验证客户端证书是否由证书颁发机构(CA)颁发:

 1caPool := x509.NewCertPool()
 2ok := caPool.AppendCertsFromPEM(caCert)
 3if !ok {
 4        panic(errors.New("could not add to CA pool"))
 5}
 6
 7tlsConfig := &tls.Config{
 8        ClientCAs:  caPool,
 9        ClientAuth: tls.RequireAndVerifyClientCert,
10}
11
12//tlsConfig.BuildNameToCertificate()
13server := &http.Server{
14        Addr:      ":8080",
15        TLSConfig: tlsConfig,
16}
17
18server.ListenAndServeTLS(certWeb, keyWeb)

在标准的TLS验证场景中,TLS客户端会连接到TLS服务器的8080端口,然后向服务器提供证书的“trust chain”(信任链),其中包括客户端证书、root CA证书以及中间所有CA证书。TLS服务器处理TLS握手,验证客户端证书,检查客户端是否可信(即客户端证书是否由服务器信任的CA签名)。通常TLS握手过程如下图所示:
image
分析Go语言的crypto/x509库,最终我们会进入x509/tls/handshake_server.go:doFullHandshake()函数代码段:

 1...
 2if c.config.ClientAuth >= RequestClientCert {
 3        if certMsg, ok = msg.(*certificateMsg); !ok {
 4                c.sendAlert(alertUnexpectedMessage)
 5                return unexpectedMessageError(certMsg, msg)
 6        }
 7        hs.finishedHash.Write(certMsg.marshal())
 8
 9        if len(certMsg.certificates) == 0 {
10                // The client didn't actually send a certificate
11                switch c.config.ClientAuth {
12                case RequireAnyClientCert, RequireAndVerifyClientCert:
13                        c.sendAlert(alertBadCertificate)
14                        return errors.New("tls: client didn't provide a certificate")
15                }
16        }
17
18        pub, err = hs.processCertsFromClient(certMsg.certificates)
19        if err != nil {
20                return err
21        }
22
23        msg, err = c.readHandshake()
24        if err != nil {
25                return err
26        }
27}
28...

根据代码,服务器会处理收到的客户端证书,然后调用x509/tls/handshake_server.go:processCertsFromClient()函数。如果需要验证客户端证书,服务器就会创建一个VerifyOptions结构,其中包含如下信息:

root CA池,即已配置的一系列可信CA(由服务器控制),用来验证客户端证书

中间CA池,即服务端收到的一系列中间CA(由客户端控制)

已签名的客户端证书(由客户端控制)

其他字段(可选项)


 1if c.config.ClientAuth >= VerifyClientCertIfGiven && len(certs) > 0 {
 2        opts := x509.VerifyOptions{
 3                Roots:         c.config.ClientCAs,
 4                CurrentTime:   c.config.time(),
 5                Intermediates: x509.NewCertPool(),
 6                KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
 7        }
 8
 9        for _, cert := range certs[1:] {
10                opts.Intermediates.AddCert(cert)
11        }
12
13        chains, err := certs[0].Verify(opts)
14        if err != nil {
15                c.sendAlert(alertBadCertificate)
16                return nil, errors.New("tls: failed to verify client's certificate: " + err.Error())
17        }
18
19        c.verifiedChains = chains
20}

为了澄清问题机理,我们需要理解服务端如何管理证书池,以便通过高效的方式来验证证书。证书池实际上就是一个证书列表,可以根据实际需求通过3种不同的方式来访问。一种访问方式如下图所示:池中证书可以通过索引数组(这里为Certs)来访问,以CN, IssuerName, SubjectKeyId字段作为哈希字段。

image
验证过程
服务端使用VerifyOptions参数调用Verify()函数来处理客户端证书(即chain:certs[0]中的第一个证书)。

然后Verify()会根据客户端提供的证书链来处理待验证的客户端证书,但首先需要使用buildChains()函数建立并检查整条验证链:

1var candidateChains [][]*Certificate
2if opts.Roots.contains(c) {
3        candidateChains = append(candidateChains, []*Certificate{c})
4} else {
5        if candidateChains, err = c.buildChains(make(map[int][][]*Certificate), []*Certificate{c}, &opts); err != nil {
6                return nil, err
7        }
8}

而buildChains()函数会依次调用占用CPU资源的一些函数,递归处理这条链上的每个元素。

buildChains()函数依赖于findVerifiedParents()函数,而后者可以通过IssuerName或者AuthorityKeyId映射访问证书池,识别上级证书,,然后返回候选证书索引,以便后续根据客户端控制的证书池来验证该证书。

在正常情况下,程序会提取IssuerName及AuthorityKeyId,并且认为这些值为唯一值,只会返回一个待验证的证书:

 1func (s *CertPool) findVerifiedParents(cert *Certificate) (parents []int, errCert *Certificate, err error) {
 2    if s == nil {
 3        return
 4    }
 5    var candidates []int
 6
 7    if len(cert.AuthorityKeyId) > 0 {
 8        candidates = s.bySubjectKeyId[string(cert.AuthorityKeyId)]
 9    }
10    if len(candidates) == 0 {
11        candidates = s.byName[string(cert.RawIssuer)]
12    }
13
14    for _, c := range candidates {
15        if err = cert.CheckSignatureFrom(s.certs[c]); err == nil {
16            parents = append(parents, c)
17        } else {
18            errCert = s.certs[c]
19        }
20    }
21
22    return
23}

buildChains()函数会在客户端发给TLS服务器的整条证书链上执行如下操作:

在(服务端)root CA池上调用findVerifiedParents(client_certificate),查找待验证证书的签发机构(判断是否为root CA),然后根据AuthorityKeyId(如果不为nil)或者原始的issuer值(如果为nil)检查所有找到的证书的签名

在(客户端提供的)中间CA池上调用findVerifiedParents(client_certificate),查找已验证证书的签发机构(判断是否为中间CA),然后根据AuthorityKeyId(如果不为nil)或者原始的issuer值(如果为nil)检查所有找到的证书的签名

获取上一级中间签名节点

在新发现的中间节点上调用buildChains(),然后重复前面描述的签名检查过程
image
DoS攻击
攻击者可以构造一种非预期场景,其中所有的中间CA证书使用的都是同一个名称,并且AuthKeyId值为nil,这样当调用buildChains()和findVerifiedParent()函数时,就会造成CPU DoS攻击效果。findVerifiedParent()函数会返回与该名称匹配的所有证书(这里返回的是整个证书池),然后检查所有证书的签名。检查完毕后,会再次递归调用buildchains()函数处理找到的上一级证书,最后处理到root CA为止。每一次检查过程实际上都会处理整个中间CA池,因此单单一个TLS连接就会耗尽所有可用的CPU资源。
image
五、漏洞影响
攻击者可以精心构造一条证书链,使客户端证书校验过程耗尽服务端所有CPU资源,降低目标主机响应速度。只需要1个连接就能导致这种攻击效果。根据Go的调度程序规则,只有两个CPU核心会受到影响,CPU使用率达到100%,攻击者可以创建新连接,强制调度程序分配更多资源来校验签名,最终导致目标服务或目标主机无响应。

六、缓解措施
Go语言社区已经通过如下措施修复该问题:

原文发布时间为:2018-12-24
本文作者: Golang语言社区
本文来自云栖社区合作伙伴“Golang语言社区”,了解相关信息可以关注“Golangweb”微信公众号

相关文章
|
5月前
|
监控 网络协议 Go
Golang抓包:实现网络数据包捕获与分析
Golang抓包:实现网络数据包捕获与分析
|
NoSQL Java Go
记一次Golang内存分析——基于go pprof
## 1. 背景 阿里云Redis线上在某些任务流中使用`redis-port`来进行实例之间的数据同步。`redis-port`是一个MIT协议的开源软件,主要原理是从源实例读取RDB快照文件、解析、然后在目标实例上应用灌数据的写命令。为了限制每个进程的最大内存使用,我们使用cgroup来做隔离,最近线上出现redis-port在同步数据时`OOM`的情况,最高内存使用达到了`10G`以上
24692 0
|
2月前
|
存储 缓存 算法
Golang高性能内存缓存库BigCache设计与分析
【2月更文挑战第4天】分析Golang高性能内存缓存库BigCache设计
64 0
|
7月前
|
存储 Java 编译器
golang逃逸技术分析
golang逃逸技术分析
25 0
|
10月前
|
存储 Java 编译器
Golang逃逸技术分析
Golang逃逸技术分析
75 0
|
监控 NoSQL Java
|
Web App开发 Go
Golang中的Remove和RemoveAll的对比分析
任何编程语言都有自己的系统库,Golang 也不例外。今天我们讨论一下 Golang 的 os 包的两个删除方法:Remove 和 RemoveAll。它们二者都可以删除文件,但是又存在一定的差异,搞清楚这些差异,在日常编码才不会出现“双兔傍地走,安能辨我是雄雌”的窘境。
1134 0
|
算法 安全 Java
[典藏版]Golang三色标记+混合写屏障GC模式全分析
从三色标记演进混合写屏障的GC模式全场景分析,主要介绍Golang自V1.3以来所采用的内存清理模式,分别具有标记清除、三色标记、写屏障机制,其中一些Golang的设计理念和垃圾回收理念是非常值得去借鉴和学习。本章节主要以推演的形式逐一介绍Golang垃圾回收的处理机制。
851 0
[典藏版]Golang三色标记+混合写屏障GC模式全分析
|
Kubernetes Java 编译器
golang 中函数使用值返回与指针返回的区别,底层原理分析
本文通过分析在 Go 函数中使用变量时的一些问题,变量在分配内存时会在堆和栈两个地方存在,在堆和栈上分配内存的不同,以及何时需要在堆上分配内存的变量。
512 0
|
存储 NoSQL Java
Golang 程序启动流程分析
本文主要介绍 Golang 程序启动流程中的关键代码,启动过程的主要代码是通过 Plan9 汇编编写的,如果没有做过底层相关的东西看起来还是非常吃力的,笔者对其中的一些细节也未完全搞懂,如果有兴趣可以私下讨论一些详细的实现细节,其中有一些硬编码的数字以及操作系统和硬件相关的规范理解起来相对比较困难。针对 Golang runtime 中的几大组件也会陆续写出相关的分析文章。
205 0