0

全链基带漏洞利用分析(第 3 部分)

 7 months ago
source link: https://paper.seebug.org/3101/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

译者:知道创宇404实验室翻译组
原文链接:https://labs.taszk.io/articles/post/full_chain_bb_part3/

在 Hardwear.io 的 Basebanheimer 演讲中,介绍了一种方法:通过利用联发科基带 Pivot 漏洞 CVE-2022-21765 ,在其较旧的 Helio 系列芯片组上使用 32 内核 Linux 的任意代码执行。

基于之前的想法,理论上这个漏洞也可以在联发科最新的芯片组系列(Dimensity,使用 64 位内核)上被利用。

1 漏洞:CVE-2022-21765 和 CVE-2022-21769

回顾一下,这些漏洞在 Linux 内核驱动程序中提供了 OOB 读写功能,该驱动程序为应用程序(AP)和蜂窝处理器(CP)之间提供了一个接口,联发科称之为 CCCI 驱动程序。具体而言,这些错误源于环形缓冲区实现过程中,未对存储在 AP 和 CP 之间共享内存中的环形缓冲区的偏移量和长度值进行合理性验证。下面是有问题的代码:

56bdaef8-aad3-47ac-8379-f7d5489d9445.png-w331s

但在这一点上我们面临许多挑战,因为可获得的 OOB 原语是有限的。

首先,我们出界的最大限度为2*UINT_MAX。其次,也是最关键的一点,由于我们正在破坏内核用于其环形缓冲区操作的偏移量,因此我们不能直接控制:

  • 读取操作被读入的位置(即 OOB 读取“泄漏”的目标),
  • 写入操作写入的位置(即写入的值)

2 利用 Linux 内核中的 ioremap OOB 错误

在完成对天玑系列的开发之后,我们偶然看到了 p0ly 和 Vincent Dehors 关于特斯拉 pwn 的最新攻击演讲。他们展示了一个有趣的漏洞利用链,其最后一步巧妙地以类似方式利用了一个与之极为相似的漏洞。这揭示了一个有趣的现象,即截然不同的供应商如何设法解决相同的问题。

他们的方法与 Brandon Azad 的初始想法具有共通之处:针对 vmalloc 区域中的其他分配(这些环缓冲区是ioremap() ed,由 vmalloc 区域提供服务)。

我们同样采用了 Brandon 的这个想法,并尝试分配了 _do_fork

因此,这让我们有了一个攻击目标的想法,但仍有几个关键问题需要解决:

  • 我们能否将有限的 OOB R/W 原语转换为对写入值有足够控制的原语。
  • 我们如何绕过 KASLR。
  • 我们能否在目标上获得可预测的 vmalloc 区域布局,从而选择合适的_do_fork受害者。

3 改进 CVE-2022-21765/CVE-2022-21769 的 OOB 漏洞

AP 和 CP 使用的多个环形缓冲区大多数都非常嘈杂,这给我们的利用带来了困难。

幸运的是,我们发现仅用于远程文件系统 (RemoteFS) 实现的环形缓冲区在初始启动后变得非常平稳。因此,这为我们提供了一个利用的机会,无需过多担心其正常的运作。

更重要的是,RemoteFS API 为我们提供了理想的工具,可以将越界(OOB)原语转换为几乎完全受控的读取和写入操作:

  • 为了写入内存,我们可以先使用 RemoteFS 的常规 File Write API 来准备受控数据。
  • 然后,我们可以使用文件读取 API “回读”这些数据,并使用 OOB write 原语创建 write-what-where 的操作,具体见CVE-2022-21765
  • 我们也可以反向执行相同的操作,以便读取任意内存(从何处读取):文件写入时损坏的环形缓冲区,以将所需的泄漏内存存储到文件中,然后使用文件读取 API 按预期将其读回。

对于“在哪里写入”,我们必须考虑到一个限制,即写入的值不是完全受控的。这是因为每次环形缓冲区的写入都包含了头部和尾部,如下图所示:

022da574-b3b7-4f5b-9a29-28c00f35be8b.png-w331s

因此,我们需要选择一个可以容忍临近字节被头部和尾部"噪音"覆盖为副作用的损坏目标。

4 寻找可靠的 Vmalloc 受害者并绕过 KASLR

在我们的研究中,我们发现了许多潜在的目标,包括线程堆栈和 bpf 程序,这些都可以在/proc/vmallocinfo中查看。

ccci_targets

虽然 vmalloced 地址没有应用 KASLR 随机化,但受运行时提供的“自然”熵的影响,布局并不是完全静态的。

尽管如此,早期分配仍具有可预测的且稳定的模式。由于我们的分配发生在 vmalloc 的早期阶段,因此它最终会到达一个可预测的地址,并且相邻的分配也是可预测的,具有很高的准确性。

基于这一点,我们能够确定在 vmalloc 中靠_do_fork定位的分配。

da78dbdc-d8d7-49a3-b149-8e8a46b974ec.png-w331s

总而言之,vmalloc 区域中的_do_fork分配表示内核中的线程堆栈。这些堆栈在系统调用执行期间被用户空间线程使用,同时也被内核线程用于存储其执行堆栈。

特别是内核线程具有相当可预测的调用堆栈,因为 Linux 调度程序以完全相同的方式生成它们。所以这些堆栈的顶部是推送的堆栈帧 ret_from_forkworker_threadkthreadschedule 等。

因此,通过覆盖调度的内核线程的相应堆栈帧,我们可以轻松地“竞争”其调度,劫持该内核线程的执行,创建一个 ROP 链,并从那里执行自定义代码。

e2a60706-c292-479f-a1a5-5df477b579dd.png-w331s

此外,选择这样的目标还为我们提供了绕过 KASLR 的直接途径。在案例中我们发现,在 RemoteFS 环形缓冲区中,我们总能找到一个包含固定指针+0x3eb8``kthread的区域(与可作为覆盖目标的区域相同),这简化了泄露内核映像 KASLR 滑动的过程。

5 内核 RCE 的 ROP 链

p0ly 和 Vincent Dehors 采用了经典的 ROP 方法,即通过覆盖字符串来执行 ROP 调用。这种方法既高效又干净,但其局限性在于只能以 kworker 根用户的身份执行命令,如poweroff_cmdpoweroff_work_func

这种情况在过去可能意味着攻击的成功结束,但在现在的 Android 系统中,SELinux 对用户施加了严格的限制,以至于这种方法几乎无法实现有效的操作。

甚至由于缺乏打开网络套接字的权限,无法执行回连Shell。因此,在面对搭载联发科天玑芯片组的智能手机时,我们需要寻找更强大的攻击手段。

Brandon Azad 使用了 ROP 技术,然而这对我们来说是行不通的,因为联发科内核阻止了___bpf_prog_run()这种方法。

特别是,联发科天玑内核被强制执行了 BPF JIT 编译,这导致内核中完全省略了这个 API:

#ifndef CONFIG_BPF_JIT_ALWAYS_ON
static unsigned int ___bpf_prog_run(u64 *regs, const struct bpf_insn *insn,
                    u64 *stack)
...

相反,我们所做的是查看我们拥有的 BPF JIT 实现。事实证明,始终对 eBPF 程序进行 JIT 的意味着module_alloc调用(简单的为调用者分配 RWX 内存)存在于内核中。当然,这为 ROP 链到任意 shellcode 提供了一个完美的环境。

void *module_alloc(unsigned long size)
{
  u64 module_alloc_end = module_alloc_base + MODULES_VSIZE;
  gfp_t gfp_mask = GFP_KERNEL;
  void *p;

  ...

  p = __vmalloc_node_range(size, MODULE_ALIGN, module_alloc_base,
        module_alloc_end, gfp_mask, PAGE_KERNEL_EXEC, 0,
        NUMA_NO_NODE, __builtin_return_address(0));
...

我们仍需找到工具来为 ROP 链的步骤准备必要的寄存器值,并需要比仅使用一个寄存器更精确的控制。

其中一个困难是,像 Linux 内核中的 memcpy 这样的函数通常被优化得很好,它们甚至不使用堆栈(因此它们的结语没有提供方便的 ROP 小工具链接),这是因为它们需要在系统启动的早期阶段运行,那时堆栈可能还未初始化。不过幸运的是,memcpy周围存在一些包装器,能够增加堆栈的使用(可以将此与第 3 种方法中使用 copy_from_user 的方法进行比较)。

最终,我们可以利用 ROP 中的 memcpy 将所需的 shellcode 从一个可靠的固定地址复制到新的 RWX 区域。在我们的案例中,我们能够再次利用我们的线程堆栈,并使用该堆栈框架的顶部do_fork作为暂存区域。

当这些步骤在 ROP 链中组合起来后,我们可以执行完全任意的 shellcode。

 1. set x0 = 0x100 (size of the injected code), x1 = <dummy>
 2. module_alloc(x0:size) -> x0:dst
 3. set x8 = x0
 4. set x0 = 0x100 (size of the injected code), x1 = <dummy>
 5. set x2 = x0
 6. set x0 = <dummy>, x1 = <code source>
 7. set x0 = x8
 8. memcpy(x0:dst, x1:src, x2:size) (preserves x0)
 9. set x8 = x0
10. jump on x8

6 漏洞利用演示

最后,下方链接是在天玑芯片组设备(小米 POCO M3 5G)上利用 RCE 漏洞的视频演示:

https://www.youtube.com/embed/0ICLk3HO_zY?si=s_NdlXPhvtPAujFU

如视频中展示的,为了简单起见,我们的概念验证只执行了一个 shellcode,它将所有寄存器设置为独特的模式,以证明代码的有效执行(摘自 poc 源代码)。

#define __SHELLCODE_SIZE__ (0x100)
const int shellcode_size = __SHELLCODE_SIZE__;
const ulong shellcode_addr = target_vmalloc + 0x1000;

uint shellcode[__SHELLCODE_SIZE__/4] = {
  0x00000000, // padding for exploit_write
  0xd2802020, // mov  x0, #0x101
  0xd2802221, // mov  x1, #0x111
  0xd2802422, // mov  x2, #0x121
  0xd2802623, // mov  x3, #0x131
  0xd2802824, // mov  x4, #0x141
  0xd2802a25, // mov  x5, #0x151
  0xd2802c26, // mov  x6, #0x161
  0xd2802e27, // mov  x7, #0x171
  0xd2803028, // mov  x8, #0x181
  (...)
  0xd280563a, // mov x26, #0x2b1
  0xd280583b, // mov x27, #0x2c1
  0xd2805a3c, // mov x28, #0x2d1
  0xd2805c3d, // mov x29, #0x2e1
  0xd2805e3e, // mov x30, #0x2f1
  0xd65f03c0, // ret
};

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3101/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK