Brief
本次复现的目标是Android的经典漏洞PingPong root (CVE-2015-3636)。
这是一个Android 7时代非常经典的旧漏洞,由于大部分的令人眼花撩乱的安全限制机制都是出现在Android P之后的,复现起来对我这种新手会更加的友好一点,同时,也可以更加清晰的梳理PXN的绕过方式;
这是我第一次相对完整的复现和分析一个Android kernel相关的漏洞,梳理过程主要参考了f0rm2l1n
的博客,所以更多的重点放在了梳理整个漏洞利用的流程以及关键位置的利用技巧。跳过了一些内核调试相关的部分(直接照搬前辈们些好的exploits), 后续调试的基本功更加扎实点了再回来细化调试的部分。
关于CVE-2015-3636,该漏洞的poc描述了一个位于ICMP socket在连接过程中引发的BUG,在一定条件下可以”进化”成一个UAF的漏洞,利用ret2dir技术可以在userspace中劫持到socket的关键结构,利用ROP链完成PXN标记位置的改写;最终利用kernel write的原语完成SELinux状态的改写以及cred
的结构的改写,完成提权;
复现环境的搭建
漏洞模拟环境
在2022年的今天,找到一台能刷Android 7的设备并不简单,但是52pojie的某次比赛题目为复现PingPong Root提供了一个带有漏洞环境的Android模拟器,直接运行即可。使用4B5F5F4B
在Github上发布的exploit源代码即可成功获取到root。
但是即便如此,在复现这个漏洞的时候,仍然有一些坑需要注意:
使用shell用户权限(uid=2000),直接运行exploit会没有创建socket的权限而抛出
Permission denied
的异常;可使用如下的方法解决:利用
adb shell
连接模拟器,此时是root权限,su shell
切换至shell用户即可;利用
sysctl -w net.ipv4.ping_group_range="0 2147483647"
开启权限(这个方案可行但是模拟器上似乎不太好用)
4B5F5F4B
提供的exploit是适配题目的模拟器环境的,其内核中一些偏移均与真机环境中不同,在其他环境中尝试复现该漏洞需要更改一些偏移;
以上,即可开启一次愉快的漏洞之旅了;
kernel 源代码获取
从清华源可以获取到源代码:
1 | git clone https://aosp.tuna.tsinghua.edu.cn/kernel/common.git |
由于时间久远,在2022年的今天,这个仓库已经deprecated
了;
从PoC开始理解漏洞
公开的Poc代码如下:
1 |
|
在PoC中,首先创建了一个SOCK_DGRAM
+IPPROTO_ICMP
, 熟悉的ICMP协议也就是常用的ping使用的协议;紧接着,PoC分别对一个AF_INET
簇的socketaddr_in
调用了connect函数;对一个AF_UNSPEC
簇的socketaddr_in
重复调用了两次connect函数;最终,我们获得了一个kernel panic
1 | Call trace: |
通过CallTrace的路径,下面开始分析,为何会出现Kernel Panic
首先是syscall的入口:
1 | // net/socket.c |
通过sockfd_lookup_light
将userspace中传入的文件描述符fd
转化成为一个sock struct
, 之后调用sock->ops->connect
指向的函数来处理具体的connect函数;通过panic中的Call Trace可以看到,处理函数为inet_dgram_connect
1 | // net/ipv4/af_inet.c |
在这个位置,根据socketaddr->sa_family
的不同,将会有不同的处理流程;对于第一次connect调用,socketaddr->sa_family=AF_INET
会跳过两个if语句,直接调用inet_autobind
最终内核会将socket hash计算后存入ping_hashslot;
而对于后两次connect函数调用(触发漏洞/kernel panic的位置),由于socketaddr->sa_family=AF_UNSPEC
因此会调用到disconnect函数执行,这里同样是根据sock struct
结构决定具体的处理函数,通过Call Trace可以确定为udp_disconnect
1 | // net/ipv4/udp.c |
此时,会判断该sock是否绑定了地址或是出于加锁状态,否则,就会调用unhash即存在漏洞的函数ping_unhash
1 | // net/ipv4/ping.c |
在ping_unhash
函数中,首先会通过sk_hashed
判断sock struct是否处于hash状态,其判断流程如下:
1 | // include/net/sock.h |
总结一下,就是判断sk->sk_node->pprev
是否为空,那么问题是,当sock struct
被unhash一次之后,sk->sk_node->pprev会变成空么?既然PoC已经做到Kernel Panic了,答案已经很明显了,sk->sk_node->pprev并不会为空,而是 …
让我们注意下hlist_nulls_del的处理流程:
1 | // include/linux/list_null.h |
sk->sk_node->pprev被指向了LIST_POISON2
而非空;由于第二次connect AF_UNSPEC
簇的地址时,sk_hashed
会引用sk->sk_node->pprev,而其已经被指向了一个不可被引用的地址因此抛出了下面的异常
1 | Unable to handle kernel paging request at virtual address 00001360 |
到此为止,PoC的流程就已经分析完毕了,从PoC中虽然没有直接出现UAF,但是可以发现两次调用connect时可以触发两次unhash操作的,由于该操作中会有内存释放,因此,后续极有可能会出现double free
的问题;
找到UAF原语
在上面对PoC的分析过程中,可以发现Kernel Panic
的原因其实是访问到了错误的内存地址LIST_POISON2
上,实际上通过mmap
可以保护起LIST_POISON2
所指向的地址,继续运行下去,代码如下:
1 |
|
由此,第二次调用connect便可以继续运行ping_unhash的后续操作:
1 | // net/ipv4/ping.c |
终于,找到了double free 的代码位置;
至此我们有了控制一个sock struct的机会,一般情况下,会选择控制位于__sk_common
的skc_prot
中的close函数指针,当userspace中触发close调用的时候就可以触发漏洞的利用了;
这里的sock struct的劫持使用的是*phsymap spraying*
这种漏洞利用技术就是延伸自ret2dir, 由于这是我第一次接触到这种有趣的方式,后续会有专门的章节来说明;
这部分的利用代码如下:
1 | void spraying() { |
其中,fetching函数可以帮我们在众多的sock struct 中成功的找到一个可以被控制的,并将对应的physmap_page信息记录在全局变量payload中,而命中的sock将被记录在全局变量exp_sock中,这个函数在一次漏洞中是可以被反复利用的;
控制流劫持 & 完成提权
完成提取的大体流程如下(下面代码片段中的地址需要根据实际环境,经过一点点调试才能得到):
- 将
addr_limit
设置为0xFFFFFFFFFFFFFFFF
来关闭PXN的限制;这里直接调用到kernel_setsockopt即可;(关于PXN的介绍也将放在后续章节)
1 | printf("[+] Start hijacking pc as well as set addr_limit...\n"); |
- 重写selinux_enforcing来关闭SELinux;
1 | unsigned int data4 = 0; |
- 泄漏并改写task_struct->cred;
1 | // leaking code; |
- 最后做一些必要的清理工作,避免kernel crash
1 | // fd cleaning |
全部完成后,即可get_shell
1 | void get_shell() { |
以上,整个CVE-2015-3636的漏洞利用流程就梳理完成了;由于篇幅关系phsymap spraying与PXN绕过部分将放在下一篇blog中集中整理;今天要先碎觉了 …