MediaTek MT7622/MT7915 芯片组驱动程序RCE 利用 CVE-2024-20017 4 种不同的方式

MediaTek MT7622/MT7915 芯片组驱动程序RCE 利用 CVE-2024-20017 4 种不同的方式

合规渗透 2024-10-09 20:45

原文:https://blog.coffinsec.com/0day/2024/08/30/exploiting-CVE-2024-20017-four-different-ways.html
– 介绍

  • 背景

  • 漏洞 1:通过损坏的返回地址进行 RIP 劫持,ROP 到 system()

  • 漏洞 2:通过指针损坏进行任意写入,GOT 覆盖

  • 漏洞 3:返回地址损坏 + 通过 ROP 任意写入(完整 RELRO)

  • 漏洞 4:WAX206 返回地址损坏 + 通过指针损坏的任意读/写

  • 奖励:通过 JOP 执行任意 IOCTL 调用来触发内核错误

  • 结束语

  • 引用

介绍

好吧,我们到了。这篇文章原定于今年 3 月左右完成,恰逢我将要撰写的漏洞CVE-2024-20017的发布。不幸的是,这也恰逢我搬家,开始一份新工作,并且在这份工作上变得非常忙碌,所以我们已经近 6 个月后了。这篇文章可能是我最长的文章之一,所以请系好安全带。

去年年底,我发现并报告了wappd中的一个漏洞,wappd 是一个网络守护进程,是 MediaTek MT7622/MT7915 SDK 和 RTxxxx SoftAP 驱动程序包的一部分。该芯片组通常用于支持 Wifi6 (802.11ax) 的嵌入式平台,包括 Ubiquiti、小米和 Netgear 设备。与我发现的其他一些错误一样,我最初是在嵌入式设备(Netgear WAX206 无线路由器)上寻找错误时遇到此代码的。 wappd服务主要用于使用 Hotspot 2.0 及相关技术来配置和协调无线接口和接入点的操作。该应用程序的结构有点复杂,但它本质上是由该网络服务、一组与设备上的无线接口交互的本地服务以及使用 Unix 域套接字的各个组件之间的通信通道组成。
– 受影响的芯片组:MT6890、MT7915、MT7916、MT7981、MT7986、MT7622

  • 受影响的软件:SDK版本7.4.0.1及之前版本(适用于MT7915)/ SDK版本7.6.7.0及之前版本(适用于MT7916、MT7981和MT7986)/OpenWrt 19.07、21.02

该漏洞是由于复制操作使用直接从攻击者控制的数据包数据中获取的长度值而导致的缓冲区溢出,而没有进行边界检查。总的来说,这是一个非常容易理解的错误,因为它只是一个普通的堆栈缓冲区溢出,所以我想我应该使用这个错误作为案例研究来探索可用于此错误的多种利用策略,在此过程中应用不同的漏洞缓解措施和条件。我认为这很有趣,因为它提供了一个机会来专注于漏洞利用开发中更具创造性的部分:一旦您知道存在错误并且了解了限制,就可以想出可以影响应用程序逻辑的所有不同方式以及错误对代码执行和弹出 shell 的影响。

这篇文章将讨论这个 bug 的 4 个漏洞,从最简单的版本(没有堆栈金丝雀、没有 ASLR、损坏的返回地址)一直到为 Netgear WAX206 上附带的wappd二进制文件编写的漏洞,其中有多种缓解措施启用后,我们从 x86-64 转到 arm64。可以在此处找到漏洞利用代码;它的评论相当多,以帮助使事情变得更清晰。在阅读这篇文章时可能有助于记住这些内容,因此我在每个部分的开头都包含了相关漏洞利用的链接。

注意:下面讨论的前 3 个漏洞是针对我自己在 x86_64 机器上编译的 wappd 版本编写的,并进行了一些细微的修改(不同的缓解措施集、禁用分叉行为、编译器优化)。

背景

发现

这个错误是通过使用我第一次尝试的名为fuzzotron的基于网络的模糊器进行模糊测试时发现的。查看 Github 页面以获取更多信息,但是 tl;dr 它可以使用radamsa或blab来生成测试用例,并提供一种以最小的开销快速模糊网络服务的方法。对于这个目标,我使用radamsa进行突变,并使用 Python 手动生成起始语料库来定义预期数据包数据的结构并将其写入磁盘。我还对wapp守护程序代码进行了细微修改,以便它在收到的最后一个数据包进入时立即将其副本保存到磁盘,以确保可以保存崩溃案例以进行分类。

根本原因分析

该漏洞的发生是由于在调用IAPP_MEM_MOVE() ( NdisMoveMemory()的包装器)中使用攻击者控制的值将数据复制到 167 字节堆栈分配结构之前, IAPP_RcvHandlerSSB()中缺乏边界检查。

分别从IAPP_RcvHandlerUdp()或IAPP_RcvHandlerTcp()中的 UDP 或 TCP 套接字读取数据后,原始数据将转换为struct IappHdr并检查command字段;如果这是命令50 ,则将到达IAPP_RcvHandlerSSB()函数并将指针传递给从套接字接收的原始数据。在IAPP_RcvHandlerSSB()内部,数据被转换为 struct RT_IAPP_SEND_SECURITY_BLOCK * 并分配给指针pSendSB ;然后访问pSendSB->Length并用于计算附加到结构的数据的长度。将有效负载数据从强制转换结构指针复制到也作为参数传入的pCmdBuf指针后,使用攻击者控制的Length值调用宏IAPP_MEM_MOVE() (下面代码片段中的最后一行)字段从pSendSB->SB缓冲区字段写入函数开头声明的kdp_info结构。在此调用之前,对此值进行的唯一边界检查是检查它是否不超过 1600 字节的最大数据包长度。由于目标kdp_info结构的大小仅为 167 字节,这会导致堆栈缓冲区溢出,攻击者控制的数据最多可达 1433 字节。

IAPP_RcvHandlerSSB()中的易受攻击的代码片段如下所示:

从源到接收器的代码流

从输入到易受攻击函数的代码流程是:
– IAPP_Start()启动调用IAPP_RcvHandler()的主处理循环

  • IAPP_RcvHandler()调用select()来查找准备好的袜子,并为每个准备好的袜子调用适当的协议处理函数

  • 假设通过 UDP 接收数据包, IAPP_RcvHandler()将调用IAPP_RcvHandlerUdp() ,传入一个指针pPktBuf用于存储接收到的数据

  • IAPP_RcvHandler()调用recvfrom()从UDP套接字读取数据,假设数据已成功读取,则将数据转换为struct IappHdr并检查command字段;如果值为0x50 ,则调用IAPP_RcvHandlerSSB()来处理请求

  • 然后, IAPP_RcvHandlerSSB()将使用如上所述的原始数据包数据,在调用IAPP_MEM_MOVE ( NdisMoveMemory()的包装器)时使用嵌入数据包中的RT_IAPP_SEND_SECURITY_BLOCK结构的Length字段,该字段将从数据包数据的偏移量写入到堆栈分配的 struct kdp_info 。这就是溢出发生的地方。

注入点概述

在详细介绍漏洞利用之前,让我们花点时间回顾一下发生损坏的注入点、预期的有效负载格式以及存在的约束。

应用程序从 UDP 套接字读取的最大大小为 1600 字节,因此这是我们可以发送的有效负载的最大大小。考虑到必须存在才能到达易受攻击代码的有效负载部分,这为我们提供了大约 1430 个字节,可用于破坏其他数据。 RT_IAPP_HEADER和RT_IAPP_SEND_SECURITY_BLOCK结构体的定义如下所示。前者嵌入到后者中,这表示请求预计到达的格式;应用程序将从套接字读取的数据直接转换为这些类型。

RT_IAPP_SEND_SECURITY_BLOCK的主要有效负载部分位于SB[]字段中;数据直接附加到该结构的尾部,并且该有效负载的大小将存储在该结构的Length字段中。为了通过其他验证检查, IappHeader结构体的Length字段应保持较小;在我的有效负载中,我使用的大小为0x60 。最后, RT_IAPP_HEADER.Command字段必须设置为50才能到达易受攻击的处理程序IAPP_RcvHandlerSSB 。

除了这些基本约束/要求之外,没有任何其他问题需要解决,例如避免空字节或其他限制值。

漏洞 1:通过损坏的返回地址、ROP 到 system() 进行 RIP 劫持

  • 构建:非分叉,无优化

  • 缓解措施:NX

我们首先从实现代码执行的最简单路径开始,假设没有采取漏洞利用缓解措施(不可执行堆栈除外)。这意味着地址是可预测的并且不需要泄漏。

此漏洞是典型的 RIP 劫持,利用堆栈溢出来破坏保存的返回地址并重定向执行。这非常简单:溢出堆栈,将溢出对齐以破坏保存的返回地址与要跳转到的所需地址,然后等待函数返回并使用损坏的值。您跳转到的内容以及如何利用它来获得更多控制权是一块空白画布(大部分情况下)。在这个漏洞中,我们通过使用损坏跳转到 ROP 小工具来保持简单,该小工具将弹出一个指向字符串的指针,该字符串包含要运行到正确寄存器中的命令,然后调用system()来执行该命令。由于未启用 ASLR,我们假设知道system()的地址和靠近有效负载数据所在位置的堆栈地址。

成功运行后, iappd 守护进程的输出将显示对 bash 的调用失败,然后打印出字符串“LETSGO!!!”,表明echo已成功执行,然后干净退出。

(不)幸运的是,现在您几乎肯定会在嵌入式平台上找到堆栈 cookie 和 ASLR,这将防止这种微不足道的利用。在这些情况下,您将需要信息泄漏来(希望)泄漏 cookie 值,否则您只需转向不依赖于破坏已保存返回地址的其他技术。

漏洞2:通过指针损坏任意写入,GOT 覆盖

  • 构建:x86_64,非分叉,无优化

  • 缓解措施:ASLR、堆栈金丝雀、NX、部分 RELRO

  • 利用代码

继续上一节结束的地方,假设至少启用了堆栈金丝雀和 ASLR,并且上面的漏洞利用不再可行。由于我们没有信息泄漏,所以让我们将焦点从破坏堆栈上保存的返回地址转移到考虑在到达堆栈金丝雀之前我们能够造成的破坏还可以实现什么。

您可能已经知道,函数的本地声明变量存储在该函数的堆栈帧中,紧邻保存的返回地址和基指针地址之前。位于溢出缓冲区末尾和前一个堆栈帧开头之间的变量将因溢出而损坏。根据我们损坏内存后执行的代码中如何使用这些值,有可能滥用损坏的影响来获得进一步的控制。

以下是易受攻击函数IAPP_RcvHandlerSSB()的本地声明变量:

kdp_info结构是一个会因 bug 的影响而溢出的结构,并且在它之前声明的所有变量都可能被损坏。在这些情况下特别令人感兴趣的是指针,它可能会被滥用来获得强大的写入原语 – 如果我们改变指针指向的位置,应用程序使用该指针执行的任何赋值或写入都将导致数据被写入任意位置我们选择的地址。

在这种情况下,在调用IAPP_MEM_MOVE()触发损坏后,只剩下几行代码使用变量。这些行显示在下面的代码片段中:

其中最有趣的是使用BufLen中的值对OidReq->Len进行赋值:前者是一种访问,它将取消引用我们可以破坏的指针( OidReq ),后者是对我们也可以破坏的 int32 值的访问控制( BufLen )。换句话说,我们控制了赋值表达式的两边,并且可以将任意 4 字节值写入任意地址。

那么,我们可以用这个原语完成什么呢?此时有多种策略可能会起作用,这就是利用开发的创造力发挥作用的地方。如果我们的最终目标是执行system()来执行 shell 命令,我们通常必须执行以下操作:
1. 获取我们想要执行的命令字符串到内存中的已知地址

  1. 获取指向该字符串的指针,将其放入适当的寄存器中,作为第一个参数传递给system() (即放入 x86_64 上的rdi中)

  2. 将执行重定向到system()

上面链接的漏洞利用这一概念来破坏OidReq指针,并使用 4 字节写入原语将 shell 有效负载迭代写入 GOT 的一段 ( 1 );由于二进制文件是在没有 PIE 且仅部分 RELRO 的情况下构建的,因此 GOT 始终位于可预测的地址并且可写,因此我们可以将其用作有效负载的缓冲区。对此的唯一限制是,我们必须避免覆盖将在易受攻击代码的执行路径上的某个位置调用的函数的 GOT 条目,因为这会导致在漏洞利用完成之前崩溃。该漏洞利用发送多个损坏有效负载来写入 shell 命令,将每个请求上损坏的OidReq指针调整 +4 个字节,将 4 字节写入转换为任意写入内容。然后,该漏洞使用 4 字节写入来破坏read()的 GOT 条目,其中包含 ROP 小工具的地址,该小工具启动 ROP 链来调整堆栈,将 GOT 中的 shell 有效负载的地址弹出到$rdi ( 2 ),然后跳转到对位于IAPP_PID_Kill()中的system() ( 3 ) 的调用以执行 shell 负载。 read()被选为要损坏的 GOT 条目以重定向执行,因为它不在易受攻击代码的执行路径中,并且我们可以通过通过 TCP 发送请求来按需触发它,因为 TCP 连接的处理程序使用read()而不是比recvfrom() ;所有早期的有效负载都是通过 UDP 发送的。

此漏洞利用工作方式中的一个重要一点是,执行重定向与导致损坏的有效负载异步发生 – 仅当我们发送最终 TCP 请求以导致调用read()损坏的 GOT 条目时才会触发它,这意味着我们控制的数据都不位于堆栈顶部,并且我们在 TCP 数据包中发送的数据都没有被实际读取(因为read()消失了)。这是一个问题,因为我们需要在第一个 ROP 小工具返回后在堆栈顶部拥有受控值,以便我们可以保留执行控制。这就是运气的来源——在这种情况下,我们能够从早期请求中找到一些有效负载数据,这些数据是在堆栈帧顶部下方约 40 个字节处发送的(堆栈在函数/用途),因此我们可以在执行其他操作之前通过从堆栈中弹出 5 个值来获取有效负载数据。

此漏洞完全避免了损坏堆栈元数据,因此堆栈金丝雀不会发挥作用。它还只使用可预测的地址和 ROP 来避免处理 ASLR,因此不需要泄漏。

漏洞 3:返回地址损坏 + 通过 ROP 任意写入(完整 RELRO)

  • 构建:x86_64,优化级别 2,分叉守护进程

  • 缓解措施:ASLR、完整 RELRO、NX

  • 利用代码

因此,最后一个漏洞利用指针损坏来获取任意写入原语,从而能够绕过堆栈金丝雀和 ASLR,这需要允许我们将受控值写入 GOT,以便我们知道该数据的地址稍后在漏洞利用中使用。但是,如果附近没有指针可供我们破坏以获得任意写入怎么办?事实证明,如果应用程序是在优化级别设置为 2 ( -O2 ) 的情况下构建的,则沿着易受攻击代码的执行路径的各种函数会内联到在IAPP_RcvHandler()范围内运行的一个大函数中,从而导致更改堆栈布局和变量排序。这最终使得我们无法破坏我们之前用于任意写入的OidReq指针,因此必须找到另一种方法。

由于我们丢失了在之前的漏洞利用中使用的写入原语,因此我们将在此版本上禁用堆栈金丝雀,以便为我们提供一个代码重定向原语以开始(我们需要有一些东西可以开始)。此示例旨在演示一种从代码执行原语获取任意写入原语的方法,因为仅能够重定向执行通常是不够的,因此同时拥有两者总是会让事情变得更加容易。为了让事情变得有趣,我们将启用完整的 RELRO,以便 GOT 和 PLT 部分不再可写。

通过ROP任意写入

考虑到新的限制,我们需要做的第一件事是找到一种方法来获取任意写入原语,以允许我们在可预测的地址写入命令有效负载。由于我们可以影响执行流程,因此最好的选择是使用 ROP 来获得它。与任何依赖 ROP 的漏洞一样,您的漏洞利用所针对的二进制文件需要在主可执行文件中包含所需的 ROP 小工具(共享对象将受到 ASLR 的影响),这需要一定的运气。

如果我们考虑一下之前的读/写原语是如何工作的,就会发现一个指针值被取消引用,并且一个值被分配(即写入)到它指向的内存。这在装配中会是什么样子?大概是这样的:

因此,如果我们能找到一个(或多个)小工具来允许我们执行此类操作,并且我们可以控制用于操作两侧的值,那么我们应该能够获得任意写入原语。事实证明,幸运就站在我们这一边!下面的小工具 ( GADGET_A ) 可用:

GADGET_A
– 0x405574 :

  • mov rdx, QWORD PTR [rsp+0x50]; :将$rsp+0x50 (栈顶+80)处的值读入$rdx

  • mov QWORD PTR [rdx], rax :取消引用$rdx作为指针并将$rax中的值写入该位置

  • xor eax, eax; : $rax的低 32 位为 0

  • add rsp; 0x48 :将堆栈向上移动0x48字节

  • ret; :返回

伟大的!这让我们大部分完成了任务。但首先,我们需要找到一种方法将受控值放入$rax中,因为这将被写入$rdx中的地址。为此,我们需要找到一个小工具,它将从堆栈中获取值并将其放入$rax中,与之前一样。这通常很容易,因为pop操作无处不在,并且其中至少有一个弹出到$rax可能性。这是我选择用于此漏洞利用的小工具 ( GADGET_B ):

GADGET_B
– 0x0042acd8: pop rax; add rsp, 0x18; pop rbx; pop rbp; ret;

  • pop rax; : 将栈顶的值弹出到$rax中

  • add rsp, 0x18; :将$rsp增加0x18 (24)个字节;需要 +24 字节的填充来完成此操作

  • pop rbx; pop rbp; :将(新)堆栈顶部的接下来的两个值分别弹出到$rbx和$rbp中;需要 +16 字节的填充来完成此操作

  • ret; :返回

将第二个小工具与第一个小工具链接起来可以为我们提供所需的一切!现在,我们可以将任意 8 字节值写入任意地址,假设我们在执行重定向时控制堆栈顶部的值(因为我们破坏了保存的返回地址,该地址位于堆栈顶部) )。该链的有效负载如下所示,包括考虑修改堆栈指针的指令所需的填充。

与之前的漏洞类似,可以多次插入此 ROP 链,以从目标地址开始写入超过 8 个字节,但为了做到这一点,还需要一个小工具来处理GADGET_A交互方式中的细微差别与堆栈。

我们上面讨论的第一个小工具 ( GADGET_A ) 将$rsp+0x50处的值弹出到$rdx中,因此我们的有效负载需要将我们想要写入的地址放置在距该小工具在有效负载中的位置+0x50字节偏移处。然后,它将堆栈向上移动+0x48 ,使堆栈指针指向我们用作写入目标的值之前的值。这意味着下一个gadget的地址需要放在+0x48处,以便在到达ret时使用它;如果我们想执行另一次写入,这将是GADGET_B的地址,这就是问题出现的地方。跳转到GADGET_B后,它将从堆栈顶部 ( [$rsp] ) 将下一个值弹出到$rax中,但由于GADGET_A将堆栈指针移动了+0x48 ,因此当GADGET_A中达到ret时, $rsp的值增加 8 并向左指向偏移量+0x50 (我们作为写入目标传递的值),这是GADGET_B最终弹出到$rax中的值。这不是我们想要的,但幸运的是,有一个简单的方法可以解决这个问题:我们不是直接跳转到第一个链末尾的GADGET_B ,而是跳转到另一个小工具,该小工具将从堆栈中弹出单个值(从而递增$rsp到+0x58 ),我们将把地址放到GADGET_B那里,以便当这个小工具返回时我们跳转到它。

因此,考虑到这一点,这就是GADGET_B+GADGET_A子链(?)多次链接的方式:

如果最后一部分很难理解,请不要担心(它也很难写)。重要的是,当链接链的多个实例时,我们不是直接跳回GADGET_B ,而是跳转到一个小工具,该小工具将从堆栈中弹出一个值,然后返回并跳转到GADGET_B 。这样做是为了确保有效负载中的值在链的迭代之间得到正确调整。

处理完整的 RELRO

获得所需的写入原语后,我们可以使用与之前的漏洞利用相同的策略,将我们的 shell 有效负载写入可预测的地址,并稍加修改。由于 RELRO 已满,我们无法再写入 GOT 或 PLT 段,因此我们将传递给system() shell 命令写入唯一剩余的具有静态/可预测地址的可写段(假设没有 PIE)——.bss 和.数据段。完成后,漏洞利用程序会跳转到最终的 ROP 链,该链将我们写入命令的地址放入$rdi中,并通过 GOT 符号跳转到system()因此我们不需要泄漏 libc 地址。

我们获取命令执行并使用它来弹出反向 shell。

漏洞4:WAX206返回地址损坏+通过指针损坏进行任意读/写

  • 构建:aarch64,随 Netgear WAX206 一起构建

  • 缓解措施:完整的 RELRO、ASLR、NX、堆栈金丝雀*

  • 利用代码

我们已经完成了最后的探索!对于这个,我们将稍微改变一下并转向现实世界的目标:Netgear WAX206上附带的 wappd 版本。该版本是针对 aarch64 编译的,并启用了 ASLR、NX、完整的 RELRO 和堆栈 cookies。我认为它提供了一些有价值的见解,让我们了解在受控环境中编写漏洞利用程序与针对现实世界目标编写漏洞利用程序之间的差异——事情经常会以重要的方式发生变化,迫使您适应。

故事

我将改变本节的写作风格,并更多地使用叙述格式,以便我可以通过逐步了解所有内容的过程来提供一些背景信息。弄清楚这个漏洞有点困难,我认为最好将这个过程作为一个故事来讲述。之后我们将切换回前面部分中使用的样式。

免责声明:这是我第一次为 arm64 目标编写此类漏洞利用程序,在此过程中我必须学习下面提到的很多内容。因此,您应该对细节持保留态度,因为它们是我目前对事物如何/为何以某种方式工作的理解,但它们可能不是 100% 准确。如果您发现任何不正确的地方,请告诉我!

重要的变化

我将首先回顾该目标与之前目标的一些重要差异,以及它们最终如何影响最终的利用。

第一个主要变化是二进制代码的优化和内联方面的差异。无论是编译器版本不同、架构差异还是其他原因导致的结果,我最终都不确定。但结果是堆栈变量的布局发生了变化,并且破坏先前目标的OidReq指针的能力不再可行,类似于漏洞利用 3 。因此,这意味着一开始就没有任意的写入原语。代码重定向原语(之前的漏洞利用它来获取写入原语)怎么样?

这就是下一个重要区别所在:arm64 处理函数返回的方式。在arm64中,返回地址通常位于x30寄存器中,并且只有在需要覆盖它的嵌套函数调用时才会将其压入堆栈。当我使用 GDB 附加到进程时,我学到了这一点,并且可以看到我的目标跳转地址正确地放置在堆栈上以便在下一次返回时使用……然后看到当函数命中最终ret并使用时它完全被忽略x30中的值而不触及堆栈。上面提到的内联导致沿着易受攻击的代码内联到一个大型函数的路径上进行各种函数调用,基本上消除了破坏堆栈上将在ret中使用的返回地址的所有机会(内联函数不会ret ) 。最重要的是,唯一一个确实保存了可能被破坏的返回地址并且实际使用的堆栈帧是用于主请求处理循环 – 它无限运行并且不会返回,除非捕获 SIGTERM 信号(我们很快就会回来讨论这一点)。这些变化及其对最终漏洞利用的影响都有很多细微差别,但是 tl;dr,这意味着需要回到绘图板来制定新的漏洞利用策略。

好消息是,尽管checksec报告二进制文件启用了堆栈金丝雀,但在 Binja 中对其进行分析表明,编译器插入的 cookie 检查逻辑仅存在于两个函数中,并且这些函数来自外部库。这意味着我实际上根本不必担心堆栈 cookie!糟糕的是,考虑到上一段中描述的条件,损坏保存的返回地址似乎是不可能的……

通过 pPktBuf 指针损坏任意写入

根据我处理之前漏洞的方式,我认为必须有一种方法可以破坏某处的指针,所以这就是我首先解决的问题。在花了一些时间在 WAX206 上进行一些实时调试并测试不同的有效负载后,我最终发现我可以覆盖IAPP_RcvHandler()中定义的三个指针: pPktBuf 、 pCmdBuf和pRspBuf 。其中第一个pPktBuf指向用于存储从网络读取的入站请求数据的缓冲区 – 破坏该指针允许我们将其指向任意位置,然后获得后续请求的全部内容(最多1600 字节)写入该位置。伟大的!

有趣的是,正是上面提到的内联和arm64语义的影响使得到达这些指针成为可能——在正常情况下,写入足够远以到达它们将导致损坏IAPP_RcvHandlerSSB()和IAPP_RcvHandlerUdp() ,并在再次使用损坏的指针之前导致过早崩溃。在这种情况下, IAPP_RcvHandlerUdp()直接内联到IAPP_RcvHandler() (因此不使用返回地址),并且IAPP_RcvHandlerSSB()能够完成其执行,而无需将其返回地址值压入/弹出到堆栈上。已损坏。

因此,我现在有一个最多 1600 字节的写入原语到可控位置。这应该足以冲过终点线了,对吧?

当任意写入不够时

当仅从任意写入开始时,哪些漏洞利用策略可以实现代码执行?考虑到现有的缓解措施(即 ASLR)并假设没有可用的泄漏,在这种情况下实际上只有一种选择:损坏位于可预测/已知地址的某些数据,这将导致直接执行代码(例如覆盖函数指针) )或创造会导致额外腐败的条件,并可利用这些腐败来控制执行。因此,在这里我们回到漏洞利用 2中讨论的概念:找到应用程序以可利用的方式使用的可损坏数据。

我将节省您的时间(和挫败感),让您不必费力去寻找下一篇文章的所有可能途径,现在就告诉您:什么也没有。虽然有多个全局结构填充了函数指针,但它们都没有在请求处理循环中使用。具有可行目标的一些其他数据结构的数据部分也未被使用。完整的 RELRO 意味着损坏的 GOT/PLT 条目也将被排除。这给我们带来了这里的要点:有时甚至任意写入原语也不足以获得代码执行。我认为在漏洞利用开发过程中跟踪每条线索并尝试每一个可能的角度总是一个好主意,但现实是有时根本没有任何线索。在一种环境中可利用的有效漏洞并不总是在另一种环境中可利用;一切都很重要。这就是为什么我也遵循“利用或 GTFO”这句座右铭——除非通过真实的利用来显示对真实目标的影响,否则很难说漏洞对现实世界的影响。

接受失败:该漏洞仅在终止时起作用

正如重要更改部分中提到的,有一个返回地址可能会被损坏: IAPP_RcvHandler()的返回地址。问题是该函数仅在捕获并处理 SIGTERM 时在进程终止时返回。我最初忽略了这一点,因为作为远程攻击者没有办法强制终止,但是,在寻找另一个执行原语时遇到了死胡同,我不得不接受失败,只是决定编写漏洞利用程序,假设该进程将终止并命中损坏的返回地址。如果我就停在这里,这篇文章的结尾会很虎头蛇尾,对吧?

最终利用概述

在回顾了最终导致最终利用的过程中的所有重要部分之后,我们现在将回到现在并讨论利用的工作原理。鉴于这篇文章已经很长了,我将避免讨论最终漏洞利用的每个细节,而是重点关注我认为最有趣或最重要的部分(如果您有任何疑问,请随时在 Twitter 上联系)跟进问题)。这个漏洞重用了之前漏洞利用中涉及的一些概念,包括使用指针损坏来获取写入原语、使用 .bss/.data 段作为主要有效负载的缓冲区,以及利用 ROP(技术 JOP,在本例中)。case) 设置调用system()的参数以获取命令执行。

总结一下我们从哪里开始:
– 通过pPktBuf指针的损坏,我们可以得到最多 1600 字节的任意写入原语

  • 我们有一种方法可以通过损坏IAPP_RcvHandler()堆栈帧中保存的返回地址来重定向代码执行(但这只会在进程收到 SIGTERM 信号时触发)

该漏洞利用分为两个请求:一个请求破坏pPktBuf指针以设置写入原语,另一个请求使用写入原语将 shell 有效负载和一些其他数据写入已知的内存区域以供以后使用。

第一个非常简单,因为真正需要做的就是发送一个足够大的有效负载以溢出到pPktBuf指针并使其指向内存中 .bss 段的开头。由于该指针用于存储传入的请求数据,因此我们发送的下一个请求的内容将被写入该地址。除了破坏这个指针之外,第一个有效负载还破坏了pCmdBuf指针,该指针用于存储从我们发送的数据包中解析出来的数据。因此, pCmdBuf需要指向内存的可写段以避免崩溃或过早中止,因此我们覆盖它以也指向 .bss 中的偏移量,但足够远以确保它不会影响在 .bss 中发送的有效负载。第二个请求。

第二个请求是真正的操作发生的地方。使用第一个请求设置写入原语后,这个新的有效负载需要完成以下任务:
1. 将 shell 命令写入调用system()时可以引用的位置

  1. 损坏保存的返回地址以将代码执行重定向到用于设置system()参数的 ROP 小工具

  2. 将x24中的值移动到x0 ( x0用于将第一个参数传递给被调用的函数)

  3. 跳转到x22中的值

  4. ROP/JOP 小工具可以:

  5. 提供system()的地址以及步骤 1 中 shell 命令的地址,以便 ROP 小工具可以使用它们。当使用损坏的返回地址并且 exec 跳转到 ROP 小工具时,这些值将被加载到寄存器中。

  6. shell 命令字符串写入的地址 -> 加载到 x0 中

  7. system().plt的地址 -> 加载到 x22 中

  8. 损坏pPktBuf 、 pCmdBuf和pRspBuf指针,将它们设置为 NULL,以避免在终止期间在IAPP_RcvHandler()中释放这些指针时触发 libc malloc 健全性检查

  9. 设置参数后将执行重定向到system() (即步骤 1 中写入的 shell 命令的地址)

前两个步骤非常简单。我们在有效负载的开头编写我们想要执行的 shell 命令;由于我们已损坏pPktBuf以指向已知位置,并且这就是第二个有效负载将被写入的位置,因此我们可以预测该字符串将位于何处。在这种情况下,由于pPktBuf已设置为 .bss 段的开头,因此命令字符串将位于 .bss 段中的 16 个字节(以考虑数据包标头和 SB 数据包结构的其他字段)。对于第二步,我们知道保存的IAPP_RcvHandler()返回地址所在的溢出偏移量,因此我们只需使用 ROP 小工具的地址覆盖该位置,我们将使用该地址来设置参数并将执行重定向到system() 。

让我们花点时间讨论一下 ROP 小工具以及 arm64 与 x86 上的 ROP 总体情况。如前所述,arm64 与 x86 中的返回语义不同,这意味着小工具的工作方式略有不同。特别是,arm64 中的 ROP 小工具不仅需要以ret结尾才能有用;还需要以 ret 结尾。他们必须以正确的堆栈操作结束,以便在执行ret之前将堆栈上的下一个值弹出到x30中。与 x86 相比,arm64 拥有更多通用寄存器这一事实意味着,与 x86 相比,找到使用您可以控制的寄存器并且正确设置ret的小工具的可能性要低得多,其中有仅使用少数寄存器,并且堆栈上的下一个内容都会在ret上自动使用。

无论如何,最终漏洞利用中使用的小工具在技术上是 JOP(面向跳转编程)小工具,因此我们完全避免了ret的问题。JOP 小工具不使用ret来重定向执行,而是直接跳转到存储在寄存器中的值。我们很幸运,因为当执行重定向到小工具时,我们能够控制少数寄存器。其中两个寄存器是x22和x24 ,因此我们可以使用以下小工具,它只需将x24中的值移动到x0 (用于将第一个参数传递给函数的寄存器),然后跳转到x22中的地址:

回到漏洞利用的其余部分,唯一需要做的另一件事是破坏pPktBuf 、 pCmdBuf和pRspBuf指针,将它们分别设置为 NULL。我们这样做是因为在IAPP_RcvHandler()结束时,在返回和使用损坏的返回地址之前,如果这些指针不为 NULL,它们将被传递给free() 。如果它们仍然指向我们之前设置的位置,我们最终将触发 libc malloc 的健全性检查并在我们能够重定向执行之前触发abort() 。

一切就绪后,我们就到达了应许之地: