最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

eBPF Verifier內(nèi)存越界實例分析

2023-04-02 13:46 作者:清澄秋爽  | 我要投稿

更多內(nèi)核安全、eBPF分析和實踐文章,請關(guān)注博客: ?

https://kernel-security.blog.csdn.net/


eBPF基礎(chǔ)架構(gòu)

eBPF程序分為兩部分: 用戶態(tài)和內(nèi)核態(tài)代碼。


eBPF內(nèi)核代碼:

  • 這個代碼首先需要經(jīng)過編譯器(比如LLVM)編譯成eBPF字節(jié)碼,然后字節(jié)碼會被加載到內(nèi)核執(zhí)行。所以 這部分代碼理論上用什么語言編寫都可以,只要編譯器支持將該語言編譯為eBPF字節(jié)碼即可;

  • 目前絕大多數(shù)工具都是用的C語言來編寫eBPF內(nèi)核代碼,包括BCC。bpftrace提供了一種易用的腳本語言來幫助用戶快速高效的使用eBPF功能,其背后的原理還是利用LLVM 將腳本轉(zhuǎn)為eBPF字節(jié)碼;


eBPF用戶態(tài)代碼:

  • 這部分代碼負責將eBPF內(nèi)核程序加載到內(nèi)核,與eBPF MAP交互,以及接收eBPF內(nèi)核程序發(fā)送出來的數(shù)據(jù);

  • 這個功能的本質(zhì)上是通過Linux OS提供的syscall(bpf syscall + perf_event_open syscall)完成的,因此這 部分代碼你可以用任何語言實現(xiàn)。比如BCC使用python,libbpf使用c或者c++,TRACEE使用Go等等;


圖片



eBPF數(shù)據(jù)源

性能分析大師Brendan Gregg(Intel Fellow)總結(jié)的Linux BPF Tracing Tools上展示了豐富多彩的eBPF鉤子類型,這些鉤子類型提供了可以加載BPF程序的范圍。

  • fentry/fexit

  • Tracepoints

  • network devices (tc/xdp)

  • network routes

  • TCP congestion algorithms

  • sockets (data level)

  • kernel functions (kprobes)

  • userspace functions (uprobes)

  • system calls


圖片



eBPF框架的發(fā)展歷程

  • 2014年9月 引入了bpf() syscall,將eBPF引入用戶態(tài)空間。自帶迷你libbpf庫,簡單對bpf()進行了封裝,功能是將eBPF字節(jié)碼加載到內(nèi)核。

  • 2015年2月份 Kernel 3.19 引入bpf_load.c/h文件,對上述迷你libbpf庫再進行封裝,功能是將eBPF elf二進制文件加載到內(nèi)核(目前已過時,不建議使用)。

  • 2015年4月 BCC項目創(chuàng)建,提供了eBPF一站式編程。

1.創(chuàng)建之初,基于上述迷你libbpf庫來加載eBPF字節(jié)碼。

2.提供了Python接口。

  • 2015年11月 Kernel 4.3 引入標準庫 libbpf

該標準庫由Huawei 2012 OS內(nèi)核實驗室的王楠提交。

  • 2018年 為解決BCC的缺陷,CO-RE(Compile Once, Run Everywhere)的想法被提出并實現(xiàn),最后達成共識:libbpf + BTF + CO-RE代表了eBPF的未來,BCC底層實現(xiàn)逐步轉(zhuǎn)向libbpf。


eBPF可移植性痛點和解決方案

在內(nèi)核版本A上編譯的eBPF程序,無法直接在另外一個內(nèi)核版本B上運行。造成可執(zhí)行差的根本原因在于eBPF程序訪問的內(nèi)核數(shù)據(jù)結(jié)構(gòu)(內(nèi)存空間)是不穩(wěn)定的,經(jīng)常隨內(nèi)核版本更迭而變化。


目前使用BCC的方案通過在部署機器上動態(tài)編譯eBPF源代碼可以來解決移植性問題。每一次eBPF程序運行都需要進行一次編譯,而且需要在部署機器上按照上百兆大小的依賴,如編譯器和頭文件Clang/LLVM + Linux headers等。同時在Clang/LLVM編譯過程中需要消耗大量的資源(CPU/內(nèi)存),對業(yè)務(wù)性能也會造成很大影響。


解決方案(CO-RE Compile Once,Run Everywhere):

1)BTF:將內(nèi)核數(shù)據(jù)結(jié)構(gòu)信息高效壓縮和存儲(相比于DWARF,可達到超過100倍的 壓縮比)

2)LLVM/Clang編譯器:編譯eBPF代碼的時候記錄下relocation相關(guān)的信息

3)Libbpf:基于BTF和編譯器提供的信息,動態(tài)relocate數(shù)據(jù)結(jié)構(gòu)


其中BTF為重要組成部分,Linux Kernel 5.2及以上版本自帶BTF文件,低版本需要手動移植。通過分析內(nèi)核源碼,可以發(fā)現(xiàn)BTF文件的生成并不需要改動內(nèi)核,只依賴:

  • 帶有debug info的vmlinux image

  • pahole

  • LLVM


這意味著,我們可以自己為低版本內(nèi)核生產(chǎn)BTF文件,以此讓低內(nèi)核版本支持CORE。


eBPF程序?qū)嵗治?/strong>

eBPF程序會被LLVM編譯為eBPF字節(jié)碼,eBPF字節(jié)碼需要通過eBPF Verifier的(靜態(tài))驗證后,才能真正運行。邊界檢查是eBPF Verifier的重點工作,目的是為了防止eBPF程序內(nèi)存越界訪問。


接下來通過在eBPF程序中簡單的增加、刪減print打印信息觸發(fā)不同原因的幾種邊界檢查異常導(dǎo)致驗證失敗的例子,進一步講解深層的原理。


程序?qū)嶒灜h(huán)境:

1)LLVM 11

2)Linux Kernel 5.8

3)Libbpf commit @9c44c8a


1)內(nèi)存越界:
















上述代碼編譯運行后,提示Verifier失敗,然后使用objdump命令來看一下具體的字節(jié)碼,通過以下字節(jié)碼程序,可以看到Verifier失敗的原因在于第14行R6寄存器(變量pos)沒有進行邊界檢查導(dǎo)致。


Root Cause:

  • 當eBPF Verifier走到第14行的時候嘗試去訪問array數(shù)組,但是此時數(shù)組的下標pos是來自bpf_get_smp_processor_id獲取到的unsigned int 類型的動態(tài)變量,此時Verifier無法判斷變量的具體數(shù)值,所以會保守認為可能會達到最大值,這樣的話就會超出array數(shù)組的范圍,造成內(nèi)存越界。




















添加邊界檢查代碼



if (pos < MAX_SIZE) ??if?r0?>?15?goto?+3?<LBB0_3>


2)Verifier驗證機制和編譯器優(yōu)化機制不一致導(dǎo)致邊界檢查不通過


① 使用錯誤寄存器做邊界檢查:






















SEC("kprobe/do_unlinkat") int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name) { ?// 獲取一個數(shù)組指針array(數(shù)組MAX_SIZE為16個字節(jié)) ?u32 key = 0; ?char *array = bpf_map_lookup_elem(&array_map, &key); if (array == NULL) ?return 0; ?// 獲取當前運行程序的CPU編號(當前機器的CPU有16個核) ?unsigned int pos = bpf_get_smp_processor_id();; ?// 修改數(shù)值 ?if (pos < MAX_SIZE){ ? ?array[pos] = 1; ?pos += 1; } // debug代碼,輸出一些上下文信息 ?bpf_printk("debug %d %d %d\n", bpf_get_current_pid_tgid() >> 32, bpf_get_current_pid_tgid(), array[1]); ?// 修改數(shù)值 ?if (pos < MAX_SIZE) ? ?array[pos] = 1; ?return 0; }


編譯這個代碼后Verifier驗證通過,可以正常運行。但是此時如果把bpf_printk打印信息刪掉,竟然提示Verifier驗證失敗,原因是R0寄存器(變量pos)沒有通過邊界檢查,但是明明已經(jīng)加了邊界檢查代碼,怎么還會出現(xiàn)問題,這么神奇!


Root Cause:

圖片




  • 由于編譯器的優(yōu)化策略,導(dǎo)致刪減bpf_printk后編譯生成的eBPF字節(jié)碼使用寄存器r1(表示pos變量)來進行邊界檢查,但是卻用r0+1(同樣表示pos變量)來訪問數(shù)組array;

  • 相比之下,從eBPF verifier的角度來看,由于在編譯過程中,r1和r0+1的關(guān)聯(lián)性丟失了,導(dǎo)致eBPF verifier無法知道pos變量已經(jīng)通過了檢查,因此錯誤的認為pos變量沒有進行邊界檢查,不允許程序運行;


② 寄存器溢出或重新加載后,狀態(tài)丟失:



















SEC("kprobe/do_unlinkat") int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name) { ?// 獲取一個數(shù)組指針array(數(shù)組MAX_SIZE為16個字節(jié)) ?u32 key = 0; ?char *array = bpf_map_lookup_elem(&array_map, &key); if (array == NULL) ?return 0; ?// 獲取當前運行程序的CPU編號(當前機器的CPU有16個核) ?unsigned long pos = bpf_get_smp_processor_id();; ?// 修改數(shù)值 ?if (pos < MAX_SIZE){ ? ?for (unsigned long i = 0; i < MAX_SIZE; i++) ? ? ?bpf_printk("debug %d %d %d\n", bpf_get_current_pid_tgid() >> 32, \ ? ? ? bpf_get_current_pid_tgid(), array[i]); ? ?array[pos] = 1; ?} ?return 0; }


在上述邊界檢查代碼中添加一段print調(diào)試打印信息后編譯驗證又會出現(xiàn)Verifier失敗,通過排查發(fā)現(xiàn)不是已知的兩類問題,依然使用objdump查看添加后的字節(jié)碼信息。


Root Cause:


圖片



  • 加入bpf_printk后通過字節(jié)碼可以看到,代碼先使用R0(表示pos變量)進行邊界檢查。由于當前寄存器數(shù)量不足,編譯器決定將將R0臨時保存到棧上的空間(R10-16,在eBPF字節(jié)碼中,R10存儲存放著 eBPF ??臻g的棧幀指針的地址),這樣R0就可以空閑出來,留給其他代碼使用,我們稱這種行為為寄存器溢出(register spill);

  • 當真正需要使用pos變量的時候,編譯器會從棧上(R10-16)將之前保存的內(nèi)容取出來賦給R1(也表示pos變量),然后使用R1對數(shù)組array進行訪問。但神奇的是,當寄存器溢出發(fā)生時,pos變量的狀態(tài)丟失了,eBPF忘記了該變量曾經(jīng)進行了邊界檢查,導(dǎo)致程序無法通過驗證;


解決方案

在源碼中加入 &= 操作符,引導(dǎo)編譯器生成理想的eBPF字節(jié)碼


array[pos?&=?MAX_SIZE?-?1]?=?1;


如果上述方法失效,無法引導(dǎo)編譯器,那么針對出錯的部分源代碼人工編寫eBPF字節(jié)碼,替代編譯器生成的字節(jié)碼











#define STR(s) #s #define XSTR(s) STR(s) #define asm_variable_bound_check(variable) ? ?\ ({ ? ? ? ? ? ? ? ? ?\ ?asm volatile ( ? ? ? ? ? ?\ ?"%[tmp] &= " XSTR(MAX_SIZE - 1) " \n" ?\ ?:[tmp]"+&r"(variable) ? ? ? ?\ ?); ? ? ? ? ? ? ? ?\ }) asm_check(pos); array[pos]?=?1;


總結(jié)

eBPF 作為 Linux 內(nèi)核一項革命性的技術(shù),起源于 Linux 內(nèi)核,該技術(shù)可以安全而高效地拓展內(nèi)核的能力,但快速發(fā)展的同時,也會存在很多新鮮出爐的問題,給廣大開發(fā)者尤其是入門者帶來個很大的困擾,本文從幾個實例的角度來對問題進行分析和解答,有相關(guān)開發(fā)疑惑的同學(xué)可以參考借鑒。






本文使用 文章同步助手 同步


eBPF Verifier內(nèi)存越界實例分析的評論 (共 條)

分享到微博請遵守國家法律
甘洛县| 根河市| 顺义区| 望都县| 义乌市| 醴陵市| 肇州县| 哈巴河县| 永胜县| 南溪县| 民县| 客服| 津南区| 肇庆市| 石棉县| 锡林郭勒盟| 延长县| 南陵县| 都兰县| 莲花县| 青河县| 九江市| 庄浪县| 杨浦区| 长寿区| 清新县| 唐海县| 达孜县| 抚宁县| 科技| 冕宁县| 永胜县| 宣城市| 湘阴县| 海丰县| 桃园市| 尚志市| 颍上县| 济阳县| 广水市| 盐亭县|