Skip to content

本文深入剖析 src/vbf/ 模块的三层解析架构:VBFParserSmall 作为编排器统筹文件 I/O 与 RSA 解密,VBFHeader 负责文本头部的键值对提取,VBFBlock 则作为二进制数据块的轻量容器。文章还将详细阐述 RSA 私钥解密 → AES 会话密钥解密 → PKCS7 去填充的完整签名验证链。

Sources: CMakeLists.txt

架构总览:三层解析模型

VBF(Vehicle Binary Format)解析器由三个核心类构成,各司其职地完成从磁盘文件到内存数据结构的转换流程。下图展示了组件间的依赖关系与数据流向:

mermaid
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 解密调度、解析流程编排、线程安全控制VBFHeadervector<VBFBlock>、原始文件缓冲区
VBFHeader文本头部边界检测、键值对标记提取、元数据转换与校验版本号、零件号、签名、校验和、擦除地址列表
VBFBlock单个刷写数据块的地址/长度/校验和/二进制数据封装起始地址、数据长度、CRC16 校验和、原始数据

Sources: VBFBlock.h, VBFHeader.h

VBF 文件二进制结构

VBF 文件由两个逻辑段组成:文本头部(header section)和二进制数据块序列(block sequence)。头部以 { 开始、以配对的 } 结束,内部是多行 key = value; 格式的键值对。头部之后紧跟连续的 Block 记录,每条记录包含 起始地址(4 字节大端) + 数据长度(4 字节大端) + 二进制数据(N 字节) + CRC16 校验和(2 字节大端)

mermaid
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 表示头部异常。

cpp
// 简化逻辑:括号配对检测
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() 采用两步定位 + 正则分割策略:

  1. 定位起始位置:在头部字符串中查找 token 名称(如 "sw_part_number"
  2. 定位结束位置:以分号 ; 作为值的终止符,截取 token = value; 片段
  3. 正则分割:使用 \s*=\s* 正则表达式将片段拆分为键和值两部分,取 vecResult[1] 作为原始值
  4. 后处理:根据 token 类型进行去引号、大小写转换、十六进制格式标准化等操作
mermaid
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_versionm_strVBFVersionVBF 格式版本(目前仅支持 2.6,非此版本发出警告)
descriptionm_strDescription文件描述信息(支持多行,以 __LF__ 拼接)
sw_part_numberm_strSWPartNumber软件零件号(去引号、转大写)
sw_versionm_strSWVersion软件版本号
sw_part_typem_strSWPartType软件零件类型
sw_current_part_numberm_strSWCurrPartNo当前软件零件号
sw_current_versionm_strSWCurrVersion当前软件版本号
data_format_identifierm_strDataFormatIdentifier数据格式标识符(支持 0x 前缀和十进制)
ecu_addressm_strECUAddressECU 逻辑地址(用于后续 Block 地址掩码计算)
file_checksumm_strFileChecksum文件级 CRC32 校验和
erasem_vecListErases擦除地址范围列表(格式:起始,结束 对)
callm_strCallECU 启动调用地址
verification_block_root_hashm_strBlockRootHash验证块根哈希
verification_block_startm_strBlockStart验证块起始地址
verification_block_lengthm_strBlockLength验证块长度
sw_signature_devm_strSWSigDev开发签名
sw_signaturem_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_signaturesw_signature_dev 共享前缀,简单的 find() 可能匹配到错误的标记。解决策略是:先用 sw_signature_dev 的结束位置截断搜索域,再从截断后的子串中定位 sw_signature,确保精确匹配。

Sources: VBFHeader.cpp

VBFBlock:数据块容器

VBFBlock 是一个纯粹的数据载体类,不包含任何解析逻辑。它通过构造函数一次性接收四个属性并接管数据所有权(使用 vector::swap 避免内存拷贝):

属性类型说明
m_unStartAddressunsigned int块在 ECU 内存中的起始地址
m_unLengthunsigned int数据长度(字节数)
m_usCheckSumunsigned short块的 CRC16 校验和
m_vecBlockDatavector<unsigned char>原始二进制数据

GetBlockData() 返回的是内部 vector引用,调用方需注意不要在异步场景下直接修改该引用指向的数据。

Sources: VBFBlock.h, VBFBlock.cpp

VBFParserSmall:解析编排与 RSA 签名验证

主解析流程

ParseVBFFile() 是全模块的入口方法,其执行流程严格按照以下顺序编排:

mermaid
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 文件的安全验证采用两层加密嵌套的设计模式:

mermaid
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 私钥加载与会话密钥解密

  1. RSA 私钥文件以 AES-128-CBC 加密形态存储在磁盘上,使用硬编码密钥 {0xAE, 0x15, 0x62, 0x75, 0x67, 0xBC, 0xA2, 0x98, 0x23, 0x11, 0xDC, 0xF1, 0xFA, 0xAA, 0xBC, 0x54} 和 IV "debug0000000000" 解密后得到 PEM 格式的 RSA 私钥
  2. strKeyInfo 参数是 Base64 编码的 RSA 加密数据,解码后通过 RSA_private_decrypt() 使用 PKCS1 填充模式解密,得到 128 位的 AES 会话密钥

第二层——AES 会话密钥解密文件体

  1. 从 RSA 解密结果的前 16 字节提取 AES-128 密钥
  2. 使用该密钥对跳过 0x40 头部的 VBF 文件剩余内容进行 AES-CBC 解密(IV 固定为 "debug0000000000"
  3. 解密后通过 DePKCS7Padding() 去除 PKCS7 填充——读取最后一个字节的值 remain,若 remain ≤ 0x10≤ nDataLen,则从尾部截断 remain 字节
cpp
// 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 字节大端]
cpp
// 大端序地址和长度解析
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 文件解析之后,建议继续阅读以下相关文档以建立完整的刷写文件处理知识体系: