Nailgun Attack

钉枪:突破ARM特权隔离

作者发现从ARMv7之后,ARM增加了核间调试的功能,例如在AP的第1个核通过寄存器设

置,可以调试第2个核上的硬件状态。处于调试状态的ARM核是没有EL权限之分的,因此通过
第1个处于EL1等级的核可以修改处于S-EL1/EL3状态的另⼀个核的寄存器值,使得在⾮安全
世界运⾏的程序可以获取到安全世界运⾏的数据,绕过TrustZone的隔离。
具体攻击详情和PoC在他们的主⻚:
https://compass.cs.wayne.edu/nailgun/

作者论文:
understanding_the_security_of_arm_debugging_features.pdf

作者PoC(两个例子:树莓派3提权到 EL3, Mate 7 TEE 指纹获取):
https://github.com/ningzhenyu/nailgun

实验复现

在Raspberry Pi 3上提权至EL3的实验

准备

  • Raspberry Pi 3或3B一块
  • 8GB以上TF卡一块,作为RPi的非易失存储
  • 2019-09-26-raspbian-buster-lite.zip 系统镜像(32bit)
  • win32diskimager-1.0.0-install.exe 镜像烧录工具
  • SDCardFormatterv5_WinEN.zip TF卡格式化工具

=坑=

  • 千万别用Kali for RPi的镜像作此实验。倒不是不能运行(反而挺好看),只是后期 Nailgun 的实验需要apt下载内核源码。而Kali for RPi的内核基于一个叫 Re4son 的个人魔改版,导致内核修改不完善,生成内核 ko 模块的时候因为 magic number 的原因无法装载。
  • Raspbian由于本实验只需要命令行,所以没有选择带桌面的版本,比较小。
  • 虽然 diskimager 和 SDCardFormatter 两个工具看起来很 low,没有平时U盘随手一格,rufus 烧镜像之惬意。但实践证明,循规蹈矩还是安全的。另外有的传闻说下载的系统镜像zip可以直接用 diskimager 烧录,千万不要信,老实解压成 img 再烧。
  • 运行期间 RPi 3 会出现黄色闪电标记,同时 dmesg 内核信息会不断报警电压不够。事实证明即使拔掉了 HDMI 输出、USB设备以及网线,RPi 3 的板子依然会报警,5V 2A 的 USB Micro 供电怎么都不够,但依然可以用就是了。中文维基百科上说要3A的电源,然而英文维基百科上说功耗不会超过2A,不知孰真孰假。
  • Raspberry Pi 3B 的处理器为 BCM2836 ,核心为 4 * A53,即支持 ARMv8_64/ARMv8_32/ARMv7 指令集。但由于 Raspbian 的版本为 32 位,且之前的 RPi 用的是 ARMv7 的架构,因此在最新的 Raspbian 中仍采用 ARMv7 的软件指令。

PoC装载

作者的 README 写得非常好,所有问题都考虑周全,直接按照编译 + 跑就好。
需要注意的是,在sudo insmod directly_read.ko后,内核会崩溃一下,导致如果还想尝试卸载/安装该内核模块只能重启电路。

原理解读

其实作者的论文里面把原理讲得也很清楚,在此快速简要科普。

实验需要基于多核 ARMv7/v8 处理器。其中一个核作为被调试处理器A,另一个用于调试器B。由于处于调试状态的 A 不会理会特权级的限制,因此只要通过 B 发送调试命令到处于调试状态的 A ,即可用 A 来访问更高权限的内容。
实验的前提是拥有 root 权限,因为需要通过insmod来装载内核模块。因此该实验模拟的主要是从 EL1 提权到 EL3,以便日后可以跨越 TEE 的隔离墙进入 S-ELx 的安全世界。当然,一个更好的模型也许是把 ko 模块写成允许用户态程序调用的通用接口,以便可以从用户态直接操作高特权级数据。

execute_ins_via_itr()函数作用是将机器码指令传送到调试器 CPU (上文说的处理器 B )。其中debug变量是用于获得处理器 A 的指针(或者叫句柄 handler?),32位宽的 ins 是机器码。

static void execute_ins_via_itr(void __iomem *debug, uint32_t ins) {
    uint32_t reg;
    // clear previous errors 
    iowrite32(CSE, debug + EDRCR_OFFSET);

    // Write instruction to EDITR register to execute it
    iowrite32(ins, debug + EDITR_OFFSET);

    // Wait until the execution is finished
    reg = ioread32(debug + EDSCR_OFFSET);
    while ((reg & ITE) != ITE) {
        reg = ioread32(debug + EDSCR_OFFSET);
    }

    if ((reg & ERR) == ERR) {
        printk(KERN_ERR "%s failed! instruction: 0x%08x EDSCR: 0x%08x\n", 
            __func__, ins, reg);  
    }
}

save_register()restore_register()用于寄存器的写和读。可以看到使用execute_ins_via_itr()写入的机器码使用了一个奇怪的数据格式,其大端格式本来是 0xee000e15 但写入时将高半和低半颠倒。

static uint32_t save_register(void __iomem *debug, uint32_t ins) {
    // Execute the ins to copy the target register to R0
    execute_ins_via_itr(debug, ins);
    // Copy R0 to the DCC register DBGDTRTX
    // 0xee000e15 <=> mcr p14, 0, R0, c0, c5, 0
    execute_ins_via_itr(debug, 0x0e15ee00);
    // Read the DBGDTRTX via the memory mapped interface
    return ioread32(debug + DBGDTRTX_OFFSET);
}

获得处理器 A 指针的方法,是通过 Linux 内核函数smp_call_function_single(1, read_scr, param, 1)。在 Linux 源码的kernel/smp.c中,其函数原型是

/*
 * smp_call_function_single - Run a function on a specific CPU
 * @func: The function to run. This must be fast and non-blocking.
 * @info: An arbitrary pointer to pass to the function.
 * @wait: If true, wait until function has completed on other CPUs.
 *
 * Returns 0 on success, else a negative status code.
 */
int smp_call_function_single(int cpu, smp_call_func_t func, void *info,
                 int wait)
           ...

即:cpu是需要进入调试模式的核,func是回调函数,info用于给func传参,wait是一个等待信号。

read_scr是 PoC 的主要函数,用于实现主要功能,依照 ARM 手册中的核间 debug 规则以机器码写入 ITR 寄存器。主要步骤:

  1. 输入 hardcoded 的密码,解锁处理器核心的 debug 模式
  2. 启用处理器 A 的 halt 功能
  3. 将处理器 A 通过 debug 命令暂停
  4. 等待处理器 A 暂停完毕
  5. 保存处理器 A 的上下文数据
  6. 通过改写 dcps3 处理器,将处理器 A 升到 EL3 状态
  7. 在 EL3 状态下读取 SCR 寄存器,验证其特权位的确是 EL3
  8. 恢复处理器 A 的上下文
  9. 重启处理器 A
  10. 等待处理器 A 重启完毕,之后输出 SCR 的值

最后,将 PoC 与内核连接起来的方式,是通过ioremap即可获得入口地址。

static int __init nailgun_init(void) {
    struct nailgun_param *param = kmalloc(sizeof(t_param), GFP_KERNEL);
    
    // Mapping the debug and cross trigger registers into virtual memory space 
    param->debug_register = ioremap(DEBUG_REGISTER_ADDR, DEBUG_REGISTER_SIZE);
    param->cti_register = ioremap(CTI_REGISTER_ADDR, CTI_REGISTER_SIZE);
    // We use the Core 1 to read the SCR via debugging Core 0
    smp_call_function_single(1, read_scr, param, 1);
    iounmap(param->cti_register);
    iounmap(param->debug_register);

    kfree(param);
    return 0;
}

所有的 Debug 相关寄存器和使用信息在 ARM 的几千页的'Architecture Reference Manual'-'External Debug'章节可以找到。

实验小结

作者着实花了很大的力气将所有想实现的功能以机器码的方式写入,这其中的坑可想而知。
然而整个攻击的原理并不复杂,寥寥几语基本可以说个大概。所谓鲁迅言:理论一分钟,实践十年功。将一个 fancy 的想法实现出来并在真是设备中成功验证,非常钦佩。

本实验据作者论文提及,在消费级设备和 ARM 架构服务器都可能存在漏洞(如下图)。虽然不是所有的开启了 Debug 模式的设备一定能够被 Nailgun 攻击影响,但这种与软件漏洞无关的硬件级漏洞,如果能利用的话后果很严重。

nailgun-a.png
nailgun-a.png

最后,该攻击原型如果通用化,用于检测任意 ARM 设备的 Debug 开启情况,也许可以成为一个大规模扫描的工具。
通过读取DBGAUTHSTATUS_EL1寄存器状态,可以知晓安全世界、非安全世界下核间 debug 的开启状态。

// ExternalSecureNoninvasiveDebugEnabled()
// =======================================
boolean ExternalSecureNoninvasiveDebugEnabled()
// The definition of this function is IMPLEMENTATION DEFINED.
// In the recommended interface, ExternalSecureNoninvasiveDebugEnabled returns the state of the
// (DBGEN OR NIDEN) AND (SPIDEN OR SPNIDEN) signal.
      if !HaveEL(EL3) && !IsSecure() then return FALSE;
      return ExternalNoninvasiveDebugEnabled() && (SPIDEN == HIGH || SPNIDEN == HIGH);

// ExternalNoninvasiveDebugEnabled()
// =================================
boolean ExternalNoninvasiveDebugEnabled()
// The definition of this function is IMPLEMENTATION DEFINED.
// In the recommended interface, ExternalNoninvasiveDebugEnabled returns the state of the (DBGEN
// OR NIDEN) signal.
      return ExternalInvasiveDebugEnabled() || NIDEN == HIGH;
  
// ExternalInvasiveDebugEnabled()
// ==============================
boolean ExternalInvasiveDebugEnabled()
// The definition of this function is IMPLEMENTATION DEFINED.
// In the recommended interface, ExternalInvasiveDebugEnabled returns the state of the DBGEN
// signal.
    return DBGEN == HIGH;  

(DBGEN == HIGH || NIDEN == HIGH) && (SPIDEN == HIGH || SPNIDEN == HIGH)

Mate 7上的实验(未真实测试)

Mate 7 采用 Kirin 925,即 4 A15 + 4 A7,均为 ARMv7 处理器。

ARMv7, ARMv8 32bit 和 ARMv8 64bit 的实现区别

Nailgun 攻击在三种架构的实现大同小异,区别在于:

ARMv8 32b

对于一些 32 位寄存器的写入方式与 ARMv8_64 模式时不同,在将机器码写到 EDITR 寄存器时,需要将高 16 位与低 16 位颠倒写入。如 dcps3 指令在 ARMv8_64 下为 0xD4A00003 应写成 0xD4A00003;而 dcps3 指令在 ARMv8_32 下为 0xF78F8003 应写成 0x8003F78F。

ARMv7

ARMv7 由于只是 32 位模式,所以不存在 ARMv8 的两种模式差异。寄存器写入方式对照 ARMv8_32 颠倒写入即可。
但是 ARMv7 在调试模式时,部分寄存器的配置有差异。具体情况需查阅手册。

获取指纹信息的地址

根据作者论文,通过逆向 Mate 7 的 TEE OS 发现,指纹信息通过函数fpc1020_fetch_image被存储在 TEE 分区的某一个特定地址。通过读取这个固定地址,获得最终指纹数据。

弥补措施

难度

  1. 许多现有的工具依赖这些寄存器进行开发调试,因此 SoC 厂家不能一出厂就封闭;
  2. 许多集成商和软件开发商并不知道这些debug方式的存在,因此无法在软件层面保护;
  3. 看不懂
  4. SoC 厂商要为 debug 鉴权做更多保护的话,费劲涨成本

可能的有效措施

  1. ARM:可以设置在调试时,被调试核心的特权级不得高于调试器核心的特权级。即在 B 核心上以 NS-EL1 装载的调试程序,被调试的 A 核心不允许处在高于 NS-EL1 的特权级;
  2. ARM:应增强对信号的鉴权机制,甚至可以对不同的 debug 信号分开给予权限;
  3. SoC 厂商:应有效的控制 debug 信号的范围,让 NS-EL1 的 debug 机制可调试 NS 范围的内容;
  4. SoC 厂商:使用新的硬件措施来辅助软件进行 debug 鉴权;
  5. OEM 和云厂商:建议关闭所有的 debug 权限;
  6. OEM 和云厂商:在内核中关闭 Linux based LKM 机制,断绝进行 LKM 内存映射的入口。
Comments
Write a Comment
'