本文基于公开资料、开源项目和个人研究过程整理,只用于科研、教学、逆向工程学习和安全研究。
本文不提供任何可直接用于商业设备的破解文件、私有签名文件、厂商密钥、完整固件包或现成绕过成品。文中涉及的 FDL1、FDL2、SPL Loader、U-Boot、boot、system 等内容,均以分析方法和研究思路为主,具体样本需要读者在自己的研究环境中自行获取、自行分析、自行验证。
不同平台、不同 SoC、不同固件版本的启动链和校验逻辑可能完全不同,本文内容不能直接套用到所有设备。请在合法合规、授权和自有设备范围内进行研究,因错误操作导致设备损坏、数据丢失或其他后果,需要由操作者自行承担。
这个项目的目标,是把 Unisoc / Spreadtrum 平台上原本依赖 PC 工具完成的 BootROM 下载流程,固化到 RP2350 微控制器中。
正常情况下,我们需要在 PC 上使用 spd_dump 一类工具,手动发送 FDL1、FDL2,再进入后续刷写或启动流程。本项目做的是:把前置逆向得到的修改版加载链,内嵌到 RP2350 工程里,让 RP2350 作为 USB Host,在目标设备进入 BootROM 模式后自动完成握手、发送和注入。
项目地址:
本文主要记录两部分内容:
文章重点放在逆向过程和最终落地流程。项目实现细节不在正文展开,读者需要复现时直接查看源码。
完整流程可以概括为:
这里要先明确一点:RP2350 只是把已经研究清楚的流程自动化重放。真正让设备能启动的关键,在前面的逆向和魔改。
在进入具体分析前,需要先把 Unisoc 刷机链里的几个角色分清楚。
BootROM 是芯片上电后最早执行的固化代码。它负责:
BootROM 这一层通常是我们和设备建立通信的入口。
FDL1 是 BootROM 下载并执行的第一阶段加载器。它负责:
本文最先分析的就是 FDL1 对 FDL2 的校验。
FDL2 是第二阶段下载器,功能比 FDL1 更完整。它通常负责:
如果 FDL2 这一层没有处理好,即使 FDL1 已经通过,后续刷入文件仍然可能失败。
SPL Loader 是 BootROM 后正式启动链中的早期加载器。它负责:
当前项目利用的关键点是:
也就是说,可以把修改后的 SPL Loader 作为 FDL 数据写入目标地址。
U-Boot 或 LK 这一层通常负责:
很多时候,前面 FDL1、FDL2、SPL 都处理完了,最后仍然卡在 U-Boot 的验证链。
进入 system 后,还可能存在:
所以 system 去云控是启动链打通后的最后一环。
为了让读者更直观看到 SPL Loader 后面还会继续校验 sml、trustos、uboot,这里放一张启动链示意图:

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

这条命令对应的流程是:
它说明了一个关键事实:
Unisoc 的刷机流程不是一次性把文件写进去,而是分阶段加载、分阶段校验、分阶段交接控制权。
因此,后面的分析不能只盯着一个文件,而要沿着整条链路逐层追。
在标准流程里,各阶段并不是无条件信任下一阶段。常见校验关系可以抽象成:
如果这些校验没有处理,即使 USB 通信和 FDL 下载都正常,仍然可能出现:
所以这不是单点 patch,而是一条多级处理链:
公开项目和 Wiki 只能提供思路,不能直接套用到 UDX710。
UDX710 的启动流程、内存布局、分区组织、厂商改动和验证链实现都可能不同。本文采用的原则是:
公开资料里最值得借鉴的是分析路线:
首先打开 udx710_fdl2,可以看到文件开头是 ASCII 字符串:

对应十六进制字节是:
这个文件头就是定位 FDL1 校验逻辑的第一个线索。
在反编译结果里,文件头不一定以 "DHTB" 字符串出现,更多时候会变成一个整数常量。
DHTB 的 ASCII 字节为:
按 32 位小端整数 解释:
也就是:
所以在反编译结果中看到 1112819780 时,它很可能就是在检查 FDL2 文件头的 DHTB。
这一步的意义是:把文件格式特征转换成可搜索的反编译常量。
拿到 1112819780 后,在 FDL1 反编译结果中搜索该常量,可以定位到图 3 所示位置:

图中核心判断逻辑为:
这段代码说明:
到这里,FDL1 对 FDL2 的识别入口已经缩小到一个很小的范围。
不过,这里通常只是头部检查,不一定是真正的签名或完整性校验。真正决定 FDL2 是否能继续执行的逻辑,还要继续沿调用链追下去。
继续从图 3 的函数往下追踪,可以定位到图 4 所示位置:

图 4 中的核心逻辑可以简化成:
这一段比前面的 DHTB 头部判断更关键:
如果校验通过,代码会继续执行:
也就是跳到被加载镜像的后续入口位置。
因此这里的控制流含义很明确:
FDL1 的处理目标不是破坏整个加载流程,而是让失败分支不再阻断后续执行。
结合图 4,阻断点集中在校验失败后的死循环:
该死循环本质上是失败陷阱。处理方式就是让程序不要停在这里,而是继续走到后面的执行路径。
实际处理时,可以将失败分支对应位置改成空操作,或调整分支逻辑,使其不再落入死循环。这样 FDL1 对 FDL2 的阻断被解除,FDL1 阶段完成。
本节完整定位链如下:
这一节体现的是逆向分析中最重要的方法:先从文件格式特征入手,再用常量引用反推处理函数,最后顺着调用链找到真正影响控制流的分支。
FDL1 处理完成后,下一步就是分析 FDL2。这里的目标不是让 FDL2 能运行,而是让它在刷入目标镜像时不再被校验逻辑阻断。
在 PC 侧验证时,可以通过如下 spd_dump 命令观察 FDL1、FDL2 的加载关系:
这条命令里最关键的是第二个 FDL 地址:
它就是当前样本中 FDL2 的加载地址。后面在 IDA / Ghidra 里分析 udx710_fdl2 时,需要用这个地址作为镜像基址进行加载。基址设置正确后,反编译中的全局变量、函数交叉引用和字符串引用才更容易对上。
这一步的意义是:
加载 FDL2 后,可以从字符串入手搜索待刷入分区名。
图 5 中可以看到,函数里出现了多个和刷写目标相关的字符串:

这些字符串很关键,因为 FDL2 在刷写不同镜像时,需要先判断当前传入的分区或镜像名属于哪一类,再进入对应的处理路径。
从图 5 的逻辑可以看出:
因此,搜索字符串不是为了看字符串本身,而是为了找到 FDL2 中“按分区名分发刷写逻辑”的入口。
顺着图 5 的函数继续追,可以定位到图 6 所示位置。

图 6 中可以看到,函数进入后会先调用:
后面继续出现多次和刷写相关的调用以及错误返回,例如:
这说明当前函数已经不是单纯的字符串分类函数,而是进入了真正刷入流程附近。
这一段的分析链可以总结为:
FDL2 这一层的处理目标,是让刷入流程不要因为分区检查、镜像检查或签名检查而中断。
从图 6 的位置继续分析,可以看到真正影响刷入结果的关键调用和错误路径。处理时的原则和 FDL1 类似:不要破坏整个刷写流程,而是处理会导致失败返回的阻断点。
在当前教学里,这一段只讲基本原理:
实践中,这一步远比一句“找到后 NOP”复杂。不同版本 FDL2 的结构、函数内联、字符串引用、错误路径、BSP 签名检查和真实刷写调用都可能不同。真正操作时需要花很多时间反复验证,不是靠一个固定偏移就能通用解决。
本文在这里给出的是最基本的分析思路:先用加载地址还原运行视角,再用分区字符串定位业务入口,最后顺着调用链找到真正影响刷入结果的分支。
FDL1 和 FDL2 的下载、刷写路径处理完成后,还要继续分析 SPL Loader、U-Boot 和 system 层。
接下来需要处理 SPL Loader。相比 FDL1 和 FDL2,这一层更加关键,因为它已经进入正式启动链,后面会继续影响 sml、trustos、uboot 等镜像能否被加载和执行。
SPL Loader 的整体思路和 FDL1 大同小异:还是先通过魔术值、镜像头、字符串或已知结构定位到校验函数,再顺着调用链找到真正阻断启动的失败分支。
不同点在于:SPL Loader 后面要处理的不再只是一个 FDL2,而是多个启动镜像。它通常会继续校验:
所以和前面的 FDL1 相比,SPL Loader 里会多出好几个校验点。这里不能只处理一个分支,而是要把相关校验路径逐一确认、逐一处理。
图 7 就是 SPL Loader 中比较典型的一段多重校验逻辑:

图中可以看到,多次出现类似的调用结构:
这和前面 FDL1 的失败逻辑很像:
区别是,SPL Loader 里这类检查不止一处。它可能分别对应不同镜像、不同加载地址或不同启动阶段。例如图中能看到多次对 sub_2802FC2C(...) 的调用,并且参数不同,这说明它不是一次简单校验,而是在按不同镜像或不同区域做连续检查。
因此 SPL Loader 这一层的处理方法可以概括成:
这里要注意,SPL Loader 是整条链里非常关键的一层。FDL1 和 FDL2 更多是在下载、刷写阶段起作用;SPL Loader 则会直接影响正式启动链。如果只处理 FDL1 / FDL2,而 SPL Loader 对后续镜像的校验仍然存在,设备仍然会在启动阶段卡死。
当前项目利用的关键点就是:
也就是说,我们最后把修改后的 SPL Loader 当作 FDL 数据写入 0x28007000,前提就是这个 SPL Loader 自身已经经过处理,能够继续放行后面的 sml、trustos 和 uboot。
本节只讲基本原理。实际分析时,SPL Loader 的校验点数量、函数内联情况、镜像加载顺序、参数含义和失败路径都要逐一核对。不能看到一个 while(1) 就认为全部完成,必须把相关校验调用全部追完。
众所周知,从上面的启动流程可以看出:SPL Loader 校验通过后,会继续启动 U-Boot / LK;U-Boot 再继续处理 boot、recovery 等启动镜像的校验和加载。
所以在 SPL Loader 之后,还需要继续看 U-Boot 这一层。U-Boot / LK 通常决定:
这一层处理完成后,启动链才算真正从 BootROM、FDL、SPL 走到了系统启动前的关键位置。
这里的分析入口依然是字符串。
因为目标是启动非签名 boot,所以可以在 U-Boot 里大量搜索 boot 相关字符串,然后结合上下文过滤无关结果。只看字符串本身没有意义,重点是找到“哪些函数在根据分区名决定是否需要校验”。
图 8 中可以看到一个很典型的函数:

这个函数里出现了:
从逻辑上看,它会判断当前镜像名:
因此图 8 这个函数可以理解为 U-Boot 里和 BSP / boot 镜像校验相关的分发入口。
继续顺着图 8 的调用关系往下追,最终定位到图 9 和图 10 这两个关键函数。
图 9:

图 9 中可以看到明显的校验日志:
这个函数的返回值很关键:
图 10:

图 10 中可以看到:
这说明它和证书类型、证书结构或签名证书验证有关。
结合图 9 和图 10,可以把 U-Boot 里的 BSP 校验链简化成:
这里的处理思路和前面的 FDL1、FDL2、SPL Loader 一样:不要大范围破坏启动流程,只处理最终决定“校验成功 / 失败”的返回点。
在 AArch64 调用约定里,函数返回值放在 W0 / X0 中:
因此,对于图 9 和图 10 这两个最终校验函数,可以把函数入口改成固定返回成功:
含义是:
AArch64 小端字节形式为:
也就是:
这样处理后,上层调用者拿到的结果始终是“校验成功”,非签名 boot 就不会再因为这两个 BSP 校验函数返回失败而被阻断。
这一层实践起来同样很复杂。实际分析时,需要确认:
所以本文这里只介绍基本思路:
真正落地时,需要结合当前样本反复验证,不能只靠这一段文字机械套用。
进入 system 后,还可能存在:
所以 system 去云控是启动链打通后的最后一环。
恭喜看到这里。到这一步,前面的 FDL1、FDL2、SPL Loader、U-Boot 校验链已经基本讲完,但真正能稳定开机,还差最后一段落地流程。
前面已经提供了足够多的定位思路:
接下来,boot 和 system 的修改就需要继续沿着同样的方法深入研究。这里不能再用一两句话概括成固定答案,因为不同版本的 boot、vendor_boot、system、厂商服务、云控策略都可能不一样。
可以把最后阶段理解成:
boot 这一层通常包含 kernel、ramdisk、启动参数和早期 init 逻辑。想让非签名 system 正常启动,需要继续分析:
system 层则重点看:
这一部分没有通用偏移,必须按当前固件自己分析。
当 uboot、boot、system 都处理完成后,需要把这些镜像刷回目标设备:
刷入后要确认:
接下来要擦除目标设备上的 splloader 分区。
这样做的目的,是让设备上电后进入 BootROM 下载模式,然后由外部 RP2350 接管后续发送流程。
流程可以理解成:
魔改后的 splloader 需要放入 RP2350 工程中,作为上电后自动发送的数据。具体如何转换、如何嵌入、如何编译,读者可以直接看项目源码。
最终编译得到 RP2350 固件:
最后将编译好的固件烧录到 RP2350,并把 RP2350 的 USB Host 线路接到目标设备 USB 口。
实际落地时要保证:
实际焊接效果如下。图中可以看到目标板 USB 口附近的 D+ / D- 引出点:
![图11:目标板 USB D+ / D- 引出点]

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

这里的核心不是单纯“飞线”,而是把前面逆向得到的魔改 splloader 固化到 RP2350 中,让目标设备每次上电后都能由 RP2350 自动完成发送流程。
最终效果就是:
到这里,整个研究链才从“逆向分析”真正落地成“硬件固化启动”。
本文重点是逆向思路和整体落地流程,项目源码部分不在这里展开。
RP2350 固化发送、USB Host、BSL/HDLC 封包、状态机、数据数组生成、编译烧录等实现细节,已经放在项目源码中。需要复现或二次开发的读者直接看仓库即可:
本案例的核心价值不在于某一个固定 patch,而在于完整的分析方法:
以 FDL1 为例,DHTB 文件头提供了一个非常清晰的入口。将它转换成小端整数 1112819780 后,就能在 FDL1 中定位头部检查函数,再继续追踪到真正的校验函数和失败死循环。
FDL2、SPL Loader、U-Boot 和 system 层也是同样的分析思路:不要直接套别人的地址和 patch,而是先找到当前机型自己的验证链,再做针对化处理。
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于 6天前
被LeoChen..编辑
,原因: