RP2350 硬件固化 CVE‑2022‑38694:突破 Unisoc BSP 安全启动链

· 2026-09-04 17:59 · 5 阅读

LeoChen.. 2026-09-04 17:59 上海

看雪论坛作者ID:LeoChen..

00

项目简介

这个项目的目标,是把 Unisoc / Spreadtrum 平台上原本依赖 PC 工具完成的 BootROM 下载流程,固化到 RP2350 微控制器中。

正常情况下,我们需要在 PC 上使用 spd_dump 一类工具,手动发送 FDL1、FDL2,再进入后续刷写或启动流程。本项目做的是:把前置逆向得到的修改版加载链,内嵌到 RP2350 工程里,让 RP2350 作为 USB Host,在目标设备进入 BootROM 模式后自动完成握手、发送和注入。

项目地址:

https://github.com/LeoChen-CoreMind/spd_flasher

本文主要记录两部分内容:

  • 逆向分析部分

    如何分析 UDX710 上 FDL1、FDL2、SPL Loader、U-Boot 和 system 层的限制;

  • 项目实现部分

    如何把已经处理好的启动链固化到 RP2350 中自动发送。

文章重点放在逆向过程和最终落地流程。项目实现细节不在正文展开,读者需要复现时直接查看源码。

01

整体流程

完整流程可以概括为:

查看标准刷机链
  -> 理解 BootROM / FDL1 / FDL2 / BSP 多层校验
  -> 分析 UDX710 的 FDL1 校验
  -> 分析 UDX710 的 FDL2 校验
  -> 分析 SPL Loader 启动地址和加载关系
  -> 分析 U-Boot 验证链
  -> system 层去云控 / 去策略回收
  -> 得到修改后的启动链
  -> 将 SPL Loader 作为 FDL 数据写入 0x28007000
  -> 再写入 0x3F28 的 exec stub
  -> RP2350 固化发送整个流程

这里要先明确一点:RP2350 只是把已经研究清楚的流程自动化重放。真正让设备能启动的关键,在前面的逆向和魔改。

02

逆向过程开始前:每一层分别负责什么

在进入具体分析前,需要先把 Unisoc 刷机链里的几个角色分清楚。

2.1 BootROM

BootROM 是芯片上电后最早执行的固化代码。它负责:

  • 初始化最基本的下载环境;

  • 通过 USB 暴露 BootROM 下载接口;

  • 接收 FDL1;

  • 校验并执行 FDL1。

BootROM 这一层通常是我们和设备建立通信的入口。

2.2 FDL1

FDL1 是 BootROM 下载并执行的第一阶段加载器。它负责:

  • 承接 BootROM;

  • 初始化更完整的下载环境;

  • 接收 FDL2;

  • 校验 FDL2;

  • 通过后跳转执行 FDL2。

本文最先分析的就是 FDL1 对 FDL2 的校验。

2.3 FDL2

FDL2 是第二阶段下载器,功能比 FDL1 更完整。它通常负责:

  • 分区读写;

  • 刷入文件;

  • 校验待刷入镜像;

  • 处理 BSP 签名;

  • 进入启动或下载命令处理逻辑。

如果 FDL2 这一层没有处理好,即使 FDL1 已经通过,后续刷入文件仍然可能失败。

2.4 SPL Loader

SPL Loader 是 BootROM 后正式启动链中的早期加载器。它负责:

  • 初始化启动环境;

  • 加载 trustos、teecfg、sml 等镜像;

  • 继续进入后续 bootloader。

当前项目利用的关键点是:

FDL1 下载地址 == SPL Loader 实际启动地址

也就是说,可以把修改后的 SPL Loader 作为 FDL 数据写入目标地址。

2.5 U-Boot / LK

U-Boot 或 LK 这一层通常负责:

  • bootloader lock 状态;

  • Verified Boot 状态;

  • do_cboot()

     一类启动函数;

  • slot / rollback / AVB / VAB 判断;

  • 启动参数和分区检查。

很多时候,前面 FDL1、FDL2、SPL 都处理完了,最后仍然卡在 U-Boot 的验证链。

2.6 system 层

进入 system 后,还可能存在:

  • 云控策略;

  • 联网校验;

  • 开机自动恢复;

  • 远程配置下发;

  • 设备状态回收。

所以 system 去云控是启动链打通后的最后一环。

2.7 启动链图示

为了让读者更直观看到 SPL Loader 后面还会继续校验 smltrustosuboot,这里放一张启动链示意图:

图片描述

这张图根据公开资料整理:TheGammaSqueeze/UnisocBypass README 中明确提到,Unisoc 的 UMS512 安全启动链使用 DHTB + SIMGHDR 格式,SPL 会对它加载的 smltrustosuboot 做 RSA-2048 签名校验,任一检查失败会进入无限循环。这个结论可以支撑本文后面对 SPL Loader、U-Boot 和 system 层继续分析的必要性。

03

从标准刷机链开始

图 1 来源于 TomKing062/spreadtrum_flash 项目,展示了 spd_dump 的典型使用方式:

spd_dump --wait 300 fdl /path/to/fdl1 fdl1_addr fdl /path/to/fdl2 fdl2_addr exec

图片描述

这条命令对应的流程是:

BootROM
-> 下载 FDL1 到 fdl1_addr
-> 执行 FDL1
-> FDL1 下载 FDL2 到 fdl2_addr
-> 执行 FDL2
-> FDL2 进入刷写、读取、启动或其他命令逻辑

它说明了一个关键事实:

Unisoc 的刷机流程不是一次性把文件写进去,而是分阶段加载、分阶段校验、分阶段交接控制权。

因此,后面的分析不能只盯着一个文件,而要沿着整条链路逐层追。

04

刷机前为什么要处理多层校验

在标准流程里,各阶段并不是无条件信任下一阶段。常见校验关系可以抽象成:

BootROM 校验 FDL1
-> FDL1 校验 FDL2
-> FDL2 校验刷入文件
-> 刷入文件还可能带 BSP 签名校验

如果这些校验没有处理,即使 USB 通信和 FDL 下载都正常,仍然可能出现:

  • BootROM 不接受 FDL1;

  • FDL1 不接受 FDL2;

  • FDL2 不接受待刷入镜像;

  • BSP 签名不通过;

  • 文件写入成功但启动链失败;

  • 进入系统后被云控或联网策略重新限制。

所以这不是单点 patch,而是一条多级处理链:

FDL1
-> FDL2
-> SPL Loader
-> U-Boot
-> system


05

UDX710 的分析原则

公开项目和 Wiki 只能提供思路,不能直接套用到 UDX710。

UDX710 的启动流程、内存布局、分区组织、厂商改动和验证链实现都可能不同。

本文采用的原则是:

借鉴公开资料的定位方法,
但所有 patch 点、函数地址、文件偏移、镜像顺序和验证结果,
都以 UDX710 自身样本的逆向结果为准。

公开资料里最值得借鉴的是分析路线:

  • 从 FDL1 的执行路径看它如何接收和校验 FDL2;

  • 从 FDL2 / U-Boot 中找 do_cboot()set_lock_status()get_lock_status()

  • 通过 trustos、teecfg、sml 等镜像还原启动链;

  • 区分下载阶段 patch 和启动阶段 patch;

  • 区分 SPL 路线和 FDL1 路线。

06

FDL1 校验定位:从 FDL2 文件头 DHTB入手

6.1 观察 FDL2 文件头

首先打开 udx710_fdl2,可以看到文件开头是 ASCII 字符串:

DHTB

图片描述

对应十六进制字节是:

44 48 54 42

这个文件头就是定位 FDL1 校验逻辑的第一个线索。

6.2 DHTB 魔数为什么是 1112819780

在反编译结果里,文件头不一定以 "DHTB" 字符串出现,更多时候会变成一个整数常量。

DHTB 的 ASCII 字节为:

D = 0x44
H = 0x48
T = 0x54
B = 0x42

按 32 位小端整数 解释:

magic = 'D' + ('H' << 8) + ('T' << 16) + ('B' << 24)
      = 0x44 + (0x48 << 8) + (0x54 << 16) + (0x42 << 24)
      = 68 + 72 * 256 + 84 * 65536 + 66 * 16777216
      = 1112819780

也就是:

0x42544844 == 1112819780

所以在反编译结果中看到 1112819780 时,它很可能就是在检查 FDL2 文件头的 DHTB

这一步的意义是:把文件格式特征转换成可搜索的反编译常量。

6.3 在 FDL1 中搜索魔数引用

拿到 1112819780 后,在 FDL1 反编译结果中搜索该常量,可以定位到图 3 所示位置:

图片描述

图中核心判断逻辑为:

if ( *a2 != 1112819780 )
return 1LL;

这段代码说明:

  • a2

     指向待解析的数据结构或缓冲区;

  • *a2

     取的是缓冲区开头的 32 位字段;

  • 如果开头字段不是 1112819780,也就是不是 DHTB,函数直接返回失败。

到这里,FDL1 对 FDL2 的识别入口已经缩小到一个很小的范围。

不过,这里通常只是头部检查,不一定是真正的签名或完整性校验。真正决定 FDL2 是否能继续执行的逻辑,还要继续沿调用链追下去。

6.4 顺着调用链追到真正失败分支

继续从图 3 的函数往下追踪,可以定位到图 4 所示位置:

图片描述

图 4 中的核心逻辑可以简化成:

v0 = MEMORY[0x28017DAC];
nullsub_1();
if ( (unsignedint)sub_2800FD2C(671117312LL, MEMORY[0x28017DAC], 0LL, 0LL, 2LL) )
{
while ( 1 )
        ;
}
((void (*)(void))(unsignedint)(v0 + 512))();
return0LL;

这一段比前面的 DHTB 头部判断更关键:

  • sub_2800FD2C(...)

     更像是真正的校验函数;

  • 返回值为非零时进入失败分支;

  • 失败分支是一个死循环:

while ( 1 )
    ;

如果校验通过,代码会继续执行:

((void (*)(void))(unsigned int)(v0 + 512))();

也就是跳到被加载镜像的后续入口位置。

因此这里的控制流含义很明确:

校验成功 -> 跳转执行 FDL2 入口
校验失败 -> 进入 while(1) 死循环

6.5 FDL1 修改思路

FDL1 的处理目标不是破坏整个加载流程,而是让失败分支不再阻断后续执行。

结合图 4,阻断点集中在校验失败后的死循环:

while ( 1 )
    ;

该死循环本质上是失败陷阱。处理方式就是让程序不要停在这里,而是继续走到后面的执行路径。

实际处理时,可以将失败分支对应位置改成空操作,或调整分支逻辑,使其不再落入死循环。这样 FDL1 对 FDL2 的阻断被解除,FDL1 阶段完成。

本节完整定位链如下:

FDL2 文件头看到 DHTB
  -> DHTB 按 32 位小端转换为 1112819780
  -> 在 FDL1 中搜索 1112819780
  -> 命中 FDL2 头部检查函数
  -> 顺着调用链继续追踪
  -> 找到真正校验函数 sub_2800FD2C(...)
  -> 发现失败后进入 while(1) 死循环
  -> 处理失败陷阱,使流程继续进入 FDL2 入口

这一节体现的是逆向分析中最重要的方法:先从文件格式特征入手,再用常量引用反推处理函数,最后顺着调用链找到真正影响控制流的分支。

07

FDL2 刷入校验定位:从加载地址和分区字符串入手

FDL1 处理完成后,下一步就是分析 FDL2。这里的目标不是让 FDL2 能运行,而是让它在刷入目标镜像时不再被校验逻辑阻断。

7.1 通过刷机命令确认 FDL2 加载地址

在 PC 侧验证时,可以通过如下 spd_dump 命令观察 FDL1、FDL2 的加载关系:

spd_dump --wait 600 exec_addr 0x3f28 fdl .\data\udx710_fdl1 0x28007000 fdl .\data\udx710_fdl2 0x9efffe00 exec

这条命令里最关键的是第二个 FDL 地址:

0x9efffe00

它就是当前样本中 FDL2 的加载地址。后面在 IDA / Ghidra 里分析 udx710_fdl2 时,需要用这个地址作为镜像基址进行加载。基址设置正确后,反编译中的全局变量、函数交叉引用和字符串引用才更容易对上。

这一步的意义是:

从刷机命令拿到 FDL2 运行地址
-> 用 0x9efffe00 作为 FDL2 镜像基址
-> 让反编译结果更接近真实运行状态

7.2 通过分区字符串定位处理函数

加载 FDL2 后,可以从字符串入手搜索待刷入分区名。

图 5 中可以看到,函数里出现了多个和刷写目标相关的字符串:

splloader
sml
uboot
boot
nr_agdsp
nr_modem
nr_v3phy
nr_nophy
nr_nrdsp1
recovery
nr_nrdsp2

图片描述

这些字符串很关键,因为 FDL2 在刷写不同镜像时,需要先判断当前传入的分区或镜像名属于哪一类,再进入对应的处理路径。

从图 5 的逻辑可以看出:

  • splloader

     会被归到一类;

  • sml

    uboot 会被归到另一类;

  • boot

    nr_modemrecovery 等分区会被归到 FDL2 处理路径;

  • 最后统一跳到后面的处理函数。

因此,搜索字符串不是为了看字符串本身,而是为了找到 FDL2 中“按分区名分发刷写逻辑”的入口。

7.3 定位真正刷入函数

顺着图 5 的函数继续追,可以定位到图 6 所示位置。

图片描述

图 6 中可以看到,函数进入后会先调用:

sub_9F05EC28(a1, a2);

后面继续出现多次和刷写相关的调用以及错误返回,例如:

return sub_9F01E448("flash error");

这说明当前函数已经不是单纯的字符串分类函数,而是进入了真正刷入流程附近。

这一段的分析链可以总结为:

用 0x9efffe00 加载 FDL2
-> 搜索 splloader / sml / uboot / boot / nr_modem 等分区字符串
-> 找到按分区名分发的函数
-> 顺着调用关系追到刷入处理函数
-> 观察 flash error 等错误路径
-> 找到影响刷入成功与否的关键分支

7.4 FDL2 修改思路

FDL2 这一层的处理目标,是让刷入流程不要因为分区检查、镜像检查或签名检查而中断。

从图 6 的位置继续分析,可以看到真正影响刷入结果的关键调用和错误路径。处理时的原则和 FDL1 类似:不要破坏整个刷写流程,而是处理会导致失败返回的阻断点。

在当前教学里,这一段只讲基本原理:

定位 FDL2 加载地址
-> 设置正确镜像基址
-> 搜索待刷入分区字符串
-> 找到分区分发函数
-> 追到真正刷入函数
-> 处理失败分支

实践中,这一步远比一句“找到后 NOP”复杂。不同版本 FDL2 的结构、函数内联、字符串引用、错误路径、BSP 签名检查和真实刷写调用都可能不同。真正操作时需要花很多时间反复验证,不是靠一个固定偏移就能通用解决。

本文在这里给出的是最基本的分析思路:先用加载地址还原运行视角,再用分区字符串定位业务入口,最后顺着调用链找到真正影响刷入结果的分支。

08

后续逆向主线

FDL1 和 FDL2 的下载、刷写路径处理完成后,还要继续分析 SPL Loader、U-Boot 和 system 层。

8.1 SPL Loader

接下来需要处理 SPL Loader。相比 FDL1 和 FDL2,这一层更加关键,因为它已经进入正式启动链,后面会继续影响 smltrustosuboot 等镜像能否被加载和执行。

SPL Loader 的整体思路和 FDL1 大同小异:还是先通过魔术值、镜像头、字符串或已知结构定位到校验函数,再顺着调用链找到真正阻断启动的失败分支。

不同点在于:SPL Loader 后面要处理的不再只是一个 FDL2,而是多个启动镜像。它通常会继续校验:

sml
trustos
uboot / LK

所以和前面的 FDL1 相比,SPL Loader 里会多出好几个校验点。这里不能只处理一个分支,而是要把相关校验路径逐一确认、逐一处理。

图 7 就是 SPL Loader 中比较典型的一段多重校验逻辑:

图片描述

图中可以看到,多次出现类似的调用结构:

if ( (unsigned int)sub_2802FC2C(...) )
{
    while ( 1 )
        ;
}

这和前面 FDL1 的失败逻辑很像:

校验函数返回失败
  -> 进入 while(1)
  -> 启动链停止

区别是,SPL Loader 里这类检查不止一处。它可能分别对应不同镜像、不同加载地址或不同启动阶段。例如图中能看到多次对 sub_2802FC2C(...) 的调用,并且参数不同,这说明它不是一次简单校验,而是在按不同镜像或不同区域做连续检查。

因此 SPL Loader 这一层的处理方法可以概括成:

通过魔术值 / 镜像头 / 字符串定位校验入口
  -> 找到实际校验函数
  -> 统计所有调用点
  -> 区分每个调用点对应的镜像或区域
  -> 找出失败后进入 while(1) 的分支
  -> 逐一处理这些失败陷阱
  -> 确认后续能继续加载 sml / trustos / uboot

这里要注意,SPL Loader 是整条链里非常关键的一层。FDL1 和 FDL2 更多是在下载、刷写阶段起作用;SPL Loader 则会直接影响正式启动链。如果只处理 FDL1 / FDL2,而 SPL Loader 对后续镜像的校验仍然存在,设备仍然会在启动阶段卡死。

当前项目利用的关键点就是:

FDL1 下载地址 == SPL Loader 实际启动地址

也就是说,我们最后把修改后的 SPL Loader 当作 FDL 数据写入 0x28007000,前提就是这个 SPL Loader 自身已经经过处理,能够继续放行后面的 smltrustos 和 uboot

本节只讲基本原理。实际分析时,SPL Loader 的校验点数量、函数内联情况、镜像加载顺序、参数含义和失败路径都要逐一核对。不能看到一个 while(1) 就认为全部完成,必须把相关校验调用全部追完。

8.2 U-Boot

众所周知,从上面的启动流程可以看出:SPL Loader 校验通过后,会继续启动 U-Boot / LK;U-Boot 再继续处理 bootrecovery 等启动镜像的校验和加载。

所以在 SPL Loader 之后,还需要继续看 U-Boot 这一层。

U-Boot / LK 通常决定:

  • bootloader lock 状态;

  • Verified Boot 状态;

  • do_cboot()

     启动路径;

  • boot_env_check
  • AVB / VAB;

  • slot 选择;

  • rollback 检查;

  • 下载模式回退逻辑。

这一层处理完成后,启动链才算真正从 BootROM、FDL、SPL 走到了系统启动前的关键位置。

8.2.1 通过 boot 字符串过滤定位

这里的分析入口依然是字符串。

因为目标是启动非签名 boot,所以可以在 U-Boot 里大量搜索 boot 相关字符串,然后结合上下文过滤无关结果。只看字符串本身没有意义,重点是找到“哪些函数在根据分区名决定是否需要校验”。

图 8 中可以看到一个很典型的函数:

图片描述

这个函数里出现了:

orca_cpe_boot_verify
boot
recovery
nr_agdsp
the cpe img do not need to verify

从逻辑上看,它会判断当前镜像名:

  • 如果是 boot 或 recovery,进入校验路径;

  • 如果是 nr_agdsp,进入另一类校验路径;

  • 其他情况则可能打印“不需要校验”。

因此图 8 这个函数可以理解为 U-Boot 里和 BSP / boot 镜像校验相关的分发入口。

8.2.2 最终定位到两个 BSP 校验函数

继续顺着图 8 的调用关系往下追,最终定位到图 9 和图 10 这两个关键函数。

图 9:
图片描述

图 9 中可以看到明显的校验日志:

sprd_verify_table_2048
verify failed: len = %d
verify failed: tablenotmatch

这个函数的返回值很关键:

return 0LL;  // 失败
return 1LL;  // 成功

图 10:

图片描述

图 10 中可以看到:

sprd_verify_cert
pss cert type: %d
invalid cert type %d!!!

这说明它和证书类型、证书结构或签名证书验证有关。

结合图 9 和图 10,可以把 U-Boot 里的 BSP 校验链简化成:

boot / recovery 镜像进入校验入口
-> 校验 table
-> 校验证书 / cert
-> 任一失败则返回失败
-> 上层阻断 boot 启动

8.2.3 Patch 原理:让校验函数固定返回成功

这里的处理思路和前面的 FDL1、FDL2、SPL Loader 一样:不要大范围破坏启动流程,只处理最终决定“校验成功 / 失败”的返回点。

在 AArch64 调用约定里,函数返回值放在 W0 / X0 中:

W0 = 0  通常表示失败
W0 = 1  通常表示成功

因此,对于图 9 和图 10 这两个最终校验函数,可以把函数入口改成固定返回成功:

MOV W0, #1
RET

含义是:

进入校验函数
-> 直接把返回值设置为 1
-> 立即返回上层
-> 上层认为 BSP / boot 校验成功

AArch64 小端字节形式为:

MOV W0,#1    ; 20 00 80 52
RET           ; C0 035F D6

也就是:

20008052 C0 035F D6

这样处理后,上层调用者拿到的结果始终是“校验成功”,非签名 boot 就不会再因为这两个 BSP 校验函数返回失败而被阻断。

8.2.4 这里只讲基本原理

这一层实践起来同样很复杂。实际分析时,需要确认:

  • 图 9、图 10 是否确实是当前样本最终生效的校验函数;

  • 上层是否还存在其他二次判断;

  • patch 后是否影响 recoverynr_agdsp 等其他镜像;

  • 返回值语义是否在当前样本中确实是 1 = 成功

  • 是否还有 AVB / rollback / slot 等后续启动限制。

所以本文这里只介绍基本思路:

搜索 boot 字符串
  -> 过滤定位 boot / recovery 校验入口
  -> 顺调用链追到 table / cert 校验函数
  -> 确认返回值语义
  -> 将关键校验函数改成 MOV W0, #1; RET
  -> 让上层永远收到校验成功

真正落地时,需要结合当前样本反复验证,不能只靠这一段文字机械套用。

8.3 system 去云控

进入 system 后,还可能存在:

  • 云控策略;

  • 联网校验;

  • 开机自动恢复;

  • 远程配置下发;

  • 设备状态回收。

所以 system 去云控是启动链打通后的最后一环。

09

从逆向到落地:完整固化开机流程

恭喜看到这里。到这一步,前面的 FDL1、FDL2、SPL Loader、U-Boot 校验链已经基本讲完,但真正能稳定开机,还差最后一段落地流程。

前面已经提供了足够多的定位思路:

通过魔数定位 FDL1 校验
-> 通过加载地址和分区字符串定位 FDL2 刷写校验
-> 通过 SPL Loader 多重校验处理 sml / trustos / uboot
-> 通过 boot 字符串定位 U-Boot / BSP 校验
-> 通过返回值 patch 让关键校验函数固定成功

接下来,boot 和 system 的修改就需要继续沿着同样的方法深入研究。这里不能再用一两句话概括成固定答案,因为不同版本的 bootvendor_bootsystem、厂商服务、云控策略都可能不一样。

可以把最后阶段理解成:

修改 boot
-> 让非签名 system 能正常启动
-> 修改 system
-> 去掉云控 / 联网策略 / 开机回收
-> 刷入魔改后的 uboot、boot、system
-> 擦除 splloader 分区
-> 将魔改后的 splloader 放入 RP2350 源码
-> 编译 RP2350 固件
-> 烧录 RP2350
-> 焊接到目标 USB 口
-> 上电自动完成 CVE-2022-38694 固化开机

9.1 修改 boot 与 system

boot 这一层通常包含 kernel、ramdisk、启动参数和早期 init 逻辑。想让非签名 system 正常启动,需要继续分析:

  • ramdisk 中的 init 脚本;

  • fstab / dm-verity / AVB 相关配置;

  • 启动参数;

  • vendor_boot 与 boot 的配合关系;

  • system 挂载失败后的回退逻辑。

system 层则重点看:

  • 云控服务;

  • 联网校验;

  • 开机恢复策略;

  • 远程配置下发;

  • 设备状态回收;

  • 与 bootloader lock 状态相关的用户态服务。

这一部分没有通用偏移,必须按当前固件自己分析。

9.2 刷入魔改后的镜像

当 ubootbootsystem 都处理完成后,需要把这些镜像刷回目标设备:

魔改 uboot
魔改 boot
魔改 system

刷入后要确认:

  • U-Boot 不再阻断非签名 boot;

  • boot 能正常挂载 system;

  • system 不再被云控或策略回收;

  • 重启后状态仍然保持。

9.3 擦除 splloader 分区

接下来要擦除目标设备上的 splloader 分区。

这样做的目的,是让设备上电后进入 BootROM 下载模式,然后由外部 RP2350 接管后续发送流程。

流程可以理解成:

擦除 splloader
-> 设备无法从原始 splloader 正常启动
-> 上电进入 BootROM 下载路径
-> RP2350 作为 USB Host 自动发送魔改 splloader

9.4 将魔改 splloader 放入源码并编译 RP2350 固件

魔改后的 splloader 需要放入 RP2350 工程中,作为上电后自动发送的数据。具体如何转换、如何嵌入、如何编译,读者可以直接看项目源码。

最终编译得到 RP2350 固件:

spd_flasher.uf2
spd_flasher.bin

9.5 烧录 RP2350 并焊接到 USB 口

最后将编译好的固件烧录到 RP2350,并把 RP2350 的 USB Host 线路接到目标设备 USB 口。

实际落地时要保证:

  • RP2350 处于 USB Host 模式;

  • D+ / D- 连接正确;

  • GND 共地;

  • 目标设备供电稳定;

  • RP2350 上电时机和目标设备上电时机匹配。

实际焊接效果如下。图中可以看到目标板 USB 口附近的 D+ / D- 引出点:

![图11:目标板 USB D+ / D- 引出点]
图片描述

RP2350 模块烧录固件后,固定到目标板上,并将 USB D+ / D-、供电和 GND 接入目标板:

![图12:RP2350 固定并接入目标 USB 线路]
图片描述

这里的核心不是单纯“飞线”,而是把前面逆向得到的魔改 splloader 固化到 RP2350 中,让目标设备每次上电后都能由 RP2350 自动完成发送流程。

最终效果就是:

目标设备上电
  -> 进入 BootROM
  -> RP2350 自动握手
  -> RP2350 发送魔改 splloader
  -> RP2350 发送 exec stub
  -> 目标进入修改后的启动链
  -> 实现 CVE-2022-38694 固化开机

到这里,整个研究链才从“逆向分析”真正落地成“硬件固化启动”。

10

项目实现

本文重点是逆向思路和整体落地流程,项目源码部分不在这里展开。

RP2350 固化发送、USB Host、BSL/HDLC 封包、状态机、数据数组生成、编译烧录等实现细节,已经放在项目源码中。需要复现或二次开发的读者直接看仓库即可:

https://github.com/LeoChen-CoreMind/spd_flasher


11

结语

本案例的核心价值不在于某一个固定 patch,而在于完整的分析方法:

从标准刷机链入手
-> 分清 BootROM / FDL1 / FDL2 / SPL / U-Boot / system 的职责
-> 用文件格式特征反推校验函数
-> 顺着调用链找到真正阻断控制流的位置
-> 处理失败分支
-> 把验证过的启动链固化到 RP2350

以 FDL1 为例,DHTB 文件头提供了一个非常清晰的入口。将它转换成小端整数 1112819780 后,就能在 FDL1 中定位头部检查函数,再继续追踪到真正的校验函数和失败死循环。

FDL2、SPL Loader、U-Boot 和 system 层也是同样的分析思路:不要直接套别人的地址和 patch,而是先找到当前机型自己的验证链,再做针对化处理。

免责声明

本文基于公开资料、开源项目和个人研究过程整理,只用于科研、教学、逆向工程学习和安全研究。

本文不提供任何可直接用于商业设备的破解文件、私有签名文件、厂商密钥、完整固件包或现成绕过成品。文中涉及的 FDL1、FDL2、SPL Loader、U-Boot、boot、system 等内容,均以分析方法和研究思路为主,具体样本需要读者在自己的研究环境中自行获取、自行分析、自行验证。

不同平台、不同 SoC、不同固件版本的启动链和校验逻辑可能完全不同,本文内容不能直接套用到所有设备。请在合法合规、授权和自有设备范围内进行研究,因错误操作导致设备损坏、数据丢失或其他后果,需要由操作者自行承担。

*本文为看雪论坛精华文章,由 LeoChen.. 原创,转载请注明来自看雪社区

9月10日【议题征集】截止

图片

球分享

球点赞

球在看

点击阅读原文查看更多

阅读原文

跳转微信打开