跳转至

CountDown: Refcount-guided Fuzzing for Exposing Temporal Memory Errors in Linux Kernel

论文地址:CountDown: Refcount-guided Fuzzing for Exposing Temporal Memory Errors in Linux Kernel

Abstract

当前内核模糊测试工具往往是基于覆盖率来被动地触发和引用计数有关的漏洞,可能会错过很多潜在的漏洞。而这篇论文的作者提出了一种基于引用计数引导的内核fuzzer:CountDown

Introduction

UAF(Use After Free)是通过Dangling References(悬垂引用)来访问已经释放的内存对象的操作。Linux使用refcount(引用计数器)来缓解UAF漏洞。当refcount为0时,该内存对象没有引用,应该被释放掉。

只要能保证refcount和内存对象的真正引用数量一致,就能有效避免UAF漏洞。换句话说,如果refcount和真正引用不一致,比如refcount已经为0.但实际的真正引用还存在,此时如果释放了该内存对象,还能从悬垂引用中访问到该内存对象,就会导致UAF漏洞。

Syzkaller通过生成系统调用序列来覆盖内核代码,其随机选择一个现有的系统调用,然后插入一个新的与之强相关的系统调用,以期能触发未覆盖的内核代码路径。这种原理并不利于寻找与引用计数相关的漏洞,因为引用计数相关漏洞往往需要多次执行有关引用计数的代码来使计数归零,这一过程通常并不触发新的代码分支,并不被Syzkaller这样的fuzzer所重视。

CountDown的设计目的是突破代码覆盖率反馈的限制,其设计思路是:收集不同系统调用对引用计数的影响,利用共用的引用计数来设计系统调用关系网络,通过优先组合操作过更多共用的引用计数的系统调用来触发更复杂的引用计数操作,最终诱发与引用计数相关的UAF漏洞。

CountDown包含四个关键部分:

  1. 动态学习基于引用计数的系统调用之间的关系
  2. 基于关系辅助生成新的调用序列和测试程序
  3. 以新访问的引用计数作为新的种子,更新关系
  4. 运用关系定制程序来触发漏洞,如让refcount为0

CountDown项目地址:CountDown

Background and Problem

Refcount in Linux Kernel

Linux内核有两个主要的引用计数的结构体:refcount_tkref_t,其中后者是前者的封装。

函数kref_getkref_put分别用于增加和减少引用计数。当引用计数归零,内核会调用release函数来释放内存对象。初始化引用计数时,内核会调用kref_init函数,这个函数内部会调用refcount_set函数来设置引用计数的初始值,一般为1。

除了以上两个结构体,内核还使用原子变量来作为引用计数,但比例较小。

当引用计数值小于实际引用数时,内存管理器会过早释放对象;当计数值大于实际引用数时,管理器无法释放闲置对象,进而导致内存泄漏。

一个例子,CVE-2021-23134:

C
1
2
3
4
5
6
7
8
int llcp_sock_bind(...){
    // reference += 1; counter += 1;
    llcp_sock->local = nfc_llcp_sock_get(local);
    // ???;            counter -= 1;
    nfc_llcp_sock_put(llcp_sock->local);
    // + reference -= 1;
    // + llcp_sock->local = NULL; <-- patch
}
x 例子中,llcp_sock->local的引用计数在调用nfc_llcp_sock_get函数后增加了1,引用数量也增加了1;在调用nfc_llcp_sock_put函数后,引用计数减少了1,但引用数量并没有减少。反复调用llcp_sock_bind函数后,引用计数始终为1,但引用数量却会不断增加。如果调用close函数释放该对象,引用数量只会减少1,多出来的引用就会变成悬垂引用。

Existing Techiniques of Kernel Fuzzing