使用WebRTC开发Android Messenger:第2部分

Natalie Silvanovich 2020年9月4日
这是一个由三部分组成的系列文章,内容涉及:利用WebRTC中的BUG和利用Messenger应用程序。本系列文章重点阐述了当应用程序不能应用于WebRTC补丁程序以及通信和安全问题通知中断时可能出问题的方面。
文 / Natalie Silvanovich
 

 
原文链接:
https://googleprojectzero.blogspot.com/2020/08/exploiting-android-messengers-part-2.html

 

Part 2: A Better Bug

 

使用WebRTC开发Android Messenger:第1部分中,我探讨了是否有可能在RTP处理中使用两个内存损坏bug来利用WebRTC。当我成功移动指令指针时,我无法破解ASLR,因此我决定寻找更适合此目的的漏洞。

usrsctp

 

 

我首先浏览了过去提交的WebRTC bugs,以查看是否有可能破坏ASLR 即使很早就修复了bug,它也表明可能在何处发现了类似的漏洞。这样的bugCVE-2020-6831,该漏洞在usrsctp中的越界读取。

 

usrsctp是WebRTC使用的流控制传输协议(SCTP)的实现。使用WebRTC的应用程序可以打开数据通道,该通道允许将文本或二进制数据从对等方传输。数据通道通常用于允许在视频通话期间交换文本消息,或在发生某些事件时告诉对等方,例如另一个对等方禁用其摄像头。  SCTP是数据通道的基础协议。在WebRTC中,SCTP类似于RTP,其中RTP用于音频和视频内容,SCTP用于数据。

我花了一些时间检查usrsctp代码中的漏洞。 我最终找到了CVE-2020-6831,这是从usrsctp中的堆栈缓冲区溢出。bug使攻击者可以完全控制溢出的大小和内容。 Samuel Groß建议,这个bug可以用来破坏ASLR,方法是覆盖堆栈cookie,然后一次覆盖一个字节的返回地址,并根据应用程序是否崩溃来检测值是否正确。不幸的是,事实证明,此bug无法通过WebRTC访问,因为它需要客户端套接字连接到侦听套接字,而在WebRTC中,两个套接字都是客户端套接字。

 

我一直在寻找,最终找到了CVE-2020-6514 这是WebRTC如何与usrsctp交互的一个非常不寻常的bug  usrsctp支持自定义传输,在这种情况下,集成商需要为每个连接提供一对无效指针,以提供源地址和目标地址。 这些指针的未取消引用的值随后被usrsctp用作地址,这意味着该值包含在某些数据包中。 WebRTC中,地址指针设置为WebRTC使用的SctpTransport实例的地址。 结果是在每个SCTP连接期间,此对象在内存中的位置将发送到远程对等方。从技术上讲,这是WebRTC中的bug,尽管usrsctp的设计也有缺陷,因为对自定义地址使用void*类型会强烈鼓励集成器使用该值的指针,尽管这是不安全的。

我希望此bug足以破解ASLR,但事实并非如此。对于漏洞利用,我需要一个已加载库的位置以及堆的位置,因此我在Android设备上进行了一系列测试,以查看这些位置之间是否存在任何关联,结果是没有任何关联。堆指针的位置不足以确定加载的库的位置。

我一直在寻找,我注意到usrsctp处理ASCONF块的方式中存在一个漏洞,这些块用于管理动态IP地址。该错误的来源如下:

if (param_length > sizeof(aparam_buf)) { SCTPDBG(SCTP_DEBUG_ASCONF1, "handle_asconf: param length (%u) larger than buffer size!\n", param_length); sctp_m_freem(m_ack); return; }   if (param_length <= sizeof(struct="" sctp_paramhdr))="" {<="" span=""> SCTPDBG(SCTP_DEBUG_ASCONF1, "handle_asconf: param length (%u) too short\n", param_length); sctp_m_freem(m_ack); }
 

请注意,对sctp_m_freem的第二次调用缺少了一个返回值,因此m_ack变量可以在释放后使用。发现此漏洞后,我注意到该bug已在usrsctp和WebRTC的较新版本中进行了修补。后来我得知,另一位Google员工Mark Wodrich在2019年9月19日将其报告为usrsctp中的Bug 376。

 

Revealing Memory with Bug376

 

在分析一个“后用”bug时,两个重要的问题是释放了什么,以及如何使用它。在Bug 376中,释放的对象是一个mbuf结构,一种用于存储入站和出站数据包内容的类型。

mbuf结构从一个子结构m_hdr开始,它的定义如下:

struct m_hdr { struct mbuf *mh_next; /* next buffer in chain */ struct mbuf *mh_nextpkt; /* next chain in queue/record */ caddr_t mh_data; /* location of data */ int mh_len; /* amount of data in this mbuf */ int mh_flags; /* flags; see below */ short mh_type; /* type of data in this mbuf */ uint8_t          pad[M_HDR_PAD];/* word align                  */ }
 

那么,这个结构是如何使用的呢?

 

查看ASCONF处理的其余部分,它最终被添加到一个出站包队列中,以确认发送的包。

TAILQ_INSERT_TAIL(&stcb->asoc.asconf_ack_sent, ack, next);
 

这使得如果将释放的m_buf结构替换为带有指向内存连续指针的结构(例如,CVE-2020-6514显示的SctpTransport指针)的结构,则该错误很可能被用于显示远程对等机的内存。

我试图通过发送与m_buf结构大小相同的RTP包来实现这一点。有一个很好的诀窍可以让大量特定大小的分配在WebRTC中无法释放。视频包在被组合成帧之前被存储在一个列表中,因此,如果一个帧的末尾从未被发送,它们将被永久存储,只要没有达到最大数量的包。不幸的是,这导致了一个意想不到的问题。WebRTC使用的OpenSSL碰巧有一些堆分配,其大小与m_buf结构的大小相同,如果它们恰好被分配到释放的m_buf结构的位置,它们将被写入m_buf send进程中,这出于某种原因将导致OpenSSL中的不可恢复状态。应用程序没有崩溃,它只会陷入某种循环中,拒绝接受更多的连接。

所以我决定在usrsctp中分配内存来代替m_buf结构会更好。SCTP允许将包含任意数量的块的数据包发送到主机,并且在大多数情况下,它们被当作一个数据包序列来处理。更好的是,在当前数据包中的所有块都被处理之前,添加了释放的m_buf结构的出站数据包队列不会发送任何数据包。这意味着应该可以发送一个包,其中包含一个触发该错误的块,然后发送一个块,该块将释放的内存设置为所需的值,然后将其发送回攻击者。由于在释放m_buf结构和安全地重新分配内存之间不需要发生网络通信,因此避免了OpenSSL的问题。

不幸的是,在usrsctp中对malloc的调用很少,其大小可以由传入流量控制,并且没有一个允许指定整个包内容。我能找到的最好的方法是处理数据流重置块。代码如下,为清楚起见删除了一些部分。

if (asoc->str_reset_seq_in == seq) { len = ntohs(req->ph.param_length); number_entries = ((len - sizeof(struct sctp_stream_reset_out_request)) / sizeof(uint16_t)); tsn = ntohl(req->send_reset_at_tsn); asoc->last_reset_action[1] = asoc->last_reset_action[0]; if (...) { ... } else if (SCTP_TSN_GE(asoc->cumulative_tsn, tsn)) { /* we can do it now */ ... } else { /* * we must queue it up and thus wait for the TSN's * to arrive that are at or before tsn */ struct sctp_stream_reset_list *liste; int siz; siz = sizeof(struct sctp_stream_reset_list) + (number_entries * sizeof(uint16_t)); SCTP_MALLOC(liste, struct sctp_stream_reset_list *, siz, SCTP_M_STRESET); if (liste == NULL) { /* gak out of memory */ asoc->last_reset_action[0] = SCTP_STREAM_RESET_RESULT_DENIED; sctp_add_stream_reset_result(chk, seq, asoc->last_reset_action[0]); return; } liste->seq = seq; liste->tsn = tsn; liste->number_entries = number_entries; memcpy(&liste->list_of_streams, req->list_of_streams, number_entries * sizeof(uint16_t)); TAILQ_INSERT_TAIL(&asoc->resetHead, liste, next_resp);
 

此代码分配了liste结构,该结构可用于替换释放的mbuf结构。它有一个非常幸运的功能,那就是与mbuf结构的mh_next属性对齐的next_resp属性恰好是mbuf类型。如果这是另一种类型,则会导致问题,因为usrsctp在发送数据包之前会遍历整个mbuf链。

一个不太幸运的特性是,与mbuf结构的mh_data属性一致的属性恰好是当前重置序列号和传输序列号(TSN)。在这种方法中,两者都要经过多次检查。重置序列号需要完全等于初始化连接时设置的序列号(在INIT或COOKIE_ECHO块中),还需要等于SctpTransport指针的低位四个字节。可以通过发送COOKIE_ECHO块来通过此检查,该块在触发错误之前将重置序列号设置为所需的值。

更具挑战性的是在TSN上执行的检查。它与累积TSN进行比较,后者最初被设置为与重置序列号相同的值。实际执行的比较是一个“序列号大于”,它确定一个值是在另一个值之前还是在后面,假设序列号在所有位都被设置时滚动到零。例如,如果当前序列号为0xFFFFFFFF,则值2将通过“序列号大于”检查,但值0xFFFFFFFE和0x80000001将失败。从传入数据包中读出的TSN必须是SctpTransport指针的前四个字节,而累积的TSN必须是该指针的后四个字节,因为它与重置序列号的值相同。所以这实际上是指针的两半部分之间的比较。TSN是一个很小的数字,小于0x80,因为它是指针的顶部,所以每当指针的第31位未设置时,此比较将大致返回true,并在设置指针时大致返回所需的false结果。

指针的第31位是由ASLR随机确定的,以及SctpTransport实例在堆上分配的位置,这意味着它被设置为大约50%的时间。通常情况下,我可以接受50%有效的漏洞攻击,因为这意味着它很可能只需尝试几次就可以成功,但在这种情况下,这不是真的,因为它在同一个ASLR布局上会有一次又一次失败的倾向。ASLR布局是在Android设备启动时确定的,并且在重新启动之前不会再次更改。所以我需要一种方法在重置序列号被设置之后改变累积的TSN。

事实证明,使用FWD_TSN块类型是可行的,该类型允许一个对等方请求另一个对等方将其累积的TSN最多向前移动4096字节。通过重复发送此块类型,可以将累积的TSN向前移动足够多的位,以使第31位翻转。这需要相当多的数据块,但是将这些数据块组合成更少的数据包并尽可能快地发送出去,它可以在几秒钟内翻转过来。

总而言之,这个bug可以用来让目标设备发回SctpTransport实例的内存,该实例包含指向类的vtable的指针,最后给出WebRTC库的位置并破坏ASLR。

仔细想想,我不认为WebRTC库是我的漏洞利用的最佳库,因为WebRTC集成器将它静态地与其他库链接起来并使用各种工具链是很正常的。更容易知道libc的位置,libc来自Android系统,变化较小。所以我添加了这个bug的第二个用法,从全局偏移表读取malloc的位置,这是从已经读取的SctpTransport vtable的固定偏移量。这允许计算libc的位置。

 

Moving the InstructionPointer (Again)

使用WebRTC开发Android Messenger:第1部分中,我弄清楚了如何使用RTP内存损坏错误来移动指令指针,但是在提交CVE-2020-6514之后,Jann Horn建议也可以使用该bug来移动指令指针。当WebRTC使用SctpTransport指针作为地址时,它不仅使用它来标识连接,而且实际上也使用它将指针强制转换为SctpTransport类,并在发送从usrsctp接收的出站数据包时对其进行虚拟调用。

同时,usrsctp通常根据数据包中的标识符确定出站数据包的地址,但是唯独在一种情况下,它需要从数据包本身提取地址:在处理COOKIE_ECHO块时。通常,不可能将不可信的指针放在这种块类型中,因为通常会从传入的数据包中回显它们,并且需要对其进行签名。但是,Jann注意到签名密钥的随机数生成非常弱。初始化usrsctp时,将调用以下代码。

srandom(getpid());
 

然后通过调用rand为随机数生成器提供种子。

 

启动SCTP连接时发送的INIT块包含用于身份验证的随机生成的密钥,该密钥由用于密钥的同一随机数生成器生成。我编写了一个脚本,根据这个密钥确定远程PID的值,方法是对0到70000之间的每个数字调用srand,并查看哪个会导致随机数生成器生成相同的身份验证密钥。然后就可以推断出密钥的值。

现在,此密钥允许攻击设备发送包含任何内容的COOKIE_ECHO块,包括将地址更改为自定义指针。这允许移动指令指针,因为下一次发送出站数据包时,将对提供的任何地址进行虚拟调用,当对等方用COOKIE_ACK响应时,将立即进行虚拟调用。在上面的部分中,我还讨论了如何使用COOKIE_ECHO包来更改重置序列号,同时还讨论了如何实际发送它们。它是用同样的方法。

 

我现在有两种可能的方法来设置利用漏洞的指令指针。我选择继续使用这个,因为它使用usrsctp,这也是打破ASLR所必需的,而RTP-one使用了一个不同的特性。我觉得减少需要启用的特性的数量可以增加它所使用的应用程序的数量,因为有时应用程序会禁用特定的WebRTC功能。

 

Putting it All Together

 

具有利用漏洞所需的所有必要功能后,我需要将它们全部整合在一起。我的一般策略是在已知位置的堆上创建一个假对象,然后对该对象进行虚拟调用。假对象将在同一个缓冲区中有一个假vtable,它将指向system,后者将运行shell命令。

缺少的一环是如何在已知位置填充堆内存。一种可能是使用RTP来分配与SctpTransport对象大小相同的内存,希望它在对象后面的地址或可预测的位置分配。我试过这个方法,大概50%的时间都有效,但考虑到我有办法读懂记忆,我想我可以做得更好。

 

我注意到SctpTransport类包含一个CopyOnWriteBuffer对象,名为partial_incoming_message_,它有时用于存储传入的SCTP数据。如果rtcp支持不完整的数据包,那么这些数据包将通过不完整的scp。这些存储在部分“传入”消息对象中,直到接收到数据包的其余部分。所以我想如果我通过SCTP把假对象的数据发送到目标设备,它最终会填充这个缓冲区,我可以读取地址。(请注意,这实际上需要两次读取,因为在CopyOnWriteBuffer对象与其支持数据之间存在两级间接寻址。

我试过了,效果很好,但还有另一个问题。为了用一个假vtable创建一个假对象,这个假对象需要引用它自己,但是这个方法只允许我知道内存被写入后的位置,并且不能更改。我仔细看了一下这个功能是如何工作的。设置缓冲区的代码如下。

 

transport->partial_incoming_message_.AppendData(          reinterpret_cast          ... if (!(flags & MSG_EOR) && (transport->partial_incoming_message_.size() < kSctpSendBufferSize)) {        return 1;      } ... transport->invoker_.AsyncInvoke RTC_FROM_HERE, transport->network_thread_, rtc::Bind(&SctpTransport::OnInboundPacketFromSctpToTransport, transport, transport->partial_incoming_message_, params, flags)); transport->partial_incoming_message_.Clear();
 

这里发生的情况是,传入的数据总是立即附加到partial_incoming_message_缓冲区中,然后,如果它是不完整的片段,则函数将返回。否则,它将使线程排队以处理数据,然后清除缓冲区。

考虑到可能尚未完成的排队线程仍然需要数据,我开始怀疑清除的工作原理。事实证明,CopyOnWriteBuffer类会保留对数据的引用,并且仅在剩余零个引用的情况下才将其删除。否则,它将减少引用计数并为缓冲区分配当前大小的新数据。这意味着可以在写入数据之前读取_incoming_message_缓冲区的位置,因为它实际上是在清除期间分配的。只要由AppendData写入的数据更短或与已清除的最大大小相同,该内存就不会被重新分配。

这允许我在一个已知的位置创建一个堆缓冲区并填充它。最后一步是找出要填充的内容。我首先用序列号填充它,然后使用它崩溃的地址来计算要更改的内存。在使用crash locations创建假vtable之后,我最终在一个到X8的分支上发生了崩溃,唯一的另一个可控寄存器是X21。X0当然被设置为假vtable的位置,因为这次崩溃是由于一个虚拟调用造成的,X1和X23也是如此。

令人惊讶的是,libc有一个完美的工具来应对这种情况。

 

do_nftw(char const*,int (*) …) + 0x138   LDR             X0, [X23,#0x30] LDR             X1, [X23,#0x70] BLR             X21
 

将X23中加载的值设置为system,并将一个字符串参数复制到伪虚函数表的偏移0x30处,从而导致系统被该参数调用!

为了快速概述,以下是利用该漏洞所需的步骤,依次为:

1. 根据INIT块中的密钥确定PID,然后确定秘密密钥

2. 从SctpTransport对象读取vtable

3. 从全局偏移量表中读取malloc的位置

4. 用所需大小的数据填充partial_incoming_message_缓冲区

5. 清除了partial_incoming_message_缓冲区,因此分配了新缓冲区

6. 从SctpTransport对象读取partial_incoming_message_缓冲区的地址

7. 从缓冲区结构中读取partial_incoming_message_后备缓冲区的地址

8. 基于malloc的位置,partial_incoming_message_缓冲区填充有漏洞利用数据

9. 触发该漏洞,先虚拟调用小工具,然后再调用系统

现在我发现了一个漏洞,它在WebRTC的Android应用程序示例中起作用。

还可输入800
全部评论
作者介绍

LiveVideoStack

音视频技术社区

文章

粉丝

视频

阅读排行
热门视频

SRS实用手册-一剪定乾坤(10)

杨成立/RTC服务器团队负责人

WebRTC视频数据流程分析

许建林/《WebRTC Native开发实战》书籍作者