云原生模糊测试:Istio - 40 次崩溃和高严重性 CVE

在这篇博文中,我们将深入介绍我们为设置 Istio 的连续模糊测试所做的工作。这项工作是与 Istio 维护人员和 Google 开源安全团队合作完成的。

这些努力的结果是在 Istio 中发现了 40 多个独特的崩溃,包括CVE-2022-23635,它允许任何人(包括未经身份验证的用户)发送可能导致控制平面服务器崩溃并充当拒绝服务攻击的恶意负载。

Istio 是一个开源服务网格,提供保护、连接和监控分布式服务的功能。许多组织都使用它来处理大量网络流量,包括 AirBnB、eBay、Atlassian、Sales Force、T-Mobile 和 Rappi。它是用 Go 编程语言编写的,并使用 Envoy 代理的扩展版本来处理各种与代理相关的任务。

挑战

持续对 Istio 进行模糊测试需要克服三个主要挑战。

第一个挑战是 Istio 主要处理结构化数据,而go-fuzz 模糊引擎只是为模糊目标提供字节数组。因此,我们需要一种方便的方法将原始字节数组转换为高级 Go 数据类型,例如结构。为此,我们开发了go-fuzz-headers库,该库可用于轻松创建填充了模糊数据的 Go 数据结构。

第二个挑战是将 Istio 集成到OSS-Fuzz基础设施中。这是在参与的早期完成的,以利用 OSS-Fuzz 提供的持续模糊基础设施。使用 OSS-fuzz,可以自动运行任意数量的 fuzzer,并且可以在 OSS-fuzz 仪表板的覆盖构建中监控覆盖。

第三个挑战是提出一组可以优化分析 Istio 代码的模糊器。简而言之,我们必须想出许多可以以多种方式执行 Istio 代码的 fuzzer。我们总共为 Istio 开发了 60 个模糊器。我们不会在这里详细介绍每一个,但是,所有的 fuzzer 都可以在 Istio 存储库中找到:https://github.com/istio/istio/tree/master/tests/fuzz 。

为什么要模糊 Go 代码?

Fuzzing 具有以高度自主的方式发现错误的直接好处。一旦编写了模糊线束,它就可以在很长一段时间内继续寻找错误,而无需太多人工干预。对于用 Go 编写的软件,此类错误可能是越界、零解引用、超时、内存不足、运行时错误、off-by-1 和逻辑错误。在撰写本文时,已经有 64 个关键的开源 Go 项目加入了 OSS-Fuzz,其中发现并修复了数百个与稳定性和安全性相关的错误。

为什么模糊 Istio 很重要

Istio 构成了越来越多的公司计算基础设施的基础,提供服务发现、流量管理、授权和身份验证以及可观察性。这意味着即使是很小的错误也可能会影响到主要的用户,而严重的错误可能是灾难性的。正因为如此,Istio 团队近年来致力于改善我们的测试覆盖率和安全状况,以帮助发现潜伏的错误,并防止新错误的出现。模糊测试是该旅程的下一步。

时间线

Istio 维护人员在 2019 年尝试了模糊测试,并在 Istio 本身以及关键依赖项中发现了错误。2019 年年中,设置了一个跟踪器问题,以提高模糊覆盖率并集成到 OSS-Fuzz 中。2020 年 12 月,Istio及其两个首批fuzzer 集成到 OSS-Fuzz中。其中一个 fuzzer 在 Kubernetes 本身中发现了一个问题,该问题最初是私下报告给 Kubernetes 维护人员,后来在一个公共问题中被跟踪。2021 年 6 月开始改进模糊覆盖率的工作,截至 2022 年 2 月,已将 60 个模糊器合并到 Istio,所有模糊器都由 OSS-Fuzz 连续运行。

发现

在对 Istio 进行模糊测试的第一年中,总共报告了 70 次崩溃。其中,有 17 起是由于运行时环境中的错误,与 Istio 本身无关,4 起因构建失败而报告崩溃。模糊器本身有 4 个重复 2 和崩溃,这些都是无效的。剩下 43 项与 Istio 相关的发现。调查结果细分如下:

生产代码:

  • 9 次超时。
  • 1 内存不足。
  • 18 个零取消引用。
  • 3 手动添加的恐慌。
  • 1 次读取未导出的字段。
  • 4 个索引/切片超出范围。
  • 1 无效类型断言
  • 1个逻辑错误

测试代码:

  • 2 测试助手崩溃。
  • 来自测试助手的 3 人死亡。

安全严重错误:CVE-2022-23635

模糊测试检测到的一个特别有趣的错误是CVE-2022-23635。受影响的代码非常简单,经过良好测试,并且在生产中使用了一年多。

冲击/攻击向量

在这种情况下,此错误特别有影响,因为它位于对客户端进行身份验证的关键代码路径上。这意味着任何人,包括未经身份验证的用户,都能够发送可能导致控制平面服务器崩溃并充当拒绝服务攻击的恶意负载。虽然Istio 旨在应对与控制平面的短期断开连接,但持续停机会阻止配置更新、端点发现更新和新工作负载的启动。工作负载流失的用户很容易受到此漏洞的影响。在最坏的情况下,用户工作负载正在重新启动(由于升级、可抢占节点、另一个漏洞利用或各种其他原因),这可能导致整个集群范围内的中断。

深潜

为了理解这个错误,我们将简短地深入研究根本原因。以下代码段显示了ExtractJwtAud出现问题的函数:

代码语言:javascript
复制
func ExtractJwtAud(jwt string) ([]string, bool) {
        jwtSplit := strings.Split(jwt, ".")
        if len(jwtSplit) != 3 {
                return nil, false
        }
        payload := jwtSplit[1]
    payloadBytes, err := base64.RawStdEncoding.DecodeString(payload)
    if err != nil {
            return nil, false
    }

    structuredPayload := &jwtPayload{}
    err = json.Unmarshal(payloadBytes, &structuredPayload)
    if err != nil {
            return nil, false
    }

    return structuredPayload.Aud, true

}

资源

模糊器在这段特定的代码中发现了一个零指针取消引用FuzzJwtUtil。如果jwt等于“ .bnVsbM.”,那么structuredPayload在 return 语句中将是 nil,这会导致ExtractJwtAudnil-dereference 崩溃:

代码语言:javascript
复制
       return structuredPayload.Aud, true

structuredPayload当检查返回的错误时nil,返回 的事实structuredPayload.Aud似乎违反直觉json.Unmarshal。我们分解ExtractJwtAud来看看这个崩溃是如何发生的:

为了理解这一点,我们将遍历代码。传递给的字符串ExtractJwtAud是一个JWT令牌,它由 3 个 base64 编码的 JSON 对象组成。ExtractJwtAud只使用中间元素:

代码语言:javascript
复制
jwtSplit := strings.Split(jwt, ".")
if len(jwtSplit) != 3 {
return nil, false
}
payload := jwtSplit[1]

然后对payload字符串进行解码:

代码语言:javascript
复制
payloadBytes, err := base64.RawStdEncoding.DecodeString(payload)
if err != nil {
return nil, false
}

如果函数的jwt参数ExtractJwtAud等于“ .bnVsbM.”,那么payloadBytes最终将是[]byte(“null”),即带有字符“null”的字节切片。然后代码继续解组payloadBytesstructuredPayload

代码语言:javascript
复制
structuredPayload := &jwtPayload{}
err = json.Unmarshal(payloadBytes, &structuredPayload)
if err != nil {
return nil, false
}

代码通过捕获任何抛出的错误来检查解组是否成功json.Unmarshal

如果json.Unmarshal没有抛出任何错误,则继续执行函数的 return 语句,函数的 return 语句将 return structuredPayload.Aud。但是,在这种情况下structuredPayload实际上是这样nil,并且 Istio 会因 nil 指针取消引用而恐慌:

代码语言:javascript
复制
return structuredPayload.Aud, true
}

structuredPayload由于json.Unmarshal. 我们可以为这个问题创建一个简单的复制器,因为我们知道payloadBytes它就[]byte(“null”)在调用之前json.Unmarshal

代码语言:javascript
复制
package main

import (
"encoding/json"
"fmt"
)

type jwtPayload struct {
Aud []string json:"aud"
}

func main() {
structuredPayload := &jwtPayload{}
fmt.Println("before json.Unmarshal: ", structuredPayload)
err := json.Unmarshal([]byte("null"), &structuredPayload)
if err != nil {
return
}
fmt.Println("after json.Unmarshal: ", structuredPayload)
}

运行此文件将打印出以下内容:

代码语言:javascript
复制
在 json.Unmarshal 之前:&{[]}
在 json.Unmarshal 之后:<nil>

(双)指针是这里的关键。我们没有传递 a ,而是传递了一个指向的*jwtPayload双指针。在双指针的情况下,其行为与传递单个指针时的行为相同,但有一个例外 - 如果双取消引用字符串是,则内部指针将设置为。 **jwtPayloadjson.Unmarshaljson.Unmarshal"null"nil

修复

此修复非常简单 - 只需删除额外的指针间接传递 a*jwtPayloadjson.Unmarshal

代码语言:javascript
复制
func ExtractJwtAud(jwt string) ([]string, bool) {
jwtSplit := strings.Split(jwt, ".")
if len(jwtSplit) != 3 {
return nil, false
}
payload := jwtSplit[1]

payloadBytes, err := base64.RawStdEncoding.DecodeString(payload)
if err != nil {
return nil, false
}

  • structuredPayload := &jwtPayload{}
  • structuredPayload := jwtPayload{}
    err = json.Unmarshal(payloadBytes, &structuredPayload)
    if err != nil {
    return nil, false
    }

return structuredPayload.Aud, true
}

Istio 在https://github.com/istio/istio/commit/5f3b5ed958ae75156f8656fe7b3794f78e94db84中修复了这个问题,其中还包括一个带有字符串的测试用例来捕获回归。

其他受影响的项目

代码本身在某种意义上并不特定于 Istio 的行为。事实上,在整个生态系统中的许多其他主要 Go 项目中都发现了相同的代码模式。要查看您的项目是否包含相同的错误,请参阅此工具以获取更多信息。

结束的想法

在这篇博文中,我们将深入介绍我们最近在设置 Istio 连续模糊测试方面的工作。Istio 是一个 Go 应用程序和一个 Cloud Native 服务网格,两者都不是 fuzzing 的传统目标。我们对 Istio 代码的结果和贡献感到高兴,这有助于为可靠性和安全性提供更高的保证。

在过去的两年中,我们对云原生应用程序进行了越来越多的模糊测试。这包括Envoy、Vitess、Kubernetes、Fluent-bit、Containerd、Flux、Runc、Linkerd2 -proxy等等。所有这些项目都集成到免费的开源安全服务 OSS-Fuzz 中。在这次对应用程序新环境(即云原生软件)进行模糊测试的过程中,我们总体上获得了积极的体验,并期待为这个令人兴奋和重要的领域做出更多贡献。