Zoom是一个视频会议平台,伴随着疫情的发展变得越来越流行。与我调查过的其他视频会议系统不同,在这些系统中,当一个用户发起一个呼叫时,其他用户必须立即接受或拒绝,而Zoom呼叫通常是提前安排的,并通过电子邮件邀请加入。在过去,我之所以没有优先审查Zoom,是因为我认为任何针对Zoom客户端的攻击都需要用户多次单击。然而,最近在Pwn2Own上公布了针对Windows Zoom客户端的零点击漏洞,表明它确实存在纯远程的攻击面。下面,我们将详细介绍针对Zoom的调查结果。
在审计过程中,我们发现了两个安全漏洞,并且已经报告给了Zoom。其中,一个是影响Zoom客户端和MMR服务器的缓冲区溢出漏洞,另一个是只对MMR服务器上的攻击者有用的信息泄漏漏洞。并且,这两个漏洞都已于2021年11月24日得到了相应的修复。
Zoom攻击面概述
Zoom的主要功能是多用户电话会议,即Zoom会议,并且还支持其他功能,包括音频、视频、屏幕共享和文本消息。用户可以通过多种方式参加Zoom会议。首先,Zoom为许多平台提供了功能齐全的可安装客户端,这些平台包括Windows、Mac、Linux、Android和iPhone。此外,用户也可以使用浏览器链接加入Zoom会议,但这种方式所支持的Zoom功能较少。最后,用户可以通过在按键电话上拨打邀请中提供的电话号码来参加会议,但这种方式只允许访问会议的音频流。这项研究的重点是Zoom客户端软件,因为其他加入电话会议的方式都是使用现有的设备功能。
实际上,除了电话会议之外,Zoom客户端还支持其他通信功能,这些功能可在用户的Zoom联系人之间使用。所谓Zoom联系人,就是另一个用户使用Zoom用户界面添加为联系人的用户。在成为Zoom联系人之前,必须经过两个用户的许可。之后,用户即使不进行会议,也能互相发送文本信息,并启动频道进行持续的小组对话。此外,如果任何一个用户主持会议,他们可以以类似于电话的方式邀请另一个用户:另一个用户立即得到通知,他们只需单击一下就可以加入会议。这些功能代表了Zoom的零点击攻击面。请注意,这个攻击面只适用于那些已经说服目标接受他们为联系人的攻击者。同样,会议功能只是针对Zoom联系人的一键攻击面的一部分,因为其他用户需要单击多次才能进入会议。
也就是说,对于一个具有明确目标的攻击者来说,即使需要多次点击,说服目标加入Zoom电话并非难事,而且某些组织使用Zoom的方式也带来了有趣的攻击场景。例如,许多组织会召开公开的Zoom会议,Zoom也支持付费网络研讨会功能,一大群不知名的参与者可以加入单向视频会议。攻击者有可能加入一个公共会议,并针对其他与会者发动攻击。Zoom还依赖服务器来传输音频和视频流,而且端到端加密默认是关闭的。因此,攻击者有可能破坏Zoom的服务器并获取会议数据。
Zoom消息
我首先审查了Zoom的零点击攻击面。将Linux客户端加载到IDA中,发现大量服务器通信似乎都是通过XMPP协议进行的。根据二进制文件中的字符串,很明显,XMPP协议的解析是通过一个叫做gloox的库来进行的。我使用AFL和其他覆盖率导向的fuzzer对这个库进行了模糊处理,但没有找到任何漏洞。然后,我研究了Zoom如何使用通过XMPP提供的数据。
XMPP流量似乎是通过SSL发送的,所以,我根据日志字符串找到了二进制代码中的SSL_write函数,并使用Frida将其钩住。我发现输出包含了许多XMPP节(消息)以及其他网络流量,我对其进行了分析,以确定XMPP是如何被Zoom使用的。实际上,XMPP被用于会议之外的Zoom客户端之间的大多数通信,例如消息和频道,并且当一个Zoom联系人邀请另一个Zoom联系人参加会议时,它也被用于发出信号(建立呼叫)。
我花了一些时间查看客户端的二进制文件,试图确定客户端是如何处理XMPP的,例如,如果一个节包含文本消息,那么这个消息是如何提取并显示在客户端的。尽管Zoom客户端包含了许多日志字符串,但这个任务仍然具有很大的挑战性,我最终向队友Ned Williamson寻求帮助,以帮忙查找客户端找的符号。他发现,Android Zoom SDK的几个旧版本都包含符号。虽然这些版本大约已有5年的历史,并且只包含了一些客户端使用的库,因此并没有完整地呈现客户端的视图,但它们对理解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客户端是如何处理音频和视频内容的。与我分析过的所有其他视频会议系统一样,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_t msg_db, &this->str_len, consume_bytes, error_out); str_len = this->str_len; if ( str_len ) { mem = operator new[](str_len); out_len = 0; this->str_mem = mem; ssb::i_stream_t read_str_with_len(msg_db, mem, &out_len);
read_str_with_len的定义如下所示:
int __fastcall ssb::i_stream_t read_str_with_len(msg_db_t* msg, signed __int8 *mem, unsigned int *len) { if ( !msg->invalid ) { ssb::i_stream_t if ( !msg->invalid ) { if ( *len ) ssb::i_stream_t 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网络研讨会期间。于是,我购买了一个Zoom网络研讨会的许可证,并能够通过发送这个数据包使自己的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,则该变量不是数组,并根据其类型从数据字段中读取数值。如果length字段为任何其他值,则将数据字段强制转换为指针,并根据数组长度读取数组。
对于这种实现方式,我最担心的问题是它可能容易出现类型混乱。一种可能性是数字值可能与数组指针混淆,这样的话,攻击者能够使用其指定的指针创建变量。但是,客户端和MMR都将它们视为数组的变量执行非常严格的类型检查。另一种可能是指针可能与数值混淆。如果将值返回给攻击者,这可能使攻击者能够确定他们控制的缓冲区的地址。我在MMR代码中发现了几个位置,其中指针以这种方式转换为数值并记录下来,但攻击者无法在这些位置获得不正确的强制转换值。最后,我查看了数组数据的具体处理方式,发现有几处会将字节数组变量转换为字符串,但是有的地方并没有检查字节数组是否带有null终止符。这意味着,当将这些变量转换为字符串时,该字符串可能包含未初始化内存的内容。
通常来说,用户发送到MMR的数据包会立即转发给其他用户,而不需要让服务器对其进行反序列化。对于一些漏洞来说,这是一个有用的特性,例如,这正是前面讨论的CVE-2021-34423能够在客户端被触发的原因。然而,变量的信息泄露必须出现在服务器上,才会对攻击者有用。当客户端反序列化一个传入的数据包时,它是在设备上使用的,所以即使反序列化的字符串包含敏感信息,这些信息也不太可能从设备上传输出去。同时,MMR的存在是为了将信息从一个用户传输到另一个用户,所以,如果一个字符串被反序列化,说明它很可能将发送给另一个用户,或者以可观察的方式改变服务器行为。因此,我试图找到一种方法,让服务器反序列化一个变量,并将其转换为一个字符串。我最终发现,当用户在浏览器中登录Zoom时,浏览器是无法处理序列化的数据包的,所以,MMR必须将它们转换为字符串,这样才能通过网络请求来访问它们。事实上,我发现如果从user_name变量中移除null终止符,它就会被转换为字符串,并作为用户的显示名称发送到浏览器。
该漏洞的编号为CVE-2021-34424,并已于2021年11月24日得到了相应的修复。我在自己的MMR以及Zoom的公共MMR上进行了测试,在这两种情况下,都可以利用该漏洞并返回指针数据。
漏洞利用方法的尝试
之后,我试图利用这些漏洞对自己本地MMR服务器发动攻击,虽然成功地利用了部分漏洞,但我无法使其正常工作。我首先调查了创建一个可能在Zoom客户端之外触发每个漏洞的客户端的可能性,但是客户端身份验证看起来很复杂,而且我也缺少这部分代码的符号,所以,我没有继续深究,因为我怀疑这将非常耗时。取而代之的是,我通过从通过Frida挂钩的Linux Zoom客户端触发这些漏洞,以分析它们的可利用性。
我首先研究了堆损坏对MMR进程的影响。由于我的MMR服务器运行在使用现代glibc堆的Centos7上,因此,利用堆unlinking技术似乎希望不大。所以,我转而尝试覆盖堆上分配的C++对象的vtable。
我编写了几个在服务器上钩住malloc函数的Frida脚本,并使用它们来监控传入流量是如何影响内存分配的。事实证明,攻击者控制MMR服务器上内存分配的方法并不多,但是,这些方法对利用此漏洞非常有用。攻击者可以向服务器发送几种导致内存分配到堆上的数据包类型,然后在处理完成时释放内存,但攻击者可以同时触发内存的分配和释放操作的数据包类型并不多。此外,MMR服务器是在使用唯一堆内存的单独线程中执行不同类型的处理的,因此,可能发生这种类型分配的许多代码区域(如连接管理),将在不同的堆内存中分配内存,而不是在发生错误的线程中分配内存。我找到的唯一一个在同一个内存区中进行此类分配有关的操作,正好与会议设置有关:当用户加入会议时,在堆上分配某些对象,然后在它们离开会议时释放这些对象。不幸的是,这些内存分配很难自动化,因为它们需要许多唯一的用户帐户才能重复执行内存分配,而且分配需要可观的时间(秒)。
后来,我编写了一堆Frida脚本,用于在正常的MMR操作中寻找与C++对象相邻的、带有vtables的、具有合适大小的空闲块。并且,其中分配的几段内存,在大小方面还是能够符合这个标准的,同时,由于CVE-2021-34423漏洞允许攻击者指定被溢出的缓冲区的大小,因此,我们可以藉此破坏邻近对象的内存。不幸的是,由于堆验证机制是非常稳健的,所以在大多数情况下,在对被破坏的对象进行虚拟调用之前,MMR进程会因为堆验证失败而崩溃。我最终通过关注那些小到足以被堆存储在fastbins中的分配长度来解决这个问题,因为被存储在fastbins中的堆块不会包含可验证的堆元数据。实践证明,大小为58的块是最好的选择,如果通过分配这种长度的内存来触发这个漏洞,就有机会控制虚拟调用的指针;当然,我们无法保证百发百中,相反,每十次中才有一次能够得手。
下一步是找出我可以控制的指针的指向,结果这一步比我预期的要难得多。当我进行这项研究时,MMR进程还没有启用ASLR(它在2021年11月28日发布的4.6.20211128.136版本中开始启用),所以,我希望在二进制文件中找到该调用可以定向到的一系列位置,这些位置最终将以使用可控参数调用execv结束,因为MMR初始化代码包含对这个函数的许多调用。然而,服务器的一些特性使得这一过程变得非常困难。首先,只有MMR二进制文件可以加载到固定位置。所以,在无需绕过ASLR的情况下,只有实际的MMR代码可用,而堆和系统库是不可用的。其次,如果MMR崩溃,它会有一个指数级的backoff,最终导致它每小时重复一次。这会限制攻击者利用漏洞的尝试次数。攻击者可能花费数天甚至数周的时间试图利用服务器,这是可以实现的,但仍然将他们限制在数百次尝试的范围内。这意味着对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最近已经启用了ASLR机制。也就是说,如果类似于我报告的漏洞仍然存在于MMR服务器中,那么攻击者很可能会绕过它,所以Zoom继续提高MMR代码的健壮性是非常重要的。
还需要注意的是,这项研究之所以能够进行,是因为Zoom允许客户设置自己的服务器,与此同时,我所调查的其他拥有专有服务器的视频会议解决方案都不允许这样做,所以目前还不清楚这些结果与其他视频会议平台相比如何。
总的来说,虽然在这次研究中发现的客户端漏洞与Project Zero在其他视频会议平台中发现的类似,但服务器漏洞尤其让人揪心,特别是当服务器缺乏ASLR和支持非端到端加密的操作模式时。
Zoom的这些漏洞,是由多个视频会议应用安全问题常见诱因导致的。一个是Zoom中包含的巨大的代码量。其中,有很大一部分代码我无法确定其功能,而且许多可以被反序列化的类貌似并不常用。这既增加了安全研究的难度,也增加了攻击面,代码越多,出现漏洞的几率就越大。此外,Zoom使用了许多专有的格式和协议,这意味着了解平台的攻击面和创建工具来操作特定的接口是非常耗时的。此外,为了使用我们测试的功能,还需要支付大约1500美元的许可费。这些安全研究的障碍可能意味着Zoom缺乏充分的安全研究,因此,其中可能还有许多低垂的漏洞有待发现。
不过,在这次安全评估中,我最大的担忧是Zoom MMR服务器中缺乏ASLR保护机制。因为ASLR机制可以说是防止内存损坏漏洞的最重要的缓解措施,大多数其他缓解措施在某种程度上都依赖于它的有效性。在绝大多数的软件中,都没有充分的理由禁用它。最近有一种发展方向,那就是通过转向内存安全语言和实施增强的内存缓解措施来减少软件对内存损坏漏洞的敏感性,但这有赖于供应商使用他们编写软件的平台所提供的安全措施。所有为支持ASLR的平台编写的软件都应该启用它(以及其他基本的内存缓解措施)。
Zoom的封闭性也对分析过程造成了极大的障碍。大多数视频会议系统都使用开源软件,无论是WebRTC还是PJSIP。虽然这些平台并非不存在安全问题,但由于它们是开源的,研究人员、客户和供应商都能更容易验证它们的安全属性,并了解它们所带来的风险。闭源软件带来了独特的安全挑战,我们认为Zoom可以做更多的事情来让安全研究人员和其他希望评估平台的人能够访问他们的平台。虽然Zoom安全团队帮助我访问和配置了服务器软件,但不清楚其他研究人员是否能得到相应的支持,而且软件的许可仍然很昂贵。Zoom以及其他源码封闭的、安全敏感的软件的公司,如何让安全研究人员能够访问他们的软件。
本文翻译自:https://googleprojectzero.blogspot.com/2022/01/zooming-in-on-zero-click-exploits.html?m=1如若转载,请注明原文地址