做硬件安全这些年,经常会遇到一个很尴尬的局面:设备就在手上,接口、引脚、示波器都备好了,但它内部到底在跑什么逻辑,完全看不出来。尤其是一些老旧的通信控制器、加密协处理器、门禁读卡器,固件不在外部存储里,调试口号称支持结果实际被工厂禁掉,唯一的线索就是那一排排输入输出引脚。这时候如果能把内部的有限状态机(FSM)逆向出来,很多问题就能迎刃而解。最近翻到TIFS 2021上的一篇相关论文,标题翻译过来就是“对硬件动态分析逆向出FSM”,核心思路非常对我胃口:不去折腾固件和调试口,直接从芯片外部的动态行为反推内部状态机。这篇文章适用面其实很广,做硬件安全审计的、做嵌入式测试的、做老旧设备兼容性分析的,甚至搞协议逆向的人,都能从这套方法里找到可以落地的思路。
所谓的FSM,也就是有限状态机,是所有数字硬件控制逻辑的骨架。哪怕是一颗不起眼的单片机,内部也有一个主状态机在调度指令执行、中断响应和总线访问;更复杂的设备比如SD卡控制器、USB PHY、电源管理芯片,内部往往有多个互相嵌套的状态机。只要把状态转移关系摸清了,硬件在你的眼里就不再是一个黑盒子,而是变成了一张可以阅读的逻辑地图。这篇TIFS文章的可贵之处,在于它把“动态分析逆向FSM”这件事做成了一套比较系统的方法,而不是零散的经验堆砌。整篇博文我就围绕这套方法的原理、实操流程和踩坑经验来展开,有兴趣的读者可以直接按着步骤在自己的目标板上试。
1. 为什么需要对硬件动态分析逆向FSM
1.1 硬件分析场景里的FSM
先说清楚FSM在硬件里到底扮演什么角色。数字电路基本上可以分成两大块:数据通路和控制通路。数据通路负责算、搬、存,控制通路负责决定“什么时候算、什么时候搬、什么时候存”,这个控制逻辑就是用状态机实现的。典型的实现方式是一组状态寄存器和一堆组合逻辑:状态寄存器保存当前处在哪个状态,组合逻辑根据当前状态和外部输入决定下一状态是什么、输出什么信号。
实际设备里的状态机比教科书例子复杂得多。比如一个工业现场总线接口芯片,它的主状态机可能要处理帧同步、地址过滤、数据缓冲、错误重传、链路层握手这几个大阶段,每个阶段下面还有若干子状态。手动去分析这种芯片,光靠看波形猜逻辑,时间成本会高到让人放弃。
在这里,动态分析逆向FSM可以解决的恰恰是这个问题:把所有引脚上的输入信号当作“激励”,把输出引脚的响应当作“观测”,用足够多的实验样本让内部状态的行为“显形”。只要激励覆盖得足够全,就能从输入输出关系上还原出状态转移表,再从转移表反推协议流程和控制逻辑。
1.2 动态分析解决了什么难题
做硬件逆向的人通常有两条路。一条是静态路线,想办法弄到固件、解密、反汇编,把代码读懂。问题是现在芯片厂商越来越精,固件可能放在内部ROM里,调试接口上锁,加密引擎把flash内容保护得死死的,合法读取的路径基本被堵死。另一条是动态路线,不碰固件,只看外部行为。优点是物理接触相对容易,一根探针一台逻辑分析仪就能开始,缺点是要靠实验推断,工作量比较大。
动态分析和静态分析并不是互相替代的关系,更多是互补。
| 分析维度 | 静态分析 | 动态分析(本文主题) |
|---|---|---|
| 依赖条件 | 能拿到固件或位流,且能解密 | 只需要物理引脚可访问 |
| 核心产出 | 指令序列、数据结构、算法逻辑 | 状态转移表、协议流程、控制依赖 |
| 障碍点 | 加密、封装、代码混淆 | 状态空间大、实验设计难 |
| 适用目标 | 固件逻辑审计、漏洞挖掘 | 黑盒控制逻辑还原、协议逆向 |
| 时间成本 | 前期投入低,后期分析烧脑 | 前期实验设计复杂,后期自动处理 |
TIFS 2021那篇文章里的方法属于典型的动态分析路线,它把状态机逆向过程模型化,并且讨论了在信号不完整、有噪声、状态空间巨大的情况下如何恢复出可靠模型。对硬件安全评估来说,这套方法让没有原始设计文档的第三方审计者也有机会对设备进行深入分析,这也是它最重要的一层价值。
2. FSM逆向的核心原理与方法选型
2.1 有限状态机模型的识别基础
先建立一个共同语言。一个同步时序有限状态机,形式化看就是个五元组:状态集合S、输入集合I、输出集合O、状态转移函数δ、输出函数λ。如果输出只取决于当前状态,叫Moore机;如果输出同时取决于当前状态和当前输入,叫Mealy机。硬件里两者都有,很多实际设计是两种风格的混合体,所以分析时最好两套模型都准备着。
最麻烦的一点是,状态机内部状态S是不可直接观测的。打个比方,你面前有一台自动贩卖机,你只能看到投币口和出货口,内部的硬币计数器、商品选择器对你来说全是隐变量。你能做的,就是反复投入不同组合的硬币,观察出货结果,再根据这些行为倒推内部的“档位”。硬件逆向FSM也是这个逻辑,输入序列就是投币,输出响应就是出货。
识别的基础是两个假设:第一,设备内部确实存在有限个离散状态;第二,在稳定时钟节拍下,状态和输出的关系是可重复的。只要这两个前提成立,理论上就能用足够多的实验把状态转移表学出来。实际工程里,很多设备还额外满足第三个条件:状态数量不会特别夸张,比如几十到几百个,远小于2的n次方,这给动态推断留出了操作空间。
2.2 黑盒状态推断的工作机制
黑盒推断在软件协议领域已经有比较成熟的算法底子。比如经典的L*算法就是用来学习未知自动机的:维护一个观测表,用等价查询和成员查询不断逼近真实状态机。Mealy机也有对应变体。把这套东西搬到硬件FSM逆向,理论上行得通,但工程上要加不少补丁。
硬件和软件的差异在于时序和并发。软件通信协议的状态机通常是事件驱动的,某一条消息到了才触发转移;硬件FSM则是每一个时钟沿都可能发生转移。也就是说,即便输入保持不动,只要时钟在跑,状态就可能也在切换。这就让“状态签名”变得很关键。
所谓状态签名,就是在一个时钟周期内,把当前所有可观测输出端口和控制信号的值,合并成一个特征向量。因为硬件FSM的当前状态会影响组合逻辑输出,这个特征向量就间接携带了状态信息。实际操作时,我会把每个周期的输入向量和输出向量拼起来,组成一条行为记录;然后根据输出部分相似的记录分组,分组结果就对应候选状态。分组之后,再通过下一个周期的行为差异做细分,反复迭代直到稳定。
等价状态的合并也是不可少的一步。不同状态如果对同样的输入给出同样的输出、并且跳到同一个下一状态,那它们在黑盒语义下就是等价的,可以合并成一个状态。这一步能有效对抗状态爆炸,让最终模型更接近真实设计。
2.3 为什么选择动态分析方案
选动态分析而不是别的方案,首先是因为它对固件加密和调试锁基本免疫。芯片内部跑什么程序,外部根本无法直接改变这一点;但引脚上的输入输出行为是客观存在的,只要探针接得到,数据就采集得到。对于那种存储器已熔断保护、固件完全无法提取的芯片,动态分析几乎是唯一能深入理解其内部逻辑的手段。
其次是成本可控。一套像样的逻辑分析仪和可编程电源,总投入并不夸张,比起高端芯片调试设备友好得多。分析对象也可以很灵活:小到一颗温控器芯片,大到一块完整的嵌入式控制板,只要引脚可访问,方法都通用。缺点当然也有,状态空间大时需要海量实验,而且对完全模拟电路无效,比起静态分析来更费时间。所以它的定位是“攻坚手段”,而非日常首选。
2.4 工具与实验环境选型
这个场景下,工具选型直接决定实验能不能做下去。我建议按重要性排序,先备这几样:
- 多通道逻辑分析仪:至少要16路以上,采样率最好不低于100MHz,存储深度越大越好。状态机跑得快的时候,几十万个时钟周期眨眼就过,存储深度不够会漏掉关键转移。
- 可编程激励源:就是能按照预设序列往输入引脚灌信号的设备,用FPGA开发板最灵活,普通单片机也行。关键是时序可控,能精确到单个时钟周期。
- 可编程电源:要能快速切换电压、能瞬间掉电再上电。很多状态机初始状态依赖复位时序,电源控制得好,实验可重复性会大幅提升。
- 示波器:用于确认关键波形和时序关系,处理动态分析之前的信号质量排查。
- 飞线、探针、转接板:焊接延长线时注意引脚电容,别为了图方便用太长的线,会影响高速信号边沿。
实验环境方面,静电防护是必须的,很多芯片对ESD很敏感。另外要注意探头的接地线尽量短,否则高频采样时会引入大量毛刺,直接影响后续状态分类的准确性。
3. 完整实操流程:从采集到状态机还原
3.1 第1步:目标侦查与接口梳理
拿到一块目标板,先别急着接仪器。我会花至少半天时间做接口梳理。首先是看原理图,把所有可能跟控制逻辑相关的引脚标记出来:时钟输入、复位输入、数据/地址总线、片选线、读写使能、状态指示引脚、中断输出、串口调试引脚等等。没有原理图的,就从PCB走线反推,用万用表蜂鸣档找出芯片引脚连到哪里去。
这一步的目标不是把所有引脚都分析出来,而是找出“对状态机建模最有价值的信号”。我的经验是:时钟线是必找的,复位线是第二重要的。如果没有时钟线可参考,实验会非常被动,因为无法确定状态转移的采样时刻。很多板子即使主芯片加密了,晶振引脚和复位引脚仍然是物理可及的,这就够用了。
接口梳理完,建议画一张引脚功能假设表,记录每个引脚的信号方向、电平域、可能功能。后面做激励和采集时,这张表就是你的作战地图。表里的每个错误假设都会直接体现在模型质量上,所以宁可多花时间核对,也不要急着上电乱戳。
3.2 第2步:激励生成与复位管理
动态分析的实验设计核心是激励生成。好的激励要让目标状态机“动起来”,而且每个状态转移路径都有机会被走到。最原始的做法是随机翻转输入引脚,俗称“引脚fuzzing”,优点是省事,缺点是有大量实验打在无关组合上,效率低。
实用做法是分三级设计激励集。第一级叫“自然序列”,就是按照设备正常的工作流程,输入一组组符合协议语义的信号。比如门禁读卡器,就模拟读卡器天线唤醒、发送请求、接收响应的完整时序。这一级能快速覆盖主流程,把主状态机的大框架跑出来。第二级是“边界扰动”,在主流程基础上修改局部参数,比如缩短帧间隔、改变地址字段、插入非法指令,目的是覆盖状态机里的异常分支。第三级是“随机模糊”,对输入总线做概率性翻转,用作兜底,专门捕获前两级没有覆盖到的犄角旮旯。
复位管理同样关键。很多状态机在上电后有一个确定的复位值,但有些设计里部分寄存器不受复位信号控制,导致每次上电初始状态不同。实操时,我会固定一套复位流程:先断电,再把所有输入引脚置为默认电平,然后上电,等待电源稳定,再释放复位。每次实验都严格按这个固定顺序来,确保初始状态可复现。
3.3 第3步:信号采集与预处理
采集是动态分析里体力活最重的一环。接好逻辑分析仪的通道映射后,先做一次短时采集,确认信号翻转符合预期、没有明显毛刺,再开始批量实验。
采样率设置有个经验值:至少是被测时钟频率的4到8倍。比如目标芯片时钟是25MHz,逻辑分析仪采样率最好定在100MHz以上。太低的采样率会把窄脉冲吃掉,状态机模型会漏掉重要的临时转移。存储深度方面,一次实验最好能覆盖从复位到稳定状态的完整过程,宁可多采也不要因为缓存溢出丢掉尾巴。
预处理一定要做这两件事。第一是毛刺过滤,逻辑分析仪采集到的信号经常有几十纳秒宽度的尖峰,这类信号通常是组合逻辑竞争冒险或者探头耦合噪声,不处理会把状态聚类结果搅浑。做法很简单,在软件里配置最小脉宽过滤,把小于一个阈值(比如20ns)的脉冲丢掉。第二是总线解码,将多根数据线按二进制组合成总线数值,降低数据处理维度。比如8根数据线的一次采集,解码成一个0到255的整数,比分别记录8个bit要直观得多。
3.4 第4步:序列切分与状态聚类
采集完成后的原始trace是不能直接用来建模的。首先要切分。每次实验从复位结束开始,到实验结束为止,去掉前导无效数据,只保留有效激励窗口。这个窗口的起点就是复位释放后的第一个时钟沿,终点可以取实验计划时长,也可以取设备回到空闲状态。
切分完成之后,进入状态聚类的核心环节。我的做法是写一个Python脚本,按以下思路处理:
# 伪代码示意,用于从对齐后的trace构造状态转移表 records = [] for t in range(T-1): cur_sig = signature(t) # 当前周期的输出签名 cur_in = input_vector(t) # 当前周期的输入向量 nxt_sig = signature(t+1) # 下一个周期的输出签名 records.append((cur_sig, cur_in, nxt_sig)) # 初次分组:把输出签名相同的周期聚为候选状态 groups = {} for r in records: groups.setdefault(r[0], []).append(r) # 细化:检查同一候选状态中,相同输入下的下一状态是否一致 # 如果不一致,则按下一状态差异拆分,直到稳定这里最考验经验的是签名向量的构造。如果直接把所有输出引脚都塞进签名,组合逻辑的瞬时输出会让每个周期看起来都像独立状态,分组会非常碎。正确的做法是只保留“受状态寄存器直接驱动”的输出,比如状态指示位、模式标志位;而把数据总线上的组合逻辑输出当作转移条件而不是状态签名的一部分。这个判断需要一点数字电路基础,也是纯自动算法不好替代的部分。
状态聚类的结果是一张候选状态表,每个状态包含多个出现次数和对应的输入输出样本。接下来要做的就是状态合并。从候选状态A和B出发,如果对每一种输入x,A的下一状态集合和B的下一状态集合经过递归比较之后完全相同,且输出也相同,就合并。反复执行这个过程,直到没有可合并状态为止。合并后的状态数,往往就是设备真实状态数的合理估计。
3.5 第5步:状态转移表合成与输出
状态合并完成后,就可以生成最终的状态转移表了。表的基本格式是四列:当前状态、输入条件、下一状态、输出动作。为了让它更好读,我会把自动分配的状态编号(S0、S1、S2)映射成有业务含义的名字,比如“等待帧头”和“接收负载”,映射依据是仿真时观察到的输出标志和典型输入条件。
生成状态转移图这一步我一般用绘图工具手工调整布局,因为完全自动化的布局经常逻辑混乱,没法直接用于分析。真正要交出去给团队看的,是一个带注释的状态图加一份CSV格式的转移表。前者用于理解整体流程,后者用于编写后续的协议测试用例。
值得一提的是,很多设备内部不止一个状态机,它们之间还会互相握手。动态分析得到的主状态机只能反映一路控制流程,但往往已经足够支撑大多数安全分析需求。如果确实需要分析多个交互状态机,就得设计激励同时改变两条控制链路,在数据上做多变量关联,难度会明显上一个台阶,案例也更复杂。
3.6 第6步:模型验证与覆盖率评估
这一步很多人会跳过,但恰恰是我最不建议省掉的部分。状态机的学习结果本质上是一个假设模型,必须经过验证才能用于后续分析。验证的基本思路是留出法:前面设计激励集时,故意预留一组“验证序列”,不参与模型学习,等模型建立后再把验证序列打到设备上,比对模型预测和实际输出是否一致。
覆盖率评估要看三个数字。状态覆盖率,即学到的状态数占模型预测总状态数的比例;转移覆盖率,即被至少一条激励序列走到的转移边占全部转移边的比例;判定覆盖率,即每个状态里的输入条件分支有多少种组合被执行过。对硬件FSM逆向来说,转移覆盖率比状态覆盖率更能反映模型完整度。如果转移覆盖率低于85%,我基本会判定模型不可靠,需要补实验。
还有一种情况要留意:模型验证时设备有点脏,输出时序里混有毛刺,导致比对不一致。这种问题不一定是模型错了,也可能是采集质量不行。先复查信号完整性,再说模型的事。回灌测试也是一种实用性很强的验证方式,把学到的状态转移关系写成上位机脚本,模拟设备的对外行为,如果能通过这个模拟器和真实主机正常通信,那模型的可信度就很高了。
4. 常见问题与排查技巧实录
4.1 状态爆炸:学出一个天气一样多变的状态表
这是新手最容易撞上的问题。学出来的状态数量动辄几百上千,比芯片理论上应有的状态数大一个数量级。症状表现为状态之间互不相同,相似行为无法归类。根本原因多数不在算法,而在前置处理:把太多组合逻辑输出塞进了状态签名里。组合逻辑的输出每个时钟周期都可能随输入变化,但这并不代表状态变了。解决办法是重新选择签名向量,只保留由触发器直接驱动的信号,或者干脆把签名向量换成输出寄存器的锁存值。
4.2 多时钟域导致的采样错位
部分芯片内部有多个时钟域,外部引脚上的信号变化并不是统一节拍。直接对全trace统一时钟沿采样,会把不同时钟域的行为混在一起,状态聚类结果自然混乱。排查方法是先观察输出信号的跳变频率,看看是否存在两组明显不同频率的信号;如果有,就要把签名向量按时钟域分开处理,各自建模型,最后再通过握手信号关联起来。更稳妥的做法是尽量找回主时钟信号,以主时钟沿为全局采样基准。
4.3 复位不干净导致的模型漂移
设备刚上电时,内部寄存器并不一定都处在默认值。尤其是有些设计只在复位引脚上重置部分寄存器,其余靠上电随机状态启动。这会导致同样的输入序列,两次实验得到不同的起始状态,模型无论怎么聚类都无法收敛。处理技巧是:在每次实验开始前,执行多次复位操作后等待足够长的稳定时间,确保状态机进入空闲状态再开始记录;如果实在找不到统一初始状态,就要在分析时标记“多初始状态”分支,不要强行合并。
4.4 毛刺和异步输入干扰
硬件的握手信号经常是异步的,和主时钟无关,容易在采样沿附近发生亚稳态抖动。逻辑分析仪看起来就是某个信号频繁闪烁,把状态聚类结果扰得一团糟。应对方法是两招并用:第一,软件滤波,把窄于几倍采样周期的毛刺直接忽略;第二,在签名向量构造时把这类异步握手信号排除,只作为事件触发条件而非状态特征。实在排不掉,就降低采样率,让信号稳定后再采样。
4.5 保护机制导致的状态锁定
现在的芯片普遍带看门狗、上电时序检测、电压跌落保护。实验过程中如果违反了上电时序,或者输入翻转频率过快让芯片误判为攻击,状态机会直接跳到锁定状态,对外表现为所有输出都不再变化。碰到这种状况,先别急着优化算法,回到实验环境排查:用可编程电源严格控制上电时序,把激励速率限制在正常通信速度的2倍以内,并且每次实验之间加一次完整的掉电复位。等设备行为恢复正常可重复了,再跑大规模激励集。
| 现象 | 可能原因 | 排查/解决方法 |
|---|---|---|
| 状态数爆炸 | 签名向量包含组合逻辑输出 | 改为触发器直驱信号作为签名 |
| 模型时序混乱 | 多时钟域混用统一采样沿 | 分时钟域建模,再关联握手信号 |
| 模型不稳定 | 复位条件不统一 | 固定复位流程,增加稳定等待时间 |
| 聚类结果受毛刺影响 | 异步信号采样抖动 | 脉宽过滤,排除握手信号 |
| 设备突然无响应 | 保护机制触发 | 检查上电时序,降低激励速率 |
5. 写在最后:一点个人体会
做FSM逆向的次数多了,我越来越觉得这件事更像侦探调查而不是数学推导。你永远不知道模型里多出来的那个神秘状态到底是一个真实存在的异常分发逻辑,还只是某根数据线上的噪声。好的分析师往往不是算法用得最花哨的那个,而是最会控制实验条件和观察细节的那个人。给后来者的建议很简单,先找一块引脚少、时钟简单、行为规律的单片机开发板练手,把整个流程跑通,再去碰复杂的多时钟域芯片。第一块板子不要贪功能全,能成功还原出一个几十个状态的模型,你就已经跨过了这道门槛;后面真正困难的不是工具,而是你在设备面前是否足够耐心。