聚焦零点击漏洞利用
作者:Natalie Silvanovich,Project Zero
Zoom 是一款在疫情期间广受欢迎的视频会议平台。与我调查过的其他视频会议系统不同(那些系统需要由一名用户发起呼叫,其他用户必须立即接受或拒绝),Zoom 通话通常是提前安排并通过电子邮件邀请加入的。过去,我并未优先审查 Zoom,因为我认为针对 Zoom 客户端的任何攻击都需要用户多次点击。然而,最近在 Pwn2Own 上披露了针对 Windows Zoom 客户端的零点击攻击,表明它确实存在完全远程的攻击面。以下文章详细记录了我对 Zoom 的调查。
此次分析导致两个漏洞被报告给 Zoom。一个是影响 Zoom 客户端和 MMR 服务器的缓冲区溢出漏洞,另一个是仅对 MMR 服务器上的攻击者有用的信息泄露漏洞。这两个漏洞已于 2021 年 11 月 24 日修复。
Zoom 攻击面概述
Zoom 的主要功能是支持多用户会议通话(称为会议),具备音频、视频、屏幕共享和通话内文本消息等多种功能。用户可以通过多种方式加入 Zoom 会议。首先,Zoom 为许多平台(包括 Windows、Mac、Linux、Android 和 iPhone)提供了功能齐全的可安装客户端。用户也可以使用浏览器链接加入 Zoom 会议,但能使用的 Zoom 功能较少。最后,用户可以通过在按键式电话上拨打邀请中提供的电话号码来加入会议,但这只允许访问会议的音频流。本研究主要关注 Zoom 客户端软件,因为其他加入通话的方式使用的是设备现有功能。
除了会议之外,Zoom 客户端还支持用户 Zoom 联系人可用的其他几种通信功能。Zoom 联系人是用户通过 Zoom 用户界面添加为联系人的另一个用户。双方用户必须同意才能成为 Zoom 联系人。之后,用户可以在会议之外相互发送文本消息,并开启频道进行持续的群组对话。此外,如果任一用户主持会议,他们可以邀请另一用户加入,方式类似于电话呼叫:另一用户会立即收到通知,只需点击一下即可加入会议。这些功能代表了 Zoom 的零点击攻击面。请注意,此攻击面仅对已说服目标接受其为联系人的攻击者可用。同样,会议仅对 Zoom 联系人构成一键攻击面的一部分,因为其他用户需要多次点击才能进入会议。
尽管如此,对于一个执着的攻击者来说,说服目标加入 Zoom 通话(即使需要多次点击)可能并不那么困难,而且一些组织使用 Zoom 的方式呈现出有趣的攻击场景。例如,许多团体举办公共 Zoom 会议,Zoom 支持付费的 Webinar 功能,允许大量未知与会者加入单向视频会议。攻击者有可能加入公共会议并针对其他与会者。Zoom 还依赖服务器传输音频和视频流,并且默认情况下不启用端到端加密。攻击者有可能入侵 Zoom 的服务器并获取会议数据的访问权限。
Zoom 消息
我首先查看了 Zoom 的零点击攻击面。将 Linux 客户端加载到 IDA 中,似乎其大部分服务器通信是通过 XMPP 进行的。根据二进制文件中的字符串,可以清楚地看出 XMPP 解析是使用名为 gloox 的库执行的。我使用 AFL 和其他覆盖引导的模糊测试工具对这个库进行了模糊测试,但没有发现任何漏洞。然后我查看了 Zoom 如何使用通过 XMPP 提供的数据。
XMPP 流量似乎是通过 SSL 发送的,因此我根据日志字符串在二进制文件中定位了 SSL_write 函数,并使用 Frida 进行挂钩。输出包含许多 XMPP 节(消息)以及其他网络流量,我对其进行了分析以确定 Zoom 如何使用 XMPP。XMPP 用于 Zoom 客户端之间在会议之外的大部分通信,例如消息和频道,也用于当 Zoom 联系人邀请另一个 Zoom 联系人参加会议时的信令(呼叫建立)。
我花了一些时间浏览客户端二进制文件,试图确定客户端如何处理 XMPP,例如,如果一个节包含文本消息,该消息如何被提取并显示在客户端中。尽管 Zoom 客户端包含许多日志字符串,但这仍然具有挑战性,最终我请我的队友 Ned Williamson 帮助定位客户端的符号。他发现 Android Zoom SDK 的几个旧版本包含符号。虽然这些版本大约有五年历史,并且不能完整呈现客户端(因为它们只包含客户端使用的一些库),但它们对于理解 Zoom 如何使用 XMPP 非常有帮助。
可以通过扩展类 StanzaExtension 并实现方法 newInstance 来定义如何将标签转换为 C++ 对象,从而将应用程序定义的标签添加到 gloox 的 XMPP 解析器中。然后使用 MessageHandler 类处理解析后的 XMPP 节。应用程序开发人员扩展此类,实现 handleMessage 方法,其中包含基于接收到的节内容执行应用程序功能的代码。Zoom 在 CXmppIMSession::handleMessage 中实现了其 XMPP 处理,这是一个大型函数,是大多数消息传递和呼叫功能的入口点。许多 XMPP 标签的最终处理阶段在类 ns_zoom_messager::CZoomMMXmppWrapper 中,该类包含许多以 'On' 开头的方法,用于处理特定事件。我花了相当多的时间分析这些代码路径,但没有发现任何漏洞。有趣的是,在我完成这项研究后,Thijs Alkemade 和 Daan Keuper 发布了他们 Pwn2Own 漏洞的技术文章,其中涉及该区域的漏洞。
RTP 处理
之后,我调查了 Zoom 客户端如何处理音频和视频内容。与我分析过的所有其他视频会议系统一样,它使用实时传输协议(RTP)来传输这些数据。根据 Linux 客户端二进制文件中包含的日志字符串,Zoom 似乎使用 WebRTC 的一个分支来处理音频。由于我在之前的文章中已经大量研究过这个库,因此没有进一步调查。对于视频,Zoom 实现了自己的 RTP 处理,并使用名为 Zealot(libzlt)的自定义底层编解码器。
在 IDA 中分析 Linux 客户端时,我找到了我认为是视频 RTP 入口点的位置,并使用 afl-qemu 对其进行了模糊测试。这导致了多次崩溃,主要是在 RTP 扩展处理中。我尝试修改客户端发送的 RTP 来复现这些漏洞,但另一端的设备没有接收到,我怀疑服务器对其进行了过滤。我尝试通过启用端到端加密来绕过此问题,但 Zoom 不加密 RTP 头部,只加密 RTP 数据包的内容(这是大多数 RTP 实现的典型做法)。
出于对 Zoom 服务器过滤工作原理的好奇,我决定设置 Zoom On-Premises Deployment。这是一个 Zoom 产品,允许客户设置本地服务器来处理其组织的 Zoom 通话。这需要相当多的配置,最终我联系了 Zoom 安全团队寻求帮助。他们帮助我使其正常工作,我非常感谢他们对这项研究的贡献。
Zoom On-Premises Deployments 由两个主机组成:控制器和多媒体路由器(MMR)。分析到每个服务器的流量后,很明显 MMR 是在 Zoom 客户端之间传输音频和视频内容的主机。将 MMR 进程的代码加载到 IDA 中,我定位了 RTP 处理的位置,它确实将扩展作为其转发逻辑的一部分进行解析并正确验证,丢弃任何格式错误的 RTP 数据包。
MMR 上处理 RTP 的代码似乎与我之前在设备上模糊测试的代码不同,因此我也在服务器代码上设置了模糊测试。这具有挑战性,因为代码位于 MMR 二进制文件中,该二进制文件未编译为可重定位二进制文件(稍后会详细说明)。这意味着我无法像通常对没有源代码的二进制文件进行模糊测试那样,将其作为库加载并调用二进制文件中的特定偏移量。相反,我编译了自己的模糊测试存根,将我想要模糊测试的函数作为定义了 fopen 的可重定位文件调用,并在执行 MMR 二进制文件时使用 LD_PRELOAD 加载它。然后,我的代码将在 MMR 二进制文件首次调用 fopen 时控制执行,并能够调用被模糊测试的函数。
这种方法有很多缺点,最大的缺点是模糊测试存根不能接受命令行参数,执行速度相当慢,而且许多模糊测试工具不尊重目标上的 LD_PRELOAD。尽管如此,我还是能够使用 Mateusz Jurczyk 出色的 DrSanCov 进行代码覆盖模糊测试,但没有结果。
数据包处理
在分析 RTP 流量时,我注意到 Zoom 客户端和 MMR 服务器都处理大量似乎不是 RTP 或 XMPP 的数据包。查看带有符号的 SDK,有一个库似乎做了大量的序列化:libssb_sdk.so。该库包含大量类,这些类定义了具有相同声明的 load_from 和 save_to 方法,因此它们很可能都实现了相同的虚类。
load_from 方法的一个参数是类 msg_db_t 的对象,它实现了一个支持读取不同数据类型的缓冲区。反序列化由 load_from 方法通过从 msg_db_t 对象读取所需数据来执行,序列化由 save_to 方法通过向其写入来执行。
使用 Frida 挂钩几个 save_to 方法并将写入的输出与通过 SSL_write 发送的数据进行比较后,很明显这些序列化类是 Zoom 远程攻击面的一部分。检查每个 load_from 方法,有几个包含类似于以下代码(来自 ssb::conf_send_msg_req::load_from)。
ssb::i_stream_tssb::msg_db_t,ssb::bytes_convertor::operator>>(
msg_db, &this->str_len, consume_bytes, error_out);
str_len = this->str_len;
if ( str_len )
{
out_len = 0;
this->str_mem = mem;
ssb::i_stream_tssb::msg_db_t,ssb::bytes_convertor::
read_str_with_len(msg_db, mem, &out_len);
read_str_with_len 定义如下。
int __fastcall ssb::i_stream_tssb::msg_db_t,ssb::bytes_convertor::
read_str_with_len(msg_db_t* msg, signed __int8 *mem,
unsigned int *len)
{
if ( !msg->invalid )
{
ssb::i_stream_tssb::msg_db_t,ssb::bytes_convertor::operator>>(msg, len, (int)len, 0);
if ( !msg->invalid )
{
if ( *len )
ssb::i_stream_tssb::msg_db_t,ssb::bytes_convertor::
read(msg, mem, *len, 0);
}
}
return msg;
}
请注意,字符串缓冲区是根据从 msg_db_t 缓冲区读取的长度分配的,但随后从缓冲区读取第二个长度并将其用作读取字符串的长度。这意味着,如果攻击者能够操纵 msg_db_t 缓冲区的内容,他们可以指定分配的缓冲区长度,并用任意长度的数据覆盖它(最多 0x1FFF 字节,上面的代码片段中未显示)。
我通过使用 Frida 挂钩 SSL_write 并发送格式错误的数据包来测试此漏洞,它导致 Zoom 客户端在多种平台上崩溃。此漏洞被分配为 CVE-2021-34423,并于 2021 年 11 月 24 日修复。
查看 MMR 服务器的代码,我注意到出现漏洞的类 ssb::conf_send_msg_req::load_from 也存在于 MMR 服务器上。由于 MMR 将 Zoom 会议流量从一个客户端转发到另一个客户端,因此它可能也会反序列化此数据包类型,这是合理的。我在 IDA 中分析了 MMR 代码,发现此类的反序列化仅在 Zoom Webinars 期间发生。我购买了 Zoom Webinar 许可证,并通过发送此数据包成功使自己的 Zoom MMR 服务器崩溃。我不愿意在 Zoom 的公共 MMR 服务器上测试此类漏洞,但相同的代码很可能也存在于 Zoom 的公共服务器中。
进一步查看反序列化,我注意到所有反序列化的对象都包含一个类型为 ssb::dyna_para_table_t 的可选字段,这基本上是一个属性表,允许将名称字符串到变体对象的映射包含在反序列化的对象中。表中的变体由结构体 ssb::variant_t 实现,如下所示。
struct variant{
char type;
short length;
var_data data;
};
union var_data{
char i8;
char* i8_ptr;
short i16;
short* i16_ptr;
int i32;
int* i32_ptr;
long long i64;
long long i64*;
};
type 字段的值对应于变体数据的宽度(1 表示 8 位,2 表示 16 位,3 表示 32 位,4 表示 64 位)。length 字段指定变体是否为数组及其长度。如果其值为 0,则变体不是数组,并根据其类型从 data 字段读取数值。如果 length 字段有任何其他值,则将 data 字段强制转换为指针,并读取该大小的数组。
我对这种实现的直接担忧是它可能容易出现类型混淆。一种可能性是数值可能与数组指针混淆,这将允许攻击者创建具有他们指定指针的变体。然而,客户端和 MMR 都对它们视为数组的变体执行非常积极的类型检查。另一种可能性是指针可能与数值混淆。如果该值曾经返回给攻击者,这可能允许攻击者确定他们控制的缓冲区的地址。我在 MMR 代码中发现了几个位置,其中指针以这种方式转换为数值并记录,但没有攻击者可以获取错误转换值的地方。最后,我查看了如何处理数组数据,发现有几个位置将字节数组变体转换为字符串,但并非所有位置都检查字节数组是否具有空终止符。这意味着如果这些变体被转换为字符串,该字符串可能包含未初始化内存的内容。
大多数情况下,一个用户发送到 MMR 的数据包会立即转发给其他用户,而无需服务器反序列化。对于某些漏洞来说,这是一个有用的功能,例如,正是这一点允许前面讨论的 CVE-2021-34423 在客户端上触发。然而,变体中的信息泄露需要在服务器上发生才能对攻击者有用。当客户端反序列化传入的数据包时,它是供设备使用的,因此即使反序列化的字符串包含敏感信息,这些信息也不太可能从设备传输出去。与此同时,MMR 的存在明确是为了将信息从一个用户传输到另一个用户,因此如果字符串被反序列化,它很有可能被发送给另一个用户,或以可观察的方式改变服务器行为。所以,我试图找到一种方法让服务器反序列化一个变体并将其转换为字符串。我最终发现,当用户在浏览器中登录 Zoom 时,浏览器无法处理序列化的数据包,因此 MMR 必须将它们转换为字符串,以便可以通过 Web 请求访问它们。确实,我发现如果我从 user_name 变体中移除空终止符,它将被转换为字符串并作为用户的显示名称发送给浏览器。
该漏洞被分配为 CVE-2021-34424,并于 2021 年 11 月 24 日修复。我在自己的 MMR 以及 Zoom 的公共 MMR 上对其进行了测试,它在两种情况下都有效并返回了指针数据。
漏洞利用尝试
我尝试利用这些漏洞攻击我的本地 MMR 服务器,虽然我在漏洞利用的某些部分取得了成功,但未能使其完全工作。我首先调查了创建一个可以在 Zoom 客户端之外触发每个漏洞的客户端的可能性,但客户端身份验证似乎很复杂,并且我缺乏这部分代码的符号,因此我没有继续研究,因为我怀疑这将非常耗时。相反,我通过从使用 Frida 挂钩的 Linux Zoom 客户端触发漏洞来分析漏洞的可利用性。
我首先调查了堆损坏对 MMR 进程的影响。MMR 服务器运行在 CentOS 7 上,它使用现代的 glibc 堆,因此利用堆解链似乎没有希望。我转而研究覆盖堆上分配的 C++ 对象的 vtable。
我编写了几个在服务器上挂钩 malloc 的 Frida 脚本,并用它们来监控传入流量如何影响内存分配。事实证明,攻击者没有太多有用的方法来控制 MMR 服务器上的内存分配以利用此漏洞。攻击者可以向服务器发送几种数据包类型,导致在堆上分配内存,然后在处理完成后释放,但攻击者可以同时触发分配和释放的情况并不多。此外,MMR 服务器在不同的线程中执行不同类型的处理,这些线程使用唯一的堆区域,因此许多可能发生此类分配的代码区域(例如连接管理)在与漏洞发生线程不同的堆区域中分配内存。我能找到的唯一在同一区域中进行的此类分配与会议设置相关:当用户加入会议时,某些对象会在堆上分配,然后在用户离开会议时释放。不幸的是,这些分配很难自动化,因为它们需要许多唯一的用户帐户才能重复执行分配,并且分配需要可观的时间(几秒钟)。
最终,我编写了 Frida 脚本,在正常 MMR 操作期间查找与具有 vtable 的 C++ 对象相邻的异常大小的空闲块。有几种分配大小符合此标准,并且由于 CVE-2021-34423 允许攻击者指定被溢出的缓冲区大小,我能够损坏相邻对象的内存。不幸的是,堆验证非常健壮,因此在大多数情况下,MMR 进程会在对损坏的对象进行虚拟调用之前因堆验证错误而崩溃。最终,我通过关注足够小以被堆存储在 fastbins 中的分配大小来绕过此问题,因为存储在 fastbins 中的堆块不包含可验证的堆元数据。大小为 58 的块被证明是最佳选择,通过以该大小触发漏洞,我大约每触发十次漏洞就能控制一次虚拟调用的指针。
下一步是弄清楚将我能控制的指针指向何处,这比预期的更具挑战性。在我进行这项研究时,MMR 进程没有启用 ASLR(在 2021 年 11 月 28 日发布的版本 4.6.20211128.136 中启用了),因此我希望在二进制文件中找到一系列位置,可以将此调用定向到这些位置,最终以具有可控参数的 execv 调用结束,因为 MMR 初始化代码包含许多对此函数的调用。然而,服务器的几个特性使这变得困难。首先,只有 MMR 二进制文件加载在固定位置。堆和系统库则不是,因此只有实际的 MMR 代码可用,无需绕过 ASLR。其次,如果 MMR 崩溃,它会采用指数退避,最终导致它在每小时整点重新生成。这限制了攻击者可以进行多少次漏洞利用尝试。攻击者可能会花费数天甚至数周时间尝试利用服务器,但这仍然将他们限制在数百次尝试内。这意味着任何对 MMR 服务器的漏洞利用至少需要一定的可靠性,因此某些需要大量尝试的技术(例如在堆上分配一个大缓冲区并尝试猜测其位置)是不切实际的。
最终,我认为在堆上分配一个具有可控内容的缓冲区并确定其位置会很有帮助。这将使漏洞利用在溢出成功导致虚拟调用的情况下相当可靠,因为该缓冲区可以用作假的 vtable,并且还可以包含可用作 execv 参数的字符串。我尝试使用 CVE-2021-34424 来泄露这样的地址,但未能使其工作。
此漏洞允许攻击者提供任意大小的字符串,然后该字符串会被越界复制,直到遇到内存中的空字符,然后返回。CVE-2021-34424 有可能返回堆指针,因为 MMR 将受损的堆映射到一个通常不包含空字节的低地址,但是,我无法找到一种方法来强制在越界复制的字符串缓冲区旁边分配特定的堆指针。MMR 使用的 C++ 对象往往是虚拟对象,因此大多数对象分配的前 64 位是包含空字节的 vtable,从而结束复制。其他分配的结构,尤其是较大的结构,往往包含非指针数据。我能够通过指定长度小于 64 位的字符串使此漏洞返回堆指针,因此附近的分配有时是指针本身,但此大小的分配非常频繁,无法准确确定它们指向的堆数据。
我最后的一个想法是使用另一种类型混淆漏洞来泄露指向可控缓冲区的指针。在处理反序列化的 ssb::kv_update_req 对象时存在这样一个漏洞。此对象的 ssb::dyna_para_table_t 表包含一个名为 nodeid 的变体,它表示消息所指的特定 Zoom 客户端。如果攻击者将此变体更改为数组类型而不是 32 位整数,则指向此数组的指针的地址将被记录为字符串。我尝试将 CVE-2021-34424 与此漏洞结合使用,希望泄露的数据可能是包含指针信息的日志字符串。不幸的是,由于时间问题,我无法使其工作:日志条目需要在漏洞触发的同时几乎完全同时记录,以便日志数据仍在内存中,而我无法足够快地发送数据包。我怀疑通过改进自动化可能使其工作,因为我依赖使用 Frida 挂钩的客户端和浏览器与 Zoom 服务器交互,但我决定不继续研究,因为这需要开发大量工具。
结论
我对 Zoom 进行了安全分析并报告了两个漏洞。一个是影响 Zoom 客户端和 MMR 服务器的缓冲区溢出漏洞,另一个是仅对 MMR 服务器上的攻击者有用的信息泄露漏洞。这两个漏洞已于 2021 年 11 月 24 日修复。
Zoom MMR 服务器中的漏洞尤其令人担忧,因为该服务器处理会议音频和视频内容,因此一旦被入侵,攻击者可能能够监视任何未启用端到端加密的 Zoom 会议。虽然我未能成功利用这些漏洞,但我能够利用它们执行漏洞利用的许多环节,并且我相信攻击者通过足够的投入能够利用它们。Zoom MMR 进程中缺乏 ASLR 大大增加了攻击者入侵它的风险,Zoom 最近启用它是积极的。尽管如此,如果 MMR 服务器中仍然存在与我报告的漏洞类似的漏洞,攻击者很可能能够绕过它,因此 Zoom 继续提高 MMR 代码的健壮性也很重要。
还需要注意的是,这项研究之所以可能,是因为 Zoom 允许客户设置自己的服务器,而与此同时,我调查过的其他具有专有服务器的视频会议解决方案都不允许这样做,因此不清楚这些结果与其他视频会议平台相比如何。
总体而言,虽然本研究中发现的客户端漏洞与 Project Zero 在其他视频会议平台中发现的漏洞相当,但服务器漏洞令人惊讶,尤其是当服务器缺乏 ASLR 并且支持非端到端加密的操作模式时。
有几个因素通常会导致视频会议应用程序出现安全问题,这些因素也导致了 Zoom 中的这些漏洞。一是 Zoom 包含的代码量巨大。有大量代码我无法确定其功能,并且许多可以反序列化的类似乎并不常用。这既增加了安全研究的难度,又通过使更多可能包含漏洞的代码对攻击者可用而增加了攻击面。此外,Zoom 使用许多专有格式和协议,这意味着理解平台的攻击面并创建操作特定接口的工具非常耗时。使用我们测试的功能还需要支付大约 1500 美元的许可费。这些安全研究的障碍可能意味着 Zoom 没有被尽可能频繁地调查,可能导致简单的漏洞未被发现。
尽管如此,我在此评估中最大的担忧是 Zoom MMR 服务器缺乏 ASLR。ASLR 可以说是防止内存损坏漏洞利用的最重要缓解措施,大多数其他缓解措施在某种程度上都依赖它才能有效。在绝大多数软件中没有充分的理由禁用它。最近,人们一直在推动通过转向内存安全语言和实施增强的内存缓解措施来降低软件对内存损坏漏洞的敏感性,但这依赖于供应商使用他们编写软件的平台所提供的安全措施。所有为支持 ASLR 的平台编写的软件都应启用它(以及其他基本的内存缓解措施)。
Zoom 的封闭性也极大地影响了此次分析。大多数视频会议系统使用开源软件,要么是 WebRTC,要么是 PJSIP。虽然这些平台并非没有问题,但由于它们是开放的,研究人员、客户和供应商更容易验证其安全特性并理解它们带来的风险。闭源软件带来了独特的安全挑战,Zoom 可以采取更多措施使其平台对希望评估它的安全研究人员和其他人更加开放。虽然 Zoom 安全团队帮助我访问和配置服务器软件,但尚不清楚其他研究人员是否可以获得支持,而且软件许可仍然昂贵。Zoom 以及其他生产闭源安全敏感软件的公司应考虑如何使其软件对安全研究人员开放。