前言在嵌入式开发中固件安全是产品的最后一道防线也是最容易被忽视的环节。未经加密的固件不仅会导致产品被抄板、盗版造成直接经济损失更可能被黑客篡改、植入恶意代码引发设备被远程控制、数据泄露等严重安全事故直接威胁企业品牌声誉和用户生命财产安全。本文基于多个量产项目的实际需求重构并优化了一套工业级嵌入式安全镜像生成工具。不同于网上常见的简单 AES 加密方案本工具采用主密钥 随机数的一次一密机制在保证嵌入式设备解密效率的同时大幅提升了固件破解难度。脚本已在多个智能电表、工业网关项目中经过量产验证完整适配 Windows/Linux 平台代码模块化、注释清晰可直接复制到项目中使用无需二次开发。一、脚本核心功能本工具专为嵌入式量产与 OTA 远程升级场景设计一站式解决固件生成、加密、校验、归档全流程✅工厂烧录完整镜像自动生成自动合并 Bootloader 自定义安全镜像头 APP 程序产线可直接烧录✅OTA 加密升级固件生成采用 AES128-ECB 加密主密钥 随机数生成动态工作密钥每次升级包密钥唯一✅固件完整性校验自动计算 CRC16-CITT 校验值防止升级过程中数据损坏或被篡改✅自动版本归档按固件名 CRC 时间戳自动创建目录并归档输出文件便于版本管理和追溯✅跨平台兼容Windows/Linux 通用路径自动适配无需修改代码✅编译链一键集成可直接集成到 Keil MDK、IAR、CMake 等编译流程实现编译后自动生成安全固件二、加密方案详解一次一密2.1 传统加密方案的痛点传统的固定密钥加密方案存在严重的安全隐患一旦主密钥泄露所有历史和未来的升级包都可以被解密攻击者可以通过分析多个升级包的密文更容易破解出固定密钥无法防止重放攻击2.2 本项目采用的一次一密方案为了解决上述问题本项目采用了安全等级极高的一次一密加密机制原理如下设备端预烧录主密钥K 服务器/PC端持有相同主密钥K | | | | 1. 生成16字节随机数R | | 2. 用K加密R得到工作密钥W AES(K, R) | | 3. 用W加密固件得到密文C AES(W, 明文) | | 4. 将R和C一起发送给设备 | | | ------------------------ R C ---------- | | 1. 用预烧录的K加密R得到W AES(K, R) | 2. 用W解密C得到明文 AES(W, C) | 3. 校验CRC无误后升级2.3 方案优势极高的安全性即使截获升级包没有主密钥也无法解密每次升级密钥不同无法通过历史包破解密钥计算效率高嵌入式端只需要两次 AES 加密操作一次生成工作密钥一次解密固件对 MCU 性能要求极低实现简单无需复杂的密钥交换协议适合资源受限的嵌入式设备可扩展性强可以轻松扩展支持 AES256、SM4 等其他加密算法三、完整可运行 Python 脚本#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (c), Fujian XXXX Tech Co., Ltd. # All rights reserved. # # Change Logs: # Date Author Action # 2025-12-30 Your Name 初始版本 # 2026-01-15 Your Name 修复路径分隔符问题增加Linux兼容性 # 2026-02-20 Your Name 优化加密逻辑增加一次一密机制 # Image process tool - 工业级嵌入式安全镜像生成工具 功能 1. 生成工厂烧片用完整安全镜像包含Bootloader安全镜像头应用程序 2. 生成升级用AES128 ECB加密固件一次一密机制 3. 自动计算CRC16-CITT校验值验证固件完整性 4. 自动按版本归档输出文件便于管理 使用方式 python create_image.py prj_config boot_file_subpath raw_bin_filename_only config_name 参数说明 prj_config 项目编译配置Debug/Release boot_file_subpath Bootloader输出文件相对路径bsp/bootloader/src/output/下的子路径 raw_bin_filename_only 原始BIN文件名称不含后缀 config_name 额外配置名称预留扩展 输出文件 - 升级固件output/raw_bin_filename/CRC_时间戳/ota_名称_CRC_时间戳.bin - 烧片镜像output/raw_bin_filename/CRC_时间戳/fullimage_名称_时间戳.bin import sys import os import binascii import struct import time import platform import shutil from Crypto.Cipher import AES from Crypto import Random from datetime import datetime # -------------------------- 全局配置根据项目修改 -------------------------- # AES主密钥16字节AES128要求请务必修改为自己的密钥 MAIN_KEY bMeter02-$GL-V1$ # AES块大小固定16字节 AES_BLOCK_SIZE AES.block_size # 应用程序起始地址Bootloader最大占用空间 FIRMWARE_BASE_ADDR 0x8000 # 输出根目录相对路径 OUTPUT_ROOT_REL ../../output # Bootloader根目录相对路径 BOOTLOADER_ROOT_REL ../../bsp/bootloader/src/output # 原始BIN文件所在目录 OBJECTS_ROOT Objects # 镜像头大小固定512字节 IMAGE_HEADER_SIZE 512 # ----------------------------------------------------------------------------- def crc16_citt(message): 计算CRC16-CITT校验值Ymodem协议标准XModem也使用此标准 :param message: 待校验字节数据 :return: 16位CRC校验值 poly 0x1021 # CRC-16-CITT 多项式 reg 0x0000 # 初始寄存器值 # 补两个0字节与硬件CRC计算结果一致 message b\x00\x00 for byte in message: reg ^ (byte 8) for _ in range(8): if reg 0x8000: reg (reg 1) ^ poly else: reg reg 1 reg 0xFFFF # 保留低16位 return reg def get_normalized_path(path): 路径规范化自动适配Windows/Linux路径分隔符 :param path: 原始路径 :return: 规范化后的路径 if platform.system() Windows: return path.replace(/, \\).strip() else: return path.replace(\\, /).strip() def create_dir_if_not_exists(dir_path): 目录不存在则递归创建 :param dir_path: 目标目录路径 if not os.path.exists(dir_path): os.makedirs(dir_path) print(f创建目录{dir_path}) def generate_img_header(raw_bin_size, crc_val, timestamp): 生成TLV格式的安全镜像头部固定512字节 :param raw_bin_size: 原始BIN文件大小 :param crc_val: 原始BIN的CRC16值 :param timestamp: 时间戳整数 :return: 512字节的镜像头字节数组, 生成的16字节随机数 img_header bytearray(IMAGE_HEADER_SIZE) # 1. 固件魔数/标识位14字节用于设备端快速识别合法固件 img_header[0:14] bMeter02-GL-V1 # 2. TLV结构Header信息tag1长度32 img_header[14:16] struct.pack(H, 1) # tagHeader img_header[16:18] struct.pack(H, 32) # lenHeader # 3. TLV结构加密信息tag2长度8 img_header[18:20] struct.pack(H, 2) # tagEncryptInfo img_header[20:22] struct.pack(H, 8) # lenEncryptInfo img_header[22:26] struct.pack(I, 0) # 加密起始偏移从镜像头后开始加密 img_header[26:30] struct.pack(I, raw_bin_size) # 加密长度 # 4. TLV结构随机数tag3长度16用于生成工作密钥 img_header[30:32] struct.pack(H, 3) # tagRandom img_header[32:34] struct.pack(H, 16) # lenRandom random_bytes Random.new().read(AES_BLOCK_SIZE) img_header[34:50] random_bytes # 5. 固件基本信息 img_header[50:54] struct.pack(I, 0) # 预留字段 img_header[54:56] struct.pack(H, crc_val) # 原始固件CRC16值小端 img_header[56:60] struct.pack(I, raw_bin_size) # 原始固件大小 img_header[60:64] struct.pack(I, timestamp) # 固件生成时间戳 img_header[64:IMAGE_HEADER_SIZE] b\xFF * (IMAGE_HEADER_SIZE - 64) # 剩余空间填充0xFF print(f生成随机数16字节{binascii.b2a_hex(random_bytes).decode()}) return img_header, random_bytes def encrypt_firmware(raw_bin_data, random_bytes): 采用一次一密机制加密固件 :param raw_bin_data: 原始BIN字节数据 :param random_bytes: 16位随机数用于生成工作密钥 :return: 加密后的固件数据 # 1. 使用主密钥加密随机数生成本次升级的工作密钥 main_cipher AES.new(MAIN_KEY, AES.MODE_ECB) work_key main_cipher.encrypt(random_bytes) print(f生成工作密钥{binascii.b2a_hex(work_key).decode()}) # 2. 使用工作密钥加密固件 work_cipher AES.new(work_key, AES.MODE_ECB) # 计算需要填充的字节数ECB模式要求数据长度为块大小的整数倍 padding_len AES_BLOCK_SIZE - (len(raw_bin_data) % AES_BLOCK_SIZE) if padding_len AES_BLOCK_SIZE: padding_len 0 # 填充数据使用0填充设备端解密后根据原始长度截断 padded_data raw_bin_data b\x00 * padding_len # 加密 encrypted_data work_cipher.encrypt(padded_data) return encrypted_data def main(argv): 主函数处理参数→读取原始BIN→生成镜像头→加密固件→生成输出文件 # 1. 参数校验 if len(argv) ! 4: print(参数错误正确使用方式) print(python create_image.py prj_config boot_file_subpath raw_bin_filename_only config_name) print(示例python create_image.py Debug boot_ec800.bin meter02_ec800 Release) sys.exit(1) prj_config, boot_file_subpath, raw_bin_name, config_name argv prj_workroot os.getcwd() print(*60) print( 嵌入式安全镜像生成工具 v1.2) print(*60) print(f项目工作目录{prj_workroot}) print(f编译配置{prj_config}) print(f原始BIN名称{raw_bin_name}) print(fBootloader路径{boot_file_subpath}) print(-*60) # 2. 路径规范化 raw_bin_path get_normalized_path( os.path.join(prj_workroot, OBJECTS_ROOT, f{raw_bin_name}.bin) ) output_root get_normalized_path( os.path.join(prj_workroot, OUTPUT_ROOT_REL, raw_bin_name) ) boot_file_path get_normalized_path( os.path.join(prj_workroot, BOOTLOADER_ROOT_REL, boot_file_subpath) ) # 3. 创建输出目录 create_dir_if_not_exists(output_root) # 4. 校验原始BIN文件 if not os.path.exists(raw_bin_path): print(f错误原始BIN文件不存在 → {raw_bin_path}) sys.exit(1) raw_bin_size os.path.getsize(raw_bin_path) print(f原始BIN文件{raw_bin_path}) print(f原始BIN大小{raw_bin_size} 字节) # 5. 读取原始BIN并计算CRC with open(raw_bin_path, rb) as f: raw_bin_data f.read() crc_val crc16_citt(raw_bin_data) timestamp int(time.time()) print(f原始BIN CRC160x{crc_val:04X}) print(f生成时间戳{timestamp}) # 6. 生成安全镜像头 img_header, random_bytes generate_img_header(raw_bin_size, crc_val, timestamp) # 7. 创建版本子目录 crc_time_str f{crc_val:04x}_{datetime.now().strftime(%Y%m%d%H%M%S)} output_subdir get_normalized_path(os.path.join(output_root, crc_time_str)) create_dir_if_not_exists(output_subdir) # 8. 生成OTA加密升级固件 ota_file_name fota_{raw_bin_name}_{crc_time_str}.bin ota_file_path get_normalized_path(os.path.join(output_subdir, ota_file_name)) encrypted_firmware encrypt_firmware(raw_bin_data, random_bytes) with open(ota_file_path, wb) as f: f.write(img_header) f.write(encrypted_firmware) print(fOTA加密固件生成完成 → {ota_file_path}) print(fOTA固件大小{os.path.getsize(ota_file_path)} 字节) # 9. 生成工厂烧片用完整镜像 fullimage_name ffullimage_{raw_bin_name}_{datetime.now().strftime(%Y%m%d%H%M%S)}.bin fullimage_path get_normalized_path(os.path.join(output_subdir, fullimage_name)) # 初始化完整镜像缓冲区Bootloader区 镜像头 原始APP fullimage_size FIRMWARE_BASE_ADDR IMAGE_HEADER_SIZE raw_bin_size fullimage_buf bytearray(fullimage_size) fullimage_buf[:] b\xFF # Flash默认值为0xFF # 读取并填充Bootloader if not os.path.exists(boot_file_path): print(f错误Bootloader文件不存在 → {boot_file_path}) sys.exit(1) with open(boot_file_path, rb) as f: boot_data f.read() if len(boot_data) FIRMWARE_BASE_ADDR: print(f警告Bootloader大小({len(boot_data)}字节)超过预留空间({FIRMWARE_BASE_ADDR}字节)) fullimage_buf[:len(boot_data)] boot_data print(fBootloader已填充 → 长度{len(boot_data)} 字节) # 填充镜像头和原始APP fullimage_buf[FIRMWARE_BASE_ADDR:FIRMWARE_BASE_ADDRIMAGE_HEADER_SIZE] img_header fullimage_buf[FIRMWARE_BASE_ADDRIMAGE_HEADER_SIZE:] raw_bin_data # 写入完整镜像 with open(fullimage_path, wb) as f: f.write(fullimage_buf) print(f工厂烧片镜像生成完成 → {fullimage_path}) print(f完整镜像大小{fullimage_size} 字节) # 10. 复制原始BIN到输出目录便于调试 raw_bin_copy_path get_normalized_path( os.path.join(output_subdir, f{raw_bin_name}_raw.bin) ) shutil.copy2(raw_bin_path, raw_bin_copy_path) print(-*60) print(✅ 所有镜像生成完成) print(*60) if __name__ __main__: try: main(sys.argv[1:]) except Exception as e: print(f❌ 执行出错{str(e)}) import traceback traceback.print_exc() sys.exit(1)四、环境安装与使用方法4.1 环境安装脚本依赖pycryptodome库提供 AES 加密功能pip install pycryptodome4.2 使用方法命令格式python create_image.py 编译模式 Bootloader文件名 原始bin名 扩展配置示例python create_image.py Debug boot_ec800.bin meter02_ec800 Release4.3 输出文件结构脚本会自动在项目上级目录生成如下结构的输出文件output/ └── meter02_ec800/ └── 1234_20251230153045/ ├── ota_meter02_ec800_1234_20251230153045.bin # OTA加密升级包 ├── fullimage_meter02_ec800_20251230153045.bin # 工厂烧录完整镜像 └── meter02_ec800_raw.bin # 原始未加密BIN调试用五、嵌入式端解密示例代码C 语言以下是 STM32 等 MCU 上的解密示例代码与上述 Python 脚本完全对应#include aes.h #include crc16.h #include string.h // 与PC端相同的主密钥16字节请务必修改为自己的密钥 const uint8_t main_key[16] Meter02-$GL-V1$ ; // 镜像头结构定义 typedef struct { uint8_t magic[14]; // 魔数 Meter02-GL-V1 uint16_t tag_header; // TLV标签1 uint16_t len_header; // TLV长度32 uint16_t tag_encrypt; // TLV标签2 uint16_t len_encrypt; // TLV长度8 uint32_t encrypt_offset; // 加密起始偏移 uint32_t encrypt_len; // 加密长度 uint16_t tag_random; // TLV标签3 uint16_t len_random; // TLV长度16 uint8_t random[16]; // 随机数 uint32_t reserved; // 预留 uint16_t crc; // 原始固件CRC16 uint32_t firmware_size; // 原始固件大小 uint32_t timestamp; // 时间戳 uint8_t padding[448]; // 填充到512字节 } image_header_t; /** * brief 解密OTA固件 * param ota_data: OTA固件数据指针包含镜像头 * param decrypted_data: 解密后的数据缓冲区 * retval 0: 成功, 其他: 失败 */ int decrypt_ota_firmware(const uint8_t *ota_data, uint8_t *decrypted_data) { image_header_t *header (image_header_t *)ota_data; uint8_t work_key[16]; AES_InitTypeDef aes_init; // 1. 校验魔数 if (memcmp(header-magic, Meter02-GL-V1 , 14) ! 0) { return -1; // 非法固件 } // 2. 使用主密钥加密随机数生成工作密钥 AES_ECB_Encrypt(main_key, 128, header-random, work_key); // 3. 使用工作密钥解密固件数据 const uint8_t *encrypted_data ota_data sizeof(image_header_t); uint32_t encrypted_len header-encrypt_len; // 计算需要解密的块数 uint32_t block_count (encrypted_len 15) / 16; for (uint32_t i 0; i block_count; i) { AES_ECB_Decrypt(work_key, 128, encrypted_data i*16, decrypted_data i*16); } // 4. 校验CRC uint16_t calc_crc crc16_citt(decrypted_data, header-firmware_size); if (calc_crc ! header-crc) { return -2; // CRC校验失败 } return 0; // 解密成功 }六、集成到编译工具链6.1 集成到 Keil MDK打开 Keil 工程点击Project→Options for Target切换到User标签页在After Build/Rebuild下勾选Run #1输入命令python ..\..\tools\create_image.py #H boot_ec800.bin #L Release其中#H表示编译配置Debug/Release#L表示目标名称点击OK保存设置此后每次编译完成后Keil 会自动调用脚本生成安全固件。6.2 集成到 CMake在CMakeLists.txt中添加以下内容# 添加自定义命令编译后自动生成安全固件 add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND python ${CMAKE_SOURCE_DIR}/tools/create_image.py ${CMAKE_BUILD_TYPE} boot_ec800.bin ${PROJECT_NAME} Release WORKING_DIRECTORY ${CMAKE_BINARY_DIR} COMMENT Generating secure firmware image... )七、安全注意事项主密钥保护主密钥是整个加密体系的核心务必妥善保管。建议不要将主密钥硬编码在源代码中而是在工厂烧录时写入设备的 OTP 区域不同批次的产品可以使用不同的主密钥建立严格的主密钥管理制度限制接触人员防止重放攻击可以在镜像头中增加版本号字段设备端只接受版本号更高的固件。加密算法选择本项目使用 AES128-ECB 模式适合资源受限的 MCU。如果设备性能允许可以考虑使用更安全的 CBC 或 GCM 模式。调试接口保护量产时务必关闭 JTAG/SWD 等调试接口防止攻击者通过调试接口读取主密钥。八、常见问题解答FAQQ1: 为什么使用 ECB 模式而不是 CBC 模式A1: ECB 模式不需要初始化向量IV实现更简单计算效率更高。虽然 ECB 模式在理论上不如 CBC 模式安全但结合我们的一次一密机制每次升级使用不同的工作密钥已经可以满足绝大多数工业应用的安全需求。Q2: 主密钥泄露了怎么办A2: 如果主密钥泄露所有使用该密钥的设备都将面临安全风险。建议立即停止使用该密钥生成新的升级包通过 OTA 升级更新设备的主密钥对已泄露的设备进行召回或采取其他安全措施Q3: 如何支持更大的固件A3: 脚本没有对固件大小做限制可以支持任意大小的固件。只需要确保设备端有足够的 RAM 或 Flash 空间来存储解密后的固件。Q4: 可以同时加密 Bootloader 吗A4: 可以。但需要注意Bootloader 是设备上电后第一个运行的程序无法被自身解密。如果需要加密 Bootloader需要使用芯片厂商提供的硬件加密功能。九、总结这套脚本是经过多个量产项目验证的工业级嵌入式安全固件生成工具具备✅ 一次一密 AES128 加密安全性高✅ 自动合并 Bootloader生成工厂烧录镜像✅ 自动 CRC 校验保证固件完整性✅ 自动版本归档便于管理✅ 跨平台运行支持 Windows/Linux✅ 可直接集成到 Keil/CMake 等编译流程✅ 代码注释完整易于维护和扩展可以直接复制到你的项目中使用无需二次开发。如果有任何问题或需要进一步优化欢迎在评论区交流讨论。如果本文对你有帮助欢迎点赞、收藏⭐、评论日常深耕嵌入式、物联网、协议开发相关技术有技术答疑、项目合作、毕设指导需求均可私信私聊