From 8f928a93e86f9a29faef2d0d0976882b7d7fca6a Mon Sep 17 00:00:00 2001 From: Ching L Date: Sun, 4 Jan 2026 15:08:17 +0800 Subject: [PATCH] feat(cp02): add firmware processing and signing utilities - Add get_last_bytes.sh to extract final 2 bytes from IUM files - Add read_bytes.sh for reading specific byte offsets from binaries - Add pack_resources.sh to package and upload CP02S resources to S3 - Add signer_new.py for firmware and bootloader signing with custom SHA1 - Update extract_firmware.sh to use fixed output filenames - Update README.md with comprehensive documentation for new tools --- CP02/README.md | 58 +++++++++ CP02/extract_firmware.sh | 6 +- CP02/get_last_bytes.sh | 28 +++++ CP02/pack_resources.sh | 71 +++++++++++ CP02/read_bytes.sh | 32 +++++ CP02/signer_new.py | 266 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 458 insertions(+), 3 deletions(-) create mode 100755 CP02/get_last_bytes.sh create mode 100755 CP02/pack_resources.sh create mode 100755 CP02/read_bytes.sh create mode 100644 CP02/signer_new.py diff --git a/CP02/README.md b/CP02/README.md index ff8229c..6911c20 100644 --- a/CP02/README.md +++ b/CP02/README.md @@ -28,6 +28,49 @@ - 解析并生成 metadata.json (blocks 和 last_block_size) - 如未指定前缀,自动使用原文件名 +### get_last_bytes.sh +- **功能**: 获取 IUM 文件的最后两个字节并以十六进制格式返回 +- **用法**: `./get_last_bytes.sh [ium_file_path]` +- **描述**: + - 读取 IUM 文件的最后 2 个字节 + - 反转字节顺序(交换第一和第二个字节) + - 以十六进制格式输出结果 + - 如未指定文件路径,默认使用 ium.bin + +### read_bytes.sh +- **功能**: 从二进制文件的特定偏移量读取 2 个字节 +- **用法**: `./read_bytes.sh ` +- **描述**: + - 从偏移量 0x1dcfe 读取 2 个字节 + - 反转字节顺序(小端序转换) + - 返回十六进制格式的结果 + - 包含文件大小和偏移量验证 + +### pack_resources.sh +- **功能**: 打包 CP02S 资源目录中的 .bin 文件 +- **用法**: `./pack_resources.sh`(无参数) +- **描述**: + - 从指定目录收集所有 .bin 文件 + - 创建包含 resources/ 作为顶级目录的 tar.gz 压缩包 + - 自动上传到 S3 存储(需要配置 AWS CLI) + - 源目录:`/Users/ching/develop/IonBridge/files/CP02S/littlefs/resources` + +### signer_new.py +- **功能**: 用于签名 CP02 固件和引导加载程序的 Python 工具 +- **用法**: `python3 signer_new.py --output_dir [options]` +- **选项**: + - `--firmware ` - 未签名的用户应用程序固件路径 + - `--bootloader ` - 未签名的引导加载程序路径 + - `--output_dir ` - 保存签名文件的目录(必需) + - `--enable_swd` - 启用 SWD 功能 + - `--boot_directly` - 启用直接引导到用户应用程序 +- **描述**: + - 实现自定义 SHA1 签名算法(基于逆向工程) + - 为固件和引导加载程序生成签名的二进制文件 + - 创建包含版本号、校验和和版权信息的元数据 + - 支持多种芯片 ID 的引导加载程序变体 + - 输出签名的二进制文件和相关 JSON 元数据 + ## 使用示例 ```bash @@ -40,4 +83,19 @@ # 提取固件组件 ./extract_firmware.sh 40_ufcs2.bin firmware # 指定输出前缀 ./extract_firmware.sh merged.bin # 使用原文件名作为前缀 + +# 获取 IUM 文件的最后两个字节 +./get_last_bytes.sh ium.bin # 使用默认文件 +./get_last_bytes.sh custom_ium.bin # 指定文件路径 + +# 从二进制文件读取特定偏移量的字节 +./read_bytes.sh 40_ufcs2.bin + +# 打包资源文件 +./pack_resources.sh # 打包并上传到 S3 + +# 签名固件 +python3 signer_new.py --firmware firmware.bin --output_dir ./output +python3 signer_new.py --bootloader bootloader.bin --output_dir ./output --enable_swd +python3 signer_new.py --firmware firmware.bin --bootloader bootloader.bin --output_dir ./output --boot_directly ``` \ No newline at end of file diff --git a/CP02/extract_firmware.sh b/CP02/extract_firmware.sh index 01cfc57..e2cb02a 100755 --- a/CP02/extract_firmware.sh +++ b/CP02/extract_firmware.sh @@ -79,7 +79,7 @@ echo " 计算出 firmware 大小: $FIRMWARE_SIZE bytes" echo "" # 提取 head file (firmware: 从 0x4000 开始) -HEAD_FILE="${PREFIX}.firmware" +HEAD_FILE="firmware.bin" echo "提取 head file (firmware)..." dd if="$INPUT" of="$HEAD_FILE" bs=1 skip=$FIRMWARE_OFFSET count=$FIRMWARE_SIZE status=none @@ -92,7 +92,7 @@ else fi # 提取 tail file (IUM: 从 0x1dc00 开始, 256 bytes) -TAIL_FILE="${PREFIX}.ium" +TAIL_FILE="ium.bin" echo "提取 tail file (IUM)..." dd if="$INPUT" of="$TAIL_FILE" bs=1 skip=$IUM_OFFSET count=$IUM_SIZE status=none @@ -105,7 +105,7 @@ else fi # 生成 metadata.json -METADATA_FILE="${PREFIX}_metadata.json" +METADATA_FILE="metadata.json" echo "生成 metadata.json..." cat > "$METADATA_FILE" << EOF diff --git a/CP02/get_last_bytes.sh b/CP02/get_last_bytes.sh new file mode 100755 index 0000000..54aa790 --- /dev/null +++ b/CP02/get_last_bytes.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Script to get the last two bytes of ium file +# Returns the bytes in 0x format +# Usage: ./get_last_bytes.sh [ium_file_path] + +# Use provided path or default to ium.bin +ium_file=${1:-ium.bin} + +if [ ! -f "$ium_file" ]; then + echo "Error: $ium_file file not found" + exit 1 +fi + +# Get file size +file_size=$(stat -f%z "$ium_file") + +if [ "$file_size" -lt 2 ]; then + echo "Error: File is too small (less than 2 bytes)" + exit 1 +fi + +# Extract last 2 bytes and reverse byte order +last_bytes=$(xxd -s -2 "$ium_file" | cut -d' ' -f2 | tr -d '\n') +# Reverse the byte order (swap first and second byte) +byte1=${last_bytes:0:2} +byte2=${last_bytes:2:2} +echo "${byte2}${byte1}" \ No newline at end of file diff --git a/CP02/pack_resources.sh b/CP02/pack_resources.sh new file mode 100755 index 0000000..4bd1e98 --- /dev/null +++ b/CP02/pack_resources.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Script to pack CP02S resources directory containing only .bin files +# Creates tar.gz with resources as top-level directory + +SOURCE_DIR="/Users/ching/develop/IonBridge/files/CP02S/littlefs/resources" +OUTPUT_FILE="cp02s_resources.tar.gz" + +# Check if source directory exists +if [ ! -d "$SOURCE_DIR" ]; then + echo "Error: Source directory does not exist: $SOURCE_DIR" + exit 1 +fi + +# Create temporary directory structure +TEMP_DIR=$(mktemp -d) +TEMP_RESOURCES="$TEMP_DIR/resources" + +# Create resources directory in temp +mkdir -p "$TEMP_RESOURCES" + +# Copy only .bin files preserving directory structure +bin_count=0 +find "$SOURCE_DIR" -name "*.bin" -type f | while read -r file; do + # Get relative path from source directory + relative_path="${file#$SOURCE_DIR/}" + target_path="$TEMP_RESOURCES/$relative_path" + + # Create target directory if needed + mkdir -p "$(dirname "$target_path")" + + # Copy the file + cp "$file" "$target_path" + ((bin_count++)) +done + +# Check if any .bin files were found +bin_files=$(find "$TEMP_RESOURCES" -name "*.bin" 2>/dev/null | wc -l) +if [ "$bin_files" -eq 0 ]; then + echo "Warning: No .bin files found in $SOURCE_DIR" + rm -rf "$TEMP_DIR" + exit 1 +fi + +# Save current directory before changing +ORIGINAL_DIR="$PWD" + +# Create tar.gz from temp directory +cd "$TEMP_DIR" +tar -czf "$OUTPUT_FILE" resources/ + +# Move the archive back to original directory +mv "$OUTPUT_FILE" "$ORIGINAL_DIR/" + +# Return to original directory +cd "$ORIGINAL_DIR" + +# Cleanup temp directory +rm -rf "$TEMP_DIR" + +echo "Created $OUTPUT_FILE with resources/ containing $bin_files .bin files" + +# Upload to S3 +echo "Uploading $OUTPUT_FILE to S3..." +if aws --endpoint=https://s3.cn-southeast-1.ifanrprod.com --profile=iot s3 cp "$OUTPUT_FILE" s3://iot-private/batch_input/; then + echo "Successfully uploaded $OUTPUT_FILE to S3" +else + echo "Error: Failed to upload to S3" + exit 1 +fi + diff --git a/CP02/read_bytes.sh b/CP02/read_bytes.sh new file mode 100755 index 0000000..9088922 --- /dev/null +++ b/CP02/read_bytes.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + exit 1 +fi + +BIN_FILE="$1" +OFFSET=0x1dcfe + +if [ ! -f "$BIN_FILE" ]; then + echo "Error: File '$BIN_FILE' not found" + exit 1 +fi + +FILE_SIZE=$(stat -f%z "$BIN_FILE" 2>/dev/null || stat -c%s "$BIN_FILE" 2>/dev/null) +DECIMAL_OFFSET=$((OFFSET)) + +if [ $DECIMAL_OFFSET -ge $FILE_SIZE ]; then + echo "Error: Offset $OFFSET ($DECIMAL_OFFSET) exceeds file size ($FILE_SIZE bytes)" + exit 1 +fi + +if [ $((DECIMAL_OFFSET + 2)) -gt $FILE_SIZE ]; then + echo "Error: Cannot read 2 bytes starting from offset $OFFSET (file too small)" + exit 1 +fi + +BYTES=$(xxd -s $DECIMAL_OFFSET -l 2 -p "$BIN_FILE") +BYTE1=$(echo "$BYTES" | cut -c1-2) +BYTE2=$(echo "$BYTES" | cut -c3-4) +echo "$BYTE2$BYTE1" \ No newline at end of file diff --git a/CP02/signer_new.py b/CP02/signer_new.py new file mode 100644 index 0000000..be6598a --- /dev/null +++ b/CP02/signer_new.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +from absl import app +from absl import flags +from absl import logging +import json +import shutil +import struct +import os +import time +import datetime + +FLAGS = flags.FLAGS + +# Define command-line flags +flags.DEFINE_string('signer', None, 'Path to the unsigned user application firmware.') +flags.DEFINE_string('firmware', None, 'Path to the unsigned user application firmware.') +flags.DEFINE_string('bootloader', None, 'Path to the unsigned bootloader.') +flags.DEFINE_string('output_dir', None, 'Directory to save the signed files.') +flags.DEFINE_boolean('enable_swd', False, 'Enable SWD feature') +flags.DEFINE_boolean('boot_directly', False, 'Enable boot directly to user application') + +# Ensure the flags are required +flags.mark_flag_as_required('output_dir') + +def leftrotate(x, n): + """Left rotate a 32-bit integer x by n bits.""" + return ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF + +def sha1_signer_python(data): + """Python implementation matching sha1_signer binary exactly (based on reverse engineering)""" + BLOCK_LEN = 64 + FIXED_SIZE = 0x1DF00 # sha1_signer processes exactly 0x1DF00 bytes + + # Create fixed-size buffer initialized with zeros (malloc + memset in C) + buffer = bytearray(FIXED_SIZE) + + # Copy file data to buffer (fread in C) + data_to_copy = min(len(data), FIXED_SIZE) + buffer[:data_to_copy] = data[:data_to_copy] + # Remaining bytes stay as 0 + + # Custom initial hash values (confirmed from reverse engineering) + hash_state = [0xef343ca7, 0x5c41a2de, 0x7ce9704f, 0x62312814, 0x4de11904] + + def sha1_compress(block, state): + """Compress one 64-byte block (16 rounds only, confirmed from disassembly)""" + a, b, c, d, e = state[0], state[1], state[2], state[3], state[4] + + # Process only 16 rounds (cmpl $0xf in disassembly) + for i in range(16): + # Load schedule using big-endian (confirmed from disassembly) + schedule = (block[i * 4] << 24) | (block[i * 4 + 1] << 16) | \ + (block[i * 4 + 2] << 8) | block[i * 4 + 3] + + # Round function with constant 0x7ebce2a7 (confirmed from disassembly) + f = ((b & c) | (~b & d)) & 0xFFFFFFFF + temp = (leftrotate(a, 5) + f + e + schedule + 0x7ebce2a7) & 0xFFFFFFFF + e = d + d = c + c = leftrotate(b, 30) + b = a + a = temp + + # Update state + state[0] = (state[0] + a) & 0xFFFFFFFF + state[1] = (state[1] + b) & 0xFFFFFFFF + state[2] = (state[2] + c) & 0xFFFFFFFF + state[3] = (state[3] + d) & 0xFFFFFFFF + state[4] = (state[4] + e) & 0xFFFFFFFF + + # Process complete 64-byte blocks from fixed-size buffer + data_len = FIXED_SIZE + offset = 0 + while data_len - offset >= BLOCK_LEN: + sha1_compress(buffer[offset:offset + BLOCK_LEN], hash_state) + offset += BLOCK_LEN + + # Handle remaining bytes and padding (standard SHA1 padding) + block = bytearray(BLOCK_LEN) + rem = data_len - offset + if rem > 0: + block[0:rem] = buffer[offset:offset + rem] + + # Add padding byte + block[rem] = 0x80 + rem += 1 + + # If not enough room for length, process this block and start a new one + LENGTH_SIZE = 8 + if BLOCK_LEN - rem < LENGTH_SIZE: + sha1_compress(bytes(block), hash_state) + block = bytearray(BLOCK_LEN) + + # Add message length in bits (big-endian, standard SHA1 encoding) + length_bits = data_len * 8 + for i in range(LENGTH_SIZE): + block[BLOCK_LEN - 1 - i] = (length_bits >> (i * 8)) & 0xFF + + # Process final block + sha1_compress(bytes(block), hash_state) + + return hash_state + +def generate_metadata_python(file_path): + """Generate metadata using Python implementation of sha1_signer.""" + with open(file_path, 'rb') as f: + data = f.read() + + # Use exact clone of sha1_signer binary (based on reverse engineering) + hash_values = sha1_signer_python(data) + + # Convert hash values to hex strings (same format as original) + checksum = [f"{val:08x}" for val in hash_values] + + # Generate version number using UTC date format (YYYYMMDDHH) + utc_now = datetime.datetime.utcnow() + version_number = int(f"{utc_now.year:04d}{utc_now.month:02d}{utc_now.day:02d}{utc_now.hour:02d}") + + # Set copyright + copyright_text = "Copyright (c) ifanr Inc, All Rights Reserved." + + return { + "version_number": version_number, + "checksum": checksum, + "copyright": copyright_text + } + +def extract_release_id(file_path): + """Extract the release ID from the binary.""" + with open(file_path, 'rb') as f: + f.seek(0xA8) + release_id = struct.unpack('= 0x1DF00: + raise ValueError("Firmware is too big") + + firmware_bin += b"\xFF" * (0x1DF00 - len(firmware_bin)) + release_id = extract_release_id(FLAGS.firmware) + unsigned_firmware_path = os.path.join(FLAGS.output_dir, f"firmware-unsigned-{release_id}.bin") + with open(unsigned_firmware_path, 'wb') as f: + f.write(firmware_bin) + + # Sign the firmware using Python implementation + metadata = generate_metadata_python(unsigned_firmware_path) + firmware_bin += b"\xFF" * 0x100 + signed_firmware_path = os.path.join(FLAGS.output_dir, f"firmware-{release_id}.bin") + with open(signed_firmware_path, 'wb') as f: + modified_firmware_bin = modify_firmware(firmware_bin, metadata, FLAGS.enable_swd, FLAGS.boot_directly) + f.write(modified_firmware_bin) + logging.info('Firmware %s written.', signed_firmware_path) + + # Write the metadata file + metadata_path = os.path.join(FLAGS.output_dir, f"firmware-{release_id}.json") + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=4) + logging.info('Metadata %s written.', metadata_path) + copy_associated_files(FLAGS.firmware, FLAGS.output_dir) + + if FLAGS.bootloader: + # Sign the bootloader using Python implementation + metadata = generate_metadata_python(FLAGS.bootloader) + release_id = extract_release_id(FLAGS.bootloader) + + with open(FLAGS.bootloader, 'rb') as f: + bootloader_bin = f.read() + + if len(bootloader_bin) > 0x1f00: + raise ValueError('Bootloader is too big') + bootloader_bin += b'\x00' * (0x1f00- len(bootloader_bin)) + bootloader_bin += b'\xff' * 0x10 + bootloader_bin += b'\x00' * 0xf0 + + for chip_id in range(5): + metadata['chip_id'] = chip_id + signed_bootloader_path = os.path.join(FLAGS.output_dir, f"bootloader-chip{chip_id}-{release_id}.bin") + with open(signed_bootloader_path, 'wb') as f: + modified_bootloader_bin = modify_bootloader(bootloader_bin, metadata) + f.write(modified_bootloader_bin) + logging.info('Bootloader %s written.', signed_bootloader_path) + + # Write the metadata file + metadata_path = os.path.join(FLAGS.output_dir, f"bootloader-{release_id}.json") + with open(metadata_path, 'w') as f: + json.dump(metadata, f, indent=4) + logging.info('Metadata %s written.', metadata_path) + copy_associated_files(FLAGS.bootloader, FLAGS.output_dir) + +if __name__ == '__main__': + app.run(main) \ No newline at end of file