篡改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 | 需要快速响应和低延迟的中断。 |
中断请求 | IRQ | 用于通用中断处理。 |
软件中断或复位 | 监管模式 | 操作系统的保护模式。 |
预取或数据中止 | 中止模式 | 当从无效/未映射的内存获取数据或指令时。 |
未定义指令 | 未定义模式 | 当执行未定义的指令时。 |
另外两种模式是用户模式(自解释)和系统模式(操作系统的特权用户模式)。
异常
异常会改变处理器模式,每个异常都可以访问一组banked寄存器。这些可以被描述为仅存在于异常上下文中的一组寄存器,因此修改它们不会影响另一个异常模式的banked寄存器。不同的异常模式有不同的banked寄存器:
异常向量表
向量表是一个实际包含控制转移指令的表,这些指令跳转到相应的异常处理程序。例如,当引发软件中断时,执行转移到表中的软件中断条目,该条目又将跳转到系统调用处理程序。为什么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向量在这个远程场景中尤其是一个好主意,因为它将允许我们从中断上下文切换到进程上下文。所以我们的后门将在具有后备进程的上下文中执行,我们将能够"劫持"这个进程并用bind shell或connect back shell覆盖其代码段。但让我们不要那样做。让我们快速检查一些东西:
你看到了吗,除此之外,EVT是一个共享内存段。它可以从用户态执行,并从内核态写入*。与其覆盖进行系统调用的进程的代码段,不如将我们的代码存储在EVT中,就在我们的第一阶段之后,然后返回到那里。
每个系统调用都通过SWI向量,所以我们不必等待太久就能让进程落入我们的陷阱。
漏洞利用
我们的漏洞利用过程:
- 将我们的第一阶段和第二阶段shellcode存储在0xffff0020(一个接一个)。
- 用跳转到0xffff0020的分支覆盖SWI异常向量。
- 当发生系统调用时,我们的第一阶段shellcode将链接寄存器设置为我们的第二阶段shellcode的地址(也存储在EVT中,并将从用户态执行),然后返回到用户态。
- 调用进程将在我们的第二阶段地址"恢复执行",这只是一个bind shell。
这是阶段1-2的shellcode:
|
|
你可以在这里找到漏洞模块和漏洞利用的完整代码。运行漏洞利用:
额外内容:中断栈溢出
似乎中断栈在大多数内存布局中与EVT相邻。如果存在类似栈溢出的情况,谁知道会发生什么有趣的事情?
关于所有这些的一些事项
- 本文讨论的技术假设攻击者具有内核地址的知识,但情况可能并非总是如此。
- 我们存储shellcode的位置(0xffff0020)可能会或可能不会被另一个发行版的内核使用。
- 我在这里编写的示例代码仅仅是PoC;它们肯定可以改进。例如,在远程场景中,如果发现被劫持的进程是init进程,那么在我们从bind 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