LWN:利用sanitizer寻找bug!

关注了就能看到更多这么棒的文章哦~

Finding bugs with sanitizers

By Jake Edge
September 27, 2022
LSS EU
DeepL assisted translation
https://lwn.net/Articles/909245/

Andrey Konovalov 在他 2022 年欧洲 Linux 安全峰会(LSS EU)的演讲中以一个大胆的声明开始。"fuzzing is useless"。当然大家也猜到了,他很快对这一断言加上了限定条件,补充说 "如果没有 dynamic bug detectors (动态错误检测器)"。这些错误检测器就包括了各种类型的 "sanitizer",如 Kernel Address Sanitizer(KASAN),还有其他许多。Konovalov 详细介绍了 KASAN,并概述了 Sanitizer 领域,以及如何能进一步推动这些错误检测器的演进从而发现更多的内核错误。

他说,Fuzzer 有助于走到代码中的新路径里去,但如果没有错误检测机制的话,它们通常最终会引发一些很难 debug 的 kernel crash。sanitizer 和其他的各种错误检测器都可以把这些错误转化为有分析价值的内容,从而可以进行 fix。这些 sanitizer 组成了一系列的错误检测工具。它们最初是为检查用户空间的应用程序而创建的,但后来被移植到了内核中,并在其缩写前加上了一个 "K"。AddressSanitizer(ASan)并不是历史上出现的第一个 sanitizer,但它在很早的时候就比较知名了;其他的还有 MemorySanitizer(MSan)和 UndefinedBehaviorSanitizer(UBSan)。

sanitizer 有很多优点,从而开始流行。它们很容易使用;对于用户空间来说,它只是一个额外的编译器 flag,对于内核来说,只需要启用一个 config option。与其他提供同样功能的工具相比,sanitizer 的速度也很快。他们也很精确,因为他们报告的所有 bug 都是真正的 bug,而不是假警报;偶尔也会出现假警报,但这是由 sanitizer 的 bug 引起的,可以及时得到修复,他说。此外,sanitizer 还提供了详细的报告,说明是什么原因导致了一个错误,这使得分析和 fix 这些错误变得更加容易。

KASAN
KASAN 可以检测内核中的内存破坏;具体来说,它可以发现越界(out-of-bounds)访问、使用后释放(use-after-free)的 bug 以及重复 free(或无效的 free)操作。它在内核中各个内存区域寻找这些问题:slab allocator、page allocator、vmalloc 区域、stack 以及全局内存。它还需要编译器的支持,但这个功能在 Clang 和 GCC 中已经存在一段时间了,所以这不是什么真正的障碍。KASAN 的原始模式已经被重新命名为 "generic KASAN",因为近年来已经增加了另外两种模式;他将在演讲中集中介绍 generic KASAN,尽管也会简要地提到其他模式。

76f212f4923c746d5bf56de58d9817de.png

[Andrey Konovalov]

KASAN 由两部分组成,一个是在 Clang 和 GCC 中实现的编译器部分,另一个是在内核本身实现的 run-time 支持。编译器部分在内核编译过程中会对内存访问操作进行注入,也会为全局变量和 stack 区域添加了红色区域(也就是不可访问的、或专门污染过的内存区域)。run-time 这部分则维护着 shadow memory (用来跟踪内核内存的状态);它还与内核分配器关联起来,可以跟踪分配和释放操作。除此之外,它还负责检测何时发生了 bug,并创建一份详细的报告。

shadow memory 对在内核内存中的每八个字节的区域都会使用一个额外字节,来跟踪每个区域的状态。在检查 kernel memory 时,对齐了的 8 字节的内存块实际上只能有 9 种不同的状态来表达其中某个 byte 是允许访问还是不允许。内核分配的粒度是这样的:在 8 字节区域的开始部分有一些连续的字节是可访问的,然后是一些不可访问的字节;不存在包含了两个可访问区域的情况,也没有可访问区域不是从头开始的情况。

这使得区域的状态可以通过使用区域内可访问字节的数量来表示,用几个比特就够了,而 0 用来表示所有字节都是可访问的。因此,举例来说 7 就表示了前七个字节是可访问的。然而,由于有一个完整的字节用来表示状态了,那么就可以用多个值来描述不应该被访问的内存。然后,所有字节都不可访问的情况可以用 0-7 范围之外的标志值来指明它的内存类型(例如,slab red zone、free page、invalid vmalloc 内存等)。

shadow memory 驻留在地址空间里的一个连续的区域中,包含了已知的 offset。要想算出 shadow memory byte 的地址,只要用对应的内核地址 >> 3 来计算出 offset。

编译器对每一个内存访问都会在进行操作之前先对 shadow memory 中的相应位置进行检查。如果 shadow memory 表示 8 个字节的访问的标志是非零的,那么就会调用 kasan_report()来报出问题。同样的,小于 8 个字节的访问也会被检查,从而确保在该区域的可访问字节数符合这笔访问;否则,也会报告出来。

内核在不断地分配和释放内存,所以 KASAN 需要确保 shadow memory 被及时更新从而反映出这些变化。它通过向内核中的各种分配器都添加 hook 来实现这一点,这可以通过在内核源代码中寻找 "kasan_"来查找到。Konovalov 接着介绍了 KASAN 如何与 slab allocator 一起配合工作的细节。

当一个 slab 被初始化时,就被完全标记为不可访问。然后,slab 中的对象在被分配的时候就可以 "unpoisoned (解毒)" 了,也就是标记为可以访问的。KASAN 在每两个分配的对象之间保持了一个 poisoned red zone 区域,这减少了可以支持的对象的数量,但可以确保内核访问不会超过这个对象的尾端。在这个对象被释放时,它们也会重新被 poison。

在用 kmalloc()分配对象时,是从 kmalloc cache 中分配的,该 cache 通常大于分配请求 size;如果分配 100 字节,就会从 kmalloc-128 cache 中分配,所以有 28 个字节并未得到利用。KASAN 会把这 28 个字节都 poison 掉,并且还会在对象后面再添加一个 poisoned red zone 区域。如果内核调用 ksize() 函数来获取所分配对象的全部长度,KASAN 会对对象的末尾这部分解除限制,当然,第二个 red zone 仍然存在,可以用来检测 buffer 访问越界的情况。

KASAN 也希望能够检测到 use-after-free 的问题,但是通常内核会在一个对象被释放后很快重新分配出去。因为这时这些内存对于 KASAN 看来是有效的内存,所以如果还有一些地方残留对已经 free 的指针的访问都不能被检测出来了。于是 KASAN 会把释放了的 slab 对象先放到一个隔离列表(quarantine list)中,推迟它们被重用的时间。这就增加了 KASAN 捕获到 use-after-free 问题的机会,因为被释放的对象会保持 poison 状态一段时间。

在编译器的帮助下,KASAN 能够在内核 stack 和全局变量中插入 red zone。对于 stack 中的变量,编译器可以在变量前后插入一个 red zone,并生成代码来将 red zone 进行 poison 操作,但是让变量本身 unpoison (解毒)。在离开函数的时候,red zone 被 unpoison,因为它们包含了即将被弹出的 stack frame 的一部分。全局变量也做了类似的工作。编译器会将全局变量变成一个带有 red zone 的 structure,该结构会通过编译器添加的构造函数来进行 poison。这些构造函数也被插入到内核二进制文件以及 module 二进制文件中了。

在 KASAN 检测到一个错误时,它必须创建一个足够详细的报告来引导内核开发人员发现问题。为了做到这一点,KASAN 在 stack depot 中保存了分配和释放的 stack trace,stack depot 最初是为 KASAN 开发的,但现在被内核中的各个子系统都用来存储 stack trace 了。每个 stack trace 都用一个四字节的 handle 来标识;因为这些需要跟它们所对应的 allocation 一起保存下来,所以需要在 allocation path 上使用 red zone,而 handle 通常被存储在 free path 上的 freed object 中。

他展示了一个 KASAN 检测到问题时给出的报告的例子。他使用了 KASAN 测试套件中的一个越界访问的案例,报告中显示了访问的细节,它发生在代码中的位置,以及访问发生时的 stack trace。接下来显示了这个对象被分配的 stack trace;如果有释放该对象的相关 stack trace,那么也会显示出来。之后,KASAN 试图描述相关的内存类型,以及这个导致问题的访问的 shadow memory 的相关内容。

KASAN 不涵盖内核中的一些特殊部分,比如汇编和早期的那部分启动代码。编译器添加的工具通常是以 inline 函数的形式来进行的,但是有一个配置选项可以改变这个行为,来使用常规的函数调用。这会运行得更慢,但是可以创建一个更小的内核镜像,这在空间有限的环境中可能是有用的。

除了他刚刚介绍的在多种架构中都支持的 generic mode 外,KASAN 还有两种与 arm64 memory tagging 功能配合工作的模式;他在演讲后半部分进一步介绍了这些模式。KASAN 有两个测试套件,一个使用 KUnit 单元测试框架,另一个则没有使用 KUnit;后者的所有测试中,除了一个测试 copy_from_user()/copy_to_user()的用户空间组件之外,都正在被迁移到 KUnit 套件中。Konovalov 说,跟内核的其他部分不太一样,KASAN 在使用 Bugzilla 来处理它的相关 bug;可以查看 Bugzilla 来了解该工具中仍需要改进的很多部分。

KASAN 是 "relatively fast";他说,在 KASAN 运行时,速度会降低 2 倍,而在非 inline 模式下运行的话,速度可能会降低 3 倍。但它的内存占用相当大;它需要 1/8 的内存用于 shadow memory,1/32 的内存用于 quarantine,而 slab red zone 则会让这部分分配所占用的空间增加到约 1.5 倍。在 KASAN 被启用之后,就可以在内核上运行任何类型的测试或 fuzzing 测试了;无论 KASAN 发现什么,都会报告给 kernel log。

More sanitizers

KASAN 可以检测到大部分可能发生内存破坏的问题,但是还有其他的 sanitizer 可以用来检测其他类型的问题。例如,Kernel Memory Sanitizer(KMSAN)可以检测在内核中使用了未初始化的内存的情况;它还可以检测出未初始化的内存跨越了安全边界导致的信息泄露,比如用户空间或设备访问到了。像 KASAN 一样,KMSAN 使用了编译器工具和 shadow memory,不过它跟踪的是内存的初始化状态而不是其可访问性。KMSAN 不在 mainline 中,但 Konovalov 希望它能很快被合入 mainline。

data race 可以通过 Kernel Concurrency Sanitizer(KCSAN)来检测。和其他 sanitizer 一样,它使用了编译器注入;在这种情况下,它对 data access 数据访问上安装了 "soft" watchpoint。每次访问会被 stall 住一段时间,然后比较这段时间内数据是否有变化,从而判断是否有并发访问发生了。他说,这个描述大大地简化了其真正的实现方式,但可以说明大概的意思了。

最后,还有 Undefined Behavior Sanitizer(UBSAN),它的缩写前应该再加一个 "K"。它使用了编译器注入来测试各种使用了未定义行为的代码。例如,如果一个 8 字节的值左移了 64 位,这个操作的结果是未定义的,所以 UBSAN 会标记出来。

通常情况下,sanitizer 是在运行测试程序,或者进行 fuzzing 测试的时候运行的,但如果有一些可以在生产环境中运行的 sanitizer 就更好了,可以看看在那里可以发现什么样的错误。这种测试可能是在实际的生产系统上进行,也可能是在具有真实 workload 的测试版或 "dog food" 系统上进行的。有一些工具可以用来配合这种测试,他介绍说。

第一个是 Kernel Electric-Fence(KFENCE),它是一个内存损坏检测工具,只用于 slab 内存。它使用了一种概率方法,对一些分配进行采样,这些分配被放置在受保护的 guard page 旁边,用来检测越界访问的情况。由于它只影响很少一部分的 allocation,影响较小,所以它可以更容易部署在实际生产环境中。可以在许多台设备上同时运行它,从而更有可能发现 bug。

还有就是基于 software tag 的 KASAN mode,它使用了 memory tag 来跟踪内存状态,而不是 shadow memory。它的性能与 KASAN 相似,所以可能不适合用于生产环境,但它的内存占用比 KASAN 少得多。由于它只适用于 arm64 系统,所以比如说可以用在安卓系统的 fuzz 测试上。它还为 KASAN 的 hardware-tag mode 铺平了道路。

还有一个想法是使用 sanitizer 来缓解已知漏洞;因为它们可以检测到这类问题,所以它们可以使系统 panic,从安全和茁壮性的角度来看是比较安全的,或者也许可以只是针对有问题的访问进行 mitigate,而允许内核继续运行。到目前为止,还没有 CPU 支持 KASAN hardware-tag mode 所使用的 Arm Memory Tagging Extension(MTE),但有计划。Konovalov 说,这样就可以把 KASAN 的内存有效性检查直接移到 CPU 中,因此将会是非常快速的,可能意味着可以直接用在生产系统里。对 RAM 的影响也将大大减少。

但是,由于没有 CPU 具备这个功能,所以还不清楚性能上有多大损失,估计在更精确的 MTE sync mode 下会有 10%左右;对于 async mode,可能性能损失接近零。人们是否愿意在生产环境中承担 10%的损失来捕捉这类 bug?目前还不确定。他推荐了他去年在 LSS NA 的演讲(有视频和幻灯片),供那些有兴趣了解更多关于使用 hardware-tag KASAN mode 来解决内存内容破坏的人了解。

Extending

他说,如果你拿一个标准的 fuzzer 在内核上运行,你可能会发现其他人也报出来的同样的错误。目前已有的 bug 查找工具也都是如此;同样的 bug 被反复地发现出来。因此,需要扩展这些工具,并改变在内核上运行的工作负载,从而找到新的内核 bug。

KASAN 和 KMSAN 可以说不仅仅是 sanitizer,它们提供了跟踪各种状态下的内存的 framework,并检测出内存在意外状态下被使用的情况。因此,这些工具可以以各种方式扩展,从而捕捉到其他类型的问题。

例如,内核中代表数据包的结构(struct sk_buff)有一个 skb_shared_info 字段,就放在 struct 的最后。KASAN 不能检测到溢出到 skb_shared_info 的情况,因为它是一个单独 object 的全部组成部分。KASAN 的一个可能的扩展就是在 skb_shared_info 之前放置一个小的 red zone,这需要一些工作来确保当 socket buffer 在被(正确地)访问时,red zone 不被 poison。这种 "intra-object overflow" 目前完全没有被检测,但今后可以加入到各种内核数据结构中去。socket buffer 溢出不仅仅是一个理论中才存在的问题,因为他在 CVE-2017-1000112 的检测中就使用了这个机制,这是 Linux UDP 代码中发现的漏洞。

另一个需要研究的领域是将其他内核分配器加入到 KASAN 的检查中。现在,它没有对 per-CPU 分配器进行检查,但该分配器在内核的许多地方都有在使用,尤其是在网络子系统中。他展示了两个 KASAN 的测试案例,证明了对越界访问或以这种方式分配的内存在 use-after-free 的情况下没有任何检查。同样,其他内核分配器也可以作为改进的目标,也许包括一些专门用于 Android 的分配器。

他的最后一个例子已经被实现了。几年前,他正致力于从外部设备的角度对内核的 USB 代码进行模糊处理,并需要扩展 KMSAN 来检测 USB 设备的信息泄露问题。恶意设备需要某种方式从内核中获取信息,以确定内核中的地址,因此他当时希望找到有把未初始化的内核内存复制到 USB 设备上的行为。在他添加了这种检测之后,syzbot 就开始发现相当多的这种漏洞。

最后,Konovalov 说,有两条路可以找到更多的内核 bug:改进 fuzzer 或改进 bug 检测工具。目前改进 fuzzer 可能更容易,但很多人已经在做这个事情了,而很少有人在改进 bug 检测方面努力。除了改进现有的检测工具外,还可以创建全新的检测工具,从而寻找全新类型的 bug,比如 type confusions 或漏了 TLB flush 的动作。为了寻找更多 kernel bug,还有很多领域都可以进行实验。

[编者感谢 LWN 的用户支持去都柏林参加欧洲 Linux 安全峰会。]

全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。

欢迎分享、转载及基于现有协议再创作~

长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~

format,png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值