From Userspace(''Bug'') into iOS Kernelcache & IOKit

Prologue

之前发在内网分享下(字数超了)

原链接在这里 https://github.com/jmpews/NoteZ/issues/43

这里会分析下如何从 iOS 的 Userspace 分析到 Kernelspace 的 IOkit 的大致思路.

0. 问题简述

在使用 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texWidth, texHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureData); 创建 Texture 时, 会导致 task_info 获取到统计信息中 internal 增加, 但是 phys_footprint 并不增加.

1. PreTools

以下工具在分析的过程中起到了非常重要的作用(包括但不限于).

0. IDA
# 辅助分析 iOS KernelCache
1. https://github.com/bazad/ida_kernelcache
# 辅助分析 iOS KernelCache
2. Joker http://newosxbook.com/tools/joker.html
# IOKit Driver Class Dumper
3. iometa https://github.com/Siguza/iometa
# IOKit Runtime Dump
4. IOKitUser-1445.71.1/iodisplayregistry.c
# KernelCache 解压
5. lzssdec http://nah6.com/~itsme/cvs-xdadevtools/iphone/tools/lzssdec.cpp
# Runtime DBI(插桩) 分析
6. HookZz http://github.com/jmpews/HookZz

2. Refer Source

以下源码在分析的过程中起到了非常重要的作用(包括但不限于).

# AppleOpenSource
xnu-4570.71.2
IOKitUser-1445.71.1
IOKitTools-108
IOHIDFamily-1035.70.7
IOGraphics-519.20
# GithubSource
# macOS Display Driver with VMware
vmsvga2
# macOS Simple Display Driver
https://github.com/tSoniq/displayx

3. Analysis Process

在分析的过程, 每一个阶段, 都存在问题的确定/转化.

0. 确定 iOS Userspace 下导致 `internal` 增加, 但是 `phys_footprint` 没有增加的函数.
1. 逆向分析对应的 iOS Kernel `AGXAcceleratorG10P_B0` 内核驱动
2. 根据 `AGXAcceleratorG10P_B0` 驱动中使用 `IOBufferMemoryDescriptor` IOKit 方法. 转化为 macOS 对应的调用
3. 编写 macOS 测试驱动, 复现问题. 
4. 对 XNU 进行内核调试, 确定最终发生位置.

前置知识

本文缺少 ARM64 架构下的异常/中断机制的详细分析, 将在后续 <iOS Kernel Debugger> 详细补充.

  1. xnu 系统调用机制
  2. Mach 消息机制, 以及 MIG
  3. IOKit 机制和内存布局
  4. iOS Kernelcache 解密分析

1. xnu 系统调用机制

本质根据 IDT 做 handler 分发, 不关心的可以跳过此部分.

1.1. Legacy 传统的 int 0x80 (only for 32-bit)

xnu-4570.71.2/osfmk/x86_64/idt_table.h 可以看到.

TRAP(0x00,idt64_zero_div)
TRAP_IST1(0x01,idt64_debug)
TRAP_IST2(0x02,idt64_nmi)
USER_TRAP(0x03,idt64_int3)
USER_TRAP(0x04,idt64_into)
USER_TRAP(0x05,idt64_bounds)
TRAP(0x06,idt64_invop)
TRAP(0x07,idt64_nofpu)
TRAP_IST1(0x08,idt64_double_fault)
TRAP(0x09,idt64_fpu_over)
TRAP_ERR(0x0a,idt64_inv_tss)
TRAP_IST1(0x0b,idt64_segnp)

对于系统调用, Mach 调用.

USER_TRAP_SPC(0x80,idt64_unix_scall)
USER_TRAP_SPC(0x81,idt64_mach_scall)
USER_TRAP_SPC(0x82,idt64_mdep_scall)

对应的分发处理函数.

/*
 * Legacy interrupt gate System call handlers.
 * These are entered via a syscall interrupt. The system call number in %rax
 * is saved to the error code slot in the stack frame. We then branch to the
 * common state saving code.
 */

#ifndef UNIX_INT
#error NO UNIX INT!!!
#endif
Entry(idt64_unix_scall)
    pushq   %rax            /* save system call number */
    pushq   $(HNDL_UNIX_SCALL)
    pushq   $(UNIX_INT)
    jmp L_dispatch
    
Entry(idt64_mach_scall)
    pushq   %rax            /* save system call number */
    pushq   $(HNDL_MACH_SCALL)
    pushq   $(MACH_INT)
    jmp L_dispatch
    
Entry(idt64_mdep_scall)
    pushq   %rax            /* save system call number */
    pushq   $(HNDL_MDEP_SCALL)
    pushq   $(MACHDEP_INT)
    jmp L_dispatch

此时的 x86_64_intr_stack_frame 状态为 trapfn == %rax. (Cpu在处理中断会存在部分寄存器自动压栈, 具体参考 6.4.1 Call and Return Operation for Interrupt or Exception Handling Procedures)

struct x86_64_intr_stack_frame {
    uint16_t    trapno;
    uint16_t    cpu;
    uint32_t    _pad;
    uint64_t    trapfn;
    uint64_t    err;
    uint64_t    rip;
    uint64_t    cs;
    uint64_t    rflags;
    uint64_t    rsp;
    uint64_t    ss;
};
typedef struct x86_64_intr_stack_frame x86_64_intr_stack_frame_t;

int 0x80 后的处理流程

L_dispatch
-> *idt64_hndl_table0
-> ks_dispatch
-> ks_dispatch_user
-> L_dispatch_U32
-> L_common_dispatch
-> *(idt64_hndl_table1 + 8*(trapfn = HNDL_UNIX_SCALL))
-> hndl_unix_scall

1.2. syscall 快速系统调用 (only for x86_64)

# xnu-4570.71.2/osfmk/i386/mp_desc.c

/*
 * Set MSRs for sysenter/sysexit and syscall/sysret for 64-bit.
 */
void
cpu_syscall_init(cpu_data_t *cdp)
{
    ...
    wrmsr64(MSR_IA32_LSTAR, DBLMAP((uintptr_t) hi64_syscall));
    ...
}
Entry(hi64_syscall)
Entry(idt64_syscall)
    swapgs
     /* Use RAX as a temporary by shifting its contents into R11[32:63]
      * The systemcall number is defined to be a 32-bit quantity, as is
      * RFLAGS.
      */
    shlq    $32, %rax
    or  %rax, %r11
.globl EXT(dblsyscall_patch_point)
EXT(dblsyscall_patch_point):
//  movabsq $0x12345678ABCDEFFFULL, %rax
     /* Generate offset to the double-mapped per-CPU data shadow
      * into RAX
      */
    leaq    EXT(idt64_hndl_table0)(%rip), %rax
    mov 16(%rax), %rax
    mov     %rsp, %gs:CPU_UBER_TMP(%rax)  /* save user stack */
    mov     %gs:CPU_ESTACK(%rax), %rsp  /* switch stack to per-cpu estack */
    sub $(ISF64_SIZE), %rsp

    /*
     * Synthesize an ISF frame on the exception stack
     */
    movl    $(USER_DS), ISF64_SS(%rsp)
    mov %rcx, ISF64_RIP(%rsp)       /* rip */

    mov %gs:CPU_UBER_TMP(%rax), %rcx
    mov %rcx, ISF64_RSP(%rsp)       /* user stack --changed */

    mov %r11, %rax
    shrq    $32, %rax       /* Restore RAX */
    mov %r11d, %r11d        /* Clear r11[32:63] */

    mov %r11, ISF64_RFLAGS(%rsp)    /* rflags */
    movl    $(SYSCALL_CS), ISF64_CS(%rsp)   /* cs - a pseudo-segment */
    mov %rax, ISF64_ERR(%rsp)       /* err/rax - syscall code */
    movq    $(HNDL_SYSCALL), ISF64_TRAPFN(%rsp)
    movq    $(T_SYSCALL), ISF64_TRAPNO(%rsp)    /* trapno */
    swapgs
    jmp L_dispatch          /* this can only be 64-bit */

syscall 之后的处理流程

L_dispatch
-> *idt64_hndl_table0
-> ks_dispatch
-> ks_dispatch_user
-> L_dispatch_64bit
-> L_common_dispatch
-> *(idt64_hndl_table1 + 8*(trapfn = HNDL_SYSCALL))
-> hndl_unix_scall

hndl_syscall 中根据 rax 判断 syscall 类型, 例如正常系统调用会在 hndl_unix_scall64 处进行跳转执行.

Entry(hndl_syscall)
    TIME_TRAP_UENTRY

    movq    %gs:CPU_ACTIVE_THREAD,%rcx  /* get current thread     */
    movl    $-1, TH_IOTIER_OVERRIDE(%rcx)   /* Reset IO tier override to -1 before handling syscall */
    movq    TH_TASK(%rcx),%rbx      /* point to current task  */

    /* Check for active vtimers in the current task */
    TASK_VTIMER_CHECK(%rbx,%rcx)

    /*
     * We can be here either for a mach, unix machdep or diag syscall,
     * as indicated by the syscall class:
     */
    movl    R64_RAX(%r15), %eax     /* syscall number/class */
    movl    %eax, %edx
    andl    $(SYSCALL_CLASS_MASK), %edx /* syscall class */
    cmpl    $(SYSCALL_CLASS_MACH<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_mach_scall64)
    cmpl    $(SYSCALL_CLASS_UNIX<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_unix_scall64)
    cmpl    $(SYSCALL_CLASS_MDEP<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_mdep_scall64)
    cmpl    $(SYSCALL_CLASS_DIAG<<SYSCALL_CLASS_SHIFT), %edx
    je  EXT(hndl_diag_scall64)

    /* Syscall class unknown */
    sti
    CCALL3(i386_exception, $(EXC_SYSCALL), %rax, $1)
    /* no return */


Entry(hndl_unix_scall64)
    incl    TH_SYSCALLS_UNIX(%rcx)      /* increment call count   */
    sti

    CCALL1(unix_syscall64, %r15)
    /*
     * always returns through thread_exception_return
     */

1.3. userspace 空间的 vm_fault

* thread #96, name = '0xffffff8026811650', queue = 'cpu-1', stop reason = breakpoint 3.1
  * frame #0: 0xffffff801dd6d5ee kernel`pmap_enter_options(pmap=0xffffff80269de250, vaddr=<unavailable>, pn=<unavailable>, prot=3, fault_type=<unavailable>, flags=<unavailable>, wired=<unavailable>, options=<unavailable>, arg=<unavailable>) at pmap_x86_common.c:986 [opt]
    frame #1: 0xffffff801dcec990 kernel`vm_fault_enter(m=0xffffff80215f0750, pmap=0xffffff80269de250, vaddr=4353159168, prot=3, caller_prot=<unavailable>, wired=0, change_wiring=<unavailable>, wire_tag=<unavailable>, no_cache=<unavailable>, cs_bypass=<unavailable>, user_tag=-2139079024, pmap_options=<unavailable>, need_retry=<unavailable>, type_of_fault=<unavailable>) at vm_fault.c:3279 [opt]
    frame #2: 0xffffff801dcee19b kernel`vm_fault_internal(map=0xffffff802918b700, vaddr=4353159168, caller_prot=3, change_wiring=<unavailable>, wire_tag=0, interruptible=<unavailable>, caller_pmap=<unavailable>, caller_pmap_addr=<unavailable>, physpage_p=<unavailable>) at vm_fault.c:0 [opt]
    frame #3: 0xffffff801dd891a2 kernel`user_trap [inlined] vm_fault(map=<unavailable>, fault_type=<unavailable>, change_wiring=0, wire_tag=0, interruptible=2, caller_pmap_addr=0) at vm_fault.c:3416 [opt]
    frame #4: 0xffffff801dd8917f kernel`user_trap(saved_state=0xffffff8025ad2460) at trap.c:1118 [opt]
    frame #5: 0xffffff801dc1d0a5 kernel`hndl_alltraps + 229

2. Mach 消息机制

所有的 mach 消息, 都要经过 mach_msg.

可以通过 MIG(Mach Interface Generator).defs 生成 mach 接口. 例如:

xcrun -sdk iphoneos mig \
  -arch arm64 \
  -DIOKIT \
  -I/xxx/macOS_10.13.6/xnu-4570.71.2/osfmk \
  iokitmig.defs

这里以 IOConnectCallMethod 举例.

Usermode
---
IOConnectCallMethod => io_connect_method => mach_msg() =>

Kernelmode
---
_Xio_connect_method => is_io_connect_method

3. IOKit 机制和内存布局

xnu 里的驱动是基于 libkern 的 c++ IO Kit 框架开发, 但只是一个 c++ 子集.

所以可以通过获取驱动类的继承关系和虚表关系帮助分析 iOS Kernelcache 中的各种无符号的驱动.

例如 AGXAcceleratorG10P_B0, 这也是下文要分析的驱动:

vtab=0xfffffff006fe2b68 size=0x00001998 meta=0xfffffff007866850 parent=0xfffffff0078666b0 AGXAcceleratorG10P_B0 (com.apple.AGXG10P)
       0x0 func=0xfffffff006d6e264 overrides=0xfffffff006d67308 pac=0x0000 AGXAcceleratorG10P_B0::fn_0x0()
       0x8 func=0xfffffff006d6e268 overrides=0xfffffff006d6730c pac=0x0000 AGXAcceleratorG10P_B0::~AGXAcceleratorG10P_B0()
      0x38 func=0xfffffff006d6e280 overrides=0xfffffff006d67324 pac=0x0000 AGXAcceleratorG10P_B0::getMetaClass() const
     0x2a8 func=0xfffffff006d6e28c overrides=0xfffffff006d67330 pac=0x0000 AGXAcceleratorG10P_B0::start()
     0x2b0 func=0xfffffff006d6e350 overrides=0xfffffff006d67494 pac=0x0000 AGXAcceleratorG10P_B0::stop()
     0x370 func=0xfffffff006d6e388 overrides=0xfffffff006d674cc pac=0x0000 AGXAcceleratorG10P_B0::getWorkLoop() const
     0x548 func=0xfffffff006d6e390 overrides=0xfffffff006d675d8 pac=0x0000 AGXAcceleratorG10P_B0::fn_0x548()
     0x710 func=0xfffffff006d6e398 overrides=0xfffffff006d6779c pac=0x0000 AGXAcceleratorG10P_B0::fn_0x710()
    ...
vtab=0xfffffff006fe0068 size=0x00001998 meta=0xfffffff0078666b0 parent=0xfffffff007866540 AGXAcceleratorG10 (com.apple.AGXG10P)
       0x0 func=0xfffffff006d67308 overrides=0xfffffff006d3648c pac=0x0000 AGXAcceleratorG10::fn_0x0()
       0x8 func=0xfffffff006d6730c overrides=0xfffffff006d36490 pac=0x0000 AGXAcceleratorG10::~AGXAcceleratorG10()
      0x38 func=0xfffffff006d67324 overrides=0xfffffff006d36494 pac=0x0000 AGXAcceleratorG10::getMetaClass() const
     0x140 func=0xfffffff006d454c0 overrides=0xfffffff007564e08 pac=0x0000 AGXAcceleratorG10::copyProperty() const
     0x158 func=0xfffffff006d4644c overrides=0xfffffff007564f9c pac=0x0000 AGXAcceleratorG10::setProperties()
     0x270 func=0xfffffff006d48198 overrides=0xfffffff00758b464 pac=0x0000 AGXAcceleratorG10::systemWillShutdown()
     0x280 func=0xfffffff006d481d4 overrides=0xfffffff00756a538 pac=0x0000 AGXAcceleratorG10::configureReport()
     0x288 func=0xfffffff006d481f0 overrides=0xfffffff00756a76c pac=0x0000 AGXAcceleratorG10::updateReport()
     0x2a0 func=0xfffffff006d57ba4 overrides=0xfffffff00756ac30 pac=0x0000 AGXAcceleratorG10::probe()
     0x2a8 func=0xfffffff006d67330 overrides=0xfffffff006d366ec pac=0x0000 AGXAcceleratorG10::start()
     0x2b0 func=0xfffffff006d67494 overrides=0xfffffff006d37a0c pac=0x0000 AGXAcceleratorG10::stop()
     0x370 func=0xfffffff006d674cc overrides=0xfffffff006d3adc4 pac=0x0000 AGXAcceleratorG10::getWorkLoop() const
     0x3a0 func=0xfffffff006d674d4 overrides=0xfffffff00756c31c pac=0x0000 AGXAcceleratorG10::callPlatformFunction()
     0x4d0 func=0xfffffff006d4ada0 overrides=0xfffffff00758b2e4 pac=0x0000 AGXAcceleratorG10::setPowerState()
     0x548 func=0xfffffff006d675d8 overrides=0xfffffff006d3adcc pac=0x0000 AGXAcceleratorG10::fn_0x548()
    ...
vtab=0x0000000000000000 size=0x00001990 meta=0xfffffff007866540 parent=0xfffffff007866358 AGXFamilyAccelerator (com.apple.AGXG10P)
vtab=0x0000000000000000 size=0x00001990 meta=0xfffffff007866358 parent=0xfffffff0078658a8 AGXAccelerator (com.apple.AGXG10P)
vtab=0xfffffff006fda6a8 size=0x00000c78 meta=0xfffffff0078658a8 parent=0xfffffff007865880 IOGraphicsAccelerator2 (com.apple.iokit.IOAcceleratorFamily2)
       0x0 func=0xfffffff006d3648c overrides=0xfffffff006d363b0 pac=0x0000 IOGraphicsAccelerator2::fn_0x0()
       0x8 func=0xfffffff006d36490 overrides=0xfffffff006d363b4 pac=0x0000 IOGraphicsAccelerator2::~IOGraphicsAccelerator2()
      0x38 func=0xfffffff006d36494 overrides=0xfffffff006d363cc pac=0x0000 IOGraphicsAccelerator2::getMetaClass() const
      0x68 func=0xfffffff006d37f74 overrides=0xfffffff007569c4c pac=0x0000 IOGraphicsAccelerator2::free()
     0x2a8 func=0xfffffff006d366ec overrides=0xfffffff00756ac34 pac=0x0000 IOGraphicsAccelerator2::start()
     0x2b0 func=0xfffffff006d37a0c overrides=0xfffffff00756ac3c pac=0x0000 IOGraphicsAccelerator2::stop()
     0x370 func=0xfffffff006d3adc4 overrides=0xfffffff00756c120 pac=0x0000 IOGraphicsAccelerator2::getWorkLoop() const
     0x418 func=0xfffffff006d3a444 overrides=0xfffffff00756c998 pac=0x0000 IOGraphicsAccelerator2::message()
     0x460 func=0xfffffff006d38560 overrides=0xfffffff00756cf88 pac=0x0000 IOGraphicsAccelerator2::newUserClient()
     0x538 func=0xfffffff006d3a550 overrides=0x0000000000000000 pac=0x0000 IOGraphicsAccelerator2::fn_0x538()
    ...

3.1. Connnect(Open) Driver(Service)

将在分析 AGXAcceleratorG10P_B0 时用到.

Kernelspace
---
_Xio_service_open_extended => is_io_service_open_extended =>
---
provider_service->newUserClient() => userClient_instantiate_class->initWithTask()

Userspace
---
io_service_t _io_get_service(const char *name) {
  static io_service_t service = MACH_PORT_NULL;
  if (service == MACH_PORT_NULL) {
    DLOG("Getting IO service handle...");
    service = _IOServiceGetMatchingService(get_io_master_port(), _IOServiceMatching(name));
    if (!MACH_PORT_VALID(service)) {
      FATAL("Failed to get IO service handle (port = 0x%08x)", service);
    }
  }
  return service;
}

io_service_t platformExpertDevice = _io_get_service("IOPlatformExpertDevice");
io_connect_t connect;
kr = _IOServiceOpen(platformExpertDevice, mach_task_self(), 0, &connect);
---
-> io_service_open_extended => mach_msg

3.2. Invoke driver methods

将在分析 AGXAcceleratorG10P_B0 时用到.

Usermode
---
IOConnectCallMethod => io_connect_method => mach_msg() =>

Kernelmode
---
_Xio_connect_method => is_io_connect_method =>
---
client->externalMethod => dispatch_by_selector
(lldb) bt
* thread #6, name = '0xffffff8013341520', queue = 'cpu-1', stop reason = breakpoint 1.1
  * frame #0: 0xffffff800bf3b8d6 kernel`mach_make_memory_entry_64(target_map=<unavailable>, size=0xffffff886e9d3790, offset=0, permission=409603, object_handle=0xffffff8017546c28, parent_handle=<unavailable>) at vm_user.c:2339 [opt]
    frame #1: 0xffffff800c4b52a9 kernel`IOGeneralMemoryDescriptor::memoryReferenceCreate(this=<unavailable>, options=0, reference=0xffffff8017546c28) at IOMemoryDescriptor.cpp:611 [opt]
    frame #2: 0xffffff800c4b3651 kernel`IOGeneralMemoryDescriptor::initWithOptions(this=<unavailable>, buffers=0x0000000000000001, count=1, offset=310765616, task=<unavailable>, options=<unavailable>, mapper=0x0000000000000000) at IOMemoryDescriptor.cpp:1709 [opt]
    frame #3: 0xffffff800c4a9d58 kernel`IOBufferMemoryDescriptor::initWithPhysicalMask(this=0xffffff8017546c00, inTask=<unavailable>, options=<unavailable>, capacity=2048000, alignment=<unavailable>, physicalMask=<unavailable>) at IOBufferMemoryDescriptor.cpp:289 [opt]
    frame #4: 0xffffff800c4aa9b7 kernel`IOBufferMemoryDescriptor::inTaskWithOptions(inTask=0x0000000000000000, options=65635, capacity=0x00000000001f4000, alignment=0x0000000000001000) at IOBufferMemoryDescriptor.cpp:339 [opt]
    frame #5: 0xffffff7f8e3344ac IOKitDemoDriver`SharedMemoryAlloc(buffer=0xffffff886e9d3a28, options=65635, size=2048000) at IOKitDemoDriver.cpp:87
    frame #6: 0xffffff7f8e334595 IOKitDemoDriver`IOKitDemoDriver::userClientMap(this=0xffffff8012b77360, owningTask=0xffffff8014b4dc28, map=0xffffff8012f93610, mapSize=0xffffff886e9d3a80) at IOKitDemoDriver.cpp:112
    frame #7: 0xffffff7f8e334aac IOKitDemoDriver`IOKitDemoDriverUserClient::userClientMap(target=0xffffff801745c000, reference=0x0000000000000000, args=0xffffff886e9d3b80) at IOKitDemoDriverUserClient.cc:53
    frame #8: 0xffffff800c4cb428 kernel`IOUserClient::externalMethod(this=<unavailable>, selector=<unavailable>, args=0xffffff886e9d3b80, dispatch=0xffffff7f8e336438, target=0xffffff801745c000, reference=0x0000000000000000) at IOUserClient.cpp:5289 [opt]
    frame #9: 0xffffff7f8e334b66 IOKitDemoDriver`IOKitDemoDriverUserClient::externalMethod(this=0xffffff801745c000, selector=1, arguments=0xffffff886e9d3b80, dispatch=0xffffff7f8e336438, target=0xffffff801745c000, reference=0x0000000000000000) at IOKitDemoDriverUserClient.cc:74
    frame #10: 0xffffff800c4d4237 kernel`::is_io_connect_method(connection=0xffffff801745c000, selector=1, scalar_input=<unavailable>, scalar_inputCnt=<unavailable>, inband_input=<unavailable>, inband_inputCnt=0, ool_input=<unavailable>, ool_input_size=<unavailable>, inband_output=<unavailable>, inband_outputCnt=<unavailable>, scalar_output=<unavailable>, scalar_outputCnt=<unavailable>, ool_output=<unavailable>, ool_output_size=<unavailable>) at IOUserClient.cpp:3943 [opt]
    frame #11: 0xffffff800bf44514 kernel`_Xio_connect_method(InHeadP=<unavailable>, OutHeadP=0xffffff8012f935e0) at device_server.c:8376 [opt]
    frame #12: 0xffffff800be70d9e kernel`ipc_kobject_server(request=0xffffff8017555380, option=<unavailable>) at ipc_kobject.c:351 [opt]
    frame #13: 0xffffff800be4dc2d kernel`ipc_kmsg_send(kmsg=0xffffff8017555380, option=3, send_timeout=0) at ipc_kmsg.c:1861 [opt]
    frame #14: 0xffffff800be60d1b kernel`mach_msg_overwrite_trap(args=<unavailable>) at mach_msg.c:570 [opt]
    frame #15: 0xffffff800bf7388d kernel`mach_call_munger64(state=0xffffff80144e1640) at bsd_i386.c:573 [opt]
    frame #16: 0xffffff800be1d996 kernel`hndl_mach_scall64 + 22

4. iOS(10+) Kernelcache 解密分析

这里以 iPhone8 - 11.4.1 为例子.

4.1. 解密 iOS Kernelcache

1. download `iPhone_4.7_P3_11.0_11.4.1_15G77_Restore.ipsw` from `https://ipsw.me/`
2. wget http://nah6.com/~itsme/cvs-xdadevtools/iphone/tools/lzssdec.cpp
3. clang++ lzssdec.cpp -o lzssdec
4. check the offset which lzssdec needed
[0] % hexdump -C -n 600 /Users/jmpews/Downloads/kernelcache.release.iphone10
00000000  30 83 fd 63 59 16 04 49  4d 34 50 16 04 6b 72 6e  |0..cY..IM4P..krn|
00000010  6c 16 1c 4b 65 72 6e 65  6c 43 61 63 68 65 42 75  |l..KernelCacheBu|
00000020  69 6c 64 65 72 2d 31 33  34 38 2e 37 30 2e 31 04  |ilder-1348.70.1.|
00000030  83 fd 63 2a 63 6f 6d 70  6c 7a 73 73 28 31 d7 4d  |..c*complzss(1.M|
00000040  02 02 40 00 00 fd 61 aa  00 00 00 01 00 00 00 00  |..@...a.........|
00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000001b0  00 00 00 00 ff cf fa ed  fe 0c 00 00 01 d5 00 f6  |................|
000001c0  f0 02 f6 f0 15 f6 f0 70  0f 5a f3 f1 20 f6 f1 00  |.......p.Z.. ...|
000001d0  19 f6 f0 38 f5 f0 3f 5f  5f 54 45 58 54 09 02 1c  |...8..?__TEXT...|
5. ./lzssdec -o 0x1b4 < kernelcache.release.iphone10 > kernelcache.release.iphone10.decrypt

4.2. 获取 iOS dyld_shared_cache

cp /Volumes/xxx/System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64 /xxx/xxx

4.3 分析 iOS Pirvate Framework

可以通过 Users/jmpews/Library/Developer/Xcode/iOS DeviceSupport/11.4.1 (15G77)/Symbols/System/Library/Extensions/AGXMetalA11.bundle/ 获取到 AGXMetalA11 由于 shared cache 但会存在大量未知地址引用, 所以如需更进一步的具体分析请使用 IDA 分析 dyld_shared_cache_arm64, 尽量避免完全分析, 只要分析对应 Framework 以及其依赖即可.

5. XNU Kernel 调试

这里的 XNU Kernel 调试并没有使用 Apple 提供的 KDP, 而是基于 VMware + LLDB 的调试, 可以参考我的上篇文章 From UEFI Reversing to Xnu Kernel Debug, 这里贴一下 lldb command script.

#Help lldb figure out we're debugging x86_64
settings set plugin.process.gdb-remote.target-definition-file ~/.lldb/x86_64_target_definition.py

#Use a reasonable disassembly syntax
settings set target.x86-disassembly-flavor intel

#Tell load any lldb scripts and macros hidden inside .dSYM files
settings set target.load-script-from-symbol-file true

#Tell lldb where the source directory really is
settings set target.source-map  /BuildRoot/Library/Caches/com.apple.xbs/Sources/xnu/xnu-4570.20.62 /Users/jmpews/Downloads/xnu-4570.20.62

#This should get loaded automatically when we set the target executable
#command script import "/Library/Developer/KDKs/KDK_10.13.1_17B48.kdk/System/Library/Kernels/kernel.debug.dSYM/Contents/Resources/Python/lldbmacros/xnu.py"

#This does not appear to get loaded automatically, so we load it here.
#command script import "/Library/Developer/KDKs/KDK_10.13.1_17B48.kdk/System/Library/Kernels/kernel.debug.dSYM/Contents/Resources/Python/lldbmacros/memory.py"

# Load the kernel binary we're going to be debugging.
target create /Library/Developer/KDKs/KDK_10.13.1_17B48.kdk/System/Library/Kernels/kernel.debug

6. HookZz Runtime DBI 分析

HookZz 是本人编写的 hook framework, 在分析过程中, 为了确定了 Userspace 下问题问题的发生点, 使用了 HookZz 大量进行了插桩分析.

ZzWrap API 允许对任何函数添加 pre_callpost_call, 可以非常方便确定某一个函数是否是导致问题发生的点.

ZzDynamicBinaryInstrument API 允许对任意指令地址增加 dbi_call, 可以非常方便确定某一个函数内的哪一个区间导致问题发生的点.

分析思路

1.1 vm_fault 导致 internal+compressed 增加

首先判断问题的发生位置:

1). Userspace 下的内存分配 malloc / vm_allocate / mmap(这里其实并不用考虑, 因为 userspace 下在进行分配的时其 vm_object 一定统计在自身的 task->map)
2). IOKit 驱动内部内存分配

对于 1) 这里直接 Inlinehook malloc / vm_allocate / mmap, 并没有发现异常点. 接着怀疑是调用了驱动方法, 直接断点 mach_msg. 可以发现确实调用了 mach 系统调用.

image-20181224105434695

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 9.1
  * frame #0: 0x0000000184b9fc18 libsystem_kernel.dylib`mach_msg
    frame #1: 0x0000000185407bb8 IOKit`io_connect_method + 416
    frame #2: 0x00000001853a4038 IOKit`IOConnectCallMethod + 232
    frame #3: 0x0000000186f399c0 IOAccelerator`IOAccelResourceCreate + 148
    frame #4: 0x0000000186f75228 Metal`-[MTLIOAccelResource initWithDevice:options:args:argsSize:] + 264
    frame #5: 0x0000000186f6ad48 Metal`-[MTLIOAccelTexture initWithDevice:descriptor:sysMemSize:sysMemRowBytes:vidMemSize:vidMemRowBytes:args:argsSize:] + 176
    frame #6: 0x00000001ad604360 AGXMetalA11`___lldb_unnamed_symbol1181$$AGXMetalA11 + 580
    frame #7: 0x00000001a56ef41c AppleMetalGLRenderer`GLDTextureRec::allocMetalTexture() + 176
    frame #8: 0x00000001a56ef58c AppleMetalGLRenderer`GLDTextureRec::loadPrivateTexture(unsigned int, unsigned short*) + 56
    frame #9: 0x00000001a56ed8e8 AppleMetalGLRenderer`GLDTextureRec::update(unsigned int, unsigned short*) + 168
    frame #10: 0x00000001a56ed7f4 AppleMetalGLRenderer`GLDTextureRec::load() + 96
    frame #11: 0x00000001a56fe1f4 AppleMetalGLRenderer`gldModifyTexSubImage + 88
    frame #12: 0x00000001a6467328 GLEngine`glTexImage2D_Exec + 1752
    frame #13: 0x0000000102941934 libglInterpose.dylib`tex_image2D(__GLIContextRec*, unsigned int, int, unsigned int, int, int, int, unsigned int, unsigned int, void const*) + 388
    frame #14: 0x000000018814c738 OpenGLES`glTexImage2D + 108

这里需要判断是否确实这个 mach 调用, 导致的 internal 增加, 但 footprint 不增加, 尝试使用 HookZzZzWrapIOAccelResourceCreate 进行 PreCallPostCall 的 trace.

void common_pre_call(RegisterContext *reg_ctx, const HookEntryInfo *info)
{
  NSByteCountFormatter *formatter            = [[NSByteCountFormatter alloc] init];
  formatter.countStyle = NSByteCountFormatterCountStyleMemory;
  MemoryInfo *mem_info = memoryFootprint();
  NSLog(@"PRE==:footprint: %@, internal+compressed: %@, virtual:%@",
        [formatter stringFromByteCount:mem_info.footprint],
        [formatter stringFromByteCount:mem_info.internal + mem_info.compressed],
        [formatter stringFromByteCount:mem_info.virtual_size]);
  printf("virtual:%d\n", mem_info.virtual_size);
}

void common_post_call(RegisterContext *reg_ctx, const HookEntryInfo *info)
{
  NSByteCountFormatter *formatter            = [[NSByteCountFormatter alloc] init];
  formatter.countStyle = NSByteCountFormatterCountStyleMemory;
  MemoryInfo *mem_info = memoryFootprint();
  NSLog(@"POST==:footprint: %@, internal+compressed: %@, virtual:%@",
        [formatter stringFromByteCount:mem_info.footprint],
        [formatter stringFromByteCount:mem_info.internal + mem_info.compressed],
        [formatter stringFromByteCount:mem_info.virtual_size]);
  
  printf("virtual:%d\n", mem_info.virtual_size);
}

ZzWrap((void *)0x1a46b619c, common_pre_call, common_post_call);

但是发现 internal+compressed 没有任何增加, 那是不是确实跟这个 mach 系统调用无关呢? 接下来继续通过 Dynamic Binary Instrumentation (DBI) 来确定到底是哪个函数导致了 internal+compressed 的增加.

如下是 -[MTLIOAccelResource initWithDevice:options:args:argsSize:] 的实现, 请注意图中的标注点.

image-20190218124507242

最终在 dyld_shared_cache_arm64AGXMetalA11 如下函数确定导致 internal+compressed 的增加, 但是这段函数没有任何调用, 并且推测是一段内存拷贝的代码. 所以这里推测是 vm_fault 导致.

__int64 __fastcall sub_1A93455C8(__int64 result, __int64 a2, int a3, int a4, int a5, int a6, __int64 a7, unsigned int a8, unsigned int a9, unsigned int a10, unsigned int a11)
{
    ...
      do
      {
        v39 = 0;
        v40 = v38 + 4LL * a8;
        v41 = v26;
        do
        {
          v42 = result + 4 * (v41 + v33);
          v43 = *(v40 + 16);
          v45 = *(v40 + a7);
          v46 = *(v40 + a7 + 16);
          v44 = (v40 + a7 + a7);
          v48 = *v44;
          v49 = v44[1];
          v47 = (v44 + a7);
          v50 = *v47;
          v51 = v47[1];
          v52 = vzip2q_s32(*v40, v45);
          *v42 = vzip1q_s32(*v40, v45);
          *(v42 + 16) = vzip1q_s32(v48, v50);
          *(v42 + 32) = v52;
          *(v42 + 48) = vzip2q_s32(v48, v50);
          v42 += 128LL;
          *v42 = vzip1q_s32(v43, v46);
          *(v42 + 16) = vzip1q_s32(v49, v51);
          *(v42 + 32) = vzip2q_s32(v43, v46);
          *(v42 + 48) = vzip2q_s32(v49, v51);
          v41 = (v41 - v12) & v12;
          v40 += 32LL;
          v39 += 8;
        }
        while ( v39 < a10 );
        v33 = (v33 - v19) & v19;
        v38 += 4 * a7;
        v37 += 4;
      }
      while ( v37 < a11 );
    ...
}

1.2. 内核驱动定位

这里先确定要分析的驱动.

kern_return_t
(*orig_IOServiceOpen)(
                   io_service_t    service,
                   task_port_t  owningTask,
                   uint32_t  type,
                   io_connect_t  *  connect );

kern_return_t
fake_IOServiceOpen(
              io_service_t    service,
              task_port_t  owningTask,
              uint32_t  type,
              io_connect_t  *  connect ) {
  
  kern_return_t kr;
  kr = orig_IOServiceOpen(service, owningTask, type, connect);
  io_name_t name;
  io_string_t path;
  
  kr = IORegistryEntryGetName(service, name);
  kr = IORegistryEntryGetPath(service, "IOService", path );
  if (kIOReturnSuccess != kr) {
    FATAL("open %s (0x%x)\n", mach_error_string(kr), kr);
  }
  CFMutableDictionaryRef properties = NULL;
  kr = _IORegistryEntryCreateCFProperties(service, &properties, kCFAllocatorDefault, kNilOptions);
  NSLog(@"%@", properties);
  printf(">>> service: %d, connect: %d, type: %d, name %s, path: %s\n", service, *connect, type, name, path);
  return kr;
}

// IOServiceOpen
void *IOServiceOpen_addr = dlsym(dlopen(0, RTLD_NOW), "IOServiceOpen");
ZzReplace((void *)IOServiceOpen_addr, (void *)fake_IOServiceOpen, (void **)&orig_IOServiceOpen);

通过 Hook 获取到期间使用到的驱动 AGXAcceleratorG10P_B0.

2018-12-24 10:52:04.671007+0800 ios_memory_usage_demo[4289:2499148] {
    AGXParameterBufferMaxSize = 201326592;
    AGXParameterBufferMaxSizeEverMemless = 134217728;
    AGXParameterBufferMaxSizeNeverMemless = 67108864;
    IOClass = "AGXAcceleratorG10P_B0";
    IOGLESBundleName = AppleMetalGLRenderer;
    InternalStatistics =     {
        "Allocated PB Size" = 1048576;
        agpTextureCreationCount = 0;
        agprefTextureCreationCount = 0;
        bufferSwapCount = 0;
        clientGLWaitTime = 0;
        dataBufferCount = 0;
        dataBytesPerSample = 0;
        freeDataBufferWaitTime = 0;
        gartCacheBytes = 33554432;
        gartFreeBytes = 1361772544;
        gartSizeBytes = 1393639424;
        hardwareSubmitWaitTime = 0;
        inUseSysMemoryBytes = 0;
        iosurfaceTextureCreationCount = 0;
        oolTextureCreationCount = 0;
        orphanedNonReusableSysMemoryBytes = 0;
        orphanedNonReusableSysMemoryCount = 0;
        orphanedReusableSysMemoryBytes = 0;
        orphanedReusableSysMemoryCount = 0;
        orphanedReusableSysMemoryHitRate = 0;
        stdTextureCreationCount = 0;
    };
    InternalStatisticsAccm =     {
        "Allocated PB Size" = 1048576;
        agpTextureCreationCount = 0;
        agprefTextureCreationCount = 0;
        bufferSwapCount = 0;
        clientGLWaitTime = 0;
        dataBufferCount = 0;
        dataBytesPerSample = 0;
        freeDataBufferWaitTime = 0;
        gartCacheBytes = 33554432;
        gartFreeBytes = 1361772544;
        gartSizeBytes = 1393639424;
        hardwareSubmitWaitTime = 0;
        inUseSysMemoryBytes = 0;
        iosurfaceTextureCreationCount = 0;
        oolTextureCreationCount = 0;
        orphanedNonReusableSysMemoryBytes = 0;
        orphanedNonReusableSysMemoryCount = 0;
        orphanedReusableSysMemoryBytes = 0;
        orphanedReusableSysMemoryCount = 0;
        orphanedReusableSysMemoryHitRate = 0;
        stdTextureCreationCount = 0;
    };
    MetalPluginClassName = AGXA11Device;
    MetalPluginName = AGXMetalA11;
    MetalStatisticsName = AGXMetalStatisticsExternalA11;
    PerformanceStatistics =     {
        CommandBufferRenderCount = 0;
        "Device Utilization %" = 0;
        "Renderer Utilization %" = 0;
        SplitSceneCount = 0;
        TiledSceneBytes = 0;
        "Tiler Utilization %" = 0;
        agpTextureCreationBytes = 0;
        agprefTextureCreationBytes = 0;
        contextGLCount = 0;
        finishGLWaitTime = 0;
        freeToAllocGPUAddressWaitTime = 0;
        gartMapInBytesPerSample = 0;
        gartMapOutBytesPerSample = 0;
        gartUsedBytes = 31866880;
        hardwareWaitTime = 0;
        iosurfaceTextureCreationBytes = 0;
        oolTextureCreationBytes = 0;
        recoveryCount = 0;
        stdTextureCreationBytes = 0;
        textureCount = 873;
    };
    PerformanceStatisticsAccum =     {
        CommandBufferRenderCount = 0;
        "Device Utilization %" = 0;
        "Renderer Utilization %" = 0;
        SplitSceneCount = 0;
        TiledSceneBytes = 0;
        "Tiler Utilization %" = 0;
        agpTextureCreationBytes = 0;
        agprefTextureCreationBytes = 0;
        contextGLCount = 0;
        finishGLWaitTime = 0;
        freeToAllocGPUAddressWaitTime = 0;
        gartMapInBytesPerSample = 0;
        gartMapOutBytesPerSample = 0;
        gartUsedBytes = 31866880;
        hardwareWaitTime = 0;
        iosurfaceTextureCreationBytes = 0;
        oolTextureCreationBytes = 0;
        recoveryCount = 0;
        stdTextureCreationBytes = 0;
        textureCount = 873;
    };
}
>>> service: 9219, connect: 39939, type: 2, name AGXAcceleratorG10P_B0, path: IOService:/AppleARMPE/arm-io@8F00000/AppleT8015IO/sgx@4000000/AGXAcceleratorG10P_B0

通过 hook IOConnectCallMethod 发现, 调用的是 selector == 0 的驱动方法

connnection: 8719, selector: 0, input: 0x0, inputCnt: 0,inputStruct: 0x16ba34cf0, inputStructCnt: 96, outputStruct: 0x16ba34b30, outputStructCnt: 72 

接下来需要定位 selector = 0 对应的内核驱动方法实现. 通过使用 iometa -n -bmosv ./kernelcache.release.iphone10.decrypt > IOKitDump.mosv dump 到 IOKit 所有类的关系和 vtable 信息.

通过分析 AGXAcceleratorG10P_B0 继承于 IOGraphicsAccelerator2, 并使用 0xfffffff006d38560 IOGraphicsAccelerator2::newUserClient() 在 Connnect Servevice 时创建 UserClient.

vtab=0xfffffff006fda6a8 size=0x00000c78 meta=0xfffffff0078658a8 parent=0xfffffff007865880 IOGraphicsAccelerator2 (com.apple.iokit.IOAcceleratorFamily2)
     0x460 func=0xfffffff006d38560 overrides=0xfffffff00756cf88 pac=0x0000 IOGraphicsAccelerator2::newUserClient()
signed __int64 __fastcall sub_FFFFFFF006D38560(__int64 a1, __int64 owningTask, __int64 security_id, __int64 type, IOUserClient **handler)
{
... ignore

  switch ( (_DWORD)type_1 )
  {
    case 1:
      v14 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v8 + 0x690LL))(v8);
      v15 = v14;
      if ( v14 )
      {
        if ( sub_FFFFFFF006D23004(v14, 0LL, owningTask_1) & 1 )
          goto LABEL_17;
        goto LABEL_22;
      }
      break;
    case 2:
      v16 = (*(__int64 (__fastcall **)(__int64))(*(_QWORD *)v8 + 0x688LL))(v8);
      v15 = v16;
      if ( v16 )
      {
        if ( sub_FFFFFFF006D30AE4(v16, 0LL, owningTask_1) & 1 )
          goto LABEL_17;
        goto LABEL_22;
      }
      break;
    case 3:

... ignore
}

由于之前 IOServiceOpen 是以 type = 2 打开的服务, 根据 IDA 里的伪代码, 调用 vtable 里 offset == 0x688 的虚方法, 这里

vtab=0xfffffff006fe0068 size=0x00001998 meta=0xfffffff0078666b0 parent=0xfffffff007866540 AGXAcceleratorG10 (com.apple.AGXG10P)
     0x688 func=0xfffffff006d4bf74 overrides=0xfffffff006d38994 pac=0x0000 AGXAcceleratorG10::fn_0x688()
_QWORD *sub_FFFFFFF006D4BF74()
{
  _QWORD *v0; // x19

  v0 = (_QWORD *)OSObject::operator new(336LL);
  sub_FFFFFFF006D7ABD4();
  *v0 = &off_FFFFFFF006FE41F8;
  OSMetaClass::instanceConstructed(&AGXSharedUserClient::gMetaClass);
  return v0;
}

直接找 AGXSharedUserClientexternalMethod 看看怎么实现的 dispatch.

vtab=0xfffffff006fe41f8 size=0x00000150 meta=0xfffffff007866a98 parent=0xfffffff0078657b8 AGXSharedUserClient (com.apple.AGXG10P)
     0x538 func=0xfffffff006d768f8 overrides=0xfffffff006d32098 pac=0x0000 AGXSharedUserClient::externalMethod()

image-20190215174015762

可以看到之后分发给了父类 IOAccelSharedUserClient2, 接着跟踪, 可以看到 IOAccelSharedUserClient2externalMethod 则是直接转交给 IOUserClientexternalMethod 进行处理.

vtab=0xfffffff006fd9228 size=0x00000150 meta=0xfffffff0078657b8 parent=0xfffffff007646dc8 IOAccelSharedUserClient2 (com.apple.iokit.IOAcceleratorFamily2)
     0x538 func=0xfffffff006d32098 overrides=0xfffffff0075c033c pac=0x0000 IOAccelSharedUserClient2::externalMethod()

image-20190215174101534

这里分两种情况分析 1. w1 == #0 2. w1 != #0, 这里 w1 == selector

1). w1 == #0

经过 CSEL X3, X8, X3, EQ, 会将参数 dipatch 设置为 off_FFFFFFF006FD9A38, 经过 IOUserClient::externalMethod 此时 dipatch == off_FFFFFFF006FD9A38, target == AGXSharedUserClient.

2). w1 != #0

那继续分析 getTargetAndMethodForIndex, 因为在 IOUserClient::externalMethod 会调用到 getTargetAndMethodForIndex 进行 dispatch.

分析 AGXSharedUserClient::getTargetAndMethodForIndex, w2 也就是 selector = #unknown, 直接转发给父类 IOAccelSharedUserClient2::getTargetAndMethodForIndex

vtab=0xfffffff006fe41f8 size=0x00000150 meta=0xfffffff007866a98 parent=0xfffffff0078657b8 AGXSharedUserClient (com.apple.AGXG10P)
     0x5a8 func=0xfffffff006d7691c overrides=0xfffffff006d320bc pac=0x0000 AGXSharedUserClient::getTargetAndMethodForIndex()

image-20190215174129306

通过分析 IOAccelSharedUserClient2::getTargetAndMethodForIndex 发现通过取得 [x0,#0xF0] 中的 IOExternalMethod ** 基址, 之后通过 UMADDL X0, W2, W9, X8 取得对应 selector = #unknown 偏移的 IOExternalMethod *. 究竟 [X0,#0xF0] 里的地址到底指向哪里, 这里可以找找相关的方法实现.

vtab=0xfffffff006fd9228 size=0x00000150 meta=0xfffffff0078657b8 parent=0xfffffff007646dc8 IOAccelSharedUserClient2 (com.apple.iokit.IOAcceleratorFamily2)
     0x5a8 func=0xfffffff006d320bc overrides=0xfffffff0075c1824 pac=0x0000 IOAccelSharedUserClient2::getTargetAndMethodForIndex()

image-20190215174153332

通过对 AGXSharedUserClient 和它父类 IOAccelSharedUserClient2 部分方法进行查看, 最后发现在 0xfffffff006d30e28 初始化了 [X0,#0xF0]qword_FFFFFFF006FD9888.

vtab=0xfffffff006fd9228 size=0x00000150 meta=0xfffffff0078657b8 parent=0xfffffff007646dc8 IOAccelSharedUserClient2 (com.apple.iokit.IOAcceleratorFamily2)
     0x5c8 func=0xfffffff006d30e28 overrides=0x0000000000000000 pac=0x0000 IOAccelSharedUserClient2::fn_0x5c8()

image-20190215174239451

这里因为 selector == 0 所以这里直接分析 sub_FFFFFFF006D31FB4 即可

但是由于 iOS Kernel 里很多驱动的数据结构是未知的, 分析难度很大.

1.3. 内核驱动函数分析.

这里将部分符号化的驱动处理函数. 由于缺乏 runtime 的分析, 通过上面的静态分析, 这里也只能分析出使用到 AGXResource 等未知内存分布的内核类. 以及会在内核层使用 IOBufferMemoryDescriptor::createMappingInTask 创建内存 mapping, 并将 VirtualAddress 作为 OutputStruct 的第二个成员返回给 Userspace, 这个和我们在 userspace 看到的行为是一样的.

image-20181226105955410

image-20181226110021617

image-20181226110057803

1.4. IOMemoryDescriptor 调用分析

通过查找一些 IOKit 资料/驱动, 在 IOKit 中会大量使用 IOBufferMemoryDescriptor 进行内存页分配,

image-20190215152930676

image-20190215152914466

所以这里对 IOBufferMemoryDescriptor 特别关注下, 这里看下 IOBufferMemoryDescriptor::inTaskWithOptionsoptions

image-20190215153807119

options = input_1->x1.x_1_1_2 | 0x10063; 其中 0x10063 == kIOMemoryKernelUserShared | kIODirectionInOut | kIOMemoryPageable | kIOMemoryPurgeable, 这里通过对比一些 IOKit 驱动, 例如 VMsvga2 等, 发现这个参数仅在 Texture 绘制时出现, 那么是否和这个参数有关, 需要写一个 DemoDriver 测试下, 这里讲 iOS Kernel IOKit 的问题转化为 macOS Kernel IOKit 的问题.

1.5. 利用 DemoDriver 复现问题

// ===== FrameBufffer Test Case =====

IOReturn SharedMemoryAlloc(IOBufferMemoryDescriptor **buffer, IOOptionBits options, unsigned size) {
  IOKIT_AUTO_LOG_FLAG();

  *buffer = IOBufferMemoryDescriptor::inTaskWithOptions(0, options, size, PAGE_SIZE);
  if (!*buffer) {
    IOLog("failed to allocate buffer memory");
    return kIOReturnNoMemory;
  } else {
    return kIOReturnSuccess;
  }
}

IOReturn IOKitDemoDriver::userClientMap(task_t owningTask, DisplayXMap *map, uint32_t *mapSize) {
  IOKIT_AUTO_LOG_FLAG();
  IOReturn kr = kIOReturnSuccess;

  if (!map || !mapSize || *mapSize != sizeof *map)
    kr = kIOReturnBadArgument;

  FrameBuffferEmu *frameBuffer     = new FrameBuffferEmu;
  IOBufferMemoryDescriptor *buffer = frameBuffer->buffer;
  IOOptionBits options;
#if 0
  options = kIOMemoryKernelUserShared | kIODirectionInOut | kIOMemoryPageable | kIOMemoryPurgeable;
#elif 1
  options = kIOMemoryKernelUserShared | kIODirectionInOut | kIOMemoryPageable;
#endif

  kr = SharedMemoryAlloc(&buffer, options, PAGE_SIZE * 500);

#if 0
#elif 1
  options = kIOMapAnywhere;
#endif

  IOMemoryMap *iomap = buffer->createMappingInTask(owningTask, 0, options, 0, 0);

  map->address = iomap->getVirtualAddress();
  return kr;
}

加载刚才写的 IOKitDemoDriver.kext

sudo rm -rf IOKitDemoDriver.kext
sudo chown -R root:wheel IOKitDemoDriver.kext
sudo kextload IOKitDemoDriver.kext
kernel-exploitdeMac:Desktop kernel_exploit$ sudo chown -R root:wheel IOKitDemoDriver.kext
kernel-exploitdeMac:Desktop kernel_exploit$ sudo kextload IOKitDemoDriver.kext

这是增加了这个 flag 之后的调用结果, 很明显, 我们浮现了这个问题.


kernel-exploitdeMac:Desktop kernel_exploit$ ./IOKitDemoClient
[*] IOKitDemoDriver Open Success, Version: 1.1, Task: 0xffffff80123997e8
2019-02-15 00:03:31.787 IOKitDemoClient:footprint: 712 KB, internal: 712 KB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x111984000
2019-02-15 00:03:31.789 IOKitDemoClient:footprint: 3 MB, internal: 4.9 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.789 IOKitDemoClient:footprint: 3 MB, internal: 4.9 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x111b78000
2019-02-15 00:03:31.791 IOKitDemoClient:footprint: 3 MB, internal: 6.9 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.791 IOKitDemoClient:footprint: 3 MB, internal: 6.9 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x111d6c000
2019-02-15 00:03:31.793 IOKitDemoClient:footprint: 3 MB, internal: 8.8 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.793 IOKitDemoClient:footprint: 3 MB, internal: 8.8 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x111f60000
2019-02-15 00:03:31.794 IOKitDemoClient:footprint: 3 MB, internal: 10.8 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.795 IOKitDemoClient:footprint: 3 MB, internal: 10.8 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x112154000
2019-02-15 00:03:31.798 IOKitDemoClient:footprint: 3 MB, internal: 12.7 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.798 IOKitDemoClient:footprint: 3 MB, internal: 12.7 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x112348000
2019-02-15 00:03:31.800 IOKitDemoClient:footprint: 3 MB, internal: 14.7 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.800 IOKitDemoClient:footprint: 3 MB, internal: 14.7 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x11253c000
2019-02-15 00:03:31.802 IOKitDemoClient:footprint: 3 MB, internal: 16.6 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.802 IOKitDemoClient:footprint: 3 MB, internal: 16.6 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x112730000
2019-02-15 00:03:31.804 IOKitDemoClient:footprint: 3 MB, internal: 18.6 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.804 IOKitDemoClient:footprint: 3 MB, internal: 18.6 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x112924000
2019-02-15 00:03:31.806 IOKitDemoClient:footprint: 3 MB, internal: 20.6 MB, purges: 328, purgeable_count:5196
2019-02-15 00:03:31.806 IOKitDemoClient:footprint: 3 MB, internal: 20.6 MB, purges: 328, purgeable_count:5196
[*] userClientMap Address: 0x112b18000
2019-02-15 00:03:31.808 IOKitDemoClient:footprint: 3 MB, internal: 22.5 MB, purges: 328, purgeable_count:5196

以下是移除该 flag 后的调用结果, phys_footprintinternal 相同.

kernel-exploitdeMac:Desktop kernel_exploit$ ./IOKitDemoClient
[*] IOKitDemoDriver Open Success, Version: 1.1, Task: 0xffffff801122c7e8
2019-02-14 23:57:29.417 IOKitDemoClient:footprint: 712 KB, internal: 712 KB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x110584000
2019-02-14 23:57:29.422 IOKitDemoClient:footprint: 4.9 MB, internal: 4.9 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.422 IOKitDemoClient:footprint: 4.9 MB, internal: 4.9 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x110778000
2019-02-14 23:57:29.424 IOKitDemoClient:footprint: 6.9 MB, internal: 6.9 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.425 IOKitDemoClient:footprint: 6.9 MB, internal: 6.9 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x11096c000
2019-02-14 23:57:29.427 IOKitDemoClient:footprint: 8.8 MB, internal: 8.8 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.427 IOKitDemoClient:footprint: 8.8 MB, internal: 8.8 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x110b60000
2019-02-14 23:57:29.428 IOKitDemoClient:footprint: 10.8 MB, internal: 10.8 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.428 IOKitDemoClient:footprint: 10.8 MB, internal: 10.8 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x110d54000
2019-02-14 23:57:29.430 IOKitDemoClient:footprint: 12.8 MB, internal: 12.8 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.430 IOKitDemoClient:footprint: 12.8 MB, internal: 12.8 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x110f48000
2019-02-14 23:57:29.432 IOKitDemoClient:footprint: 14.7 MB, internal: 14.7 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.433 IOKitDemoClient:footprint: 14.7 MB, internal: 14.7 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x11113c000
2019-02-14 23:57:29.434 IOKitDemoClient:footprint: 16.7 MB, internal: 16.7 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.434 IOKitDemoClient:footprint: 16.7 MB, internal: 16.7 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x111330000
2019-02-14 23:57:29.436 IOKitDemoClient:footprint: 18.6 MB, internal: 18.6 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.436 IOKitDemoClient:footprint: 18.6 MB, internal: 18.6 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x111524000
2019-02-14 23:57:29.439 IOKitDemoClient:footprint: 20.6 MB, internal: 20.6 MB, purges: 328, purgeable_count:5180
2019-02-14 23:57:29.439 IOKitDemoClient:footprint: 20.6 MB, internal: 20.6 MB, purges: 328, purgeable_count:5180
[*] userClientMap Address: 0x111718000
2019-02-14 23:57:29.441 IOKitDemoClient:footprint: 22.5 MB, internal: 22.5 MB, purges: 328, purgeable_count:5180

1.6. kIOMemoryPurgeable 作用

对着 xnu-4570.20.62 一顿操作, 发现当存在 kIOMemoryPurgeable 时, 会设置 object->vo_purgeable_owner = ownerkernel_task, 但是这是否就是因为这个愿意导致 phys_footprint 不增加的原因呢, 这里在手动调试下正常情况下 phys_footprint 增加的原因.

image-20190215171509611

image-20190215171558703

1.7. 内核硬断调试

如果没有 kIOMemoryPurgeable, 进程 task 的 phys_footprint 为何增加呢?

这里通过获取进程 task_t task, 对应的保存 phys_footprintle_credit, 并对设置硬件写断点. (注意: lldb 内核调试的时, 硬断会失败, 具体原因由于时间原因尚未深究, 这里可以换成 GDB 或者 IDA remote GDB Debugger 两者是一样的)

p/x &(&(((task_t)0xffffff800fab09b0)->ledger->l_entries[task_ledgers.phys_footprint]))->le_credit

image-20190215162804978

获取到保存 phys_footprintle_credit 后, 在 IDA 进行硬断.

image-20190215164721344

调试器挂载

image-20190215164926945

触发硬断后, 这里直接根据堆栈和调用规约, 根据图中的标注找到 caller. (不可以单步走)

image-20190215170600442

__text:FFFFFF8012606C73 mov     rdi, [r12+168h]                 ; ledger
__text:FFFFFF8012606C7B mov     esi, cs:task_ledgers.iokit_mapped ; entry
__text:FFFFFF8012606C81 mov     rdx, rbx                        ; amount
__text:FFFFFF8012606C84 call    ledger_credit
__text:FFFFFF8012606C89 mov     rdi, [r12+168h]                 ; ledger
__text:FFFFFF8012606C91 mov     esi, cs:task_ledgers.phys_footprint ; entry
__text:FFFFFF8012606C97 mov     rdx, rbx                        ; amount
__text:FFFFFF8012606C9A call    ledger_credit
__text:FFFFFF8012606C9F mov     r9, [rbp+hint_offset]
__text:FFFFFF8012606CA6 mov     r10, qword ptr [rbp+var_68]
__text:FFFFFF8012606CAA mov     r12, [rbp+object]
__text:FFFFFF8012606CB1 test    r15d, r15d
__text:FFFFFF8012606CB4 jz      loc_FFFFFF8012606D8F

通过 lldb 看下对应的源码位置.

(lldb) b 0xFFFFFF8012606C9F
Breakpoint 2: where = kernel.development`vm_map_enter + 5407 [inlined] vm_map_iokit_mapped_region + 48 at vm_map.c:2737, address = 0xffffff8012606c9f

在 xnu 源码里发现实现位置.

image-20190215171026369

image-20190215171041444

1.8. kIOMemoryPurgeable 的对比

最终通过这里的不同点, 确定了问题的发生位置.

image-20190215171956900

1.9. 总结

当调用 glTexImage2D 函数时, iOS 的 AGXAcceleratorG10P_B0 驱动, 会调用 IOBufferMemoryDescriptor::inTaskWithOptions 其中 options0x10063 == kIOMemoryKernelUserShared | kIODirectionInOut | kIOMemoryPageable | kIOMemoryPurgeable, 由于 options 中包含 kIOMemoryPurgeable flag, 导致 object->vo_purgeable_owner 被设置为 kernel_task, 因而当发生 #PFvm_fault 时, 进入 vm_map_enter 函数进行统计的时, 没有将该内存统计给对应进程的 task_t task, 而是统计给 kernel_task.

这里有一篇文章讲解了 为什么 Texture 下要使用 purgeable memory. https://www.khronos.org/registry/OpenGL/extensions/APPLE/APPLE_object_purgeable.txt

Rerfer

https://www.khronos.org/registry/OpenGL/extensions/APPLE/APPLE_object_purgeable.txt

https://brightiup.me/2018/06/11/AppleHV-Use-After-Free-CVE-2018-4242-Writeup/CVE-2018-4242.pdf

7 个赞

厉害呀,最近的沙盒内提权漏洞都在驱动和MIG里,收藏,以后再慢慢看