124 lines
9.1 KiB
Markdown
124 lines
9.1 KiB
Markdown
---
|
||||
|
|
feature: ecat-dcu-flash-crypto-audit
|
|||
|
|
status: delivered
|
|||
|
|
updated: 2026-09-24
|
|||
|
|
branch: ecat-dcu-flash-crypto-audit
|
|||
|
|
commits: 28d8426..28d8426
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# EtherCAT 总线刷写 DCU:加密与握手完整性审计
|
|||
|
|
|
|||
|
|
## Report
|
|||
|
|
|
|||
|
|
**What was built** — 对「EtherCAT 总线刷写 DCU」路径完成加密与握手完整性审计(docs-only,无代码改动)。结论:本仓库可见的 APP 交接与布局契约上 **无载荷加密**(仅镜像尾 CRC32);所谓握手是 **0x2502 交接 + 配置页 arm 门**,不是密码学握手,且 arm/令牌语义在 BOOT(本仓库不可验证)、APP 擦页会抹掉 word8 令牌字。与电机 OTA(AES-128 + nonce 会话)严格区分。
|
|||
|
|
|
|||
|
|
**Verification** — 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` + `motor_ota_relay.cpp`(PASS);`config.h:333` word 计数仍为 8(PASS,缺陷);`ota_boot_request.cpp:198` CAN 路径仍同步 `requestEnterBoot`(PASS,缺陷);`ota_boot_request.cpp` 无 `gracefulTeardown`/`fwdgt_*`(PASS,缺陷);`0x2500` 仅注释(PASS,BOOT 盲区);spec self-check PASS。独立审查 FAIL(1 critical 事实错误 + 1 major 范围过宽)→ 修正后 re-review PASS。
|
|||
|
|
|
|||
|
|
**Journey log**
|
|||
|
|
- `clearCrashCount()` 写 `kOtaAval` 走 `writeWord`,**不会**栈越界;OOB 仅擦除路径且 word 下标 ≥8。活缺陷是擦页抹掉 word8/9,不是该调用点。
|
|||
|
|
- BOOT `ecatArmed()` / `0x2500` 会话只在 `dc_app.c` 注释里;凡 BOOT 侧行为必须标「不可验证」,不可用 APP 证据替它下无加密定论。
|
|||
|
|
- 本环境再次拦截 `git worktree add`,落在分支 `ecat-dcu-flash-crypto-audit`(非隔离 worktree)。
|
|||
|
|
- 工作区另有未跟踪/未提交改动(`esc_lan9252.cpp`、`config.h`、`ecat-wkc-periodic-dip.md`),与本单无关,未纳入提交。
|
|||
|
|
|
|||
|
|
## [S1] Problem
|
|||
|
|
|
|||
|
|
需要确认当前 **EtherCAT 总线刷写 DCU 固件** 路径是否具备加密,以及握手/鉴权链路是否完整。该路径与「电机 OTA 中继」(AES-128 + nonce 会话)不同,不能混谈。审计对象为本仓库(`GD32E508_DCU_APP`)可见的 APP 交接入口、共享布局、以及文档/注释中对 BOOT 侧 `0x2500/0x2501` 直灌会话的契约描述。
|
|||
|
|
|
|||
|
|
## [S2] Design
|
|||
|
|
|
|||
|
|
### S2.1 路径范围(唯一真源)
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
主站 EtherCAT
|
|||
|
|
│
|
|||
|
|
│ SDO 0x2502 = 1 ← APP 字典「GD32 boot request」
|
|||
|
|
▼
|
|||
|
|
EcatEnterBootRequestAsync() ← 只置 pending(异步)
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
OtaBootRequest::process() ← 主循环写配置页 + 软复位
|
|||
|
|
│ CHECK=AVAL, AVAL=AVAL, EN=NOT_AVAL
|
|||
|
|
▼
|
|||
|
|
BOOT 起机(10s OTA 窗口)
|
|||
|
|
│ ecatArmed() = !(CHECK==AVAL && EN==AVAL)
|
|||
|
|
│ 交接后 EN=NOT_AVAL ⇒ 直灌解锁(可不依赖 word8 令牌)
|
|||
|
|
▼
|
|||
|
|
BOOT 对象 0x2500 / 0x2501 ← 固件灌写会话(源码不在本仓库)
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
APP 区镜像 + 末尾 CRC32
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
对照旁路(**不在本次范围**,仅防混淆):
|
|||
|
|
|
|||
|
|
| 路径 | 加密 | 握手 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 电机 OTA 中继(CAN 0x100/0x101) | AES-128 ECB + keyed-CRC32 | nonce → K_session → confirm 三步 |
|
|||
|
|
| EtherCAT ESI/EEPROM 刷写 | 无 | 无(仅 SII CRC8) |
|
|||
|
|
| **EtherCAT 总线刷写 DCU(本次)** | **见 S2.2** | **见 S2.3** |
|
|||
|
|
|
|||
|
|
### S2.2 加密结论:**本仓库可见路径没有载荷加密**
|
|||
|
|
|
|||
|
|
| 检查项 | 结论 | 证据 |
|
|||
|
|
|--------|------|------|
|
|||
|
|
| 固件载荷 AES/对称加密(APP 可见代码) | **无** | 全库 `aes128|encrypt` 仅命中 `sys/aes128.*` 与 `sys/motor_ota_relay.cpp`;`comm/ssc`/`ota_boot_request`/`dc_app` 零加密调用 |
|
|||
|
|
| 传输层机密性(APP 交接 + 布局契约) | **未见** | `dc_app.c:305-306` 注明直灌会话在 BOOT 的 `0x2500/0x2501`;本仓库可见路径无载荷加密 |
|
|||
|
|
| BOOT `0x2500/0x2501` 是否解密/验签 | **本仓库不可验证** | 源码不在本 repo;不能据 APP 侧断言 BOOT 一定无加密 |
|
|||
|
|
| 完整性校验(布局契约) | **仅有镜像尾 CRC32** | `shared/ota_layout.h:19` `OTA_LAYOUT_APP_MAX_SIZE`(216KB-4B,末尾 CRC32) |
|
|||
|
|
| 源码/固件签名 | **未见** | 无 ECDSA/HMAC/secure boot 引用 |
|
|||
|
|
|
|||
|
|
**一句话(限定范围)**:在 **本仓库可见的 APP 交接与布局契约** 上,EtherCAT 刷写 DCU 是 **明文 SDO 灌写意图 + 镜像尾 CRC32**,**没有载荷加密**;BOOT 侧是否另有加密 **无法在本仓库证实或证伪**。
|
|||
|
|
|
|||
|
|
### S2.3 握手结论:**不是密码学握手,只是交接 + arm 门;链路不完整**
|
|||
|
|
|
|||
|
|
实际存在的「握手」是 **交接(handoff)+ 配置页 arm 门**,不是 challenge-response:
|
|||
|
|
|
|||
|
|
| 步骤 | 完整性 | 证据 |
|
|||
|
|
|------|--------|------|
|
|||
|
|
| 1. APP 交接入口 `0x2502` | **完整(异步)** | `dc_app.c:329-350` `ObjBootReqWrite` → `EcatEnterBootRequestAsync`;`ota_boot_request.cpp:257-260` 只置 pending;Flash/复位在 `process()` (`:209-248`) |
|
|||
|
|
| 2. 配置页 arm 语义 | **注释契约(BOOT 行为,本仓库不可验证)** | `dc_app.c:316-318` 描述 `ecatArmed() = !(CHECK==AVAL && EN==AVAL)`;实现不在 APP |
|
|||
|
|
| 3. word8 令牌 `ECAT_ARM_KEY=0xACA70001` | **按注释可被交接旁路;APP 擦页会毁掉令牌字** | `dc_app.c:316-318` 称交接后不必 word8;`config.h:333` word 计数=8;`ota_boot_request.cpp:54-78` 擦页只回写 8 字 → 页上 word8/9 被抹 |
|
|||
|
|
| 4. nonce / 会话密钥 / 双向认证 | **无** | 对比 `motor_ota_relay.cpp:896-955` 的 Req→Nonce→Confirm |
|
|||
|
|
| 5. BOOT 侧会话对象协议 | **本仓库不可验证** | `dc_app.c:305-306`:`0x2500/0x2501` 在 BOOT 字典;APP 无实现 |
|
|||
|
|
| 6. CAN 旁路 `0x320+"OTA!"` | **不完整** | `ota_boot_request.cpp:187-200` `handleCanFrame` 仍 **同步** `requestEnterBoot()`(VULN-08 未落地) |
|
|||
|
|
|
|||
|
|
**一句话**:所谓握手 = **「让我进 BOOT」+「配置页门禁」**,**无加密握手、无会话密钥、无双向认证**;门禁语义本身在 BOOT(本仓库不可验证),且 APP 擦页会把 word8 令牌字抹掉。
|
|||
|
|
|
|||
|
|
### S2.4 阻断「完整」结论的现存缺陷(APP 侧可证实)
|
|||
|
|
|
|||
|
|
1. **VULN-11 仍在源码(擦页抹字已证实;栈越界仅限擦除路径)**:`sys/config.h:333` `CONFIG_OTA_CONFIG_WORD_COUNT = 8U`,应为 `OTA_LAYOUT_WORD_COUNT`(`shared/ota_layout.h:48` = 10)。`requestEnterBoot()` 对 word2 写 `NOT_AVAL` 会走 `otaConfigSet` 擦除路径(`ota_boot_request.cpp:50-80`):只备份/回写 8 字 → 页上 word8 `ECAT_ARM` / word9 `CRASH_COUNT` 被抹成 `0xFFFFFFFF`。栈 `words[]` 越界写仅发生在 **目标 word 下标 ≥8 且 value==NOT_AVAL** 时(`:58`);`clearCrashCount()`(`:129-131`)写的是 `kOtaAval`,走 `writeWord`(`:50-51`),**不会**触发该越界。
|
|||
|
|
2. **VULN-08 仍在源码**:`ota_boot_request.cpp:187-200` CAN `0x320` 路径在 `taskCanRxParse` 上下文同步 `requestEnterBoot()`(内部 `fmc_page_erase` `:62`,20–30ms),未走 `requestEnterBootAsync`(`:175-181`)。
|
|||
|
|
3. **交接后 Flash 擦写防护未按审计加固落地**(对照 `docs/audit_ecat_spi_wifi_ota_compatibility.md` §3):
|
|||
|
|
- 仍调用 Flash 库 `fmc_word_program`/`fmc_page_erase`(`ota_boot_request.cpp:30,62,73`),非 SRAM 寄存器直写;
|
|||
|
|
- `process()` 在擦写前只关 SPI0/EXTI10_15(`:216-217`);`__disable_irq()` 仅在复位前(`:244`),**不覆盖** `fmc_page_erase` 窗口;
|
|||
|
|
- 未见 FWDGT 复位前扩窗(无 `fwdgt_*`);
|
|||
|
|
- 未见 LAN9252 PREOP / CAN STB / EXMC 优雅关停(无 `gracefulTeardown`)。
|
|||
|
|
4. **BOOT 侧盲区**:`0x2500/0x2501` 是否校验 CRC32、是否接受任意明文、是否有会话状态机,**本仓库无法证明完整**(对象仅出现在 `dc_app.c:305-306` 注释)。
|
|||
|
|
|
|||
|
|
### S2.5 与电机 OTA 的对照(防误读)
|
|||
|
|
|
|||
|
|
| 维度 | 电机 OTA 中继 | EtherCAT 刷写 DCU |
|
|||
|
|
|------|---------------|-------------------|
|
|||
|
|
| 载荷加密 | AES-128 ECB 48B/包 | **APP 可见路径无**(BOOT 不可验证) |
|
|||
|
|
| 密钥协商 | `K_master` + nonce → `K_session` | **无** |
|
|||
|
|
| 确认帧 | confirm = AES(nonce⊕0x55) | **无** |
|
|||
|
|
| 完整性 | keyed-CRC32(session‖plain) | 镜像尾 CRC32 |
|
|||
|
|
| 会话绑定 | session_num + pkt_num | **未见** |
|
|||
|
|
| 传输保护 | ARD 单发 + BRS 压制 + 通道锁 | SDO 邮箱;通道锁与本路径无关 |
|
|||
|
|
|
|||
|
|
## [S3] Out of Scope
|
|||
|
|
|
|||
|
|
- 电机 OTA 中继的 AES/握手实现质量(另有完整状态机,不在本单)。
|
|||
|
|
- EtherCAT ESI/EEPROM(`gen_eeprom.py` / `ecat_flash_eeprom.py`)刷写。
|
|||
|
|
- BOOT 工程 `GD32E50X_BOOT` 内 `0x2500/0x2501` 具体实现(源码不在本仓库)。
|
|||
|
|
- 对上述缺陷的代码修复(本单为「检查」;修复需另开 implement 任务并获确认)。
|
|||
|
|
- 电机/DCU 联合端到端刷写台架实测。
|
|||
|
|
|
|||
|
|
## Tasks
|
|||
|
|
|
|||
|
|
- [x] T1: 梳理 EtherCAT 刷写 DCU 全路径与对象边界 — acceptance: S2.1 流程与对象归属可对照源码/注释逐点定位 (covers: S2.1)
|
|||
|
|
- [x] T2: 判定载荷加密有无 — acceptance: S2.2 表中每行有仓库内证据或明确「未见」 (covers: S2.2)
|
|||
|
|
- [x] T3: 判定握手/arm 链完整性 — acceptance: S2.3 表逐步给出完整/不完整/不可验证,并指出门禁旁路与令牌破坏路径 (covers: S2.3)
|
|||
|
|
- [x] T4: 列出阻断「完整」结论的 APP 侧缺陷 — acceptance: S2.4 各条可指到 `file:line` (covers: S2.4)
|
|||
|
|
- [x] T5: 输出与电机 OTA 的对照 — acceptance: S2.5 无混淆两路径的加密/握手结论 (covers: S2.5)
|