Parallels Desktop 使用“Parallels ToolGate”半虚拟 PCI 设备在guest操作系统和host操作系统之间进行通信。此设备由 Parallels guest中的Vendor ID 0x1AB8 和Device ID 0x4000 标识。

guest驱动程序和host虚拟设备使用 ToolGate 消息传递协议进行通信。为了提供summary,guest驱动程序准备一条消息并将消息的物理地址写入TG_PORT_SUBMIT [PORT IO ADDRESS+0x8]. 然后,host映射guest提供的物理地址,解析消息,并将控制权转移到相应的请求处理程序以进行进一步处理。Parallels 中的许多漏洞都在这些请求处理程序中。

ToolGate 请求格式是一个可变大小的结构,可以跨多个页。客户机通过将TG_PAGED_REQUEST结构的物理地址写入虚拟设备的 IO 端口,将数据作为内联字节或指向分页缓冲区的指针发送到host。

图 1 - guest内存中的可变大小 TG_PAGED_REQUEST 结构

图 2 - TG_PAGED_REQUEST 中的可变大小 TG_PAGED_BUFFER 结构

然后host映射物理地址指向的页面,根据guest提供的信息准备host缓冲区,然后调用请求处理程序。请求处理程序可以使用内联字节或分页缓冲区以读取和写入数据。这些数据缓冲区是使用一组函数访问的,大致定义如下:

TG_DataBuffer *TG_GetBuffer(TG_ReqBuffer *buf, uint32_t index, uint32 writable)` `uint32_t TG_ReadBuffer(TG_DataBuffer *buf, uint64_t offset, void *dst, size_t size)` `uint32_t TG_WriteBuffer(TG_DataBuffer *buf, uint64_t offset, void *src, size_t size)``void *TG_GetInlineBytes(TG_DataBuffer *buf)

Pwn2Own 2021的上STARLabs 提交 ( ZDI-21-937 ) 的是一个不受控的内存分配漏洞,其中在堆栈中分配了guest提供的大小值。如果guest提供的大小大于堆栈的总大小,则可以将堆栈指针 (RSP) 移入进程内存的其他区域(例如,另一个线程的堆栈区域)。

图 3 - 正常堆栈操作(左)与由于大量分配引起的堆栈跳跃(右)

处理TG_REQUEST_INVSHARING (0x8420)时,Parallels Desktop 不会验证guest提供的作为ByteCount 的值TG_PAGED_BUFFER, 在堆栈中分配时,此不受信任的大小值可用于将处理 ToolGate 请求的线程的堆栈顶部移动到另一个目标线程以覆盖其内容。有一个大小为 4KB 的保护页,但是可以跳过这个分配页面。Qualys早在 2017 年就针对这类 bug 发表了一篇名为The Stack Clash的详细论文,这也使得各种编译器增加了缓解措施,以防止这种保护页跳转。

https://www.qualys.com/2017/06/19/stack-clash/stack-clash.txt

以下是 Parallels Desktop 16.1.3 中存在漏洞的代码部分。在调用 TG_ReadBuffer()期间,另一个线程的堆栈内存可以被guest控制的值覆盖。

图 4 - TG_REQUEST_INVSHARING 中的漏洞

这里最有趣的问题是Apple Clang 的编译器是否对堆栈冲突做了缓解?当alloca(value)在堆栈中进行可变大小分配char buffer[value]时,Apple Clang 编译器会检测分配___chkstk_darwin()以验证分配请求的大小。___chkstk_darwin()本质上在堆栈中分配 PAGE_SIZE 的内存,然后,对于每个分配的页面,如果可访问,则探测新的堆栈顶部。如果到达保护页,探测步骤将失败,会导致崩溃,不再可能将堆栈指针移动到任意位置。

图 5 - 具有堆栈冲突缓解的示例代码

很明显,Parallels Desktop 没有启用此缓解措施,因为___chkstk_darwin()在可变大小堆栈分配期间没有调用。在这一点上,有几个问题:

-- Parallels 是否使用-fno-stack-check compilerflag禁用了缓解措施? -- 是不是使用的是缓解措施以前的配置?

Mac OS otool 可用于获取有关构建环境的大量信息。具体来说,LC_VERSION_MIN_MACOSX可以提供有关支持的 macOS 版本的信息。下下面是otool -l prl_vm_app输出:

图 6 - prl_vm_app 的 LC_VERSION_MIN_MACOSX 信息

使用的 SDK 是 10.15 (Catalina),这是一个很新的版本。此外,Parallels 还将支持的最低 macOS 版本设置为 10.13,使其与 High Sierra 兼容。这与他们的知识库文章中提供的兼容性信息一致。尽管如此,10.13 的向后兼容性是否会禁用编译器缓解措施?下面是-mmacosx-version-min=10.13使用和不使用编译的示例代码的比较:

图 7 - 向后兼容 10.13 禁用 ___chkstk_darwin()(右)

目前还不清楚 Parallels 是否___chkstk_darwin()明确禁用了 -fno-stack-check,但设置-mmacosx-version-min=10.13有相同的效果。在-mmacosx-version-min=10.14(Mojave) 中也观察到相同的行为。有趣的是,GCC 已经内联了堆栈探测检查,而不是依赖于外部库函数。这可能是由于外部依赖性,因为在 Apple Clang 中使用向后兼容标志(macosx-version-min、target 等)进行编译最终使缓解措施失效。

在较旧的编译器(例如 Mojave 上的 Apple LLVM 版本 10.0.1)中,除非-fstack-check明确提供标志,否则默认情况下不会启用堆栈缓解措施。因此,当macosx-version-min为低于 10.15 的任何版本编译时,最近的编译器似乎完全放弃了缓解措施。这可以通过同时提供-fstack-check和-mmacosx-version-min标志来解决。但是,High Sierra 的兼容性可能又是一个问题。重点是,macosx-version-min即使在最新版本的 Mac OS 上,单独使用也可以使漏洞可利用。

图 8 - 图 8. Apple Clang 调用 ___chkstk_darwin(左)与 GCC 缓解内联(右)

Parallels Desktop 中是否还有其他类似的漏洞?我们如何使这个过程自动化,堆栈帧的大小在编译时通常是已知的。此外,还可以跟踪移动堆栈指针的操作。Binary Ninja 具有静态数据流功能,可以跟踪函数中任何点的堆栈帧偏移量。但是,当堆栈中存在可变大小分配时,无法静态确定堆栈偏移量。这个确切的属性可用于查找其他类似漏洞。

图 9 - 图 9. 已知堆栈偏移量(左)与 alloca() 后的未确定值(右)

上述低级 IL 中的索引 88,其中加载了 RSP 寄存器。

88 @ 10080bcce rsp = r15

在这里,新的栈顶使用guest提供的大小计算并加载到 RSP 中。Binary Ninja提供get_possible_reg_values()和get_possible_reg_values_after()API来对寄存器取静态确定的值。寄存器值还与类型信息 (RegisterValueType) 相关联。这是加载操作前后 RSP 中的堆栈帧偏移值:

>>> current_function.get_low_level_il_at(here).get_possible_reg_values("rsp")

>>> current_function.get_low_level_il_at(here).get_possible_reg_values_after("rsp")

RSP 始终与StackFrameOffset RegisterValueType关联. 但是,当 RSP 值未知时,它被标记为 UndeterminedValue。使用此类型信息,搜索TG_ReadBuffer()未确定 RSP 值的所有引用。如果在调用 之前未确定 RSP,则可以推断在调用TG_ReadBuffer()之前在堆栈中进行了可变大小的分配。

>>> tg = bv.get_symbol_by_raw_name("TG_ReadBuffer")

>>> for ref in bv.get_code_refs(tg.address):

... function = ref.function

... il = function.get_low_level_il_at(ref.address)

... if il is not None:

... rsp = il.get_possible_reg_values("rsp")

... if rsp.type == RegisterValueType.UndeterminedValue:

... print(hex(ref.address))

0x1001c6e35

0x10025591a

0x10080bcd9

上面的查询产生了 3 个结果;一个是 Pwn2Own 提交和另外两个未知的漏洞。

0x1001c6e35 - ZDI-21-1056 - TG_REQUEST_GL_CREATE_CONTEXT 0x10025591a - ZDI-21-1055 - TG_REQUEST_DIRECT3D_CREATE_CONTEXT 0x10080bcd9 - ZDI-21-937 - (Pwn2Own) - TG_REQUEST_INVSHARING

图 10 - 使用 Binary Ninja 发现的 Pwn2Own 漏洞和其他同类漏洞

在某些情况下,为了向后兼容实现编译可能会默默地放弃某些缓解措施,从而使整类漏洞都可以被利用,也许供应商应该考虑在这种情况下发布单独的文件版本。

Binary Ninja 的静态数据流功能和 Python API 在自动化漏洞查找中有较好的使用。

参考及来源:https://www.zerodayinitiative.com/blog/2021/9/9/analysis-of-a-parallels-desktop-stack-clash-vulnerability-and-variant-hunting-using-binary-ninja