利用 ARM 核间调试漏洞获得 SoC 硬件最高权限(下)

第二篇 公众号推文。由于各种怕这怕那的原因,最后什么狠料都没敢爆,唉。


前情提要

上篇 中,我们给出了大致的漏洞原因和攻击链,在本篇中,我们将给出具体的实验思路和步骤。出于法律风险的考虑,我们不会给出源码和具体的参数。有兴趣的读者可以根据思路和论文,自行摸索。我们采用此技术针对几种常见的 IoT 嵌入式平台进行了分析实验,如果您有意探讨更多技术细节,请联系我们:[email protected]

以下实验步骤在 AArch64 上适用。

LKM 的使用

由于实践环节大量依赖 LKM (Loadable Kernel Module) 作为注入脚本。在此为不熟悉 Linux 内核编程的小伙伴简要介绍一下要点。

LKM 是能够在内核态运行的程序,由 insmod 接口在 root 权限下加载。

如何使用 LKM

  1. 内核头文件

   LKM 的编译过程需要运行 LKM 平台的内核信息。主要是一些模块符号和内核头文件中函数的格式,以便 LKM 在运行时正确地找到所需的内核接口。一般而言,x86 平台 Debian 系 mainline 的头文件通过 apt install linux-headers-$(uname -r) 可以自动获得。但与 x86 平台基本就 I 家和 A 家设计厂商不同,以嵌入式设备为主的 ARM 平台,各个设计厂商对自家 SoC 设计方案五花八门,使得 Linux 内核对不同 SoC 驱动支持程度不一;或者由于设备上资源所限,OEM 对内核进行了定制化修改,使得该设备的内核头文件无法通过 apt 等主流渠道获得。

此时要找到内核编译 LKM 所需的内核头文件,建议以下方式:

  • 查看该 SoC 是否有开发板,因为开发板往往会提供开源的内核源码,有内核源码也就有了内核头文件
  • 查看该 SoC 是否被某个版本的 Linux 内核 mainline 所接受,尝试更换内核到该版本
  • OEM 原则上来说应该对所使用的内核开源(比如安卓手机厂商),找到该内核源码
  • 逛逛爱好者论坛,寻找第三方做好的 ROM 或 kernel,再依此寻找第三方内核源码
  1. 加载 LKM

   通过 insmodrmmod 可以方便的加载和卸载 LKM。需要注意的是 LKM 的编写与平常的程序代码有所不同,内核函数与 libc 的差异自不必说,LKM 需要有特定的加载和退出函数,请自行查阅。

  1. 做成驱动

   如果想更方便的运行 Nailgun 攻击,可以考虑把内核态的代码做成驱动的方式,提供给用户态的程序调用。在用户态下执行代码一是写起来方便,二是加载驱动后就不再需要 root 权限。

如何观察 LKM 运行结果

通过 printk 在内核态输出的结果,只能通过 dmesgcat /var/log/kern.log 查看。

动手

经过上面的准备,刀已磨好,下面开始动手。

查看调试接口的开放情况

翻阅 ARM 架构参考手册(DDI0487),DBGAUTHSTATUS_EL1 寄存器标注了低八位寄存器的意义。

  • SNID [7:6],安全非侵入式调试
  • SID [5:4],安全侵入式调试
  • NSNID [3:2],非安全非侵入式调试
  • NSID [1:0],非安全侵入式调试

在论文 [1] 中,作者详细描述了这四种调试方式的差异,简单来说,如果低八位的值为 0xFF...

读取该寄存器的方法,可以通过 inline assembly 的方式,直接读这个寄存器的值就好了。

翻找调试寄存器基址

如果 DBGAUTHSTATUS_EL1 的值是 0xFF,下一步就是找到 Externel Debug Registers 和 Cross Trigger Interface registers 正式进行 Nailgun 实验。

从软件上看,EDR 和 CTI 是两组地址编码为 0x1000 大小的寄存器。访问该寄存器组中的独立寄存器,是通过基址 + 偏移的方式。偏移在 ARM 手册中都给出了准确值,困难的是这两个模块并不属于开放给第三方的外设,因此在公开的 SoC 手册里几乎不可能找到准确的寄存器基址(极大概率在 SoC 手册上被划入了 reserved 或语焉不详)。对此,只能来硬的。

饱和式搜索——穷举!

幸运的是,在 CTIDEVID 寄存器中,对于 Cortex-A53 而言,它低几位值是固定的,为 0xnn040800。因此在一个合理的地址范围内,搜索一个以 0xFC8 结尾,0x1000 为间隔的地址,其值低 24 位为 0x040800 的匹配并不算太难。需要注意的是,ARM 手册中的这个地址是物理地址,在 LKM 中进行编码的时候需要通过 ioremap() 进行地址映射。

如果幸运女神真的瞄了你一眼,获得了匹配,请进入下一步。

如果在扫描的过程中频繁遇到内核崩溃,系统假死,芯片烫烫烫得要命... 我们可以通过一个工具缩小范围。

ARM 的工作小组提供了一个工具,帮助通过查看 ROM Table 中特定位置的值,协助确定搜索的范围。他们的工具开源在 这里 。使用方法请自行阅读。

通常来说 SoC 中每一个核心对应一对 EDR 和 CTI,其用来调试该核心,所以八核 A53 会找到八组对应寄存器,不过也有例外……(坑可真多)

让代码在 Core 1 上运行

由于我们要停住 SoC 的一个核心,将代码在另一个核心上执行,因此需要告诉系统,指定代码运行的核心编号。

smp_call_function_single(1, payload_func, arg_struct, 1);

以上从 Nailgun 论文里抠出的代码,意思是:将 payload_func 在 Core 1 上执行,其中 payload_func 有一个参数叫 arg_struct。所以,把需要实现的功能写到 payload_func,把需要的地址放到 arg_struct 以指针传入,运行这条函数,不出意外的话可以得到正确执行。

通过核间调试访问 Core 0

如果以上步骤都成功完成,那么按照论文作者的 PoC [2] 就可以直接通过调试接口操作 Core 0 核心进行读写,读写方式就是最基础的 value = ioread(addr)iowrite(addr, value) 。步骤如下:

  1. 输入密码

   开启调试都有密码的,写默认密码 0xc5acce55 (CS Access) 到制定寄存器

  1. 暂停 Core 0

   通过 EDR 的一个寄存器,给 Core 0 一个外部调试的中断,使得 Core 0 能够保存现场,清空流水线,等待开启调试

  1. 开启调试

   通过 CTI 的寄存器一系列操作,使得通过 CTI 可以发送指令给 Core 0

  1. Core 0 确认状态

   检查各个标志位后,写一个 ack 返回给 Core 0 的 CTI,使得 CTI 可以接受 Core 1 的新指令

  1. 保存栈,通过人脑翻译将汇编指令转换成机器码,写到 EDR 的寄存器里
  2. 请开始你的表演
  3. 还原栈,第五步的逆操作
  4. 停止调试
  5. Core 1 通过 CTI 重启 Core 0
  6. 检查重启标志位后,发送 ack 给 CTI,以便 CTI 完成使命

用一个不严谨的流程图来表示,就是 Core 1 通过 CTI 和 EDR 来操作 Core 0 的运行。

在 Core 0 上执行代码

通过调试寄存器让 Core 0 执行代码,其根本方式就是将待执行的指令翻译成机器码,将机器码写到 EDITR 寄存器(32位),通过查询 EDSCR 标志位,得知指令的执行情况。遇到权限错误无法执行的指令,EDSCR 会在特定标志位给出指示。原作者的代码如下:

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);  
    }
}

最终效果,是在内核不崩溃的情况下读出 SCR_EL3 寄存器的值。

因为 ARM 手册写着,只有当前状态处于 EL3 时,才允许读取该寄存器。

内核崩溃的处置

由于 Nailgun Attack 是涉及未知硬件实现的软件代码,因此在实验当中一定会频繁遇到内核崩溃的情况。在此有几条建议,帮助大家趟坑。

  • 由于 Nailgun Attack 的根源在 SoC 硬件,所以运行什么版本的 Linux 内核或发行版都没有差别。实验的时候尽量找功能少(对,就是别带 GUI)、驱动少、稳定的系统为上。然而一旦在某个系统上攻击成功,同款芯片在任何软件系统中均可能依葫芦画瓢受到影响
  • 内核不论出现严重错误(SError)无法动弹或者只是啊哦(Oops)抱怨几句,都请拔电重启
  • 从内核 dump 的 backtrace 中可以找到导致内核崩溃的原因,但总的来说大多数是由于黑盒状态的硬件设计差异导致,在确认逻辑正确的情况下,修改代码收效甚微,只能尽力找到绕开的办法

总结

Nailgun Attack 可能是一个很早以前就存在,却没有被大家认识的硬件实现漏洞。这种漏洞可能缘于 ARM、SoC 设计方、OEM 产品方等多方在产品设计时因考虑问题的角度不同,因而对调试功能的态度和能力不同所导致。一个本应是方便开发和调试的功能,最终成为终端产品上广泛存在的漏洞。对此,如何防止未来出现类似多方安全性问题,值得大家思考。安全,作为一个贯穿整个产业链的的概念,在产品的设计、生产、销售、使用和回收等各个环节都需要有所准备。

最后,围绕 Nailgun Attack,我们开发了一个自动化的检测工具,将针对 Nailgun 漏洞对目标设备进行远程扫描,快速得知设备被漏洞影响的程度。相关信息我们将在近期公布,欢迎垂询。

OSR 安全检测实验室服务介绍

万物互联的物联网时代更需要安全护航。物联网的蓬勃发展衍生出许多创新商业模式和应用,一些新的安全漏洞和隐私泄露等问题往往也会因此暴露。OSR 安全检测实验室通过不断吸收并快速转化前沿技术成果,为各方提供以下服务:

  • 物联网设备安全性评估
  • 系统级安全架构培训
  • 行标级安全认证辅导
  • 定制化安全方案咨询

联系 Email:[email protected]

参考资料

[1] Understanding the Security of ARM Debugging Features http://www.cs.wayne.edu/fengwei/paper/nailgun-sp19.pdf

[2] https://github.com/ningzhenyu/nailgun/blob/master/PoC/Read_SCR/

Comments
Write a Comment
'