// SPDX-License-Identifier: GPL-2.0-only /* * Copyright (C) 2020 Vincent Wiemann * * This program is derived from ZyXEL's GPL U-Boot code. * Copyright (C) 2011-2011 ZyXEL Communications, Corp. */ #define _POSIX_SOURCE #define _POSIX_C_SOURCE 199309L /* getopt */ #include #include #include #include #include #include #include #include #include #include #include "md5.h" #define ZYXEL_MAGIC 0xdeadbeaf #define MAX_MODELS 5 #define CHECKSUM_SIZE sizeof(uint32_t) #define MAX_FILES 32 #define MAX_FILENAME 64 #define DATE_SIZE 32 #define REV_SIZE 32 #define error(fmt, ...) \ do { \ printf("Error: " fmt, ##__VA_ARGS__); \ exit(1); \ } while (0) enum { FILE_TYPE_BM = 1, FILE_TYPE_KERNEL, FILE_TYPE_CORE, FILE_TYPE_DB, FILE_TYPE_CONF, FILE_TYPE_WTP }; struct file_type_tbl { int type; char *str; }; struct file_type_tbl file_types[] = { { FILE_TYPE_BM, "bm" }, { FILE_TYPE_KERNEL, "kernel" }, { FILE_TYPE_CORE, "core" }, { FILE_TYPE_DB, "db" }, { FILE_TYPE_CONF, "conf", }, { FILE_TYPE_WTP, "wtp" } }; static int FILE_TYPE_COUNT = sizeof(file_types) / sizeof(struct file_type_tbl); struct fw_header { uint32_t checksum; /* MD5-based checksum */ uint32_t magic; /* 0xdeadbeaf */ uint16_t version; /* version of the firmware archive */ uint16_t files_count; /* number of files contained */ uint16_t models_count; /* number of supported models */ uint16_t models[MAX_MODELS]; /* supported models' IDs */ uint32_t total_length; /* total length of the firmware archive */ uint16_t files_offset; /* offset of the first file */ uint16_t header_length; /* length of this header */ uint16_t info_length; /* length of the file information header */ uint16_t __padding1[3]; /* reserved for future use */ char capwap_version[32]; /* e.g. "undefined" */ char model_name[32]; /* e.g. "NWA512X-FAT" */ } __attribute__((packed)); struct fw_header_file { uint16_t type; /* file type (e.g. FILE_TYPE_KERNEL) */ uint16_t flags; /* unknown */ uint32_t length; /* length of the file */ uint32_t checksum; /* checksum of the file */ uint32_t flash_offset; /* write offset from beginning of flash */ char filename[MAX_FILENAME]; /* original file name */ char target[128]; /* target "file" name (e.g. "kernel", "zldfs") */ char revision[REV_SIZE]; /* revision string */ char date[32]; /* creation date string */ } __attribute__((packed)); struct fw_header_kernel { char bm_version[64]; char kernel_version[64]; char core_version[64]; char capwap_version[32]; char model_name[32]; uint32_t bm_checksum; uint32_t kernel_checksum; uint32_t zld_checksum; uint32_t core_checksum; uint16_t max_models; uint16_t models[MAX_MODELS]; char padding[512 - 64 * 4 - 4 * 4 - 2 - MAX_MODELS * 2 - 4]; uint32_t baudrate; } __attribute__((packed)); struct firmware_file { struct fw_header_file header; size_t offset; char filepath[PATH_MAX]; }; struct firmware { struct fw_header header; struct firmware_file *files; size_t files_count; struct fw_header_kernel kernel_header; char lower_endian; }; static size_t get_file_size(FILE *fp) { size_t pos = (size_t)ftell(fp); size_t file_size; fseek(fp, 0, SEEK_END); file_size = (size_t)ftell(fp); fseek(fp, (long int)pos, SEEK_SET); return file_size; } static void copy_from_to_file(FILE *fp_src, size_t src_offset, FILE *fp_dst, size_t dst_offset, size_t length) { int buf[512]; size_t len; if (src_offset) fseek(fp_src, (long int)src_offset, SEEK_SET); if (dst_offset) fseek(fp_src, (long int)dst_offset, SEEK_SET); while (length) { if (length >= sizeof(buf)) { len = sizeof(buf); } else { len = length; } length -= len; if (fread(buf, len, 1, fp_src) != 1) error("Failed to read"); if (fwrite(buf, len, 1, fp_dst) != 1) error("Failed to write"); } } static void extract_to_file(FILE *fp_src, char *dst, size_t length, size_t offset) { FILE *fp_dst; if (!(fp_dst = fopen(dst, "wb"))) error("Failed to open %s for writing", dst); copy_from_to_file(fp_src, offset, fp_dst, 0, length); fclose(fp_dst); } static void dump_firmware_header(struct fw_header *header_p) { int i; printf("FIRMWARE HEADER\n"); printf("checksum = 0x%08x, magic = 0x%08x\n", header_p->checksum, header_p->magic); printf("version = 0x%04x, files_count = %12d\n", header_p->version, header_p->files_count); printf("models_count = %12d, total_length = %12d\n", header_p->models_count, header_p->total_length); printf("files_offset = 0x%04x, header_length = %12d\n", header_p->files_offset, header_p->header_length); printf("info_length = %12d, capwap_version = %12s\n", header_p->info_length, header_p->capwap_version); printf("model_name = %s\n", header_p->model_name); printf("models ="); for (i = 0; i < header_p->models_count && i < MAX_MODELS; i++) printf(" 0x%04x", header_p->models[i]); printf("\n\n"); } static int get_file_type_id(char *type) { struct file_type_tbl *ft = file_types; int i; for (i = 0; i < FILE_TYPE_COUNT; i++, ft++) if (!strcmp(type, ft->str)) return ft->type; printf("Supported file types:\n"); for (i = 0, ft = file_types; i < FILE_TYPE_COUNT; i++, ft++) printf("%8s (ID 0x%x)\n", ft->str, ft->type); error("Unknown file type \"%s\"\n", type); return 0; } static char *get_file_type_string(int type) { struct file_type_tbl *ft = file_types; int i; for (i = 0; i < FILE_TYPE_COUNT; i++, ft++) if (ft->type == type) return ft->str; return NULL; } static void dump_file_header(struct fw_header_file *file_header) { printf("\nfilename=%s, type=%s, flags=0x%x target=%s, revision=%s\n", file_header->filename, get_file_type_string(file_header->type), file_header->flags, file_header->target, file_header->revision); printf("date=%s, length=%u, checksum=0x%08x, flash_offset=0x%08x\n", file_header->date, file_header->length, file_header->checksum, file_header->flash_offset); } static void dump_kernel_header(struct fw_header_kernel *kernel_header) { int i; printf("KERNEL HEADER (%lu bytes)\n", sizeof(struct fw_header_kernel)); printf("bm_version = %s\n", kernel_header->bm_version); printf("kernel_version = %s\n", kernel_header->kernel_version); printf("core_version = %s\n", kernel_header->core_version); printf("capwap_version = %s\n", kernel_header->capwap_version); printf("model_name = %s\n", kernel_header->model_name); printf("bm_checksum = 0x%08x\n", kernel_header->bm_checksum); printf("kernel_checksum = 0x%08x\n", kernel_header->kernel_checksum); printf("zld_checksum = 0x%08x\n", kernel_header->zld_checksum); printf("core_checksum = 0x%08x\n", kernel_header->core_checksum); printf("max_models = %u\n", kernel_header->max_models); printf("models ="); for (i = 0; i < 5; i++) printf(" 0x%04x", kernel_header->models[i]); printf("\n"); printf("baudrate = %u\n", kernel_header->baudrate); printf("\n"); } static void translate_fw_header(struct fw_header *header_p) { int i; header_p->magic = ntohl(header_p->magic); header_p->version = ntohs(header_p->version); header_p->files_count = ntohs(header_p->files_count); header_p->models_count = ntohs(header_p->models_count); for (i = 0; i < MAX_MODELS; i++) header_p->models[i] = ntohs(header_p->models[i]); header_p->total_length = ntohl(header_p->total_length); header_p->files_offset = ntohs(header_p->files_offset); header_p->header_length = ntohs(header_p->header_length); header_p->info_length = ntohs(header_p->info_length); } static void translate_file_header(struct fw_header_file *file_header) { file_header->type = ntohs(file_header->type); file_header->flags = ntohs(file_header->flags); file_header->length = ntohl(file_header->length); file_header->checksum = ntohl(file_header->checksum); file_header->flash_offset = ntohl(file_header->flash_offset); } static void translate_kernel_header(struct fw_header_kernel *kernel_header) { int i; kernel_header->bm_checksum = ntohl(kernel_header->bm_checksum); kernel_header->kernel_checksum = ntohl(kernel_header->kernel_checksum); /*kernel_header->zld_checksum = ntohl(kernel_header->zld_checksum);*/ kernel_header->core_checksum = ntohl(kernel_header->core_checksum); kernel_header->max_models = ntohs(kernel_header->max_models); for (i = 0; i < MAX_MODELS; i++) kernel_header->models[i] = ntohs(kernel_header->models[i]); kernel_header->baudrate = ntohl(kernel_header->baudrate); } static void checksum_add_from_buf(MD5_CTX *ctx, void *buf, size_t length, size_t offset) { char *begin = &((char *)buf)[offset]; MD5_Update(ctx, begin, length); } static void checksum_add_from_file(MD5_CTX *ctx, FILE *fp_src, size_t length, size_t offset) { int buf[512]; size_t len; fseek(fp_src, (long int)offset, SEEK_SET); while (length) { if (length >= sizeof(buf)) { len = sizeof(buf); } else { len = length; } length -= len; if (fread(buf, len, 1, fp_src) != 1) error("Failed to read for checksum calculation"); checksum_add_from_buf(ctx, buf, len, 0); } } static uint32_t checksum_finish(MD5_CTX *ctx) { unsigned char md5sum[16]; uint32_t checksum = 0; int i; MD5_Final(md5sum, ctx); for (i = 0; i < 16; i += 4) checksum += ((uint32_t)md5sum[i] << 24 | (uint32_t)md5sum[i + 1] << 16 | (uint32_t)md5sum[i + 2] << 8 | (uint32_t)md5sum[i + 3]); if (checksum == 0) checksum = 1; return checksum; } static uint32_t checksum_calculate(FILE *fp, size_t kernel_offset) { struct fw_header_kernel dummy; MD5_CTX ctx; size_t file_size; fseek(fp, 0, SEEK_SET); file_size = get_file_size(fp); MD5_Init(&ctx); checksum_add_from_file(&ctx, fp, kernel_offset - CHECKSUM_SIZE, CHECKSUM_SIZE); /* use a zeroed out kernel version header */ memset(&dummy, 0, sizeof(dummy)); checksum_add_from_buf(&ctx, &dummy, sizeof(dummy), 0); checksum_add_from_file(&ctx, fp, file_size - kernel_offset - sizeof(dummy), kernel_offset + sizeof(dummy)); return checksum_finish(&ctx); } static uint32_t checksum_calculate_file(char *filename) { MD5_CTX ctx; FILE *fp; size_t file_size; if (!(fp = fopen(filename, "rb"))) error("Failed to open %s for writing\n", filename); file_size = get_file_size(fp); MD5_Init(&ctx); checksum_add_from_file(&ctx, fp, file_size, 0); fclose(fp); return checksum_finish(&ctx); } static void parse_firmware(struct firmware *fw, FILE *fp) { struct firmware_file *file; size_t file_size, file_offset, kernel_offset; uint32_t checksum = 0; int i; if (!fw) error("Failed to allocate firmware struct\n"); file_size = get_file_size(fp); if (file_size < sizeof(fw->header)) error("File too small\n"); if (1 != fread(&fw->header, sizeof(fw->header), 1, fp)) error("Failed to read firmware header\n"); if (ntohl(fw->header.magic) == ZYXEL_MAGIC) { fw->lower_endian = 1; translate_fw_header(&fw->header); } else if (fw->header.magic != ZYXEL_MAGIC) { error("Unsupported magic. Expected 0x%x, but found 0x%x\n", ZYXEL_MAGIC, fw->header.magic); } if (fw->header.models_count != MAX_MODELS) error("Wrong number of models. Expected %u, but found %u\n", MAX_MODELS, fw->header.models_count); dump_firmware_header(&fw->header); if (fw->header.total_length != file_size) error("File size does not match. Expected %lu, but found %u\n", file_size, fw->header.total_length); file_offset = sizeof(fw->header) + fw->header.files_count * sizeof(struct fw_header_file); if (file_offset != fw->header.files_offset) error("File offset does not match definition in header\n"); if (file_size < file_offset) error("File too small for %u file headers\n", fw->header.files_count); if (NULL == (fw->files = malloc(fw->header.files_count * sizeof(struct firmware_file)))) error("Failed to allocate memory for %u file structs\n", fw->header.files_count); for (i = 0, file = fw->files; i < fw->header.files_count; i++, file++) { if (1 != fread(file, sizeof(struct fw_header_file), 1, fp)) error("Failed to read file header #%u\n", i + 1); if (fw->lower_endian) translate_file_header(&file->header); if (file_offset + file->header.length > fw->header.total_length) error("File offset exceeds size of firmware archive\n"); if (file->header.type == FILE_TYPE_KERNEL) kernel_offset = file_offset; file->offset = file_offset; file_offset += file->header.length; } if (!kernel_offset) error("Kernel image missing for checksum calculation\n"); /* as we know the kernel offset, we can calculate the checksum * as it must be excluded from checksum calculation */ checksum = checksum_calculate(fp, kernel_offset); if (fw->lower_endian) checksum = ntohl(checksum); if (checksum != fw->header.checksum) printf("WARN: Checksum mismatch. Calculated 0x%x\n", checksum); fseek(fp, (long int)kernel_offset, SEEK_SET); if (1 != fread(&fw->kernel_header, sizeof(fw->kernel_header), 1, fp)) error("Failed to read kernel header\n"); if (fw->lower_endian) translate_kernel_header(&fw->kernel_header); dump_kernel_header(&fw->kernel_header); } static void extract_firmware(struct firmware *fw, char *filename) { struct firmware_file *file; FILE *fp; int i; if (!(fp = fopen(filename, "rb"))) error("Failed to open firmware archive for extraction %s\n", filename); parse_firmware(fw, fp); printf("Extracting files..."); for (i = 0, file = fw->files; i < fw->header.files_count; i++, file++) { dump_file_header(&file->header); if (file->header.type == FILE_TYPE_KERNEL) { /* strip kernel header */ extract_to_file( fp, file->header.filename, file->header.length - sizeof(struct fw_header_kernel), file->offset + sizeof(struct fw_header_kernel)); } else { extract_to_file(fp, file->header.filename, file->header.length, file->offset); } printf("Calculated file checksum is 0x%08x\n", checksum_calculate_file(file->header.filename)); } free(fw->files); fclose(fp); } static void init_fw_header(struct firmware *fw) { int i; for (i = 0; i < MAX_MODELS; i++) fw->header.models[i] = 0xffff; fw->kernel_header.baudrate = 115200; fw->kernel_header.max_models = MAX_MODELS; /* ZyXEL messed up their code and included a 32 bit pointer */ fw->header.header_length = sizeof(fw->header) + 4; fw->header.magic = ZYXEL_MAGIC; } static void write_headers(FILE *fp, struct firmware *fw) { struct firmware_file *file; unsigned int i; fseek(fp, 0, SEEK_SET); if (1 != fwrite(&fw->header, sizeof(fw->header), 1, fp)) error("Failed to write firmware header\n"); for (i = 0, file = fw->files; i < fw->files_count; i++, file++) if (1 != fwrite(&file->header, sizeof(struct fw_header_file), 1, fp)) error("Failed to write file header #%u\n", i + 1); } static void usage(char *progname) { printf("Usage: %s\n" " For extraction:\n" " -e extract firmware \n\n" " For creation:\n" " -v set hexadecimal firmware \n" " -b set hexadecimal bootmanager \n" " -c set capwap string\n" " -m set string\n" " -d set (up to %u) hexadecimal \n" " (multiple input files)\n" " -i add input \n" " -o set hexadecimal flash offset for file\n" " -r set revision string for file\n" " -t choose file \n" " -x set (partition) of file\n" " \n", progname, MAX_MODELS); exit(1); } int main(int argc, char *argv[]) { struct firmware fw; struct firmware_file *file; struct firmware_file *core_file = NULL; struct stat attr; static const char *optstr = "e:v:b:c:m:d:i:o:r:t:x:h"; const char *capwap_version = "undefined"; const char *separator = " | "; char *filename; FILE *fp_src, *fp_dst; size_t kernel_offset = 0; unsigned int i; int opt; memset(&fw, 0, sizeof(fw)); init_fw_header(&fw); if (argc < 1) usage(argv[0]); opt = getopt(argc, argv, optstr); if (opt == -1) usage(argv[0]); while (opt != -1) { if (optarg == NULL) usage(argv[0]); switch (opt) { case 'e': extract_firmware(&fw, optarg); exit(0); case 'v': fw.header.version = (uint16_t)strtol(optarg, NULL, 16); if (!fw.header.version) error("Invalid version number\n"); break; case 'b': fw.kernel_header.bm_checksum = (uint32_t)strtol(optarg, NULL, 16); break; case 'c': strncpy(fw.header.capwap_version, optarg, sizeof(fw.header.capwap_version) - 1); break; case 'm': strncpy(fw.header.model_name, optarg, sizeof(fw.header.model_name) - 1); break; case 'd': if (fw.header.models_count == MAX_MODELS) error("Max. number of supported models is %u\n", MAX_MODELS); fw.header.models[fw.header.models_count] = (uint16_t)strtol(optarg, NULL, 16); fw.header.models_count++; break; case 'i': filename = optarg; if (!fw.files && NULL == (fw.files = malloc( MAX_FILES * sizeof(struct firmware_file)))) error("Failed to allocate %u file structs\n", MAX_FILES); if (fw.files_count == MAX_FILES) error("Maximum number of files reached (%u)\n", MAX_FILES); if (stat(optarg, &attr)) error("Stat failed on %s\n", optarg); strftime(fw.files[fw.files_count].header.date, DATE_SIZE - 1, "%Y-%m-%d %H:%M:%S", localtime(&attr.st_mtime)); strncpy(fw.files[fw.files_count].filepath, optarg, PATH_MAX - 1); filename = strrchr(optarg, '/'); if (!filename) filename = optarg; strncpy(fw.files[fw.files_count].header.filename, filename, MAX_FILENAME - 1); fw.files_count++; break; case 'o': if (!fw.files_count) error("Specify offset after filename\n"); fw.files[fw.files_count - 1].header.flash_offset = (uint32_t)strtol(optarg, NULL, 16); break; case 'r': if (!fw.files_count) error("Specify file revision after filename\n"); strncpy(fw.files[fw.files_count - 1].header.revision, optarg, REV_SIZE - 1); break; case 't': if (!fw.files_count) error("Specify file type after filename!\n"); fw.files[fw.files_count - 1].header.type = get_file_type_id(optarg); break; case 'x': if (!fw.files_count) error("Specify file target after filename!\n"); strncpy(fw.files[fw.files_count - 1].header.target, optarg, sizeof(fw.files[0].header.target) - 1); break; case 'h': default: usage(argv[0]); } opt = getopt(argc, argv, optstr); } if (!fw.header.models_count) error("Supported model IDs missing (option -d)\n"); if (!fw.header.version) error("Version number missing (e.g. -v 0x100)\n"); if (!fw.kernel_header.bm_checksum) error("Bootmanager checksum is missing (option -b)\n"); if (!strlen(fw.header.model_name)) error("Model name missing (option -m)\n"); if (!strlen(fw.header.capwap_version)) strncpy(fw.header.capwap_version, capwap_version, sizeof(fw.header.capwap_version) - 1); fw.header.models_count = MAX_MODELS; fw.header.files_count = fw.files_count; memcpy(fw.kernel_header.models, fw.header.models, sizeof(fw.header.models)); for (i = 0; i < fw.files_count; i++) { if (!fw.files[i].header.type) error("No file or type specified for file %s\n", fw.files[i].filepath); if (!strlen(fw.files[i].header.target)) error("Target missing for %s (e.g. -x zldfs)\n", fw.files[i].filepath); if (!strlen(fw.files[i].header.revision)) error("Revision missing for %s\n", fw.files[i].filepath); } filename = argv[optind]; if (!(fp_dst = fopen(filename, "w+b"))) error("Failed to open %s for writing\n", filename); write_headers(fp_dst, &fw); fw.header.info_length = sizeof(struct fw_header_file); fw.header.files_offset = sizeof(fw.header) + fw.files_count * fw.header.info_length; if ((size_t)ftell(fp_dst) != fw.header.files_offset) error("Oops. Something went wrong writing the file headers"); fw.header.total_length = fw.header.files_offset; for (i = 0, file = fw.files; i < fw.files_count; i++, file++) { file->header.checksum = checksum_calculate_file(file->filepath); if (!(fp_src = fopen(file->filepath, "rb"))) error("Failed to open %s for writing\n", filename); file->offset = fw.header.total_length; file->header.length = get_file_size(fp_src); if (file->header.type == FILE_TYPE_KERNEL) if (1 != fwrite(&fw.kernel_header, sizeof(fw.kernel_header), 1, fp_dst)) error("Failed to write kernel header\n"); copy_from_to_file(fp_src, 0, fp_dst, 0, file->header.length); if (file->header.type == FILE_TYPE_KERNEL) { file->header.length += sizeof(fw.kernel_header); kernel_offset = file->offset; fw.kernel_header.kernel_checksum = file->header.checksum; if (strlen(file->header.revision) + strlen(separator) + strlen(file->header.date) >= sizeof(fw.kernel_header.kernel_version)) error("Kernel file revision too long\n"); strcat(fw.kernel_header.kernel_version, file->header.revision); strcat(fw.kernel_header.kernel_version, separator); strcat(fw.kernel_header.kernel_version, file->header.date); } else if (file->header.type == FILE_TYPE_CORE) { core_file = file; } fw.header.total_length += file->header.length; translate_file_header(&file->header); fclose(fp_src); } /* update headers with correct lengths and endianness */ translate_fw_header(&fw.header); write_headers(fp_dst, &fw); if (!kernel_offset) error("Kernel image needed for checksum calculation\n"); /* update headers with correct checksum */ fw.header.checksum = htonl(checksum_calculate(fp_dst, kernel_offset)); fseek(fp_dst, 0, SEEK_SET); fwrite(&fw.header.checksum, sizeof(fw.header.checksum), 1, fp_dst); fw.kernel_header.zld_checksum = fw.header.checksum; strncpy(fw.kernel_header.model_name, fw.header.model_name, sizeof(fw.kernel_header.model_name) - 1); strncpy(fw.kernel_header.capwap_version, fw.header.capwap_version, sizeof(fw.kernel_header.capwap_version) - 1); translate_kernel_header(&fw.kernel_header); if (core_file) { fw.kernel_header.core_checksum = core_file->header.checksum; if (strlen(core_file->header.revision) + strlen(separator) + strlen(core_file->header.date) >= sizeof(fw.kernel_header.core_version)) error("Core file revision string too long\n"); strcat(fw.kernel_header.core_version, core_file->header.revision); strcat(fw.kernel_header.core_version, separator); strcat(fw.kernel_header.core_version, core_file->header.date); } fseek(fp_dst, (long int)kernel_offset, SEEK_SET); fwrite(&fw.kernel_header, sizeof(fw.kernel_header), 1, fp_dst); fclose(fp_dst); exit(0); }