作者:b1cc@墨云科技VLab Team
原文鏈接:https://mp.weixin.qq.com/s/w0HYPpdMxhcPvKvtSJf_CQ

2021年10月12日,日本安全廠商 Flatt security 披露了 Linux 內核提權漏洞CVE-2021-34866。11月5日,@HexRabbit 在 Github 上公布了此漏洞的利用方式,并寫文分析,技術高超,行文簡潔。但作為初次研究相關內容,筆者做了一些較基礎的內容補充。

eBPF

eBPF 是一種在訪問內核服務和硬件的新技術。在這項技術誕生之前,如果需要在 Linux 內核執行定制的代碼有兩種方式,一是提交代碼到原生內核項目中,不用贅述其難度;二是使用內核模塊,以擴展的方式添加代碼到內核執行,可以在運行時動態加載和卸載。但內核模塊也有明顯的缺點:需要對每個內核版本進行適配;如果代碼有問題容易導致內核崩潰。

eBPF 可以較好地解決在內核空間實現自定義代碼的問題。eBPF 是 Linux 內核中高度靈活和高效的類似虛擬機的技術,有自己的字節碼語法和特定的編譯器,允許以安全的方式在各個掛鉤點執行字節碼。它可用于許多 Linux 內核子系統,最突出的是網絡、跟蹤和安全。

在實現上,通過調用bpf_prog_load()可以創建 eBPF 程序。其中需傳入一個包含 eBPF 指令的結構體bpf_insn

Verifier

安全性是 eBPF 的突出特點。如果需要加載一個 eBPF 程序到內核中,需要通過Verifier的檢查。它會檢查 eBPF 程序中是否有死循環、程序大小、越界、參數錯誤等。Verifier中的主要檢查函數是bpf_check(),在這函數中,有一個針對 helper 函數參數檢查的函數check_map_func_compatibility,就是此次漏洞所在的函數。

Helper 函數

eBPF 程序并不能調用任意的內核函數,這會導致 eBPF 程序與特定的內核版本綁定。因此 eBPF 提供的是一些常用且穩定的 API,這些 API 被稱為 helper函數,用于 eBPF 與內核交互數據。所有的 helper 的名稱和作用在bpf.h中有聲明。每種 eBPF 程序類型的有不同的可用的 helper 函數。下面列舉出我們后面分析相關的 helper 函數原型和作用。

/* long bpf_ringbuf_output(void *ringbuf, void *data, u64 size, u64 flags)
 * Description
 * Copy *size* bytes from *data* into a ring buffer *ringbuf*.
 * If **BPF_RB_NO_WAKEUP** is specified in *flags*, no notification
 * of new data availability is sent.
 * If **BPF_RB_FORCE_WAKEUP** is specified in *flags*, notification
 * of new data availability is sent unconditionally.
 * If **0** is specified in *flags*, an adaptive notification
 * of new data availability is sent.
 *
 * An adaptive notification is a notification sent whenever the user-space
 * process has caught up and consumed all available payloads. In case the user-space
 * process is still processing a previous payload, then no notification is needed
 * as it will process the newly added payload automatically.
 * Return
 * 0 on success, or a negative error in case of failure.
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * void *bpf_ringbuf_reserve(void *ringbuf, u64 size, u64 flags)
 * Description
 * Reserve *size* bytes of payload in a ring buffer *ringbuf*.
 * *flags* must be 0.
 * Return
 * Valid pointer with *size* bytes of memory available; NULL,
 * otherwise.
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * u64 bpf_ringbuf_query(void *ringbuf, u64 flags)
 *Description
 *Query various characteristics of provided ring buffer. What
 *exactly is queries is determined by *flags*:
 *
 ** **BPF_RB_AVAIL_DATA**: Amount of data not yet consumed.
 ** **BPF_RB_RING_SIZE**: The size of ring buffer.
 ** **BPF_RB_CONS_POS**: Consumer position (can wrap around).
 ** **BPF_RB_PROD_POS**: Producer(s) position (can wrap around).
 *
 *Data returned is just a momentary snapshot of actual values
 *and could be inaccurate, so this facility should be used to
 *power heuristics and for reporting, not to make 100% correct
 *calculation.
 *Return
 *Requested value, or 0, if *flags* are not recognized.
 */
#define __BPF_FUNC_MAPPER(FN)\
FN(ringbuf_output),\
FN(ringbuf_reserve),\
FN(ringbuf_query),\

helper 函數并不是直接被調用的,而是被用宏BPF_CALL_0~BPF_CALL_5封裝為系統調用的形式,在編寫 eBPF 指令時會直接通過這種宏的形式調用 helper 函數。bpf_func_proto類型的結構體則記錄的是 helper 函數的信息,包括返回值類型、參數類型等。調用的例子如下所示:

BPF_CALL_4(bpf_map_update_elem, struct bpf_map *, map, void *, key,
           void *, value, u64, flags)
{
    WARN_ON_ONCE(!rcu_read_lock_held());
    return map->ops->map_update_elem(map, key, value, flags);
}

const struct bpf_func_proto bpf_map_update_elem_proto = {
    .func           = bpf_map_update_elem,
    .gpl_only       = false,
    .ret_type       = RET_INTEGER,
    .arg1_type      = ARG_CONST_MAP_PTR,
    .arg2_type      = ARG_PTR_TO_MAP_KEY,
    .arg3_type      = ARG_PTR_TO_MAP_VALUE,
    .arg4_type      = ARG_ANYTHING,
};

Maps

Map是一種鍵值對,eBPF 程序可以用來和內核或者用戶空間共享數據。Maps 工作的示意圖如下:

eBPF 程序可以通過 helper 函數來操作 map,map有不同類型,其中各類型的數據結構是有差別的。下面列出現在支持的 map 類型,共31種:

enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC,
BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_ARRAY,
BPF_MAP_TYPE_PROG_ARRAY,
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
BPF_MAP_TYPE_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
BPF_MAP_TYPE_SK_STORAGE,
BPF_MAP_TYPE_DEVMAP_HASH,
BPF_MAP_TYPE_STRUCT_OPS,
BPF_MAP_TYPE_RINGBUF,
BPF_MAP_TYPE_INODE_STORAGE,
BPF_MAP_TYPE_TASK_STORAGE,
BPF_MAP_TYPE_BLOOM_FILTER,
};

Ringbuf

Ringbuf是CPU 共享緩沖區,可以用于從內核向用戶空間發送數據。管理 Ringbuf 的 map 類型是BPF_MAP_TYPE_RINGBUF,它的數據結構是:

struct bpf_ringbuf_map {
struct bpf_map map;
struct bpf_ringbuf *rb;
};

struct bpf_ringbuf {
wait_queue_head_t waitq;
struct irq_work work;
u64 mask;
struct page **pages;
int nr_pages;
spinlock_t spinlock ____cacheline_aligned_in_smp;
/* Consumer and producer counters are put into separate pages to allow
 * mapping consumer page as r/w, but restrict producer page to r/o.
 * This protects producer position from being modified by user-space
 * application and ruining in-kernel position tracking.
 */
unsigned long consumer_pos __aligned(PAGE_SIZE);
unsigned long producer_pos __aligned(PAGE_SIZE);
char data[] __aligned(PAGE_SIZE);
};

漏洞成因

漏洞出現在check_map_func_compatibility函數中,這個函數主要檢測的內容是調用的 helper 函數和對應的 map 類型是否匹配。可以看到這是一個雙向的檢查,第一個 switch 檢查是檢查創建的map->map_type是否可以調用需要的 helper 函數,第二個 switch 檢查是檢查調用的 helper 函數是否可以處理相應的 map 類型。

static int check_map_func_compatibility(struct bpf_verifier_env *env,
struct bpf_map *map, int func_id)
{
if (!map)
return 0;

/* We need a two way check, first is from map perspective ... */
switch (map->map_type) {
     case BPF_MAP_TYPE_PROG_ARRAY:
if (func_id != BPF_FUNC_tail_call)
goto error;
......
         default:
break;
}

/* ... and second from the function itself. */
switch (func_id) {
         case BPF_MAP_TYPE_PROG_ARRAY:
if (func_id != BPF_FUNC_tail_call)
goto error;
break;
     ......
}

return 0;
error:
verbose(env, "cannot pass  %d into func %s#%d\n",
map->map_type, func_id_name(func_id), func_id);
return -EINVAL;
}

在第一個 switch 中,有22個 case 進行檢查,但是這并未完全覆蓋 map 的類型,剩余的不需要進行這一步檢查的 map 類型是:

BPF_MAP_TYPE_PERCPU_HASH

BPF_MAP_TYPE_PERCPU_ARRAY
BPF_MAP_TYPE_LPM_TRIE
BPF_MAP_TYPE_STRUCT_OPS
BPF_MAP_TYPE_LRU_HASH
BPF_MAP_TYPE_ARRAY
BPF_MAP_TYPE_LRU_PERCPU_HASH
BPF_MAP_TYPE_HASH
BPF_MAP_TYPE_UNSPEC

同樣的,在第二個 switch 中,也并非對所有的 helper 進行了檢查。

以上雙向檢查的缺漏,導致了此次漏洞的產生。先查看修復漏洞的 commit 。可以看到,漏洞產生的 map 類型是BPF_MAP_TYPE_RINGBUF,并且在第二個檢查中加入了BPF_FUNC_ringbuf_output,BPF_FUNC_ringbuf_reserve,BPF_FUNC_ringbuf_query函數的檢查。而這些函數是會對 ringbuf 進行操作的,如果定義一個非BPF_MAP_TYPE_RINGBUF類型的 map 類型,并調用了上面的三個函數,那么就會將非BPF_MAP_TYPE_RINGBUF 類型的結構體當成是BPF_MAP_TYPE_RINGBUF類型的結構體進行解析,從而導致了類型混淆。

@@ -5150,8 +5150,6 @@ static int check_map_func_compatibility(struct bpf_verifier_env *env,
case BPF_MAP_TYPE_RINGBUF:
if (func_id != BPF_FUNC_ringbuf_output &&
    func_id != BPF_FUNC_ringbuf_reserve &&
-    func_id != BPF_FUNC_ringbuf_submit &&
-    func_id != BPF_FUNC_ringbuf_discard &&
    func_id != BPF_FUNC_ringbuf_query)
goto error;
break;
@@ -5260,6 +5258,12 @@ static int check_map_func_compatibility(struct bpf_verifier_env *env,
if (map->map_type != BPF_MAP_TYPE_PERF_EVENT_ARRAY)
goto error;
break;
+case BPF_FUNC_ringbuf_output:
+case BPF_FUNC_ringbuf_reserve:
+case BPF_FUNC_ringbuf_query:
+if (map->map_type != BPF_MAP_TYPE_RINGBUF)
+goto error;
+break;
case BPF_FUNC_get_stackid:
if (map->map_type != BPF_MAP_TYPE_STACK_TRACE)
goto error;

對該漏洞可用 BPF_MAP_TYPE_LPM_TRIE代替BPF_MAP_TYPE_RINGBUF,并調用BPF_FUNC_ringbuf_reserve來觸發類型混淆:

int vuln_mapfd = bpf_create_map(BPF_MAP_TYPE_LPM_TRIE, key_size, 0x3000, 1, BPF_F_NO_PREALLOC);
...
struct bpf_insn prog[] = {    
BPF_LD_MAP_FD(BPF_REG_1, vuln_mapfd),
...
BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_ringbuf_reserve),
}

接著需要繞過__bpf_ringbuf_reserve函數內對bpf_ringbuf結構體中size,consumer_pos,producer_pos的檢查;然后通過覆蓋提前通過堆噴射設計好的bpf_array來泄露內核基址和堆地址,通過偽造bpf_array.map.ops指向的bpf_map_ops來分別修改其中的map_delete_elemmap_fd_put_ptr指針為fd_array_map_delete_elemcommit_creds,最后調用bpf_delete_elem()來觸發已經被修改的函數指針,調用commit_creds(&init_cred),達到提權的目的。

修復方案

1.漏洞在 5.13.14 內核版本已修復,請及時更新。
2.設置/proc/sys/kernel/unprivileged_bpf_disabled為1,禁止非特權用戶使用 eBPF 來進行緩解。

參考鏈接

https://flatt.tech/cve/CVE-2021-34866/

https://github.com/HexRabbit/CVE-writeup/tree/master/CVE-2021-34866

https://blog.hexrabbit.io/2021/11/03/CVE-2021-34866-writeup/

https://blog.hexrabbit.io/2021/02/07/ZDI-20-1440-writeup/#smap

https://ebpf.io/what-is-ebpf

https://docs.cilium.io/en/stable/bpf/


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1988/