刷进去就跑飞:查了一圈签名,最后发现是 memcpy 叛变了
本文最后更新于22 天前,其中的信息可能已经过时,如有错误请留言评论。

TL;DR:驻留在 Boot 区的安全库引用了 App 区才链接的 libc memcpy,库里的 call 目标因此在出厂时被写死。整片烧录始终自洽,分体刷写换新 App 后布局一挪就跳到错误地址,直接跑飞。修法:驻留区必须自包含,并用 nm 符号白名单把这条规则变成构建门禁。

起因

控制器是 Boot + App 分体结构,App 走 UDS over CAN 刷写。Boot 区驻留着一个安全库(SecLib),PRNG、CMAC 验签都在里面;App 通过一张固定地址的跳表 SecLib_apiS 调用它。这个库出厂烧进什么就是什么,刷写阶段永远不更新。

事情起于一个很小的改动:去掉 App 工程里的 boot 库依赖。去掉之后链接器顺手把 SecLib_apiS 丢了——调试时找不到符号,第一反应怀疑是编译器优化,其实只是没人引用它,被正常回收了。

问题本身好解决,但手头有两个方案,都编出来对比:

  • 方案一:在链接脚本里加 EXTERN(SecLib_apiS ...),强制符号留下;
  • 方案二:在代码里写一段永远不会执行的假引用,骗链接器把符号链进来。

两版 hex 都出来了,都过了签名工具,然后诡异的事来了:方案一刷进去正常跑;方案二刷进去就飞。同一份源码、同一套签名工具,差别只是“用哪种方式留住一个符号”。

先怀疑签名——错了

第一个怀疑对象是签名。两版 hex 内容不一样,万一签名工具对块边界的处理有差异呢?于是把 Boot 里验签用的密钥和算法抠出来,离线把两版镜像的 CMAC 都复算了一遍,都是合法的。而且方案一能跑,说明刷写链路、验签流程整体是通的。排除。

再怀疑启动代码——又错了

接着翻 hex 发现另一个可疑点:App-only 的 hex 在 0x80000000 还带着 624 字节的启动代码,_start 和复位向量都在。一个不带 Boot 的 App 要 _start 干嘛?会不会跟 Boot 的跳转打架?

看着很像凶手,但有个致命矛盾:方案一也带着这 624 字节,它怎么就能跑?所以顶多算个卫生问题(后来顺手从公用 ld 里清掉了),不是根因。

两个方向都扑空的时候,确实有点烦躁。每个单点都是绿的:签名对、Boot 对、App 整片烧进去也能跑,偏偏“分体刷写”这个组合是红的。

调试器一看,真相很简单

最后老老实实上调试器,从跑飞现场往前倒,很快定位到:驻留在 Boot 区的 tc_ctr_prng_init() 里,执行到一次 memcpy 时,跳转目标落在了 App 区。

愣了一下才反应过来这意味着什么。

SecLib 是驻留代码,它的机器码——包括里面每条 call 的目标地址——在出厂烧录那一刻就写死了,CAN 刷写永远碰不到它。但它里面的 memcpy 是 libc 的,而 libc 跟着 App 链接。也就是说,驻留区里有一条 call,目标地址指向 App 区的某个位置。

整片烧录时,Boot 和 App 来自同一次链接,这个地址天然是对的,所以这个雷永远不会响。CAN 刷写换新 App 就不一样:方案二多了一段死代码,整个 App 布局往后挪,memcpy 搬了家,驻留区里那条写死的 call 跳到错误的地方,跑飞。

方案一为什么能跑?不是它更优,是侥幸:它的布局变化没挪动 memcpy 的落点。只要驻留区还引用着 App 区的符号,下次随便哪个改动都可能踩响。

最坑的是:tc_ctr_prng_init 的源码里根本没有明写 memcpy(...),那是编译器看到结构体赋值,自己隐式合成出来的调用。拿着源码评审一百遍也看不出来,必须看目标文件。

修复

方向明确后,改起来反而简单:

  1. 把 SecLib 里所有对 libc 内存函数的依赖,换成驻留区内部自实现的 CS_MemCpy;可能被编译器隐式合成 memcpy 的写法(结构体整体赋值等)也一并改写;
  2. 重新构建 libBootloader.a;
  3. 对重建后的库做符号审计:nm 拉出所有未解析符号,逐个核对,只允许落在驻留区内部,或者链接脚本钉死地址的契约符号上。出现 memcpy、memset 这类 libc 符号即为不合格。

审计通过后,两版方案的 hex 在功能上就等价了。最终留了方案一,死代码删掉;那段假引用本来就是临时手段,又难看又有副作用。

复盘:这条事故教的三件事

这次故障折腾人的地方在于它违反直觉:坏的代码不在你刷进去的东西里,而在你刷不进去的东西里。拿着新 App 翻来覆去地查,其实问题出在那块你以为“永远不变所以永远可靠”的驻留区。

驻留区必须自包含。 凡是分体刷写更新不到的区域,里面代码的每一次调用、每一个数据引用,都必须落在驻留区内部或钉死地址的契约符号上。指向 App 区的引用就是定时炸弹:整片烧录时天然自洽,永远不暴露,只在分体刷写之后爆炸。

隐式 libc 调用,源码评审防不住。 编译器会背着你合成 memcpy、memset,唯一的办法是构建后对驻留库做 nm 符号白名单审计,把它变成门禁,别靠人眼。

“整片烧录正常、分体刷写后跑飞”是个固定症状模式。 再遇到,别先怀疑新镜像和签名。推荐顺序:先固化驻留区的实际版本和布局,然后 objdump 扫驻留段所有 call/jump 目标是否越界,再排除签名差异,最后才轮到新 App 本身。另外注意区分真跑飞和“Boot 拒绝启动”——后者还能响应诊断,长得像但不是一回事。

要留符号就用 EXTERN。 死代码那种假引用语义难看、有运行时副作用、还白白扰动布局,没有任何理由用。

希望下一次;遇到了,少怀疑一天人生。

文末附加内容
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇