本文深入剖析 src/vbf/ 模块的三层解析架构:VBFParserSmall 作为编排器统筹文件 I/O 与 RSA 解密,VBFHeader 负责文本头部的键值对提取,VBFBlock 则作为二进制数据块的轻量容器。文章还将详细阐述 RSA 私钥解密 → AES 会话密钥解密 → PKCS7 去填充的完整签名验证链。
Sources: CMakeLists.txt
架构总览:三层解析模型
VBF(Vehicle Binary Format)解析器由三个核心类构成,各司其职地完成从磁盘文件到内存数据结构的转换流程。下图展示了组件间的依赖关系与数据流向:
flowchart TD
A["VBFParserSmall\n(编排器)"] --> B["VBFHeader\n(头部解析器)"]
A --> C["VBFBlock\n(数据块容器)"]
A --> D["CFlashFileUtils\n(RSA/AES 解密)"]
D --> E["OpenSSL\n(RSA_private_decrypt)"]
D --> F["OpenSSL\n(AES_cbc_encrypt)"]
B --> G["MyStringUtils\n(字符串工具)"]
B --> H["Tracer\n(日志系统)"]VBFParserSmall 是唯一的对外入口,它持有 VBFHeader 实例和 vector<VBFBlock> 集合。调用方通过 ParseVBFFile() 传入 VBF 文件路径、RSA 私钥文件路径和密钥信息字符串,即可获得解析完成的头部元数据和数据块列表。
Sources: VBFParserSmall.h
类职责对比
| 类 | 核心职责 | 输出的关键数据 |
|---|---|---|
| VBFParserSmall | 文件打开、RSA/AES 解密调度、解析流程编排、线程安全控制 | VBFHeader、vector<VBFBlock>、原始文件缓冲区 |
| VBFHeader | 文本头部边界检测、键值对标记提取、元数据转换与校验 | 版本号、零件号、签名、校验和、擦除地址列表 |
| VBFBlock | 单个刷写数据块的地址/长度/校验和/二进制数据封装 | 起始地址、数据长度、CRC16 校验和、原始数据 |
Sources: VBFBlock.h, VBFHeader.h
VBF 文件二进制结构
VBF 文件由两个逻辑段组成:文本头部(header section)和二进制数据块序列(block sequence)。头部以 { 开始、以配对的 } 结束,内部是多行 key = value; 格式的键值对。头部之后紧跟连续的 Block 记录,每条记录包含 起始地址(4 字节大端) + 数据长度(4 字节大端) + 二进制数据(N 字节) + CRC16 校验和(2 字节大端)。
graph LR
subgraph "VBF 文件布局"
H["Header\n{ key=value; ... }"] --> B1["Block 1\nAddr(4B) | Len(4B) | Data(N) | CRC16(2B)"]
B1 --> B2["Block 2\nAddr(4B) | Len(4B) | Data(N) | CRC16(2B)"]
B2 --> BN["Block N\n..."]
end当启用 RSA 签名验证时,文件前 0x40(64)字节 被视为外部签名头而被跳过,fseek(fp, 0x40, SEEK_SET) 将读取指针定位到实际 VBF 内容的起始位置。
Sources: VBFParserSmall.cpp
VBFHeader:文本头部的解析引擎
头部边界检测
头部解析的第一步是定位结束位置。FindEOFHeader() 方法采用括号计数法:从前向后扫描缓冲区,遇到 { 计数加一,遇到 } 计数减一;当计数器归零且当前字符为 } 时,该位置即为头部的结束边界。搜索范围上限为 m_nMaxHeader = 65536 字节,超出则返回 -1 表示头部异常。
// 简化逻辑:括号配对检测
for (int i = 0; i < m_nMaxHeader; i++) {
bracketcount = buffer[i] == '{' ? bracketcount + 1 : bracketcount;
bracketcount = buffer[i] == '}' ? bracketcount - 1 : bracketcount;
if (bracketcount == 0 && buffer[i] == '}') return i;
}检测到边界后,getHeader() 将缓冲区从起始位置到边界位置的内容拷贝为 m_strHeader 字符串,并将所有 \r\n 和 \n 换行符统一替换为内部标记 __LF__,以便后续正则表达式跨行处理。
Sources: VBFHeader.cpp
键值对提取机制
核心解析方法 getTokenInfo() 采用两步定位 + 正则分割策略:
- 定位起始位置:在头部字符串中查找 token 名称(如
"sw_part_number") - 定位结束位置:以分号
;作为值的终止符,截取token = value;片段 - 正则分割:使用
\s*=\s*正则表达式将片段拆分为键和值两部分,取vecResult[1]作为原始值 - 后处理:根据 token 类型进行去引号、大小写转换、十六进制格式标准化等操作
flowchart LR
A["m_strHeader\n字符串"] --> B["find(token, start)\n定位键名"]
B --> C["find(';', pos)\n定位分号"]
C --> D["substr + trim\n截取键值对片段"]
D --> E["SplitStringByRegExp\n\\s*=\\s*"]
E --> F["vecResult[1]\n提取值"]
F --> G["后处理\n去引号/大小写/Hex转换"]Sources: VBFHeader.cpp
支持的 VBF 标记一览
convertHeader() 方法按顺序解析以下标记并填充对应的成员变量:
| 标记常量 | 对应成员 | 说明 |
|---|---|---|
vbf_version | m_strVBFVersion | VBF 格式版本(目前仅支持 2.6,非此版本发出警告) |
description | m_strDescription | 文件描述信息(支持多行,以 __LF__ 拼接) |
sw_part_number | m_strSWPartNumber | 软件零件号(去引号、转大写) |
sw_version | m_strSWVersion | 软件版本号 |
sw_part_type | m_strSWPartType | 软件零件类型 |
sw_current_part_number | m_strSWCurrPartNo | 当前软件零件号 |
sw_current_version | m_strSWCurrVersion | 当前软件版本号 |
data_format_identifier | m_strDataFormatIdentifier | 数据格式标识符(支持 0x 前缀和十进制) |
ecu_address | m_strECUAddress | ECU 逻辑地址(用于后续 Block 地址掩码计算) |
file_checksum | m_strFileChecksum | 文件级 CRC32 校验和 |
erase | m_vecListErases | 擦除地址范围列表(格式:起始,结束 对) |
call | m_strCall | ECU 启动调用地址 |
verification_block_root_hash | m_strBlockRootHash | 验证块根哈希 |
verification_block_start | m_strBlockStart | 验证块起始地址 |
verification_block_length | m_strBlockLength | 验证块长度 |
sw_signature_dev | m_strSWSigDev | 开发签名 |
sw_signature | m_strSW_sig | 正式软件签名 |
版本校验的特别处理:当解析到的 vbf_version 不等于 "2.6" 时,系统仅通过 Tracer::warn 发出警告而非中断解析,这为兼容未来可能的新版本预留了弹性空间。
Sources: VBFHeader.cpp
擦除列表的特殊解析
erase 标记的值格式为花括号包裹的十六进制地址对序列,例如 {0x00000000, 0x0000FFFF}, {0x00010000, 0x0001FFFF}。formatErase() 方法通过正则 \{|\}|\"|,|__LF__ 分割后,交替提取每两个相邻元素组成 起始地址,结束地址 字符串对。同时会剥离 0x / 0X 前缀,输出纯十六进制字符串。
Sources: VBFHeader.cpp
签名字段的歧义处理
getPSWSignature() 方法需要处理一个关键歧义:sw_signature 和 sw_signature_dev 共享前缀,简单的 find() 可能匹配到错误的标记。解决策略是:先用 sw_signature_dev 的结束位置截断搜索域,再从截断后的子串中定位 sw_signature,确保精确匹配。
Sources: VBFHeader.cpp
VBFBlock:数据块容器
VBFBlock 是一个纯粹的数据载体类,不包含任何解析逻辑。它通过构造函数一次性接收四个属性并接管数据所有权(使用 vector::swap 避免内存拷贝):
| 属性 | 类型 | 说明 |
|---|---|---|
m_unStartAddress | unsigned int | 块在 ECU 内存中的起始地址 |
m_unLength | unsigned int | 数据长度(字节数) |
m_usCheckSum | unsigned short | 块的 CRC16 校验和 |
m_vecBlockData | vector<unsigned char> | 原始二进制数据 |
GetBlockData() 返回的是内部 vector 的引用,调用方需注意不要在异步场景下直接修改该引用指向的数据。
Sources: VBFBlock.h, VBFBlock.cpp
VBFParserSmall:解析编排与 RSA 签名验证
主解析流程
ParseVBFFile() 是全模块的入口方法,其执行流程严格按照以下顺序编排:
flowchart TD
A["ParseVBFFile(file, rsaKey, keyInfo)"] --> B["🔒 lock_guard\n全局互斥锁"]
B --> C["OpenVBFFile\n打开并读取文件"]
C --> D{"RSA 密钥\n是否提供?"}
D -->|是| E["跳过 0x40 头部\nfseek(fp, 0x40, SEEK_SET)"]
E --> F["InitRSA\n加载并解密 RSA 私钥"]
F --> G["DecryptFlashFile\nAES-CBC 解密文件体"]
G --> H["DePKCS7Padding\n去除填充字节"]
D -->|否| H2["直接读取全部内容"]
H --> I["ParseHeader\n解析 VBF 文本头部"]
H2 --> I
I --> J["ParseBlocks\n解析二进制数据块"]
J --> K["🔒 解锁\n返回 true"]整个方法由全局互斥锁 g_mutexVBFParser 保护,确保同一时刻只有一个线程执行 VBF 解析。此外,方法首尾包裹了 _VM_PROTECT_ 宏(VMStart() / VMEnd()),用于在启用代码虚拟化保护的编译配置下对关键逻辑进行加壳。
Sources: VBFParserSmall.cpp
RSA 签名验证的完整加密链
VBF 文件的安全验证采用两层加密嵌套的设计模式:
flowchart TD
subgraph "第一层:RSA 解密会话密钥"
A["strKeyInfo\n(Base64 编码的\n加密会话密钥)"] --> B["base64_decode"]
B --> C["RSA_private_decrypt\n(RSA_PKCS1_PADDING)"]
D["strRSAPrivateKeyFileName\n(加密的 PEM 私钥文件)"] --> E["AES_DecryptRSAPrivateKey\n(硬编码 AES-128-CBC)"]
E --> F["Buffer2EVPkey\nPEM_read_bio_PrivateKey"]
F --> C
end
subgraph "第二层:AES 解密文件体"
C --> G["m_Key(128 位 AES 会话密钥)"]
G --> H["AES_DecryptEx\n(AES-128-CBC)"]
I["pBuffer\n(跳过 0x40 的 VBF 文件体)"] --> H
H --> J["DePKCS7Padding\n去填充"]
J --> K["明文 VBF 数据"]
end第一层——RSA 私钥加载与会话密钥解密:
- RSA 私钥文件以 AES-128-CBC 加密形态存储在磁盘上,使用硬编码密钥
{0xAE, 0x15, 0x62, 0x75, 0x67, 0xBC, 0xA2, 0x98, 0x23, 0x11, 0xDC, 0xF1, 0xFA, 0xAA, 0xBC, 0x54}和 IV"debug0000000000"解密后得到 PEM 格式的 RSA 私钥 strKeyInfo参数是 Base64 编码的 RSA 加密数据,解码后通过RSA_private_decrypt()使用 PKCS1 填充模式解密,得到 128 位的 AES 会话密钥
第二层——AES 会话密钥解密文件体:
- 从 RSA 解密结果的前 16 字节提取 AES-128 密钥
- 使用该密钥对跳过 0x40 头部的 VBF 文件剩余内容进行 AES-CBC 解密(IV 固定为
"debug0000000000") - 解密后通过
DePKCS7Padding()去除 PKCS7 填充——读取最后一个字节的值remain,若remain ≤ 0x10且≤ nDataLen,则从尾部截断remain字节
// PKCS7 去填充逻辑
int remain = pBuffer[nDataLen - 1];
if (remain > nDataLen || remain > 0x10) return 0; // 异常:保持原样
return nDataLen - remain; // 正常:截断填充字节Sources: FlashFileUtils.cpp, FlashFileUtils.cpp, FlashFileUtils.cpp, FlashFileUtils.h
Block 解析的二进制解码
ParseBlocks() 从头部结束位置的下一个字节开始,循环读取数据块直到文件末尾。每个块的解析格式完全遵循 VBF 规范:
[地址: 4 字节大端] [长度: 4 字节大端] [数据: N 字节] [CRC16: 2 字节大端]// 大端序地址和长度解析
unsigned int unStartAddress = (a1 << 0x18) | (a2 << 0x10) | (a3 << 0x08) | (a4 << 0x00);
unsigned int unLength = (b1 << 0x18) | (b2 << 0x10) | (b3 << 0x08) | (b4 << 0x00);
// 大端序校验和解析
unsigned short unCheckSum = (c1 << 0x08) | (c2 << 0x00);值得注意的是,代码中原有的 CRC16 校验逻辑被 #ifndef FLASH_TEST 预处理指令注释掉了,意味着在当前的编译配置中不执行块级校验和验证——数据完整性验证被上移至文件级的 RSA 签名验证。
从头部解析出的 ecu_address 被通过 MyStringUtils::toLong(strECUAddress, 16) 转换为十六进制整型存入 m_usMASK,该值可用于后续 ECU 地址匹配。
Sources: VBFParserSmall.cpp
数据聚合方法
GetVBFDataSize() 通过遍历所有 Block 并累加各自的 GetLength() 值来计算全部有效载荷的总字节数。GetVBFData() 则直接返回整个文件缓冲区 m_vecFileBuffer 的副本(注意:此处返回的是按值传递的 vector,会触发完整内存拷贝,调用方应使用移动语义或引用接收以避免开销)。
Sources: VBFParserSmall.cpp
线程安全与代码保护
ParseVBFFile() 入口处的 std::lock_guard<std::mutex> lock(g_mutexVBFParser) 保证了整个解析流程的串行化执行。这意味着在多 ECU 并行刷写场景下(参见 ParallelECUFlasher:基于 TaskPool 的多 ECU 并行刷写引擎),VBF 文件的解析操作会被互斥锁排队,避免并发解析导致的数据竞争。
此外,#ifdef _VM_PROTECT_ 条件编译块通过 VMStart() / VMEnd() 对关键代码段进行虚拟机加壳保护,这是一种代码混淆技术,用于防止逆向工程分析加密逻辑。
Sources: VBFParserSmall.cpp, VBFParserSmall.cpp, VBFParserSmall.cpp
模块集成关系
VBFParserSmall 通过 CMake 的 aux_source_directory(vbf VBF) 被编译进 otxproxy 共享库,但其在现有源码中并未被 otxFlash 目录下的刷写器直接引用(VbfFileDownloader 使用 MCD3D 协议的 MCDFlashJob 进行文件传输,而非本地解析)。这意味着 VBF 解析器是一个独立可复用的基础设施模块,可以被任何需要读取 VBF 文件元数据和二进制块的调用方使用。
当刷写流程需要从 VBF 文件中提取块地址映射、校验文件完整性或验证 RSA 签名时,此模块提供了完整的解析能力。配合 加密算法集:AES、DES-CBC、SHA256、ECDSA 签名与 Base64 编解码 中提供的底层加密原语,构成了从文件加密存储到安全刷写的完整信任链。
Sources: CMakeLists.txt, CMakeLists.txt
阅读后续
了解 VBF 文件解析之后,建议继续阅读以下相关文档以建立完整的刷写文件处理知识体系:
- BinFileDownloader 与 VbfFileDownloader:不同格式刷写文件下载器 —— 理解 VBF 解析结果如何被刷写下载器消费,以及 BinFile 格式的差异化处理
- 加密算法集:AES、DES-CBC、SHA256、ECDSA 签名与 Base64 编解码 —— 深入本文依赖的底层加密原语(AES-CBC、Base64、SHA256)
- 刷写器架构总览:从 ECUFlasher 接口到多 ECU 并行刷写的继承体系 —— 了解 VBF 解析在整个刷写调用链中的位置