首页
社区
课程
招聘
[原创]今天学点不一样的打破Linux Bsp安全启动链,自己控制安全启动链与用RP2350 固化CVE-2022-38694注入实践
发表于: 6天前 1579

[原创]今天学点不一样的打破Linux Bsp安全启动链,自己控制安全启动链与用RP2350 固化CVE-2022-38694注入实践

6天前
1579

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

本文不提供任何可直接用于商业设备的破解文件、私有签名文件、厂商密钥、完整固件包或现成绕过成品。文中涉及的 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 后面还会继续校验 smltrustosuboot,这里放一张启动链示意图:

图片描述

这张图根据公开资料整理:TheGammaSqueeze/UnisocBypass README 中明确提到,Unisoc 的 UMS512 安全启动链使用 DHTB + SIMGHDR 格式,SPL 会对它加载的 smltrustosuboot 做 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,这一层更加关键,因为它已经进入正式启动链,后面会继续影响 smltrustosuboot 等镜像能否被加载和执行。

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 自身已经经过处理,能够继续放行后面的 smltrustosuboot

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

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

所以在 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 校验链已经基本讲完,但真正能稳定开机,还差最后一段落地流程。

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

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

可以把最后阶段理解成:

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

system 层则重点看:

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

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

刷入后要确认:

接下来要擦除目标设备上的 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..编辑 ,原因:
收藏
免费 12
打赏
分享
最新回复 (1)
雪    币: 3275
活跃值: (755)
能力值: ( LV8,RANK:130 )
在线值:
发帖
回帖
粉丝
2
感觉嵌入式都没啥人玩
5天前
0
游客
登录 | 注册 方可回帖
返回