多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

RK3568安全启动与SO库芯片绑定实战:从OTP烧写到防移植加固

RK3568安全启动与SO库芯片绑定实战:从OTP烧写到防移植加固 1. 项目缘起与整体设计思路1.1 为什么要做安全启动加SO库芯片绑定先说结论这套方案解决的核心问题是固件被非法移植到非授权硬件上运行。RK3568 作为瑞芯微旗下主流的四核 Cortex-A55 应用处理器被大量用在工业网关、边缘计算盒子、电力终端、充电桩控制板这类设备上。这类设备有个共同特点——硬件本身不值多少钱但里面跑的算法、协议栈、业务逻辑才是真正值钱的东西。很多团队辛辛苦苦调了半年的 EtherCAT 主站、运动控制算法、私有加密协议最后打包成一个.so动态库丢到设备里结果设备一到客户手里固件被人 dump 出来直接烧到另一块便宜的板子上就跑起来了前期投入全打水漂。我见过最离谱的一个案例某工业网关厂商的固件被完整提取后对方只换了块 RK3566 的核心板RK3566 和 RK3568 引脚高度兼容很多外围电路不用改连外壳都没换就出货了。所以单纯靠固件加密是不够的必须把软件和这颗芯片本身绑死。安全启动Secure Boot解决的是固件不被篡改芯片绑定Chip Binding解决的是固件不被移植两者叠加才形成完整的防护闭环。而 OTPOne-Time Programmable一次性可编程存储区就是承载这个绑定关系的物理载体——它烧进去就改不了这是整个信任链的根。1.2 整体方案的分层结构我把这套流程拆成四层来看从下往上依次是层级作用关键载体硬件信任根存储不可篡改的密钥/哈希RK3568 OTP 区域启动链验证逐级校验固件签名BootROM → SPL → U-Boot → Kernel系统层校验校验根文件系统完整性dm-verity / AVB应用层绑定SO 库与芯片唯一标识绑定自定义校验逻辑 OTP 中的 Chip ID这里要特别说明一点安全启动和 SO 库绑定不是一回事但必须配合使用。安全启动保证的是从 BootROM 到内核这一路没被改过它管不到你应用层那个.so文件。而 SO 库绑定如果单独做攻击者完全可以把你的校验逻辑 patch 掉再运行。所以正确姿势是先用安全启动把整条启动链锁死让攻击者没法轻易改系统再在应用层做芯片绑定形成纵深防御。1.3 方案选型的几个关键取舍在动手之前有几个决策点必须先想清楚否则后面会反复返工。第一个取舍OTP 烧不烧、烧什么。RK3568 的 OTP 区域容量有限而且烧写次数是物理限制的——虽然官方文档说某些区域可以烧多次但实践中一旦烧错基本就废了。我的建议是OTP 里只烧最核心的东西比如 Secure Boot 的公钥哈希、Chip Unique ID 的锁定标志。业务相关的绑定信息尽量放在加密分区里用 OTP 里的密钥去解密这样灵活性和安全性兼顾。第二个取舍SO 库绑定的粒度。是绑定到整颗芯片的唯一 ID还是绑定到某个 OTP 里烧的授权码前者简单但没法做授权管理比如你想限制某台设备只能用 30 天后者灵活但需要额外的授权服务器配合。我一般推荐双因子Chip ID 做硬件指纹OTP 授权码做业务授权两个都对上才放行。第三个取舍校验放在哪一层。放在 SO 库加载时校验dlopen之前还是放在 SO 库内部首次调用时校验前者容易被 hook后者稍微隐蔽一点但也不是绝对安全。我的做法是两处都放加载时做一次粗校验关键函数入口再做一次细校验增加破解成本。2. 核心细节解析与实操要点2.1 RK3568 安全启动的信任链是怎么跑起来的RK3568 的启动流程大致是BootROM → SPLTPLSPL→ U-Boot → Kernel → Rootfs。安全启动的本质就是在这条链的每一环用上一环的公钥去验证下一环的签名。具体来说BootROM 里固化了一段只读代码它会去读 OTP 里烧的公钥哈希Public Key Hash然后用这个哈希去校验 SPL 的签名。SPL 验证通过后SPL 里又包含了 U-Boot 的公钥去校验 U-Boot。以此类推。这条链上任何一环签名对不上启动就直接停在那一环设备变砖其实是变安全砖需要重新烧正确的固件。这里有个很多人踩的坑RK3568 的 OTP 里烧的是公钥的哈希不是公钥本身。公钥是打包在固件里的。这样做的好处是 OTP 占用小坏处是你换密钥对的时候如果 OTP 已经烧了旧公钥的哈希新固件用新私钥签名就对不上了。所以密钥一旦确定OTP 烧写前一定要反复确认。2.2 密钥生成与签名工具链瑞芯微提供了一套rk_sign_tool工具链核心命令是rk_sign_tool和boot_merger。整个流程分三步第一步生成密钥对。用 RSA 2048 或 4096我一般用 4096虽然签名慢一点但安全边际高# 生成私钥 openssl genrsa -out private_key.pem 4096 # 提取公钥 openssl rsa -in private_key.pem -pubout -out public_key.pem第二步用私钥对各个阶段的固件签名。RK3568 的签名工具需要指定一个配置文件里面写清楚哪个文件用哪个密钥签# sign_config.ini 示例 [SPL] file spl.bin key private_key.pem [UBOOT] file uboot.bin key private_key.pem [KERNEL] file boot.img key private_key.pem第三步用boot_merger把签名后的各段固件合并成最终的update.img。注意签名工具对文件路径和格式很敏感建议所有文件放在同一个目录下操作路径里不要有中文和空格否则会报一些莫名其妙的错误。2.3 OTP 烧写的正确姿势OTP 烧写是整个流程里最不可逆、最容易出事的一步。RK3568 的 OTP 操作通过rkdeveloptool或者 U-Boot 下的otp命令完成。烧写前必须做的检查确认固件已经用正确的密钥签名并且能在不烧 OTP 的情况下正常启动先验证签名逻辑没问题确认公钥哈希计算正确。哈希算法一般是 SHA-256计算的是公钥的 DER 编码确认设备电量充足或者用稳定电源烧写过程中断电基本等于报废烧写命令示例在 U-Boot 下# 读取当前 OTP 状态 otp read 0 0x20 # 烧写公钥哈希到指定 OTP 地址 otp write 0x20 hash_value # 锁定 OTP 区域这一步之后不可再改 otp lock 0x20提示otp lock一旦执行该区域永久只读。我建议先在开发板上完整走一遍流程确认无误后再上量产板。量产时最好写个脚本自动校验哈希值避免人工输入出错。2.4 SO 库芯片绑定的实现原理SO 库绑定的核心思路是在 SO 库内部读取芯片的唯一标识和预先烧录的授权信息做比对不匹配就不执行核心逻辑。RK3568 的芯片唯一标识可以从几个地方拿CPU ID通过读取/proc/cpuinfo里的 Serial 字段但这个字段在某些固件版本里可能为空OTP 中的 Chip ID最可靠通过ioctl或者直接读/sys/bus/nvmem/devices/.../nvmem获取eFuse 区域部分批次有独立的 eFuse 存储区我一般用 OTP 里的 Chip ID因为它是芯片出厂时烧的全球唯一且不可改。读取方式// 通过 nvmem 接口读取 Chip ID int fd open(/sys/bus/nvmem/devices/rockchip-otp0/nvmem, O_RDONLY); unsigned char chip_id[16]; pread(fd, chip_id, 16, CHIP_ID_OFFSET); close(fd);拿到 Chip ID 后和授权信息比对。授权信息可以存在加密分区里用 OTP 里的密钥解密后得到。比对逻辑要写得隐蔽一点不要一个if-else就完事可以拆成多个函数、加一些无意义的运算混淆。2.5 校验逻辑的加固技巧单纯比对 Chip ID 太容易被绕过了攻击者只要找到那个if语句改成永远为真就行。我常用的加固手段有几个第一把校验结果分散到多个变量里。不要用一个bool ok而是用多个int变量最后用一个复杂的表达式综合判断。比如int a check_part1(); int b check_part2(); int c check_part3(); // 只有三个都通过result 才等于预期值 int result (a * 31 b * 17 c * 7) ^ 0x5A5A; if (result ! EXPECTED_VALUE) { // 触发异常路径 }第二关键函数做内联校验。把核心算法函数拆成几段每段入口都校验一次攻击者要 patch 就得 patch 好几处。第三加反调试。检测ptrace、检测/proc/self/status里的TracerPid发现被调试就改变行为注意不是直接退出而是返回错误结果让攻击者以为是自己算错了。注意这些加固手段都是提高破解成本不是绝对安全。真正的安全边界还是靠安全启动把系统锁死让攻击者没法轻易动态调试。3. 实操过程与核心环节实现3.1 环境准备与工具清单先列一下我这次实操用的环境和工具方便你对照项目版本/型号说明芯片RK3568正点原子 ATK-DLRK3568 开发板SDKRockchip Linux SDK 5.10内核 5.10U-Boot 2017.09签名工具rk_sign_tool 1.3瑞芯微官方烧写工具rkdeveloptool 1.4Linux 下用交叉编译aarch64-linux-gnu-gcc 9.3编译 SO 库调试串口1500000 波特率RK3568 默认SDK 的获取和编译这里不展开假设你已经能正常编译出boot.img、uboot.img、rootfs.img这些基础固件。3.2 第一步生成签名密钥并配置签名先生成密钥对注意私钥一定要保管好最好离线保存mkdir -p ~/rk3568_keys cd ~/rk3568_keys openssl genrsa -out rk3568_private.pem 4096 openssl rsa -in rk3568_private.pem -pubout -out rk3568_public.pem # 计算公钥哈希SHA-256 openssl rsa -in rk3568_private.pem -pubout -outform DER | sha256sum记下这个哈希值后面烧 OTP 要用。然后在 SDK 目录下配置签名。RK3568 SDK 里一般有build.sh或者mk_firmware.sh里面会有签名相关的配置项。找到RK_SIGN_ENABLE之类的开关打开它并指定密钥路径。3.3 第二步签名并打包固件以 U-Boot 为例签名命令大致是这样# 进入 SDK 的 u-boot 目录 cd u-boot # 编译 make rk3568_defconfig make -j$(nproc) # 签名具体参数以你的 SDK 版本为准 ../tools/rk_sign_tool/rk_sign_tool \ --key ../rk3568_keys/rk3568_private.pem \ --input u-boot.bin \ --output u-boot-signed.binKernel 和 rootfs 类似处理。全部签完后用boot_merger合并boot_merger --pack update.img \ --spl spl-signed.bin \ --uboot u-boot-signed.bin \ --boot boot-signed.img \ --rootfs rootfs-signed.img3.4 第三步烧写 OTP 并验证安全启动这一步是整个流程的分水岭。先把设备进入 MaskROM 模式按住恢复键上电然后用rkdeveloptool烧写# 查看设备 rkdeveloptool ld # 烧写完整固件先不烧 OTP验证签名固件能启动 rkdeveloptool db rk3568_spl_loader.bin rkdeveloptool wl 0 update.img rkdeveloptool rd如果签名固件能正常启动说明签名逻辑没问题。接下来烧 OTP# 进入 U-Boot 命令行启动时按 CtrlC # 烧写公钥哈希 otp write 0x20 你的公钥哈希 # 锁定 otp lock 0x20 # 重启 reset重启后如果设备正常启动说明安全启动生效了。这时候你可以做个测试故意烧一个没签名的固件看设备是不是停在启动阶段。如果是说明信任链起作用了。提示测试变砖的时候一定要确保你手上有正确的签名固件和烧写工具否则真的救不回来。我一般会准备两块板子一块做实验一块做备份。3.5 第四步编写 SO 库芯片绑定逻辑先写一个读取 Chip ID 的函数#include stdio.h #include fcntl.h #include unistd.h #include string.h #define OTP_PATH /sys/bus/nvmem/devices/rockchip-otp0/nvmem #define CHIP_ID_OFFSET 0x08 #define CHIP_ID_LEN 16 int read_chip_id(unsigned char *out) { int fd open(OTP_PATH, O_RDONLY); if (fd 0) { return -1; } ssize_t n pread(fd, out, CHIP_ID_LEN, CHIP_ID_OFFSET); close(fd); return (n CHIP_ID_LEN) ? 0 : -1; }然后写校验逻辑。假设授权信息存在/etc/license.bin是用 OTP 里的密钥加密的int verify_binding(void) { unsigned char chip_id[16]; if (read_chip_id(chip_id) ! 0) { return -1; } unsigned char license[64]; FILE *fp fopen(/etc/license.bin, rb); if (!fp) { return -1; } fread(license, 1, sizeof(license), fp); fclose(fp); // 解密 license这里用简单的异或演示实际用 AES unsigned char decrypted[16]; for (int i 0; i 16; i) { decrypted[i] license[i] ^ license[i 16]; } // 比对 int diff 0; for (int i 0; i 16; i) { diff | (chip_id[i] ^ decrypted[i]); } return (diff 0) ? 0 : -1; }编译成 SO 库aarch64-linux-gnu-gcc -shared -fPIC -O2 -o libcore.so core.c3.6 第五步集成到应用并测试应用层加载 SO 库之前先做一次校验SO 库内部关键函数再做一次// 应用层 void *handle dlopen(libcore.so, RTLD_NOW); if (!handle) { // 处理错误 } // 调用 SO 库里的校验函数 int (*verify)(void) dlsym(handle, verify_binding); if (verify() ! 0) { // 校验失败走异常路径 }测试的时候我一般会做三组对照正常板子 正常固件 → 应该正常跑正常板子 篡改过的 license → 应该失败另一块板子 正常固件 → 应该失败因为 Chip ID 不同三组都符合预期说明绑定逻辑生效了。4. 常见问题与排查技巧实录4.1 安全启动相关的高频问题问题一烧了 OTP 之后设备起不来串口没有任何输出。这是最吓人的情况。先别慌大概率是公钥哈希烧错了或者签名固件本身有问题。排查步骤确认设备是否还能进 MaskROM 模式按住恢复键上电看rkdeveloptool ld能不能识别如果能识别说明 BootROM 还活着重新烧正确的签名固件如果识别不了检查 USB 线和供电换台电脑试试注意RK3568 的 OTP 一旦锁定公钥哈希就改不了了。如果烧错了唯一的办法是换芯片。所以量产前一定要在开发板上验证充分。问题二签名固件能启动但烧了 OTP 后启动到一半卡住。这种情况一般是某一级固件的签名对不上。比如 SPL 签名对了但 U-Boot 没签或者签错了。排查方法用rk_sign_tool的校验功能逐个检查每级固件的签名确认签名配置里每一级都指定了正确的密钥确认boot_merger打包时用的是签名后的文件不是原始文件问题三换密钥后旧固件还能启动。这说明 OTP 没烧成功或者烧的地址不对。用otp read读一下对应地址确认哈希值是不是你期望的。如果读出来是空的或者全 0说明没烧进去。4.2 SO 库绑定相关的高频问题问题一读不到 Chip ID/sys/bus/nvmem/devices/下没有对应节点。不同 SDK 版本OTP 的 sysfs 路径可能不一样。先用ls /sys/bus/nvmem/devices/看一下实际有哪些节点然后逐个试。有些版本是rockchip-otp0有些是rk3568-otp。如果完全没有可能是内核配置里没打开 OTP 驱动需要重新配置内核。问题二SO 库在开发板上能跑在量产板上失败。大概率是 Chip ID 读取偏移量不对。不同批次的芯片OTP 布局可能有细微差异。建议在代码里做兼容处理比如先读一个已知的固定值比如厂商 ID确认偏移量正确后再读 Chip ID。问题三校验逻辑被绕过。如果发现有人破解了你的绑定先别急着加更多混淆先检查安全启动是不是真的生效了。很多时候问题不在应用层而是安全启动根本没锁住攻击者直接改了系统。用otp read确认 OTP 状态用签名校验工具确认固件签名。4.3 常见问题速查表现象可能原因排查方向烧 OTP 后无法启动公钥哈希错误检查哈希计算和烧写地址启动卡在某一级该级固件签名缺失逐级检查签名配置读不到 Chip IDsysfs 路径不对遍历 nvmem 设备节点绑定在量产板失败OTP 偏移量差异做偏移量兼容处理校验被绕过安全启动未生效确认 OTP 锁定状态SO 库加载失败依赖库缺失用ldd检查依赖4.4 几条踩坑总结第一条OTP 操作一定要有回滚预案。我现在的习惯是每次烧 OTP 之前先把当前 OTP 内容完整读出来备份虽然烧错了也回不去但至少知道错在哪。第二条签名密钥不要放在代码仓库里。我见过有团队把私钥直接 commit 到 git 里这等于没做安全启动。私钥应该离线保存签名在独立的构建机上做。第三条SO 库绑定不要只做一层。加载时校验、函数入口校验、关键数据校验三层叠加。攻击者要破解就得同时 patch 三个地方成本高很多。第四条测试要充分。我一般会准备至少 5 块不同批次的板子做交叉测试确保 Chip ID 读取和绑定逻辑在所有批次上都正常。曾经遇到过某个批次的 OTP 布局和之前不一样导致绑定全部失败幸好量产前发现了。第五条文档要写清楚。这套流程涉及密钥管理、OTP 烧写、固件签名多个环节任何一环出错都可能导致设备报废。我一般会写一份详细的操作手册每一步都有截图和校验命令让产线的人照着做就行减少人为失误。最后分享一个实用的小技巧在 SO 库的校验逻辑里可以加一个授权有效期的检查从 OTP 里读一个烧录时间戳和当前系统时间比对。这样即使设备被破解授权过期后也没法继续用。当然系统时间可以被改所以这个时间戳最好用安全启动保护起来或者从网络时间服务器获取。这个思路在做租赁设备、试用设备的时候特别有用。
返回列表