❄️ 我的个人专栏:
《智能软件工程AI4SE》
《嵌入式面试总结》
《嵌入式处理器架构解析》
《嵌入式与虚拟化》
《嵌入式软件测试》
🌟 Simplicity is the ultimate sophistication
摘要:本文系统梳理嵌入式软件静态测试中安全导向代码审查的方法论,聚焦越权与重放两类业务逻辑漏洞的人工发现路径。文章从业务逻辑漏洞难以被自动化工具发现的原因切入,给出审查前的资产梳理、状态机绘制与数据流清单等准备工作,并分别针对越权漏洞(校验缺失、可绕过、与动作分离)和重放漏洞(无新鲜度字段、校验不记录、时间窗口过宽)提出审查切入点、典型代码特征与攻击场景推演方法,最后总结五步审查流程、漏洞记录要素及纵深防御与新鲜度持久化的修复建议。
1. 引言
在嵌入式软件的安全测试体系中,静态测试往往聚焦于内存越界、空指针、资源泄漏等通用缺陷。然而,对于承载业务逻辑的嵌入式系统——如智能门锁、车载控制器、工业网关——真正致命的漏洞往往不在内存层面,而在于业务逻辑本身:越权访问、重放攻击、状态机跳变等。这类漏洞难以通过自动化工具发现,必须依赖安全导向的人工代码审查。
本文围绕嵌入式软件静态测试中的安全审查实践,系统梳理手动发现越权与重放两类业务逻辑漏洞的方法论,包括审查切入点、代码特征识别、攻击场景推演和修复建议。
2. 为什么业务逻辑漏洞难以被自动化工具发现
传统静态分析工具擅长检测数据流、控制流和内存安全类问题,但对业务语义的理解非常有限。越权和重放漏洞的本质是「业务规则被绕过」,而非「代码路径写错」,因此自动化工具很难给出有效告警。
- 越权漏洞:代码语法完全正确,只是缺少权限校验或校验逻辑可被绕过,工具无法判断「此处是否应当校验身份」。
- 重放漏洞:报文格式合法、校验通过,只是缺少新鲜度(nonce、时间戳、序列号)验证,工具无法理解「同一报文不应被接受两次」的业务约束。
- 状态机漏洞:系统在异常状态下接受了本不该接受的指令,工具难以建模完整业务状态机。
因此,安全导向的代码审查必须由人工完成,且需要一套系统化的方法论,而不是漫无目的地读代码。
3. 审查前的准备工作
在开始逐行审查之前,审查者需要先建立对系统的整体认知。准备工作充分与否,直接决定审查的效率和深度。
3.1 梳理资产与信任边界
首先明确系统中哪些数据、功能、指令属于敏感资产,哪些代码路径跨越了信任边界。典型的信任边界包括:外部通信接口与内部处理逻辑之间、用户态与特权态之间、不同权限角色之间。
3.2 绘制业务状态机
从需求文档和代码中提取系统的业务状态机,明确每个状态下允许执行的指令集合。状态机是发现越权和重放漏洞的重要参照系——凡是「当前状态下不应被接受的指令却被执行」的情况,都值得深挖。
3.3 建立数据流清单
列出所有外部输入入口(串口、CAN、以太网、无线、按键等),以及每个入口对应的处理函数、数据流向和最终影响。重点关注「外部输入直接驱动业务动作」的路径。
4. 越权漏洞的手动审查方法
越权漏洞的本质是:低权限主体执行了高权限操作,或未认证主体执行了需认证操作。在嵌入式系统中,越权往往表现为「缺少校验」「校验可绕过」「校验与动作分离」三类。
4.1 审查切入点:每个业务动作的入口函数
对每个对外暴露的业务动作(开锁、升级、配置修改、固件读取等),逐一检查其入口函数是否包含身份与权限校验。审查时问三个问题:
- 该动作是否需要认证?如果需要,校验在哪里执行?
- 校验失败时是否真正终止了执行流程,还是仅仅打印日志后继续?
- 该校验是否可以被绕过(如通过另一条代码路径到达同一动作)?
4.2 典型越权代码特征
以下代码特征应引起审查者高度警惕:
- 校验与动作分离:权限校验在函数 A 中完成,实际执行业务动作的函数 B 没有独立校验,攻击者可直接调用 B。
- 仅前端校验:嵌入式设备中,若权限判断只依赖上位机或 App 的界面控制,而设备端接口本身不校验,则攻击者绕过界面即可越权。
- 默认放行:校验函数在异常分支返回「通过」,或初始化时权限标志默认为最高权限。
- 硬编码口令或密钥:所有设备使用相同的口令或密钥,一旦泄露即全局越权。
- 整数与枚举比较错误:权限等级使用整数表示,比较时使用「大于」而非「大于等于」,或枚举值定义混乱导致低权限值反而更大。
4.3 攻击场景推演
发现可疑代码后,不要停留在「这里好像缺校验」,而要推演完整的攻击场景:攻击者通过哪个入口、发送什么数据、经过哪条路径、最终达成什么越权效果。场景推演能帮助判断漏洞的真实可利用性,也能为修复建议提供依据。
/* 示例:仅校验一次,后续直接执行动作 */ int handle_command(uint8_t *buf, uint16_t len) { if (check_auth(buf) != AUTH_OK) { log_error("auth failed"); return -1; /* 校验失败,正常返回 */ } return execute_action(buf, len); /* 动作执行函数本身无校验 */ } /* 攻击者若绕过 handle_command 直接调用 execute_action,即可越权 */5. 重放漏洞的手动审查方法
重放漏洞的本质是:系统无法区分「当前收到的指令」与「之前已执行过的指令」,导致攻击者截获并再次发送合法报文时,系统重复执行。在嵌入式系统中,重放攻击尤其危险——门锁被重复打开、车辆被重复解锁、工业指令被重复执行。
5.1 审查切入点:所有带副作用的指令处理函数
对每个会产生外部副作用的指令(开锁、解锁、启动、停止、配置写入等),检查其处理流程是否包含新鲜度验证。审查时问三个问题:
- 该指令是否包含时间戳、序列号、随机数(nonce)等新鲜度字段?
- 接收方是否验证了这些字段的唯一性或时效性?
- 验证通过后,是否记录了已使用的值以防止重复使用?
5.2 典型重放漏洞代码特征
- 无新鲜度字段:指令报文只有功能码和参数,没有时间戳或序列号,天然可重放。
- 有字段但不校验:报文包含序列号字段,但接收方解析后直接丢弃,未做任何比较。
- 校验但不记录:接收方检查了序列号大于上次值,但没有保存「上次值」,重启后归零,攻击者可从头重放。
- 时间窗口过宽:时间戳校验允许的偏差过大(如 ±5 分钟),攻击者可在窗口内重放。
- 重放窗口内重复执行:同一指令在短时间内重复到达,系统未做去重处理。
5.3 状态机与重放的交叉审查
重放漏洞与状态机缺陷经常叠加出现。例如:门锁在「已解锁」状态下再次收到「解锁」指令,若系统未检查当前状态,则重放攻击可无限次触发解锁动作。审查时应将「指令新鲜度验证」与「当前状态合法性验证」结合检查。
/* 示例:有序列号但未持久化,重启后重放窗口重新打开 */ uint32_t last_seq = 0; /* 仅内存变量,掉电丢失 */ int process_unlock(uint8_t *buf, uint16_t len) { uint32_t seq = get_seq(buf); if (seq <= last_seq) { return -1; /* 序列号不大于上次,拒绝 */ } last_seq = seq; /* 仅更新内存,未写入非易失存储 */ do_unlock(); return 0; } /* 设备重启后 last_seq 归零,攻击者可重放历史合法报文 */6. 审查流程与记录规范
安全导向的代码审查不是随意的读代码,而应遵循规范的流程,并留下可追溯的记录。
6.1 五步审查流程
- 范围确认:与开发团队确认本次审查覆盖的模块、接口和业务场景。
- 资产与入口梳理:建立敏感资产清单和外部输入入口清单。
- 逐项审查:按第 4、5 节的方法,对每个业务动作逐一审查越权和重放风险。
- 场景验证:对可疑点进行攻击场景推演,必要时编写最小复现用例。
- 输出报告:记录漏洞位置、触发条件、影响范围和修复建议。
6.2 漏洞记录要素
每条漏洞记录应包含以下要素,便于后续跟踪和修复验证:
- 漏洞类型(越权 / 重放 / 状态机)
- 涉及文件与函数(精确到行号)
- 触发条件与攻击路径
- 影响范围(哪些资产受影响)
- 严重等级(高 / 中 / 低)
- 修复建议与验证方法
7. 修复建议
针对越权漏洞,核心修复思路是「纵深防御」:在入口函数、业务动作函数两层都做权限校验,且校验失败必须终止执行;权限等级使用枚举而非裸整数,避免比较错误。
针对重放漏洞,核心修复思路是「新鲜度验证 + 持久化记录」:指令报文增加序列号或随机数,接收方验证后必须将已使用值写入非易失存储,防止重启后重放窗口重新打开;对于时间敏感指令,可叠加时间戳校验并收紧允许偏差。
针对状态机缺陷,应在每个业务动作入口检查当前状态是否允许执行该动作,非法状态转换直接拒绝并记录日志。
8. 总结
安全导向的代码审查是嵌入式软件静态测试中不可替代的一环。越权和重放漏洞隐藏在业务逻辑之中,自动化工具难以发现,必须依靠人工审查。本文提出的方法论核心可以概括为:以资产和信任边界为起点,以业务状态机为参照,以每个业务动作入口为审查单元,逐项验证权限校验与新鲜度验证的完备性,并通过攻击场景推演确认漏洞的真实可利用性。
在实际项目中,建议将安全审查纳入常规开发流程,在每次需求变更和代码评审时同步执行,而不是等到发布前才集中审查。越早发现业务逻辑漏洞,修复成本越低,系统安全性也越有保障。