本文主要参考thor的文章,完成了在ubuntu16.04.1@kernel 4.10.0-19 上的root利用。内容主要是总结一下目前的利用思路(ret2user和bypass smep)以及在实践时遇到的一些小问题。
根据上篇 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释放的内存。
最终堆喷射的两个要求是:
寻找堆喷射的函数时,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来堆喷射的代码为:
环境
关闭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版本有关,需要自行修改。
对应汇编如下:
exploit地址
SMEP是,位于CR4寄存器的第20位。开启了smep后,kernel就不难直接执行用户态的shellcode了。查看smep是否开启:cat /proc/cpuinfo | grep smep
运行之前的exp,内核oops信息如下:
这里使用了内核rop的方式修改了CR4寄存器。
由于使用内核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处执行:
用户态shellcode需要把rcx中的值给rbp,执行完patch的功能后,leave恢复栈再退出。
此外,有几个注意点,大部分已经解决了:
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信息,使用方法见之前的环境搭建文章。
当然还有一些没解决的:
下载:exploit地址
最后,感谢SetRet和thor两位前辈的文章,当然还有一些其他帮助过我的前辈们。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2019-1-29 12:25
被心许雪编辑
,原因: