Add MediaTek md1img and GFH firmware image parsers (#5974)
- Introduced md1img.h and md1img.c for parsing MediaTek md1img container format. - Implemented mtk.h and mtk.c for parsing MediaTek GFH firmware images (md1rom). - Added plugin support for md1img and mtk formats in bin_md1img.c and bin_mtk.c. - Updated meson.build to include new source files and plugins. - Enhanced RzBuffer utility with LZMA alone decompression support. --------- Co-authored-by: Giovanni <561184+wargio@users.noreply.github.com>
This commit is contained in:
parent
bb3b7cc7b1
commit
9d37b7cdf2
12 changed files with 1828 additions and 1 deletions
665
librz/bin/format/mtk/md1img.c
Normal file
665
librz/bin/format/mtk/md1img.c
Normal file
|
|
@ -0,0 +1,665 @@
|
|||
// SPDX-FileCopyrightText: 2025 godcodehunter
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
/**
|
||||
* \file Parser for MediaTek md1img container format.
|
||||
*
|
||||
* The md1img format is a simple section-based container used by MediaTek
|
||||
* to package modem firmware components: md1rom (GFH firmware image),
|
||||
* debug info (CATI format), DSP images, certificates, etc.
|
||||
*
|
||||
* Each section has a header with magic 0x58881688, a name, data size,
|
||||
* memory address, and other metadata, followed by the section data
|
||||
* and alignment padding.
|
||||
*
|
||||
* The md1rom section is parsed internally using the mtk GFH parser to
|
||||
* provide proper base address, entry points, and code/header sections.
|
||||
* Debug symbols are extracted from the CATI-format dbginfo section.
|
||||
*
|
||||
* References:
|
||||
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy (md1img)
|
||||
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_dbg_info.ksy (CATI)
|
||||
*/
|
||||
#include "md1img.h"
|
||||
|
||||
// --- CATI debug info parsing ---
|
||||
|
||||
static void mtk_dbg_symbol_free(MtkDbgSymbol *sym) {
|
||||
if (sym) {
|
||||
free(sym->name);
|
||||
free(sym);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Parse symbols from a single CATI debug entry.
|
||||
*
|
||||
* The debug entry format (after "CATI" magic + type):
|
||||
* unk3(u32), unk_str1(strz), unk_str2(strz), unk_str3(strz), date_str(strz),
|
||||
* symbols_start(u32), files_start(u32),
|
||||
* symbol_entries: [strz name, u32 addr1, u32 addr2] ... until empty name
|
||||
*/
|
||||
static bool md1img_parse_cati_debug(RzBuffer *b, RzPVector /*<MtkDbgSymbol *>*/ *symbols) {
|
||||
// Skip unk3
|
||||
rz_buf_seek(b, 4, RZ_BUF_CUR);
|
||||
|
||||
// Skip 4 strz fields (unk_str1, unk_str2, unk_str3, date_str)
|
||||
for (int i = 0; i < 4; i++) {
|
||||
ut64 len = rz_buf_read_string(b, NULL);
|
||||
if (len == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip symbols_start and files_start
|
||||
rz_buf_seek(b, 8, RZ_BUF_CUR);
|
||||
|
||||
// Parse symbol entries (limit guards against corrupted/missing terminator)
|
||||
int count = 0;
|
||||
while (count < 500000) {
|
||||
char *name = NULL;
|
||||
ut64 len = rz_buf_read_string(b, &name);
|
||||
if (len == 0 || !name) {
|
||||
free(name);
|
||||
break;
|
||||
}
|
||||
// Empty string = terminator
|
||||
if (name[0] == '\0') {
|
||||
free(name);
|
||||
break;
|
||||
}
|
||||
|
||||
ut32 addr1 = 0, addr2 = 0;
|
||||
if (!rz_buf_read_le32(b, &addr1) || !rz_buf_read_le32(b, &addr2)) {
|
||||
free(name);
|
||||
break;
|
||||
}
|
||||
|
||||
MtkDbgSymbol *sym = RZ_NEW0(MtkDbgSymbol);
|
||||
if (!sym) {
|
||||
free(name);
|
||||
break;
|
||||
}
|
||||
sym->name = name;
|
||||
sym->addr = addr1;
|
||||
// For corrupted entries
|
||||
sym->size = (addr2 > addr1) ? (addr2 - addr1) : 0;
|
||||
rz_pvector_push(symbols, sym);
|
||||
count++;
|
||||
}
|
||||
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Parse a CATI container wrapping one or more debug entries.
|
||||
*/
|
||||
static bool md1img_parse_cati_container(RzBuffer *b, RzPVector /*<MtkDbgSymbol *>*/ *symbols) {
|
||||
// unk1(u32), size(u32)
|
||||
ut32 unk1 = 0, container_size = 0;
|
||||
if (!rz_buf_read_le32(b, &unk1) || !rz_buf_read_le32(b, &container_size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 0x10 = CATI header size (magic[4] + type[4] + unk1[4] + size[4])
|
||||
if (container_size < 0x10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The nested entries are within (container_size - 0x10) bytes
|
||||
ut64 entries_end = rz_buf_tell(b) + (container_size - 0x10);
|
||||
ut64 buf_size = rz_buf_size(b);
|
||||
// Clamp to actual buffer size in case container_size is corrupted
|
||||
if (entries_end > buf_size) {
|
||||
entries_end = buf_size;
|
||||
}
|
||||
|
||||
// Parse nested CATI headers
|
||||
while (rz_buf_tell(b) + 8 <= entries_end) {
|
||||
ut8 magic[MTK_CATI_MAGIC_SIZE];
|
||||
if (rz_buf_read(b, magic, MTK_CATI_MAGIC_SIZE) != MTK_CATI_MAGIC_SIZE ||
|
||||
memcmp(magic, MTK_CATI_MAGIC, MTK_CATI_MAGIC_SIZE) != 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
ut32 cati_type = 0;
|
||||
if (!rz_buf_read_le32(b, &cati_type)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (cati_type != MTK_CATI_TYPE_DEBUG && cati_type != MTK_CATI_TYPE_DEBUG_DSP) {
|
||||
break;
|
||||
}
|
||||
md1img_parse_cati_debug(b, symbols);
|
||||
}
|
||||
|
||||
return rz_pvector_len(symbols) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Parse a CATI debug info buffer and extract symbols.
|
||||
*/
|
||||
static RzPVector /*<MtkDbgSymbol *>*/ *md1img_parse_dbginfo(RzBuffer *b) {
|
||||
rz_return_val_if_fail(b, NULL);
|
||||
|
||||
ut8 magic[MTK_CATI_MAGIC_SIZE];
|
||||
if (rz_buf_read_at(b, 0, magic, MTK_CATI_MAGIC_SIZE) != MTK_CATI_MAGIC_SIZE) {
|
||||
return NULL;
|
||||
}
|
||||
if (memcmp(magic, MTK_CATI_MAGIC, MTK_CATI_MAGIC_SIZE) != 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rz_buf_seek(b, 0 + MTK_CATI_MAGIC_SIZE, RZ_BUF_SET);
|
||||
|
||||
ut32 cati_type = 0;
|
||||
if (!rz_buf_read_le32(b, &cati_type)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RzPVector *symbols = rz_pvector_new((RzPVectorFree)mtk_dbg_symbol_free);
|
||||
if (!symbols) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
if (cati_type == MTK_CATI_TYPE_CONTAINER) {
|
||||
ok = md1img_parse_cati_container(b, symbols);
|
||||
} else if (cati_type == MTK_CATI_TYPE_DEBUG || cati_type == MTK_CATI_TYPE_DEBUG_DSP) {
|
||||
ok = md1img_parse_cati_debug(b, symbols);
|
||||
}
|
||||
|
||||
if (!ok || rz_pvector_len(symbols) == 0) {
|
||||
rz_pvector_free(symbols);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return symbols;
|
||||
}
|
||||
|
||||
// --- md1img container parsing ---
|
||||
|
||||
static bool md1img_read_section_hdr(RzBuffer *b, ut64 section_start, Md1imgSection *sec) {
|
||||
ut64 offset = section_start;
|
||||
|
||||
// Check magic
|
||||
ut8 magic[MD1IMG_MAGIC_SIZE];
|
||||
if (!rz_buf_read_offset(b, &offset, magic, MD1IMG_MAGIC_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
if (memcmp(magic, MD1IMG_MAGIC, MD1IMG_MAGIC_SIZE) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read dsize
|
||||
if (!rz_buf_read_le32_offset(b, &offset, &sec->dsize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read name (32 bytes, null-terminated)
|
||||
if (!rz_buf_read_offset(b, &offset, (ut8 *)sec->name, MD1IMG_NAME_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
sec->name[MD1IMG_NAME_SIZE - 1] = '\0';
|
||||
|
||||
// Read maddr and mode
|
||||
if (!rz_buf_read_le32_offset(b, &offset, &sec->maddr) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->mode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify ext_magic
|
||||
ut8 ext_magic[MD1IMG_EXT_MAGIC_SIZE];
|
||||
if (!rz_buf_read_offset(b, &offset, ext_magic, MD1IMG_EXT_MAGIC_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
if (memcmp(ext_magic, MD1IMG_EXT_MAGIC, MD1IMG_EXT_MAGIC_SIZE) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read remaining header fields
|
||||
if (!rz_buf_read_le32_offset(b, &offset, &sec->hdr_size) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->hdr_version) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->img_type) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->img_list_end) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->align_size) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->dsize_extend) ||
|
||||
!rz_buf_read_le32_offset(b, &offset, &sec->maddr_extend)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sec->hdr_size < MD1IMG_MIN_HDR_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sec->data_offset = section_start + sec->hdr_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
RZ_IPI bool md1img_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b) {
|
||||
rz_return_val_if_fail(b, false);
|
||||
|
||||
if (rz_buf_size(b) < MD1IMG_MIN_HDR_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ut8 magic[MD1IMG_MAGIC_SIZE];
|
||||
if (rz_buf_read_at(b, 0, magic, MD1IMG_MAGIC_SIZE) != MD1IMG_MAGIC_SIZE) {
|
||||
return false;
|
||||
}
|
||||
return memcmp(magic, MD1IMG_MAGIC, MD1IMG_MAGIC_SIZE) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Register the section's virtual file and run per-section parsers
|
||||
* (GFH for md1rom, CATI for dbginfo). Takes ownership of \p vbuf.
|
||||
*/
|
||||
static void md1img_load_section(Md1imgObj *md1, int sec_idx, RZ_BORROW RZ_NONNULL const Md1imgSection *sec, RZ_OWN RZ_NONNULL RzBuffer *vbuf) {
|
||||
RzBinVirtualFile *vfile = RZ_NEW0(RzBinVirtualFile);
|
||||
if (!vfile) {
|
||||
rz_buf_free(vbuf);
|
||||
return;
|
||||
}
|
||||
vfile->buf = vbuf;
|
||||
vfile->buf_owned = true;
|
||||
vfile->name = rz_str_dup(sec->name);
|
||||
rz_pvector_push(md1->vfiles, vfile);
|
||||
|
||||
// Parse md1rom section with mtk GFH parser
|
||||
if (rz_str_casestr(sec->name, "md1rom") && md1->md1rom_idx < 0) {
|
||||
md1->md1rom_idx = sec_idx;
|
||||
// Search for GFH magic within md1rom data (may not be at offset 0)
|
||||
ut64 gfh_off = 0;
|
||||
ut8 magic_buf[4];
|
||||
while (gfh_off + MTK_GFH_MIN_FILE_SIZE <= sec->dsize) {
|
||||
if (rz_buf_read_at(vbuf, gfh_off, magic_buf, 4) != 4) {
|
||||
break;
|
||||
}
|
||||
ut32 magic_val = rz_read_le32(magic_buf);
|
||||
if ((magic_val & MTK_GFH_MAGIC_MASK) == MTK_GFH_MAGIC) {
|
||||
break;
|
||||
}
|
||||
gfh_off += 4;
|
||||
}
|
||||
if (gfh_off + MTK_GFH_MIN_FILE_SIZE <= sec->dsize) {
|
||||
RzBuffer *gfh_buf = rz_buf_new_slice(vbuf, gfh_off, sec->dsize - gfh_off);
|
||||
if (gfh_buf) {
|
||||
md1->mtk = mtk_obj_new(gfh_buf);
|
||||
rz_buf_free(gfh_buf);
|
||||
}
|
||||
if (md1->mtk) {
|
||||
md1->gfh_offset = gfh_off;
|
||||
RZ_LOG_INFO("md1img: parsed GFH from '%s' at offset 0x%" PFMT64x " (load_addr=0x%x, entry=0x%x)\n",
|
||||
sec->name, gfh_off, md1->mtk->file_info.load_addr, md1->mtk->entry_vaddr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse CATI debug info from dbginfo sections
|
||||
if (rz_str_casestr(sec->name, "dbginfo") && sec->dsize > MTK_CATI_MAGIC_SIZE) {
|
||||
RzPVector *syms = md1img_parse_dbginfo(vbuf);
|
||||
// Try LZMA alone decompression if raw CATI parsing failed (dbginfo may be LZMA-compressed)
|
||||
if (!syms) {
|
||||
RzBuffer *decompressed = rz_buf_new_empty(0);
|
||||
if (decompressed && rz_lzma_alone_dec_buf(vbuf, decompressed, 4096)) {
|
||||
syms = md1img_parse_dbginfo(decompressed);
|
||||
}
|
||||
rz_buf_free(decompressed);
|
||||
}
|
||||
if (syms) {
|
||||
if (!md1->dbg_symbols) {
|
||||
md1->dbg_symbols = syms;
|
||||
} else {
|
||||
// Merge symbols from additional dbginfo sections
|
||||
void **it;
|
||||
rz_pvector_foreach (syms, it) {
|
||||
rz_pvector_push(md1->dbg_symbols, *it);
|
||||
}
|
||||
// Disown elements before freeing (ownership transferred)
|
||||
syms->v.free = NULL;
|
||||
rz_pvector_free(syms);
|
||||
}
|
||||
RZ_LOG_INFO("md1img: loaded debug symbols from '%s'\n", sec->name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RZ_IPI bool md1img_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb) {
|
||||
rz_return_val_if_fail(bf && obj && b, false);
|
||||
|
||||
Md1imgObj *md1 = RZ_NEW0(Md1imgObj);
|
||||
if (!md1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
md1->sections = rz_vector_new(sizeof(Md1imgSection), NULL, NULL);
|
||||
md1->vfiles = rz_pvector_new((RzPVectorFree)rz_bin_virtual_file_free);
|
||||
md1->md1rom_idx = -1;
|
||||
|
||||
if (!md1->sections || !md1->vfiles) {
|
||||
rz_vector_free(md1->sections);
|
||||
rz_pvector_free(md1->vfiles);
|
||||
free(md1);
|
||||
return false;
|
||||
}
|
||||
|
||||
ut64 buf_size = rz_buf_size(b);
|
||||
ut64 offset = 0;
|
||||
int count = 0;
|
||||
|
||||
// Limit iteration count as a safeguard against corrupted headers
|
||||
while (offset + MD1IMG_MIN_HDR_SIZE <= buf_size && count < 256) {
|
||||
Md1imgSection sec = { 0 };
|
||||
if (!md1img_read_section_hdr(b, offset, &sec)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate that data fits in the buffer
|
||||
if (sec.data_offset + sec.dsize > buf_size) {
|
||||
RZ_LOG_WARN("md1img: section '%s' data exceeds file size, truncating\n", sec.name);
|
||||
sec.dsize = buf_size - sec.data_offset;
|
||||
}
|
||||
|
||||
int sec_idx = rz_vector_len(md1->sections);
|
||||
rz_vector_push(md1->sections, &sec);
|
||||
|
||||
// Create a virtual file for this section's data and parse it
|
||||
RzBuffer *vbuf = rz_buf_new_slice(b, sec.data_offset, sec.dsize);
|
||||
if (vbuf) {
|
||||
md1img_load_section(md1, sec_idx, &sec, vbuf);
|
||||
}
|
||||
|
||||
// Advance to next section: data_offset + dsize + alignment padding
|
||||
ut64 data_end = sec.data_offset + sec.dsize;
|
||||
ut64 remainder = sec.align_size > 0 ? sec.dsize % sec.align_size : 0;
|
||||
if (remainder > 0) {
|
||||
offset = data_end + (sec.align_size - remainder);
|
||||
} else {
|
||||
offset = data_end;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
if (offset < buf_size) {
|
||||
RZ_LOG_WARN("md1img: %" PFMT64u " trailing bytes after last section header\n", buf_size - offset);
|
||||
}
|
||||
|
||||
if (rz_vector_len(md1->sections) == 0) {
|
||||
rz_vector_free(md1->sections);
|
||||
rz_pvector_free(md1->vfiles);
|
||||
free(md1);
|
||||
return false;
|
||||
}
|
||||
|
||||
obj->bin_obj = md1;
|
||||
return true;
|
||||
}
|
||||
|
||||
RZ_IPI void md1img_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
if (!bf || !bf->o || !bf->o->bin_obj) {
|
||||
return;
|
||||
}
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
rz_vector_free(md1->sections);
|
||||
rz_pvector_free(md1->vfiles);
|
||||
rz_pvector_free(md1->dbg_symbols);
|
||||
mtk_obj_free(md1->mtk);
|
||||
free(md1);
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzBinInfo *md1img_info(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
RzBinInfo *info = RZ_NEW0(RzBinInfo);
|
||||
if (!info) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
info->file = bf->file ? rz_str_dup(bf->file) : NULL;
|
||||
info->type = rz_str_dup("MediaTek md1img container");
|
||||
info->machine = rz_str_dup("MediaTek Modem");
|
||||
info->arch = rz_str_dup("mips");
|
||||
info->cpu = rz_str_dup("nanomips");
|
||||
info->rclass = rz_str_dup("firmware");
|
||||
info->subsystem = rz_str_dup("modem");
|
||||
info->has_va = true;
|
||||
info->bits = 32;
|
||||
info->big_endian = false;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
RZ_IPI ut64 md1img_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
return MTK_MODEM_BADDR;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *md1img_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new(free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (md1->mtk) {
|
||||
RzBinAddr *entry = RZ_NEW0(RzBinAddr);
|
||||
if (entry) {
|
||||
entry->vaddr = md1img_baddr(bf) + (md1->mtk->file_info.jump_offset - md1->mtk->code_offset);
|
||||
entry->paddr = md1->gfh_offset + md1->mtk->file_info.jump_offset;
|
||||
rz_pvector_push(ret, entry);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinVirtualFile *>*/ *md1img_virtual_files(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_virtual_file_free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void **it;
|
||||
rz_pvector_foreach (md1->vfiles, it) {
|
||||
RzBinVirtualFile *vf = *it;
|
||||
RzBinVirtualFile *clone = rz_bin_virtual_file_clone(vf);
|
||||
if (clone) {
|
||||
rz_pvector_push(ret, clone);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *md1img_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_map_free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Only map the md1rom section into the virtual address space.
|
||||
// Other sections (md1dsp, md1drdi, certs, etc.) belong to different
|
||||
// processors or address spaces and would conflict with md1rom if mapped.
|
||||
// They remain accessible as virtual files for raw data viewing.
|
||||
if (md1->md1rom_idx >= 0) {
|
||||
Md1imgSection *sec = rz_vector_index_ptr(md1->sections, md1->md1rom_idx);
|
||||
if (md1->mtk) {
|
||||
ut64 paddr = md1->gfh_offset + md1->mtk->code_offset;
|
||||
mtk_append_maps(md1->mtk, paddr, sec->name, ret);
|
||||
} else {
|
||||
RzBinMap *map = RZ_NEW0(RzBinMap);
|
||||
if (map) {
|
||||
map->name = rz_str_dup(sec->name);
|
||||
map->vfile_name = rz_str_dup(sec->name);
|
||||
map->paddr = 0;
|
||||
map->psize = sec->dsize;
|
||||
map->vaddr = sec->maddr;
|
||||
map->vsize = sec->dsize;
|
||||
map->perm = RZ_PERM_RX;
|
||||
rz_pvector_push(ret, map);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *md1img_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_section_free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (md1->mtk && md1->md1rom_idx >= 0 && md1->mtk->code_size > 0) {
|
||||
Md1imgSection *sec = rz_vector_index_ptr(md1->sections, md1->md1rom_idx);
|
||||
RzBinSection *code = RZ_NEW0(RzBinSection);
|
||||
if (code) {
|
||||
code->name = rz_str_newf("%s.code", sec->name);
|
||||
code->paddr = md1->gfh_offset + md1->mtk->code_offset;
|
||||
code->size = md1->mtk->code_size;
|
||||
code->vsize = md1->mtk->code_size;
|
||||
code->vaddr = md1img_baddr(bf);
|
||||
code->perm = RZ_PERM_RX;
|
||||
rz_pvector_push(ret, code);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinSymbol *>*/ *md1img_symbols(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_symbol_free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!md1->dbg_symbols) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void **it;
|
||||
rz_pvector_foreach (md1->dbg_symbols, it) {
|
||||
MtkDbgSymbol *dbg = *it;
|
||||
RzBinSymbol *sym = RZ_NEW0(RzBinSymbol);
|
||||
if (!sym) {
|
||||
continue;
|
||||
}
|
||||
sym->name = rz_str_dup(dbg->name);
|
||||
sym->vaddr = dbg->addr;
|
||||
sym->size = dbg->size;
|
||||
sym->type = RZ_BIN_TYPE_FUNC_STR;
|
||||
|
||||
// Compute paddr for symbols within mapped regions (kseg0 or kuseg).
|
||||
if (md1->mtk) {
|
||||
ut64 code_paddr = md1->gfh_offset + md1->mtk->code_offset;
|
||||
ut64 kseg0_end = md1img_baddr(bf) + md1->mtk->code_size;
|
||||
ut64 kuseg_base = md1->mtk->file_info.load_addr + md1->mtk->code_offset;
|
||||
ut64 kuseg_end = kuseg_base + md1->mtk->code_size;
|
||||
if (dbg->addr >= md1img_baddr(bf) && dbg->addr < kseg0_end) {
|
||||
sym->paddr = code_paddr + (dbg->addr - md1img_baddr(bf));
|
||||
} else if (dbg->addr >= kuseg_base && dbg->addr < kuseg_end) {
|
||||
sym->paddr = code_paddr + (dbg->addr - kuseg_base);
|
||||
}
|
||||
}
|
||||
|
||||
rz_pvector_push(ret, sym);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinString *>*/ *md1img_strings(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
// Container format: suppress string scanning on the raw container buffer.
|
||||
// Strings from the firmware payload are accessible via the mapped vfiles.
|
||||
return rz_pvector_new(NULL);
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzStructuredData *md1img_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
Md1imgObj *md1 = bf->o->bin_obj;
|
||||
RzStructuredData *root = rz_structured_data_new_map();
|
||||
if (!root) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
rz_structured_data_map_add_unsigned(root, "num_sections", rz_vector_len(md1->sections), false);
|
||||
|
||||
RzStructuredData *secs = rz_structured_data_map_add_array(root, "sections");
|
||||
if (secs) {
|
||||
Md1imgSection *sec;
|
||||
rz_vector_foreach (md1->sections, sec) {
|
||||
RzStructuredData *s = rz_structured_data_array_add_map(secs);
|
||||
if (!s) {
|
||||
continue;
|
||||
}
|
||||
rz_structured_data_map_add_string(s, "name", sec->name);
|
||||
rz_structured_data_map_add_unsigned(s, "data_size", sec->dsize, true);
|
||||
rz_structured_data_map_add_unsigned(s, "data_offset", sec->data_offset, true);
|
||||
rz_structured_data_map_add_unsigned(s, "maddr", sec->maddr, true);
|
||||
rz_structured_data_map_add_unsigned(s, "mode", sec->mode, true);
|
||||
rz_structured_data_map_add_unsigned(s, "hdr_size", sec->hdr_size, true);
|
||||
rz_structured_data_map_add_unsigned(s, "hdr_version", sec->hdr_version, true);
|
||||
rz_structured_data_map_add_unsigned(s, "img_type", sec->img_type, true);
|
||||
rz_structured_data_map_add_unsigned(s, "img_list_end", sec->img_list_end, true);
|
||||
rz_structured_data_map_add_unsigned(s, "align_size", sec->align_size, true);
|
||||
}
|
||||
}
|
||||
|
||||
// GFH structure from md1rom section
|
||||
if (md1->mtk) {
|
||||
MtkObj *mtk = md1->mtk;
|
||||
RzStructuredData *gfh = rz_structured_data_map_add_map(root, "gfh_file_info");
|
||||
if (gfh) {
|
||||
rz_structured_data_map_add_unsigned(gfh, "magic_version", mtk->first_common.magic_version, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "header_block_size", mtk->first_common.size, false);
|
||||
rz_structured_data_map_add_string(gfh, "header_type", mtk_gfh_type_str(mtk->first_common.type));
|
||||
rz_structured_data_map_add_string(gfh, "name", mtk->file_info.name);
|
||||
rz_structured_data_map_add_unsigned(gfh, "file_type", mtk->file_info.file_type, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "flash_type", mtk->file_info.flash_type, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "sig_type", mtk->file_info.sig_type, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "load_addr", mtk->file_info.load_addr, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "total_size", mtk->file_info.total_size, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "max_size", mtk->file_info.max_size, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "hdr_size", mtk->file_info.hdr_size, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "sig_size", mtk->file_info.sig_size, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "jump_offset", mtk->file_info.jump_offset, true);
|
||||
rz_structured_data_map_add_unsigned(gfh, "entry_point", mtk->entry_vaddr, true);
|
||||
}
|
||||
|
||||
if (rz_vector_len(mtk->extra_headers) > 0) {
|
||||
RzStructuredData *hdrs = rz_structured_data_map_add_array(root, "gfh_headers");
|
||||
if (hdrs) {
|
||||
MtkGfhHeader *extra;
|
||||
rz_vector_foreach (mtk->extra_headers, extra) {
|
||||
RzStructuredData *h = rz_structured_data_array_add_map(hdrs);
|
||||
if (!h) {
|
||||
continue;
|
||||
}
|
||||
rz_structured_data_map_add_unsigned(h, "file_offset", extra->file_offset, true);
|
||||
rz_structured_data_map_add_string(h, "type", mtk_gfh_type_str(extra->common.type));
|
||||
rz_structured_data_map_add_unsigned(h, "type_id", extra->common.type, true);
|
||||
rz_structured_data_map_add_unsigned(h, "size", extra->common.size, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (md1->dbg_symbols) {
|
||||
rz_structured_data_map_add_unsigned(root, "debug_symbols_count",
|
||||
rz_pvector_len(md1->dbg_symbols), false);
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
97
librz/bin/format/mtk/md1img.h
Normal file
97
librz/bin/format/mtk/md1img.h
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// SPDX-FileCopyrightText: 2025 godcodehunter
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
/**
|
||||
* \file Header for MediaTek md1img container format parser.
|
||||
*
|
||||
* The md1img format packages modem firmware components: md1rom (GFH image),
|
||||
* debug info (CATI format), DSP images, certificates, etc.
|
||||
*
|
||||
* References:
|
||||
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy (md1img)
|
||||
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_dbg_info.ksy (CATI)
|
||||
*/
|
||||
|
||||
#ifndef MD1IMG_H
|
||||
#define MD1IMG_H
|
||||
|
||||
#include <rz_types.h>
|
||||
#include <rz_util.h>
|
||||
#include <rz_lib.h>
|
||||
#include <rz_bin.h>
|
||||
|
||||
#include "mtk.h"
|
||||
|
||||
#define MD1IMG_MAGIC "\x88\x16\x88\x58"
|
||||
#define MD1IMG_MAGIC_SIZE 4
|
||||
#define MD1IMG_EXT_MAGIC "\x89\x16\x89\x58"
|
||||
#define MD1IMG_EXT_MAGIC_SIZE 4
|
||||
#define MD1IMG_NAME_SIZE 32
|
||||
/* Minimum section header size (fields before reserved) */
|
||||
#define MD1IMG_MIN_HDR_SIZE 0x50
|
||||
|
||||
#define MTK_CATI_MAGIC "CATI"
|
||||
#define MTK_CATI_MAGIC_SIZE 4
|
||||
/* "CTNR" as LE u32 */
|
||||
#define MTK_CATI_TYPE_CONTAINER 0x524E5443
|
||||
#define MTK_CATI_TYPE_DEBUG 1
|
||||
#define MTK_CATI_TYPE_DEBUG_DSP 2
|
||||
|
||||
/**
|
||||
* \brief A debug symbol parsed from a CATI debug info section.
|
||||
*
|
||||
* Reference: https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_dbg_info.ksy
|
||||
*/
|
||||
typedef struct mtk_dbg_symbol {
|
||||
char *name; ///< Symbol name
|
||||
ut32 addr; ///< Symbol address
|
||||
ut32 size; ///< Symbol size (addr2 - addr1, or 0 if addr2 <= addr1)
|
||||
} MtkDbgSymbol;
|
||||
|
||||
/**
|
||||
* \brief Parsed section from an md1img container.
|
||||
*
|
||||
* Each section has a header (magic + metadata) followed by data and
|
||||
* alignment padding.
|
||||
*/
|
||||
typedef struct md1img_section {
|
||||
char name[MD1IMG_NAME_SIZE]; ///< Section name (null-terminated)
|
||||
ut32 dsize; ///< Data size
|
||||
ut32 maddr; ///< Memory address
|
||||
ut32 mode; ///< Mode flags
|
||||
ut32 hdr_size; ///< Header size (total, from start of section to data)
|
||||
ut32 hdr_version; ///< Header version
|
||||
ut32 img_type; ///< Image type
|
||||
ut32 img_list_end; ///< Image list end marker
|
||||
ut32 align_size; ///< Alignment size for padding after data
|
||||
ut32 dsize_extend; ///< Extended data size (high bits)
|
||||
ut32 maddr_extend; ///< Extended memory address (high bits)
|
||||
ut64 data_offset; ///< File offset where section data starts
|
||||
} Md1imgSection;
|
||||
|
||||
/**
|
||||
* \brief Top-level parsed object for md1img container.
|
||||
*/
|
||||
typedef struct md1img_obj {
|
||||
RzVector /*<Md1imgSection>*/ *sections; ///< All parsed container sections
|
||||
RzPVector /*<RzBinVirtualFile *>*/ *vfiles; ///< Virtual file per section
|
||||
RzPVector /*<MtkDbgSymbol *>*/ *dbg_symbols; ///< Debug symbols from CATI section
|
||||
MtkObj *mtk; ///< Parsed GFH from md1rom section (or NULL)
|
||||
ut64 gfh_offset; ///< Offset of GFH within md1rom vfile
|
||||
int md1rom_idx; ///< Index of md1rom section, or -1
|
||||
} Md1imgObj;
|
||||
|
||||
RZ_IPI bool md1img_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b);
|
||||
RZ_IPI bool md1img_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb);
|
||||
RZ_IPI void md1img_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzBinInfo *md1img_info(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI ut64 md1img_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *md1img_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinVirtualFile *>*/ *md1img_virtual_files(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *md1img_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *md1img_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinSymbol *>*/ *md1img_symbols(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinString *>*/ *md1img_strings(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzStructuredData *md1img_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
|
||||
#endif
|
||||
416
librz/bin/format/mtk/mtk.c
Normal file
416
librz/bin/format/mtk/mtk.c
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
// SPDX-FileCopyrightText: 2025 godcodehunter
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
/**
|
||||
* \file Implementation of MediaTek GFH firmware image parser (md1rom).
|
||||
*
|
||||
* The GFH (Generic File Header) format is used by MediaTek for bootloader
|
||||
* and modem firmware images. The file starts with a chain of GFH headers
|
||||
* (the first must be FILE_INFO), followed by the code/data payload.
|
||||
*
|
||||
* References:
|
||||
* - https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy
|
||||
* - https://github.com/u-boot/u-boot/blob/master/tools/mtk_image.h
|
||||
*/
|
||||
#include "mtk.h"
|
||||
#include "md1img.h"
|
||||
|
||||
static inline bool mtk_is_gfh_magic(ut32 magic_version) {
|
||||
return (magic_version & MTK_GFH_MAGIC_MASK) == MTK_GFH_MAGIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Scan for GFH FILE_INFO header within the first MTK_GFH_SCAN_LIMIT bytes.
|
||||
* \return offset of the GFH header, or UT64_MAX if not found.
|
||||
*/
|
||||
static ut64 mtk_find_gfh(RzBuffer *b) {
|
||||
ut64 buf_size = rz_buf_size(b);
|
||||
ut64 limit = RZ_MIN(buf_size, MTK_GFH_SCAN_LIMIT);
|
||||
// GFH headers are typically aligned to 4 bytes
|
||||
for (ut64 off = 0; off + MTK_GFH_MIN_FILE_SIZE <= limit; off += 4) {
|
||||
ut64 cursor = off;
|
||||
MtkGfhCommonHdr hdr = { 0 };
|
||||
if (!rz_buf_read_le32_offset(b, &cursor, &hdr.magic_version) ||
|
||||
!rz_buf_read_le16_offset(b, &cursor, &hdr.size) ||
|
||||
!rz_buf_read_le16_offset(b, &cursor, (ut16 *)&hdr.type)) {
|
||||
continue;
|
||||
}
|
||||
if (mtk_is_gfh_magic(hdr.magic_version) &&
|
||||
hdr.type == MTK_GFH_TYPE_FILE_INFO &&
|
||||
hdr.size >= MTK_GFH_MIN_FILE_SIZE) {
|
||||
return off;
|
||||
}
|
||||
}
|
||||
return UT64_MAX;
|
||||
}
|
||||
|
||||
static bool mtk_read_common_hdr(RzBuffer *b, ut64 *offset, MtkGfhCommonHdr *hdr) {
|
||||
return rz_buf_read_le32_offset(b, offset, &hdr->magic_version) &&
|
||||
rz_buf_read_le16_offset(b, offset, &hdr->size) &&
|
||||
rz_buf_read_le16_offset(b, offset, (ut16 *)&hdr->type);
|
||||
}
|
||||
|
||||
static bool mtk_read_file_info(RzBuffer *b, ut64 *offset, MtkGfhFileInfo *fi) {
|
||||
if (!rz_buf_read_offset(b, offset, (ut8 *)fi->name, MTK_GFH_FILE_INFO_NAME_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
fi->name[MTK_GFH_FILE_INFO_NAME_SIZE - 1] = '\0';
|
||||
return rz_buf_read_le32_offset(b, offset, &fi->unused) &&
|
||||
rz_buf_read_le16_offset(b, offset, &fi->file_type) &&
|
||||
rz_buf_read8_offset(b, offset, &fi->flash_type) &&
|
||||
rz_buf_read8_offset(b, offset, &fi->sig_type) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->load_addr) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->total_size) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->max_size) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->hdr_size) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->sig_size) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->jump_offset) &&
|
||||
rz_buf_read_le32_offset(b, offset, &fi->processed);
|
||||
}
|
||||
|
||||
RZ_IPI RZ_BORROW const char *mtk_gfh_type_str(MtkGfhType type) {
|
||||
switch (type) {
|
||||
case MTK_GFH_TYPE_FILE_INFO:
|
||||
return "file_info";
|
||||
case MTK_GFH_TYPE_BL_INFO:
|
||||
return "bl_info";
|
||||
case MTK_GFH_TYPE_ANTI_CLONE:
|
||||
return "anti_clone";
|
||||
case MTK_GFH_TYPE_BL_SEC_KEY:
|
||||
return "bl_sec_key";
|
||||
case MTK_GFH_TYPE_BROM_CFG:
|
||||
return "brom_cfg";
|
||||
case MTK_GFH_TYPE_BROM_SEC_CFG:
|
||||
return "brom_sec_cfg";
|
||||
case MTK_GFH_TYPE_0x200:
|
||||
return "type_0x200";
|
||||
case MTK_GFH_TYPE_RSA_MAYBE:
|
||||
return "rsa_maybe";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// --- GFH format parsing ---
|
||||
|
||||
RZ_IPI bool mtk_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b) {
|
||||
rz_return_val_if_fail(b, false);
|
||||
|
||||
ut64 buf_size = rz_buf_size(b);
|
||||
if (buf_size < MTK_GFH_MIN_FILE_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject md1img containers — they have their own plugin
|
||||
ut8 first4[MD1IMG_MAGIC_SIZE];
|
||||
if (rz_buf_read_at(b, 0, first4, MD1IMG_MAGIC_SIZE) == MD1IMG_MAGIC_SIZE &&
|
||||
memcmp(first4, MD1IMG_MAGIC, MD1IMG_MAGIC_SIZE) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ut64 gfh_off = mtk_find_gfh(b);
|
||||
if (gfh_off == UT64_MAX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ut64 offset = gfh_off + MTK_GFH_COMMON_HDR_SIZE;
|
||||
MtkGfhFileInfo fi;
|
||||
if (!mtk_read_file_info(b, &offset, &fi)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (fi.hdr_size < MTK_GFH_MIN_FILE_SIZE || gfh_off + fi.hdr_size > buf_size) {
|
||||
return false;
|
||||
}
|
||||
if (fi.load_addr == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN MtkObj *mtk_obj_new(RZ_BORROW RZ_NONNULL RzBuffer *b) {
|
||||
rz_return_val_if_fail(b, NULL);
|
||||
|
||||
MtkObj *mtk = RZ_NEW0(MtkObj);
|
||||
if (!mtk) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mtk->extra_headers = rz_vector_new(sizeof(MtkGfhHeader), NULL, NULL);
|
||||
if (!mtk->extra_headers) {
|
||||
free(mtk);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ut64 gfh_off = mtk_find_gfh(b);
|
||||
if (gfh_off == UT64_MAX) {
|
||||
goto fail;
|
||||
}
|
||||
mtk->gfh_offset = gfh_off;
|
||||
|
||||
ut64 offset = gfh_off;
|
||||
if (!mtk_read_common_hdr(b, &offset, &mtk->first_common)) {
|
||||
goto fail;
|
||||
}
|
||||
if (!mtk_read_file_info(b, &offset, &mtk->file_info)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
ut64 buf_size = rz_buf_size(b);
|
||||
if (gfh_off + mtk->file_info.hdr_size > buf_size) {
|
||||
RZ_LOG_ERROR("MTK: header size (0x%x) exceeds file size\n", mtk->file_info.hdr_size);
|
||||
goto fail;
|
||||
}
|
||||
|
||||
mtk->code_offset = gfh_off + mtk->file_info.hdr_size;
|
||||
if (buf_size > mtk->code_offset) {
|
||||
mtk->code_size = buf_size - mtk->code_offset;
|
||||
}
|
||||
// jump_offset is relative to the GFH start
|
||||
ut32 code_offset_relative = mtk->file_info.hdr_size;
|
||||
mtk->entry_vaddr = MTK_MODEM_BADDR + (mtk->file_info.jump_offset - code_offset_relative);
|
||||
|
||||
// Parse additional GFH headers between the first header and the code area
|
||||
offset = gfh_off + mtk->first_common.size;
|
||||
int count = 0;
|
||||
while (offset + MTK_GFH_COMMON_HDR_SIZE <= mtk->code_offset && count < 100) {
|
||||
ut64 hdr_start = offset;
|
||||
MtkGfhCommonHdr extra_common = { 0 };
|
||||
if (!mtk_read_common_hdr(b, &offset, &extra_common)) {
|
||||
break;
|
||||
}
|
||||
if (!mtk_is_gfh_magic(extra_common.magic_version)) {
|
||||
break;
|
||||
}
|
||||
if (extra_common.size < MTK_GFH_COMMON_HDR_SIZE) {
|
||||
break;
|
||||
}
|
||||
|
||||
MtkGfhHeader entry = {
|
||||
.common = extra_common,
|
||||
.file_offset = hdr_start,
|
||||
};
|
||||
rz_vector_push(mtk->extra_headers, &entry);
|
||||
|
||||
offset = hdr_start + extra_common.size;
|
||||
count++;
|
||||
}
|
||||
|
||||
return mtk;
|
||||
|
||||
fail:
|
||||
rz_vector_free(mtk->extra_headers);
|
||||
free(mtk);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RZ_IPI void mtk_obj_free(RZ_OWN RZ_NULLABLE MtkObj *mtk) {
|
||||
if (!mtk) {
|
||||
return;
|
||||
}
|
||||
rz_vector_free(mtk->extra_headers);
|
||||
free(mtk);
|
||||
}
|
||||
|
||||
RZ_IPI bool mtk_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb) {
|
||||
rz_return_val_if_fail(bf && obj && b, false);
|
||||
MtkObj *mtk = mtk_obj_new(b);
|
||||
if (!mtk) {
|
||||
return false;
|
||||
}
|
||||
obj->bin_obj = mtk;
|
||||
return true;
|
||||
}
|
||||
|
||||
RZ_IPI void mtk_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
if (!bf || !bf->o || !bf->o->bin_obj) {
|
||||
return;
|
||||
}
|
||||
mtk_obj_free(bf->o->bin_obj);
|
||||
}
|
||||
|
||||
RZ_IPI ut64 mtk_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
return MTK_MODEM_BADDR;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *mtk_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
MtkObj *mtk = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new(free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
RzBinAddr *entry = RZ_NEW0(RzBinAddr);
|
||||
if (entry) {
|
||||
entry->paddr = mtk->gfh_offset + mtk->file_info.jump_offset;
|
||||
entry->vaddr = mtk->entry_vaddr;
|
||||
rz_pvector_push(ret, entry);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI bool mtk_append_maps(RZ_BORROW RZ_NONNULL MtkObj *mtk, ut64 paddr, RZ_BORROW RZ_NULLABLE const char *name, RZ_BORROW RZ_NONNULL RzPVector /*<RzBinMap *>*/ *ret) {
|
||||
rz_return_val_if_fail(mtk && ret, false);
|
||||
if (mtk->code_size == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// kseg0 map: main code at runtime virtual address
|
||||
RzBinMap *kseg0 = RZ_NEW0(RzBinMap);
|
||||
if (!kseg0) {
|
||||
return false;
|
||||
}
|
||||
kseg0->name = name ? rz_str_dup(name) : rz_str_dup("code");
|
||||
kseg0->vfile_name = name ? rz_str_dup(name) : NULL;
|
||||
kseg0->paddr = paddr;
|
||||
kseg0->psize = mtk->code_size;
|
||||
kseg0->vaddr = MTK_MODEM_BADDR;
|
||||
kseg0->vsize = mtk->code_size;
|
||||
kseg0->perm = RZ_PERM_RX;
|
||||
rz_pvector_push(ret, kseg0);
|
||||
|
||||
// kuseg map: same code at physical addresses (ERL=1 identity mapping).
|
||||
// The boot code uses kuseg addresses before clearing ERL in Status.
|
||||
RzBinMap *kuseg = RZ_NEW0(RzBinMap);
|
||||
if (!kuseg) {
|
||||
return false;
|
||||
}
|
||||
kuseg->name = name ? rz_str_newf("%s.kuseg", name) : rz_str_dup("code.kuseg");
|
||||
kuseg->vfile_name = name ? rz_str_dup(name) : NULL;
|
||||
kuseg->paddr = paddr;
|
||||
kuseg->psize = mtk->code_size;
|
||||
kuseg->vaddr = (ut64)mtk->file_info.load_addr + mtk->file_info.hdr_size;
|
||||
kuseg->vsize = mtk->code_size;
|
||||
kuseg->perm = RZ_PERM_RX;
|
||||
rz_pvector_push(ret, kuseg);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *mtk_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
MtkObj *mtk = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_map_free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
mtk_append_maps(mtk, mtk->code_offset, NULL, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *mtk_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
MtkObj *mtk = bf->o->bin_obj;
|
||||
RzPVector *ret = rz_pvector_new((RzPVectorFree)rz_bin_section_free);
|
||||
if (!ret) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Header section
|
||||
RzBinSection *hdr_section = RZ_NEW0(RzBinSection);
|
||||
if (hdr_section) {
|
||||
hdr_section->name = rz_str_dup("header");
|
||||
hdr_section->paddr = 0;
|
||||
hdr_section->size = mtk->code_offset;
|
||||
hdr_section->vsize = mtk->code_offset;
|
||||
hdr_section->vaddr = 0;
|
||||
hdr_section->perm = RZ_PERM_R;
|
||||
rz_pvector_push(ret, hdr_section);
|
||||
}
|
||||
|
||||
// Code section
|
||||
if (mtk->code_size > 0) {
|
||||
RzBinSection *code_section = RZ_NEW0(RzBinSection);
|
||||
if (code_section) {
|
||||
code_section->name = rz_str_dup("code");
|
||||
code_section->paddr = mtk->code_offset;
|
||||
code_section->size = mtk->code_size;
|
||||
code_section->vsize = mtk->code_size;
|
||||
code_section->vaddr = md1img_baddr(bf);
|
||||
code_section->perm = RZ_PERM_RX;
|
||||
rz_pvector_push(ret, code_section);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzBinInfo *mtk_info(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
RzBinInfo *info = RZ_NEW0(RzBinInfo);
|
||||
if (!info) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
info->file = bf->file ? rz_str_dup(bf->file) : NULL;
|
||||
info->type = rz_str_dup("MediaTek GFH");
|
||||
info->machine = rz_str_dup("MediaTek Modem");
|
||||
info->arch = rz_str_dup("mips");
|
||||
info->cpu = rz_str_dup("nanomips");
|
||||
info->rclass = rz_str_dup("firmware");
|
||||
info->subsystem = rz_str_dup("modem");
|
||||
info->has_va = true;
|
||||
info->bits = 32;
|
||||
info->big_endian = false;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
RZ_IPI RZ_OWN RzStructuredData *mtk_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf) {
|
||||
rz_return_val_if_fail(bf && bf->o && bf->o->bin_obj, NULL);
|
||||
|
||||
MtkObj *mtk = bf->o->bin_obj;
|
||||
RzStructuredData *root = rz_structured_data_new_map();
|
||||
if (!root) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// File info header
|
||||
RzStructuredData *fi = rz_structured_data_map_add_map(root, "gfh_file_info");
|
||||
if (fi) {
|
||||
rz_structured_data_map_add_unsigned(fi, "magic_version", mtk->first_common.magic_version, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "header_block_size", mtk->first_common.size, false);
|
||||
rz_structured_data_map_add_string(fi, "header_type", mtk_gfh_type_str(mtk->first_common.type));
|
||||
rz_structured_data_map_add_string(fi, "name", mtk->file_info.name);
|
||||
rz_structured_data_map_add_unsigned(fi, "file_type", mtk->file_info.file_type, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "flash_type", mtk->file_info.flash_type, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "sig_type", mtk->file_info.sig_type, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "load_addr", mtk->file_info.load_addr, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "total_size", mtk->file_info.total_size, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "max_size", mtk->file_info.max_size, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "hdr_size", mtk->file_info.hdr_size, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "sig_size", mtk->file_info.sig_size, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "jump_offset", mtk->file_info.jump_offset, true);
|
||||
rz_structured_data_map_add_unsigned(fi, "entry_point", mtk->entry_vaddr, true);
|
||||
}
|
||||
|
||||
// Extra GFH headers
|
||||
if (rz_vector_len(mtk->extra_headers) > 0) {
|
||||
RzStructuredData *hdrs = rz_structured_data_map_add_array(root, "gfh_headers");
|
||||
if (hdrs) {
|
||||
MtkGfhHeader *extra;
|
||||
rz_vector_foreach (mtk->extra_headers, extra) {
|
||||
RzStructuredData *h = rz_structured_data_array_add_map(hdrs);
|
||||
if (!h) {
|
||||
continue;
|
||||
}
|
||||
rz_structured_data_map_add_unsigned(h, "file_offset", extra->file_offset, true);
|
||||
rz_structured_data_map_add_string(h, "type", mtk_gfh_type_str(extra->common.type));
|
||||
rz_structured_data_map_add_unsigned(h, "type_id", extra->common.type, true);
|
||||
rz_structured_data_map_add_unsigned(h, "size", extra->common.size, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derived values
|
||||
rz_structured_data_map_add_unsigned(root, "code_offset", mtk->code_offset, true);
|
||||
rz_structured_data_map_add_unsigned(root, "code_size", mtk->code_size, true);
|
||||
|
||||
return root;
|
||||
}
|
||||
110
librz/bin/format/mtk/mtk.h
Normal file
110
librz/bin/format/mtk/mtk.h
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// SPDX-FileCopyrightText: 2025 mrsmith
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#ifndef MTK_H
|
||||
#define MTK_H
|
||||
|
||||
#include <rz_types.h>
|
||||
#include <rz_util.h>
|
||||
#include <rz_lib.h>
|
||||
#include <rz_bin.h>
|
||||
|
||||
#define MTK_GFH_MAGIC_MASK 0x00FFFFFF
|
||||
/* "MMM" in lower 3 bytes */
|
||||
#define MTK_GFH_MAGIC 0x004D4D4D
|
||||
#define MTK_GFH_COMMON_HDR_SIZE 8
|
||||
#define MTK_GFH_FILE_INFO_BODY_SIZE 48
|
||||
#define MTK_GFH_MIN_FILE_SIZE (MTK_GFH_COMMON_HDR_SIZE + MTK_GFH_FILE_INFO_BODY_SIZE)
|
||||
#define MTK_GFH_FILE_INFO_NAME_SIZE 12
|
||||
/* Runtime virtual base address for modem code */
|
||||
#define MTK_MODEM_BADDR 0x90000000ULL
|
||||
|
||||
typedef enum {
|
||||
MTK_GFH_TYPE_FILE_INFO = 0x0000,
|
||||
MTK_GFH_TYPE_BL_INFO = 0x0001,
|
||||
MTK_GFH_TYPE_ANTI_CLONE = 0x0002,
|
||||
MTK_GFH_TYPE_BL_SEC_KEY = 0x0003,
|
||||
MTK_GFH_TYPE_BROM_CFG = 0x0007,
|
||||
MTK_GFH_TYPE_BROM_SEC_CFG = 0x0008,
|
||||
MTK_GFH_TYPE_0x200 = 0x0200,
|
||||
MTK_GFH_TYPE_RSA_MAYBE = 0x0202,
|
||||
} MtkGfhType;
|
||||
|
||||
/**
|
||||
* \brief GFH Common Header (8 bytes, present in every GFH block).
|
||||
*
|
||||
* MediaTek Generic File Header format. The magic_version field encodes
|
||||
* "MMM" (0x4D4D4D) in the lower 3 bytes and a version number in the
|
||||
* upper byte.
|
||||
*
|
||||
* Reference: https://github.com/u-boot/u-boot/blob/master/tools/mtk_image.h
|
||||
*/
|
||||
typedef struct mtk_gfh_common_hdr {
|
||||
ut32 magic_version; ///< Lower 3 bytes = "MMM" (0x4D4D4D), upper byte = version
|
||||
ut16 size; ///< Total size of this header block (common + body)
|
||||
MtkGfhType type; ///< Header type; on-disk wire size is 2 bytes
|
||||
} MtkGfhCommonHdr;
|
||||
|
||||
/**
|
||||
* \brief GFH File Info body (48 bytes, follows common header when type=FILE_INFO).
|
||||
*
|
||||
* Contains the primary metadata for a MediaTek firmware image: load address,
|
||||
* entry point offset, header area size, and signature information.
|
||||
*
|
||||
* Reference: https://github.com/nccgroup/mtk_bp/blob/main/mtk_structs/mtk_img.ksy
|
||||
*/
|
||||
typedef struct mtk_gfh_file_info {
|
||||
char name[MTK_GFH_FILE_INFO_NAME_SIZE]; ///< Null-terminated identifier
|
||||
ut32 unused;
|
||||
ut16 file_type;
|
||||
ut8 flash_type;
|
||||
ut8 sig_type;
|
||||
ut32 load_addr; ///< Base load address in memory
|
||||
ut32 total_size; ///< Total image size
|
||||
ut32 max_size;
|
||||
ut32 hdr_size; ///< Total header area size; code starts at this file offset
|
||||
ut32 sig_size; ///< Signature size in bytes
|
||||
ut32 jump_offset; ///< File offset of entry point (from start of image)
|
||||
ut32 processed;
|
||||
} MtkGfhFileInfo;
|
||||
|
||||
/**
|
||||
* \brief A parsed additional GFH header (stored generically).
|
||||
*/
|
||||
typedef struct mtk_gfh_header {
|
||||
MtkGfhCommonHdr common; ///< Common header fields
|
||||
ut64 file_offset; ///< File offset where this header starts
|
||||
} MtkGfhHeader;
|
||||
|
||||
/* Maximum offset to scan for GFH magic when it's not at file start */
|
||||
#define MTK_GFH_SCAN_LIMIT 0x1000
|
||||
|
||||
/**
|
||||
* \brief Top-level parsed object stored as bin_obj.
|
||||
*/
|
||||
typedef struct mtk_obj {
|
||||
MtkGfhCommonHdr first_common; ///< Common header of the first (file_info) block
|
||||
MtkGfhFileInfo file_info; ///< Parsed file_info body
|
||||
RzVector /*<MtkGfhHeader>*/ *extra_headers; ///< Additional GFH headers after file_info
|
||||
ut64 gfh_offset; ///< File offset where GFH starts (0 if at beginning)
|
||||
ut32 code_offset; ///< File offset where code starts (= gfh_offset + file_info.hdr_size)
|
||||
ut32 code_size; ///< Size of code section
|
||||
ut32 entry_vaddr; ///< = MTK_MODEM_BADDR + (jump_offset - code_offset_relative)
|
||||
} MtkObj;
|
||||
|
||||
RZ_IPI RZ_OWN MtkObj *mtk_obj_new(RZ_BORROW RZ_NONNULL RzBuffer *b);
|
||||
RZ_IPI void mtk_obj_free(RZ_OWN RZ_NULLABLE MtkObj *mtk);
|
||||
RZ_IPI RZ_BORROW const char *mtk_gfh_type_str(MtkGfhType type);
|
||||
|
||||
RZ_IPI bool mtk_check_buffer(RZ_BORROW RZ_NONNULL RzBuffer *b);
|
||||
RZ_IPI bool mtk_load_buffer(RZ_BORROW RZ_NONNULL RzBinFile *bf, RZ_BORROW RZ_NONNULL RzBinObject *obj, RZ_BORROW RZ_NONNULL RzBuffer *b, RZ_BORROW RZ_NULLABLE Sdb *sdb);
|
||||
RZ_IPI void mtk_destroy(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzBinInfo *mtk_info(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI ut64 mtk_baddr(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinAddr *>*/ *mtk_entries(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinSection *>*/ *mtk_sections(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI bool mtk_append_maps(RZ_BORROW RZ_NONNULL MtkObj *mtk, ut64 paddr, RZ_BORROW RZ_NULLABLE const char *name, RZ_BORROW RZ_NONNULL RzPVector /*<RzBinMap *>*/ *ret);
|
||||
RZ_IPI RZ_OWN RzPVector /*<RzBinMap *>*/ *mtk_maps(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
RZ_IPI RZ_OWN RzStructuredData *mtk_structure(RZ_BORROW RZ_NONNULL RzBinFile *bf);
|
||||
|
||||
#endif
|
||||
|
|
@ -24,8 +24,10 @@ bin_plugins_list = [
|
|||
'mach0',
|
||||
'mach064',
|
||||
'mbn',
|
||||
'md1img',
|
||||
'mdmp',
|
||||
'mdt',
|
||||
'mtk',
|
||||
'menuet',
|
||||
'mz',
|
||||
'ne',
|
||||
|
|
@ -130,7 +132,9 @@ rz_bin_sources = [
|
|||
'p/bin_mach064.c',
|
||||
'p/bin_mbn.c',
|
||||
'p/bin_mdmp.c',
|
||||
'p/bin_md1img.c',
|
||||
'p/bin_mdt.c',
|
||||
'p/bin_mtk.c',
|
||||
'p/bin_menuet.c',
|
||||
'p/bin_mz.c',
|
||||
'p/bin_ne.c',
|
||||
|
|
@ -242,6 +246,8 @@ rz_bin_sources = [
|
|||
'format/mdmp/mdmp_pe.c',
|
||||
'format/mdmp/mdmp_pe64.c',
|
||||
'format/mdt/mdt.c',
|
||||
'format/mtk/md1img.c',
|
||||
'format/mtk/mtk.c',
|
||||
'format/le/le.c',
|
||||
'format/luac/luac_common.c',
|
||||
'format/luac/luac_bin.c',
|
||||
|
|
|
|||
31
librz/bin/p/bin_md1img.c
Normal file
31
librz/bin/p/bin_md1img.c
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// SPDX-FileCopyrightText: 2025 godcodehunter
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include "../format/mtk/md1img.h"
|
||||
|
||||
RzBinPlugin rz_bin_plugin_md1img = {
|
||||
.name = "md1img",
|
||||
.desc = "MediaTek md1img firmware container",
|
||||
.license = "LGPL3",
|
||||
.author = "godcodehunter",
|
||||
.check_buffer = &md1img_check_buffer,
|
||||
.load_buffer = &md1img_load_buffer,
|
||||
.destroy = &md1img_destroy,
|
||||
.baddr = &md1img_baddr,
|
||||
.entries = &md1img_entries,
|
||||
.virtual_files = &md1img_virtual_files,
|
||||
.maps = &md1img_maps,
|
||||
.sections = &md1img_sections,
|
||||
.symbols = &md1img_symbols,
|
||||
.strings = &md1img_strings,
|
||||
.info = &md1img_info,
|
||||
.bin_structure = &md1img_structure,
|
||||
};
|
||||
|
||||
#ifndef RZ_PLUGIN_INCORE
|
||||
RZ_API RzLibStruct rizin_plugin = {
|
||||
.type = RZ_LIB_TYPE_BIN,
|
||||
.data = &rz_bin_plugin_md1img,
|
||||
.version = RZ_VERSION
|
||||
};
|
||||
#endif
|
||||
28
librz/bin/p/bin_mtk.c
Normal file
28
librz/bin/p/bin_mtk.c
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// SPDX-FileCopyrightText: 2025 godcodehunter
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include "../format/mtk/mtk.h"
|
||||
|
||||
RzBinPlugin rz_bin_plugin_mtk = {
|
||||
.name = "mtk",
|
||||
.desc = "MediaTek GFH firmware image (md1rom)",
|
||||
.license = "LGPL3",
|
||||
.author = "godcodehunter",
|
||||
.check_buffer = &mtk_check_buffer,
|
||||
.load_buffer = &mtk_load_buffer,
|
||||
.destroy = &mtk_destroy,
|
||||
.baddr = &mtk_baddr,
|
||||
.entries = &mtk_entries,
|
||||
.sections = &mtk_sections,
|
||||
.maps = &mtk_maps,
|
||||
.info = &mtk_info,
|
||||
.bin_structure = &mtk_structure,
|
||||
};
|
||||
|
||||
#ifndef RZ_PLUGIN_INCORE
|
||||
RZ_API RzLibStruct rizin_plugin = {
|
||||
.type = RZ_LIB_TYPE_BIN,
|
||||
.data = &rz_bin_plugin_mtk,
|
||||
.version = RZ_VERSION
|
||||
};
|
||||
#endif
|
||||
|
|
@ -402,6 +402,7 @@ RZ_API bool rz_inflatew_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst,
|
|||
RZ_API bool rz_inflate_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed);
|
||||
RZ_API bool rz_lzma_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed);
|
||||
RZ_API bool rz_lzma_enc_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed);
|
||||
RZ_API bool rz_lzma_alone_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -480,3 +480,91 @@ RZ_API bool rz_lzma_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst,
|
|||
RZ_API bool rz_lzma_enc_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size, ut8 *src_consumed) {
|
||||
return lzma_action_buf(src, dst, block_size, src_consumed, true);
|
||||
}
|
||||
|
||||
#if HAVE_LZMA
|
||||
static bool lzma_alone_action_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size) {
|
||||
bool res = true;
|
||||
lzma_stream strm = LZMA_STREAM_INIT;
|
||||
const ut64 memusage_limit = 0x8000000; // 128 MB
|
||||
lzma_ret ret = lzma_alone_decoder(&strm, memusage_limit);
|
||||
if (ret != LZMA_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lzma_action action = LZMA_RUN;
|
||||
|
||||
ut8 *inbuf = RZ_NEWS(ut8, block_size);
|
||||
ut8 *outbuf = RZ_NEWS(ut8, block_size);
|
||||
if (!inbuf || !outbuf) {
|
||||
free(inbuf);
|
||||
free(outbuf);
|
||||
lzma_end(&strm);
|
||||
return false;
|
||||
}
|
||||
ut64 src_cursor = 0;
|
||||
|
||||
strm.next_in = NULL;
|
||||
strm.avail_in = 0;
|
||||
strm.next_out = outbuf;
|
||||
strm.avail_out = block_size;
|
||||
|
||||
while (true) {
|
||||
if (strm.avail_in == 0) {
|
||||
strm.next_in = inbuf;
|
||||
st64 src_readlen = rz_buf_read_at(src, src_cursor, inbuf, block_size);
|
||||
if (src_readlen < 0) {
|
||||
res = false;
|
||||
goto exit;
|
||||
}
|
||||
if (src_readlen == 0) {
|
||||
action = LZMA_FINISH;
|
||||
}
|
||||
strm.avail_in = src_readlen;
|
||||
src_cursor += src_readlen;
|
||||
}
|
||||
ret = lzma_code(&strm, action);
|
||||
if (strm.avail_out == 0 || ret == LZMA_STREAM_END) {
|
||||
size_t write_size = block_size - strm.avail_out;
|
||||
if (rz_buf_write(dst, outbuf, write_size) != write_size) {
|
||||
res = false;
|
||||
goto exit;
|
||||
}
|
||||
strm.next_out = outbuf;
|
||||
strm.avail_out = block_size;
|
||||
}
|
||||
if (ret == LZMA_STREAM_END) {
|
||||
break;
|
||||
}
|
||||
if (ret != LZMA_OK) {
|
||||
res = false;
|
||||
goto exit;
|
||||
}
|
||||
}
|
||||
|
||||
exit:
|
||||
free(inbuf);
|
||||
free(outbuf);
|
||||
lzma_end(&strm);
|
||||
return res;
|
||||
}
|
||||
#else
|
||||
static bool lzma_alone_action_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size) {
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Decompress the \p src buffer with LZMA alone (raw LZMA1) algorithm and put the decompressed data in \p dst
|
||||
*
|
||||
* Unlike rz_lzma_dec_buf() which handles .xz streams, this function handles
|
||||
* the legacy LZMA alone format (also known as LZMA1 or .lzma), identifiable
|
||||
* by the 0x5d first byte followed by a 13-byte header.
|
||||
*
|
||||
* \param src Where to read the compressed data from
|
||||
* \param dst Where to write the decompressed data to
|
||||
* \param block_size Decompression block size for I/O operations
|
||||
* \return true if decompression was successful, false otherwise
|
||||
*/
|
||||
RZ_API bool rz_lzma_alone_dec_buf(RZ_NONNULL RzBuffer *src, RZ_NONNULL RzBuffer *dst, ut64 block_size) {
|
||||
return lzma_alone_action_buf(src, dst, block_size);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
227
test/db/formats/mtk
Normal file
227
test/db/formats/mtk
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
NAME=md1rom synthetic GFH firmware image
|
||||
FILE=bins/mtk/md1rom
|
||||
CMDS=<<EOF
|
||||
iI
|
||||
echo ---
|
||||
iH
|
||||
echo ---
|
||||
iS
|
||||
echo ---
|
||||
ie
|
||||
echo ---
|
||||
s 0x90000000
|
||||
pi 4
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
arch mips
|
||||
cpu nanomips
|
||||
features N/A
|
||||
baddr 0x90000000
|
||||
binsz 0x00000160
|
||||
bintype firmware
|
||||
bits 32
|
||||
class N/A
|
||||
compiler N/A
|
||||
dbg_file N/A
|
||||
endian LE
|
||||
hdr.csum N/A
|
||||
guid N/A
|
||||
intrp N/A
|
||||
laddr 0x00000000
|
||||
lang N/A
|
||||
machine MediaTek Modem
|
||||
maxopsz 6
|
||||
minopsz 2
|
||||
os N/A
|
||||
cc N/A
|
||||
pcalign 2
|
||||
rpath N/A
|
||||
subsys modem
|
||||
stripped false
|
||||
havecode true
|
||||
va true
|
||||
static true
|
||||
linenum false
|
||||
lsyms false
|
||||
canary false
|
||||
pie false
|
||||
relrocs false
|
||||
nx false
|
||||
---
|
||||
gfh_file_info:
|
||||
magic_version: 0x14d4d4d
|
||||
header_block_size: 56
|
||||
header_type: "file_info"
|
||||
name: "MD1_ROM"
|
||||
file_type: 0x0
|
||||
flash_type: 0x0
|
||||
sig_type: 0x0
|
||||
load_addr: 0x90000000
|
||||
total_size: 0x160
|
||||
max_size: 0x160
|
||||
hdr_size: 0x60
|
||||
sig_size: 0x0
|
||||
jump_offset: 0x60
|
||||
entry_point: 0x90000000
|
||||
gfh_headers:
|
||||
- file_offset: 0x38
|
||||
type: "bl_info"
|
||||
type_id: 0x1
|
||||
size: 20
|
||||
- file_offset: 0x4c
|
||||
type: "brom_cfg"
|
||||
type_id: 0x7
|
||||
size: 16
|
||||
code_offset: 0x60
|
||||
code_size: 0x100
|
||||
|
||||
---
|
||||
paddr size vaddr vsize align perm name type flags
|
||||
---------------------------------------------------------------
|
||||
0x00000000 0x60 0x00000000 0x60 0x0 -r-- header
|
||||
0x00000060 0x100 0x90000000 0x100 0x0 -r-x code
|
||||
---
|
||||
vaddr paddr hvaddr haddr type
|
||||
----------------------------------------------------
|
||||
0x90000000 0x00000060 ---------- ---------- program
|
||||
---
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=md1img synthetic container with md1rom section
|
||||
FILE=bins/mtk/md1img
|
||||
CMDS=<<EOF
|
||||
iI
|
||||
echo ---
|
||||
iH
|
||||
echo ---
|
||||
iS
|
||||
echo ---
|
||||
ie
|
||||
echo ---
|
||||
s 0x90000000
|
||||
pi 4
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
arch mips
|
||||
cpu nanomips
|
||||
features N/A
|
||||
baddr 0x90000000
|
||||
binsz 0x00000360
|
||||
bintype firmware
|
||||
bits 32
|
||||
class N/A
|
||||
compiler N/A
|
||||
dbg_file N/A
|
||||
endian LE
|
||||
hdr.csum N/A
|
||||
guid N/A
|
||||
intrp N/A
|
||||
laddr 0x00000000
|
||||
lang N/A
|
||||
machine MediaTek Modem
|
||||
maxopsz 6
|
||||
minopsz 2
|
||||
os N/A
|
||||
cc N/A
|
||||
pcalign 2
|
||||
rpath N/A
|
||||
subsys modem
|
||||
stripped false
|
||||
havecode true
|
||||
va true
|
||||
static true
|
||||
linenum false
|
||||
lsyms false
|
||||
canary false
|
||||
pie false
|
||||
relrocs false
|
||||
nx false
|
||||
---
|
||||
num_sections: 1
|
||||
sections:
|
||||
- name: "md1rom"
|
||||
data_size: 0x160
|
||||
data_offset: 0x200
|
||||
maddr: 0x0
|
||||
mode: 0x0
|
||||
hdr_size: 0x200
|
||||
hdr_version: 0x0
|
||||
img_type: 0x0
|
||||
img_list_end: 0x0
|
||||
align_size: 0x0
|
||||
gfh_file_info:
|
||||
magic_version: 0x14d4d4d
|
||||
header_block_size: 56
|
||||
header_type: "file_info"
|
||||
name: "MD1_ROM"
|
||||
file_type: 0x0
|
||||
flash_type: 0x0
|
||||
sig_type: 0x0
|
||||
load_addr: 0x90000000
|
||||
total_size: 0x160
|
||||
max_size: 0x160
|
||||
hdr_size: 0x60
|
||||
sig_size: 0x0
|
||||
jump_offset: 0x60
|
||||
entry_point: 0x90000000
|
||||
gfh_headers:
|
||||
- file_offset: 0x38
|
||||
type: "bl_info"
|
||||
type_id: 0x1
|
||||
size: 20
|
||||
- file_offset: 0x4c
|
||||
type: "brom_cfg"
|
||||
type_id: 0x7
|
||||
size: 16
|
||||
|
||||
---
|
||||
paddr size vaddr vsize align perm name type flags
|
||||
--------------------------------------------------------------------
|
||||
0x00000060 0x100 0x90000000 0x100 0x0 -r-x md1rom.code
|
||||
---
|
||||
vaddr paddr hvaddr haddr type
|
||||
----------------------------------------------------
|
||||
0x90000000 0x00000060 ---------- ---------- program
|
||||
---
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
nop
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=md1img sections (JSON)
|
||||
FILE=bins/mtk/md1img
|
||||
CMDS=<<EOF
|
||||
iSj
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
[{"name":"md1rom.code","size":256,"vsize":256,"perm":"-r-x","paddr":96,"vaddr":2415919104}]
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=md1img empty exports do not crash
|
||||
FILE=bins/mtk/md1img
|
||||
CMDS=<<EOF
|
||||
iE
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
nth paddr vaddr bind type size lib name
|
||||
----------------------------------------
|
||||
EOF
|
||||
RUN
|
||||
|
||||
NAME=md1img exposes md1rom as a virtual file
|
||||
FILE=bins/mtk/md1img
|
||||
CMDS=<<EOF
|
||||
ol~vfile
|
||||
EOF
|
||||
EXPECT=<<EOF
|
||||
4 - r-x 0x00000160 vfile://0/md1rom
|
||||
EOF
|
||||
RUN
|
||||
|
|
@ -56,9 +56,161 @@ bool test_rz_lzma_enc(void) {
|
|||
mu_end;
|
||||
}
|
||||
|
||||
// LZMA-alone (legacy LZMA1, .lzma) fixtures generated with: xz --format=lzma -c
|
||||
// They share inflated payloads with the .xz cases above so the difference is
|
||||
// purely the container header (0x5d... instead of the .xz magic).
|
||||
struct {
|
||||
const char *inflated;
|
||||
const unsigned char deflated[160];
|
||||
size_t deflated_length;
|
||||
} test_cases_alone[] = {
|
||||
{ "1234567890abcdefghijklmnopqrstuvwxyz\n",
|
||||
{ 0x5d, 0x00, 0x00, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0x00, 0x18, 0x8c, 0x82, 0xb6, 0xc4, 0x11, 0x34, 0x5c, 0x4e, 0xe1,
|
||||
0xd6, 0x5e, 0xd1, 0x46, 0xf2, 0x6e, 0xf8, 0x5b, 0xdf, 0x60, 0xc9, 0x34,
|
||||
0x08, 0x05, 0x5f, 0xa3, 0xc3, 0x5d, 0x4c, 0xe6, 0xcd, 0x2d, 0x81, 0x37,
|
||||
0xfe, 0x2c, 0x76, 0xd3, 0x09, 0xe7, 0xff, 0xff, 0xc9, 0x9d, 0x00, 0x00 },
|
||||
60 },
|
||||
{ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n",
|
||||
{ 0x5d, 0x00, 0x00, 0x80, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0x00, 0x26, 0x1b, 0xca, 0x46, 0x67, 0x5a, 0xf2, 0x77, 0xb8, 0x7d,
|
||||
0x86, 0xd8, 0x41, 0xdb, 0x05, 0x35, 0xcd, 0x83, 0xa5, 0x7c, 0x12, 0xa5,
|
||||
0x05, 0xdb, 0x90, 0xbd, 0x2f, 0x14, 0xd3, 0x71, 0x72, 0x96, 0xa8, 0x8a,
|
||||
0x7d, 0x84, 0x56, 0x71, 0x8d, 0x6a, 0x22, 0x98, 0xab, 0x9e, 0x3d, 0xc3,
|
||||
0x55, 0xef, 0xcc, 0xa5, 0xc3, 0xdd, 0x5b, 0x8e, 0xbf, 0x03, 0x81, 0x21,
|
||||
0x40, 0xd6, 0x26, 0x91, 0x02, 0x45, 0x4f, 0x92, 0xa1, 0x78, 0xbb, 0x8a,
|
||||
0x00, 0xaf, 0x90, 0x2a, 0x26, 0x92, 0x02, 0x23, 0xe5, 0x5c, 0xb3, 0x2d,
|
||||
0xe3, 0xe8, 0x5c, 0x2c, 0xfb, 0x32, 0x21, 0xc6, 0x6f, 0x6a, 0x37, 0xb1,
|
||||
0x66, 0x20, 0xcd, 0xb7, 0x52, 0x7d, 0x66, 0xa4, 0x21, 0x08, 0xd1, 0x44,
|
||||
0x0f, 0x7f, 0xad, 0x0d, 0x64, 0xff, 0xed, 0x34, 0x38, 0x00 },
|
||||
130 },
|
||||
};
|
||||
|
||||
bool test_rz_lzma_alone_dec(void) {
|
||||
// Scenario 1: happy path. Two pre-compressed fixtures must round-trip
|
||||
// byte-for-byte against their original inflated payloads. Uses a default
|
||||
// block size large enough to consume the whole stream in one inner pass.
|
||||
for (size_t i = 0; i < RZ_ARRAY_SIZE(test_cases_alone); i++) {
|
||||
size_t inflated_len = strlen(test_cases_alone[i].inflated);
|
||||
RzBuffer *src = rz_buf_new_with_bytes(test_cases_alone[i].deflated, test_cases_alone[i].deflated_length);
|
||||
RzBuffer *dst = rz_buf_new_empty(inflated_len);
|
||||
mu_assert_true(rz_lzma_alone_dec_buf(src, dst, 1 << 13), "rz_lzma_alone_dec_buf failed on valid fixture");
|
||||
mu_assert_eq(rz_buf_size(dst), inflated_len, "decompressed size mismatch");
|
||||
char *out = calloc(inflated_len + 1, 1);
|
||||
rz_buf_read_at(dst, 0, (ut8 *)out, inflated_len);
|
||||
mu_assert_streq(out, test_cases_alone[i].inflated, "decompressed payload mismatch");
|
||||
free(out);
|
||||
rz_buf_free(src);
|
||||
rz_buf_free(dst);
|
||||
}
|
||||
|
||||
// Scenario 2: tiny block_size forces the inner while-loop to iterate many
|
||||
// times (re-fill input, re-flush output). Exercises the multi-pass code
|
||||
// path that the large block_size in scenario 1 hides.
|
||||
{
|
||||
size_t inflated_len = strlen(test_cases_alone[1].inflated);
|
||||
RzBuffer *src = rz_buf_new_with_bytes(test_cases_alone[1].deflated, test_cases_alone[1].deflated_length);
|
||||
RzBuffer *dst = rz_buf_new_empty(inflated_len);
|
||||
mu_assert_true(rz_lzma_alone_dec_buf(src, dst, 16), "rz_lzma_alone_dec_buf failed with small block_size");
|
||||
char *out = calloc(inflated_len + 1, 1);
|
||||
rz_buf_read_at(dst, 0, (ut8 *)out, inflated_len);
|
||||
mu_assert_streq(out, test_cases_alone[1].inflated, "small-block decompression does not match original");
|
||||
free(out);
|
||||
rz_buf_free(src);
|
||||
rz_buf_free(dst);
|
||||
}
|
||||
|
||||
// Scenario 3: random garbage bytes are not a valid LZMA1 stream. The
|
||||
// decoder must fail cleanly (return false) rather than crash or produce
|
||||
// uninitialized output.
|
||||
{
|
||||
const ut8 garbage[] = {
|
||||
0xDE, 0xAD, 0xBE, 0xEF, 0x42, 0x42, 0x42, 0x42,
|
||||
0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF
|
||||
};
|
||||
RzBuffer *src = rz_buf_new_with_bytes(garbage, sizeof(garbage));
|
||||
RzBuffer *dst = rz_buf_new_empty(0);
|
||||
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13), "garbage input must be rejected");
|
||||
rz_buf_free(src);
|
||||
rz_buf_free(dst);
|
||||
}
|
||||
|
||||
// Scenario 4: empty input buffer triggers the "src_readlen == 0" branch
|
||||
// immediately, which switches the action to LZMA_FINISH on an unprimed
|
||||
// stream. Decoder must fail without UB or crash.
|
||||
{
|
||||
RzBuffer *src = rz_buf_new_empty(0);
|
||||
RzBuffer *dst = rz_buf_new_empty(0);
|
||||
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13), "empty input must be rejected");
|
||||
rz_buf_free(src);
|
||||
rz_buf_free(dst);
|
||||
}
|
||||
|
||||
mu_end;
|
||||
}
|
||||
|
||||
// --- Fault injection for lzma_alone_action_buf error-handling branches ---
|
||||
|
||||
// A buffer back-end whose every operation reports failure. Used as the
|
||||
// source to trigger the "src_readlen < 0" branch on the first read attempt.
|
||||
static st64 failing_read(RZ_BORROW RzBuffer *b, RZ_OUT ut8 *buf, ut64 len) {
|
||||
(void)b;
|
||||
(void)buf;
|
||||
(void)len;
|
||||
return -1;
|
||||
}
|
||||
static const RzBufferMethods failing_src_methods = {
|
||||
.read = failing_read,
|
||||
};
|
||||
|
||||
// A buffer back-end that accepts no writes. Used as the destination to
|
||||
// trigger the "rz_buf_write(dst, ...) != write_size" branch after the
|
||||
// decoder produces its first decompressed chunk.
|
||||
static st64 failing_write(RzBuffer *b, const ut8 *buf, ut64 len) {
|
||||
(void)b;
|
||||
(void)buf;
|
||||
(void)len;
|
||||
return -1;
|
||||
}
|
||||
static const RzBufferMethods failing_dst_methods = {
|
||||
.write = failing_write,
|
||||
};
|
||||
|
||||
bool test_rz_lzma_alone_dec_error_paths(void) {
|
||||
// Branch: `if (src_readlen < 0)` — the first rz_buf_read_at on the
|
||||
// source returns -1, so the decoder bails out before processing anything.
|
||||
{
|
||||
RzBuffer *src = rz_buf_new_with_methods(&failing_src_methods, NULL, RZ_BUFFER_CUSTOM);
|
||||
RzBuffer *dst = rz_buf_new_empty(0);
|
||||
mu_assert_notnull(src, "custom failing-src buffer creation failed");
|
||||
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13),
|
||||
"src read returning -1 must abort decompression");
|
||||
rz_buf_free(src);
|
||||
rz_buf_free(dst);
|
||||
}
|
||||
|
||||
// Branch: `if (rz_buf_write(dst, outbuf, write_size) != write_size)` —
|
||||
// a valid LZMA-alone stream feeds the decoder, the decoder emits
|
||||
// decompressed bytes, but the destination buffer's write method always
|
||||
// returns -1, so the loop must bail out.
|
||||
{
|
||||
RzBuffer *src = rz_buf_new_with_bytes(test_cases_alone[0].deflated, test_cases_alone[0].deflated_length);
|
||||
RzBuffer *dst = rz_buf_new_with_methods(&failing_dst_methods, NULL, RZ_BUFFER_CUSTOM);
|
||||
mu_assert_notnull(dst, "custom failing-dst buffer creation failed");
|
||||
mu_assert_false(rz_lzma_alone_dec_buf(src, dst, 1 << 13),
|
||||
"dst write failure must abort decompression");
|
||||
rz_buf_free(src);
|
||||
rz_buf_free(dst);
|
||||
}
|
||||
|
||||
mu_end;
|
||||
}
|
||||
|
||||
int all_tests() {
|
||||
mu_run_test(test_rz_lzma_dec);
|
||||
mu_run_test(test_rz_lzma_enc);
|
||||
mu_run_test(test_rz_lzma_alone_dec);
|
||||
mu_run_test(test_rz_lzma_alone_dec_error_paths);
|
||||
|
||||
return tests_passed != tests_run;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue