Author: geneblue
Blog: https://geneblue.github.io/
距离上一个 binder 漏洞(水滴
CVE-2019-2025
)只过去寥寥数月,在国庆期间,Android
内核又被爆出一个 binder 内核提权漏洞。宇宙最强安全团队 google
pj0
已经给出了漏洞细节,详情可以看这里。看来
binder 模块还是值得长期关注的。这篇文章主要解释 google poc
的利用原理。
分析
先来看看 fix commit 吧:
1 | ANDROID: binder: remove waitqueue when thread exits. |
原来这个漏洞是 linux 内核 fuzz 工具 syzkaller
上报的。从
commit 看,这是一个 UAF 问题,函数 binder_thread_release()
用于释放 thread 结构数据,但当使用 epoll 对 binder fd 做 IO
复用时,该函数并没有在 release 的时候将 thread->wait
从
epoll 的内核链表中剔除。之后,进程退出或调用 epoll
清理操作时,内核会遍历删除 epoll 内核链表,这会再次访问之前已经释放的
thread 结构数据,UAF 产生。
UAF 的利用过程一般是,先确定 UAF
的结构数据是谁,如果该结构中有直接可用的函数指针,heapspray
等占位方式覆盖该指针,后期触发该函数即可控制
pc,这种情况比较少;对于有链表的情况,覆盖占位链表结构,触发时观察对链表的修改情况,再判断能否做内核读写;其他情形嘛,直接填充满非法数据比如
0xdeadc0dedeadc0de
之类的,看看内核啥反应。
POC
在开启 KASAN 的内核中,一个可触发 KASAN 的 poc 如下:
1 |
|
KASAN crash log:
1 | [ 464.504747] c0 3033 BUG: KASAN: use-after-free in remove_wait_queue+0x48/0x90 |
从 kasan 的 log 中,我们可以得到 uaf 触发时的调用链。
漏洞作者 Maddie Stone
贴心的给出一份可利用的 poc,从这份
poc
看,显然该漏洞是可以做到内核任意地址读写的。能做到任意读写,就能随意折腾内核,做过内核提权的同学,想必对接下来的套路就比较熟悉了。
POC:
1 | /* |
这份 poc 还是比较简洁的,在简单阅读完该 poc
后,我发现该漏洞做到内核读写竟然不需要一个硬编码地址,这意味着该漏洞的通用性会比较好,呃~~~,应该好好学习一下。遗憾的是这个漏洞影响范围比较小,目前已知只影响数款机型,这个原因
google 的漏洞细节里有说。从上面可以看出,void
leak_task_struct(void)
和 void
clobber_addr_limit(void)
是利用的两个关键函数。本文也主要解释这两个函数做利用的过程。
从 commit 看,struct binder_thread thread;
结构就是 UAF
的对象,该结构成员如下。
1 | struct binder_thread { |
1 处是漏洞触发成员,2 处 task 指定当前 task_struct
内存位置,如果能直接泄漏该处内存中数值,我们就能直接得到当前进程
task_struct
在内核中的位置。做数据占位时,需要知道 thread
结构大小和 1 处成员的结构偏移,这个可以在
binder_get_thread()
函数处增加一些 log 日志获取。测试环境
android-goldfish-4.4-dev
。
1 | static struct binder_thread *binder_get_thread(struct binder_proc *proc) |
结果:
1 | POC: binder_get_thread: binder_thread size: 0x198 wait offset = 0xa0 stats offset = 0xb8 task offset = 0x190 |
调用占位函数做 heapspray 时,我们指定 0x198 大小就很有可能会占据已释放的 thread 结构内存,在 0xa0 处放置的数据需要根据情形构造。
void
leak_task_struct(void)
函数 void leak_task_struct(void)
用于泄漏
task,这是如何做到的呢?
为了方便我们判断是否占位成功,我们在 static int
binder_thread_relase()
中将 UAF 的 thread
结构的内存地址打印出来
1 | static int binder_thread_release() |
void leak_task_struct(void)
采用 pipe
做占位和泄漏,关于利用 pipe TOCTTOU (time of check to time of use)
的利用方式,可以参考 链表游戏:CVE-2017-10661 之完全利用
一文,不过在这里 pipe 是用来泄漏数据的。
writev 的执行,占位过程的调用链如下:
在 rw_copy_check_uvector()
上添加调试日志
1 | ssize_t rw_copy_check_uvector() |
执行 ioctl(binder_fd, BINDER_THREAD_EXIT,
NULL);
,内核里会对应执行到 binder_thread_release()
释放 thread 结构数据;此时,调用 writev 执行
rw_copy_check_uvector()
分配的 iov 就很大概率是之前 thread
的内存。
占位的成功率还是比较高的。
接下来,执行 epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd,
&event);
触发漏洞,该函数在内核中执行链是:
我们在 remove_wait_queue()
中添加添加日志信息,观察漏洞触发前后 wait_queue_head_t
内存的变化。
1 | void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait) |
多次调试发现, iovec_array[IOVEC_INDX_FOR_WQ+1].iov_base
值在漏洞触发时总会被写为 wait->task_list.next
的内存地址。关键内存区域数据前后变化如下图所示:
漏洞触发后,iovec_array[IOVEC_INDX_FOR_WQ+1]
的
iov_base
更改为
0xffffffc0572224a8
,iov_len
依然保持
0x1000
,pipe read 的时候,将从
0xffffffc0572224a8
处开始读取 0x1000
大小的内存块,binder_thread
结构中的 *task
指针值将包含在此范围,这样就泄漏出了当前进程 task_struct
的内核地址。
void
clobber_addr_limit(void)
函数 void clobber_addr_limit(void)
用于关闭掉进程
addr_limit
,函数的关键是使用了 socket
做内存写,从函数名看,写的是 addr_limit
,熟悉提权相关的结构数据一般会知道 addr_limit
与
task_struct
在内存中是紧挨着存放的,从 thread_info
结构中可以看出:
当前进程的 thread_info
在内核栈中,所以
addr_limit
的地址是 task_struct
地址再加
0x8。
socketpair()
会创建一对连接好的 UNIX 族的全双工通信
socket,这与 pipe 返回的 fd 类似,只是任一个 fd 都可读可写的。
很明显,执行完 ioctl(binder_fd, BINDER_THREAD_EXIT,
NULL);
之后的 recvmsg()
是用来做数据填充的,占位刚刚释放的 binder_thread
内存。占位的调用链如下:
这与上述 pipe 的占位过程一致。需要注意 recvmsg()
在占位完成后会阻塞住,直到写端的数据到来。写端有数据到来时会按照传入的
iovec_array
的 iov_base
与
iov_len
顺序对应写入。
POC 中有两次写操作,分别写入 1 字节的 "X"
和
second_write_chunk
。没有漏洞的情形下,写入操作应该如下图所示:
0xDEADBEEF
和 0xBEEFDEAD
是非法地址,在漏洞触发时,这两处地址应该会被更改掉。
copy_to_iter
调用链如下:
1 | SyS_recvmsg() --> ___sys_recvmsg() --> sock_recvmsg() --> unix_stream_recvmsg() --> unix_stream_read_generic() --> unix_stream_read_actor() --> skb_copy_datagram_iter() --> copy_page_to_iter() --> copy_to_iter() |
在 copy_to_iter()
中加入调试日志:
1 | size_t copy_to_iter(void *addr, size_t bytes, struct iov_iter *i) |
多加些打印值,让我们观察前后数据变化,日志比较长,择取关键如下:
1 | ********** 0 ********* |
分析这份日志,可得出写 addr_limit
的过程。简单来说,0
处日志 recvmsg
占位刚释放的 binder_thread
内存;1 处日志将 1 字节数据写入到 0x100000000
即
dummy_page_4g_aligned
内存处,然后 recvmsg
阻塞住等待写端数据到来;2 处触发漏洞,将
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base
更改为
binder_thread
中的 task_list.next
地址
0xffffffc0571da2a8
;3 处表明从
0xffffffc0571da2a8
开始写 0x28
字节,也就是
second_write_chunk
的前 5 个数据,注意
0xffffffc0571da2a8
所在的内存地址是
0xffffffc0571da2b0
,写入过程的数据变化:
写之前:
1 | iov value 0xffffffc0571da2a8 --> ? |
写之后:
1 | iov value 0xffffffc0571da2a8 --> 0x1 |
继续往下写,原 iov 地址 0xbeefdead
,变成了
addr_limit
地址,也就是向 addr_limit
处写入
0x8 大小的数据 0xfffffffffffffffe
,这样就打开了
addr_limit
的限制,接下来可以随意访问内核。
总结
以上就是对该 binder 漏洞的简要分析。
2019-12-10 update: 增加 google pj0 和一个国外小哥对该漏洞的解读和 root 尝试参考链接 2020-02-22 update: 增加 Maddie Stone 在 OffsensiveCon 2010 会议上关于此漏洞的演讲