篡改ARM异常向量表
引言
几个月前,我在尝试学习内核漏洞利用时,编写了一个基于ARM的Linux内核漏洞利用挑战。我选择ARM架构主要是因为研究它看起来会很有趣。本文将描述在攻击者拥有任意地址写原语的情况下,ARM异常向量表(EVT)如何辅助内核漏洞利用。文章将涵盖本地漏洞利用场景和远程漏洞利用场景。请注意,篡改EVT在论文"Vector Rewrite Attack"[1]中已有提及,该论文简要讨论了如何在ARM RTOS上的空指针解引用漏洞中使用它。
文章分为两个主要部分。首先简要介绍ARM EVT及其从漏洞利用角度的意义(请注意,为了保持文章相对简短,将省略关于EVT的许多细节)。我们将通过两个示例展示如何滥用EVT。
我假设读者熟悉Linux内核漏洞利用并了解一些ARM汇编(真的)。
目录
- 引言
- ARM异常和异常向量表
- 异常
- 异常向量表
- 关于未定义指令异常的说明
- 篡改EVT
- 本地场景
- 漏洞利用
- 远程场景
- 漏洞利用
- 本地场景
- 额外内容:中断栈溢出
- 关于所有这些的一些事项
- 最后的话
- 参考文献
ARM异常和异常向量表
简而言之,EVT在ARM中的地位就像IDT在x86中的地位一样。在ARM世界中,异常是导致CPU停止或暂停执行当前指令集的事件。当发生异常时,CPU将执行转移到称为异常处理程序的另一个位置。有7种异常类型,每种异常类型都与一种操作模式相关联。操作模式影响处理器在系统资源方面的"权限"。总共有7种操作模式。下表将一些异常类型映射到它们相关的操作模式:
异常 | 模式 | 描述 |
---|---|---|
快速中断请求(FIQ) | FIQ模式 | 需要快速响应和低延迟的中断。 |
中断请求(IRQ) | IRQ模式 | 用于通用中断处理。 |
软件中断或复位 | 管理模式 | 操作系统的保护模式。 |
预取或数据中止 | 中止模式 | 从无效/未映射内存获取数据或指令时。 |
未定义指令 | 未定义模式 | 执行未定义指令时。 |
另外两种模式是用户模式(自解释)和系统模式,这是操作系统的特权用户模式。
异常
异常会改变处理器模式,每个异常都可以访问一组备份寄存器。这些可以描述为仅存在于异常上下文中的寄存器集,因此修改它们不会影响另一个异常模式的备份寄存器。不同的异常模式有不同的备份寄存器:
异常向量表
向量表是一个实际包含控制转移指令的表,这些指令跳转到相应的异常处理程序。例如,当引发软件中断时,执行转移到表中的软件中断条目,该条目又将跳转到系统调用处理程序。为什么EVT是如此有趣的目标?嗯,因为它加载在内存中的已知地址,并且是可写*和可执行的。在32位ARM Linux上,这个地址是0xffff0000。EVT中的每个条目也位于已知偏移处,如下表所示:
异常 | 地址 |
---|---|
复位 | 0xffff0000 |
未定义指令 | 0xffff0004 |
SWI | 0xffff0008 |
预取中止 | 0xffff000c |
数据中止 | 0xffff0010 |
保留 | 0xffff0014 |
IRQ | 0xffff0018 |
FIQ | 0xffff001c |
关于未定义指令异常的说明
覆盖未定义指令向量似乎是个好主意,但实际上并不是,因为它被内核使用。硬浮点和软浮点是两种允许模拟浮点指令的解决方案,因为许多ARM平台没有硬件浮点单元。使用软浮点,模拟代码在编译时添加到用户空间应用程序。使用硬浮点,内核让用户空间应用程序使用浮点指令,就像CPU支持它们一样,然后使用未定义指令异常在内核中模拟指令。
如果你想了解更多关于EVT的信息,请查看本文底部的参考文献,或者谷歌它。
篡改EVT
我们可以使用几个向量来获得特权代码执行。显然,覆盖表中的任何向量都可能导致代码执行,但作为懒惰的人,让我们尝试做最少的工作。最容易覆盖的似乎是软件中断向量。它在进程上下文中执行,系统调用通过那里,一切都很顺利。现在让我们看一些PoC/示例。以下所有示例已在运行于qemu中的Debian 7 ARMel 3.2.0-4-versatile上测试。
本地场景
示例漏洞模块实现了一个字符设备,该设备具有非常明显的任意写漏洞(或者是一个功能?):
|
|
基本上,利用这个很酷且现实的漏洞,你给模块一个地址,后跟要写入该地址的数据。
现在,我们的计划是通过用跳转到后门代码的代码覆盖SWI异常向量来后门内核。此代码将检查寄存器中的魔值(比如r7,它保存系统调用号),如果匹配,将提升调用进程的权限。我们在哪里存储此后门代码?考虑到我们具有对内核内存的任意写入,我们可以将其存储在用户空间或内核空间中的某处。后一种选择的好处是,如果我们选择内核空间中的适当位置,我们的代码将在机器运行时一直存在,而前一种选择是,一旦我们的用户空间应用程序退出,代码就会丢失,如果EVT中的条目未设置回其原始值,它很可能指向无效/未映射的内存,这将使系统崩溃。因此我们需要一个可执行和可写的内核空间位置。这可能在哪里?让我们仔细看看EVT:
正如预期的那样,我们看到一堆控制转移指令,但关于它们我们注意到的一件事是"最接近"的引用地址是0xffff0200。让我们看看EVT末端和0xffff0200之间有什么:
看起来那里什么都没有,所以我们有大约480字节的空间来存储后门,这绰绰有余。
漏洞利用
重述我们的漏洞利用:
- 在0xffff0020存储我们的后门。
- 用跳转到0xffff0020的分支覆盖SWI异常向量。
- 当发生系统调用时,我们的后门将检查r7 == 0xb0000000,如果为真,则提升调用进程的权限,否则跳转到正常的系统调用处理程序。
这是后门的代码:
|
|
你可以在[这里]找到漏洞模块和漏洞利用的完整代码。运行漏洞利用:
远程场景
对于此示例,我们将使用一个具有与之前类似漏洞的netfilter模块:
|
|
与前面的示例一样,此模块有一个很棒的功能,允许你将数据写入任何你想要的位置。连接到tcp/9999端口,只需给它一个地址,后跟数据大小和要写入的实际数据。在这种情况下,我们还将通过覆盖SWI异常向量和后门内核来后门内核。代码将跳转到我们的shellcode,我们也将如前面的示例一样,将其存储在0xffff020。覆盖SWI向量在这种远程场景中尤其是一个好主意,因为它将允许我们从中断上下文切换到进程上下文。因此我们的后门将在具有后备进程的上下文中执行,我们将能够"劫持"此进程并用绑定shell或连接返回shell覆盖其代码段。但让我们不要那样做。让我们快速检查一下:
你看到了吗,除此之外,EVT是一个共享内存段。它可以从用户态执行,并从内核态写入*。与其覆盖进行系统调用的进程的代码段,不如将代码存储在EVT中,紧接在第一阶段之后,然后返回到那里。
每个系统调用都通过SWI向量,因此我们不必等待太久就能让进程落入我们的陷阱。
漏洞利用
我们的漏洞利用过程:
- 在0xffff0020存储我们的第一阶段和第二阶段shellcode(一个接一个)。
- 用跳转到0xffff0020的分支覆盖SWI异常向量。
- 当发生系统调用时,我们的第一阶段shellcode将链接寄存器设置为第二阶段shellcode的地址(也存储在EVT中,将从用户态执行),然后返回到用户态。
- 调用进程将在我们第二阶段的地址"恢复执行",这只是一个绑定shell。
这是阶段1-2的shellcode:
|
|
你可以在[这里]找到漏洞模块和漏洞利用的完整代码。运行漏洞利用:
额外内容:中断栈溢出
在大多数内存布局中,中断栈似乎与EVT相邻。如果存在类似栈溢出的情况,谁知道会发生什么有趣的事情?
关于所有这些的一些事项
- 本文讨论的技术假设攻击者具有内核地址的知识,但情况可能并非总是如此。
- 我们存储shellcode的位置(0xffff0020)可能会或可能不会被另一个发行版的内核使用。
- 我在这里编写的示例代码仅仅是PoC;它们肯定可以改进。例如,在远程场景中,如果发现被劫持的进程是init进程,则在我们从绑定shell退出后,盒子将崩溃。
- 如果你没有注意到,这里呈现的"漏洞"并不是真正的漏洞,但这不是本文的重点。
*:似乎EVT可以映射为只读,因此在较新/某些版本的Linux内核中,它可能不可写。
最后的话
除其他外,grsec通过使页面只读来防止修改EVT。
如果你想玩一些有趣的内核挑战,请查看w3challs上的"kernelpanic"分支。
干杯,@amatcama
参考文献
[1] Vector Rewrite Attack
[2] Recent ARM Security Improvements
[3] Entering an Exception
[4] SWI handlers
[5] ARM Exceptions
[6] Exception and Interrupt Handling in ARM