首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
IoT安全
发新帖
6
12
[原创]某Arm9裸机门铃黑盒逆向(一)bootloader分析与主镜像提取
发表于: 2026-7-24 15:59
4334
[原创]某Arm9裸机门铃黑盒逆向(一)bootloader分析与主镜像提取
MicroOrange
2026-7-24 15:59
4334
# 前言 智能门铃设备长期以来都是硬件安全问题的重点关注对象和重灾区。然而许多这类设备为了将成本和功耗压缩到极致,使用裸机或RTOS,配上各种偏门,找不到文档和SDK的国产SOC,使得对其进行的分析变得十分麻烦。同时裸机编程的各种手写原语和未充分测试的边界条件使其安全问题更加严峻。 本文对一台闲置的智能门铃进行了**硬件分析,bootloader固件提取,启动流程分析。对bootloader固件进行反汇编,分析其运行过程中的内存布局,烧录信息的储存和读取方式,主镜像的装载和解压方式。通过逻辑分析仪监听片外flash的SPI总线,获取并解压得到主镜像供下一步分析。** # 设备与平台 1. 所用的硬件设备包括:FT232串口转USB模块,DSlogic U3Pro16逻辑分析仪,安捷伦E3631A电源,XTW-3编程器(如果需要改固件的话,本文不包括)。 2. IDA 9.0用于固件分析。 # 串口控制台探测,尝试第一次固件dump 首先确定给主电源的供电电压,未查到DCDC1控制器型号,但是考虑到标准POE电源应该是48V,为了保险这里给12V,此设备顺利启动,运行功耗2.5W左右。 将串口引脚焊接并引出,使用USB转串口模块将连接到PC,试一下常见波特率发现波特率是115200. 几乎可以确定这个设备跑的不是Linux,而是某种RTOS或者裸机,spiboot是其bootloader,可能是厂商SDK中的。0x30000000是主镜像的地址,也就是RESET向量所在的位置。 向串口发送一个空行,发现控制台没有锁,给出了一张命令表。 发现其中竟然有flash这种操作,可以直接读flash,经过测试发现是直接读片内总线上的任意地址的,设备没有隔离手段。 试读一个,直接就是一条跳转指令。**最初的想法是编写一个python脚本,通过串口自动发 flash r 00000000 一直发到flash末尾,用正则把数据抠出来,实际测试后发现因为一次只能读32位速度极其慢,大量时间浪费在无用字符,低波特率和设备控制台命令解析时间,整片dump下来需要50多个小时**。 因此打算先dump个300KB左右看看  **看到一个魔数 ANYK S3C,厂商代号和S3C**,这让人联想到三星系SOC的经典S3C启动,接下来有一些不知含义的参数。 继续往下翻,**在0x200的地方,看起来很可能就是一个固件的入口点,其头部是一条B,跳到+0x44位置**。下面这一堆,其中还包含SPIF这个ASCII字符串,附近应该是元数据.注意到,这里并不是一个aarch32的向量表,看来这个固件是由其他代码跳过去的。  在字符常量池里面发现了"SPIBOOT_VER""Burn_Info"等字符串,**几乎可以确定0x200入口的就是那个bootloader**  看一下0x200处B跳过去的地方,0x244,这附近是一个startup,在0x30770000-0x3079000附近架栈,清bss,在0x3077FA00处,然后跳到准备C环境主函数之类的地方(红红的是因为那些数是从常量区拿到的绝对寻址,现在未设置基地址,所以没法识别引用)  这就出现了一个问题:**bootloader说主镜像从0x30000000处开始跑,那bootloader自己跑在哪,SDRAM还是SRAM?谁加载和跳的它?** 我们刚才说的0x244是flash中的地址空间,然而bootloader的栈在0x30770000-0x3079000附近,它自己肯定也在附近,它的基地址应该是多少? # ARM启动流程回顾与基地址确定 回顾一下S3C的踏脚石启动流程:  BootROM是一块生产时写入的片内储存器,不可编程,上电后复位到此处,内部存着的BL0对外设和时钟系统进行最低初始化,根据引脚状态决定从哪里读取镜像,将BL1固件复制到内部的一块ITCM,不需要复杂的时序,训练,刷新和各种型号适配,因此BL0就可以初始化他。BL1是一个小固件,进一步初始化外设,初始化SDRAM或DRAM,因为它可以开发,所以可以灵活的初始化或者做一些操作. 然后搬运BL2,这就是我们所说的bootloader,uboot之类的,BL2基本是一个完整的裸机程序,从各种地方拿到镜像和文件系统,然后引导。 幸运的是,我们看的这块SOC启动流程要简单得多。 **首先这个SPIBoot程序,也就是bootloader,几乎可以肯定是BL2。** SDRAM我们已经知道是从0x30000000开始蔓延8MB或16MB,刚才我们知道bootloader在0x30770000-0x3079000附近,所以bootloader在SDRAM里。然而我们在Flash一开头看见的就是BL2,似乎并没有BL1存在,但是肯定要有代码初始化SDRAM和搬运BL2并跳过去。**因此笔者猜测,AK3760这颗芯片并没有传统意义上,随用户镜像编译的的BL1**,而是BL0和BL1全部固化在BootROM中,做出这一猜测的理由是,它不像大多数S3C芯片一样外挂SDRAM,而是直接合封确定型号的RAM,因此根本无需适配各种SDRAM的刷新和时序,可以在BootROM中直接硬编码SDRAM的初始化,这一点要等后续blog找到BootROM并反汇编的时候才能验证。 **可以猜想启动过程,BootROM最小初始化外设和SDRAM,读Flash中ANYKS3C这个魔数并解析后面那一堆的参数(可能是时钟,Flash页大小之类的),搬运bootloader,然后跳过去。bootloader再搬运和启动用户镜像。** 对于基地址的确定,可以依赖bootloader中一些编译期硬编码的地址,比如刚刚看的startup函数中,可以确定bss是从地址0x3077FA00开始的。我们看看Flash中固件的rodata也就是常量字符串之类的区(在哪结束)。 | | | | :---: | :---: | |  |  | 巧了,0xFA00就是rodata结束bss开始,我们再看startup的最后的跳转,跳到0x2FD4处,看0x2FD4处刚好有一个函数序言,而且就是main!!(名字是后标的,因为这不是elf,没有符号表),**因此可以几乎确定,整个Flash从0地址到bootloader结束全部被搬到0x30770000处。** 为什么是这里,这里正好是SDRAM区域接近8MB的末端,因为0x30000000SDRAM开始处一会要装压缩后的镜像和解压,所以bootloader被搬到高地址处。 # bootloader获取和打印主镜像信息过程 前面bootloader曾经打印出过一段主镜像信息 以此为切入点分析bootloader主要行为。发现这些字符串都是这个函数打印的,字符串传入的那个函数就是debug_printf。反编译器面对vargs有点乱套了。aBurnInfo_ptr,这里打出了那个Burn Info:字符串,所以实际上并不是从这个函数获得的,而是作为参数a1传入。 ```c int __fastcall load_and_boot_image(_DWORD *a1) { _DWORD *v2; // r7 int (__fastcall *v3)(_DWORD, _DWORD); // r4 int result; // r0 int *v5; // r2 int v6; // r3 int v7; // r3 unsigned int v8; // r6 int v9; // r1 _DWORD *v10; // r4 int v11; // r1 int (__fastcall *v12)(_DWORD); // r4 unsigned int v13; // r2 char *v14; // r0 int v15; // r3 int v16; // r0 v2 = a1 + 4; v3 = debug_printf_ptr; debug_printf_ptr(aSLoaing_ptr[0], a1 + 4); ((void (__fastcall *)(char *, _DWORD, _DWORD, _DWORD))v3)(aBurnInfo_ptr, a1[2], *a1, a1[1]); if ( *a1 > *(_DWORD *)off_30772C08 ) return ((int (__fastcall *)(char *))v3)(off_30772C0C); if ( *(_DWORD *)off_30772C10[0] == 0x800000 ) v5 = (int *)off_30772C14[0]; else v5 = (int *)off_30772C18; v6 = *v5; if ( *(_DWORD *)off_30772C10[0] == 0x800000 ) v7 = v6 - *a1; else v7 = v6 + 0x800000; v8 = (v7 + 3) & 0xFFFFFFFC; if ( !off_30772C1C(a1, v8, *a1) ) return debug_printf_ptr(aReadBiosFail_ptr, v9); v10 = (_DWORD *)off_30772C20; result = off_30772C24(v8, *a1, a1[1], *(_DWORD *)off_30772C20); if ( *v10 == result ) return debug_printf_ptr(aErrIsnTDecompr_ptr[0], v11); if ( result ) { v12 = (int (__fastcall *)(_DWORD))a1[1]; v13 = 0; v14 = (char *)v12 + 4; while ( v13 <= 0xFF ) { v15 = *((_DWORD *)v12 + v13++); if ( v15 == dword_30772C28 ) { off_30772C30(v14, off_30772C2C, 60); break; } v14 += 4; } v16 = debug_printf_ptr(off_30772C34[0], v2); return v12(v16); } return result; } ``` a1[2], *a1, a1[1],就是打印出来的页号,长度和加载地址。注意,v12 = (int (__fastcall *)(_DWORD))a1[1],v12实际是指向0x30000000的指针,return v12(v16),**实际上这就是bootloader跳转到主镜像RESET向量!**(v16那个参数是反编译器混乱了)。 根据错误处理路径的字符串推断,具体的流程分析如下: ``` 函数load_and_boot_image(_DWORD *a1) 打印 "<name> Loading..." 打印 Burn Info (page/len/run_addr) ├─ len > 上限 ────────────► 报错返回 ├─ 按 DRAM 大小算临时缓冲区地址(4字节对齐) ├─ 从 Flash page 读镜像到缓冲区 │ └─ 失败 ────────────► "Read BIOS Fail!" 返回 ├─ 解压 缓冲区 → run_addr │ └─ 失败 ────────────► "Err: Isn't decompressed" 返回 ├─ 头 1KB 扫魔数 → 命中则注入 60 字节板级参数 └─ 打印 "Decompress BIOS Ok! Jump To 0x30000000" → 跳转执行 ``` **off_30772C1C是是加载镜像一类的,off_30772C24是解压**,off_30772C30实则是memcpy。 # bootloader主函数分析 回过头去看main函数,其调用了这个load_and_boot_image。 其中部分操作函数已经根据参数猜测并检查过功能,最后命名,**命名只是猜测,不一定准确。** 看看spi_boot_main的几个初始化函数 ```c spi_boot_main 24:51 这里就是函数局部变量声明结束后的开头 memcpy_ptr(v19, off_307732B8, 5); *((_DWORD *)off_307732C0 + 1) = 0; ram_size_alias = read_ram_size_alias(); v1 = ram_size_alias; *(_DWORD *)off_307732C8[0] = ram_size_alias; if ( ram_size_alias == 0x800000 ) { v4 = (_DWORD *)off_307732D4[0]; *(_DWORD *)off_307732D4[0] = *(_DWORD *)off_307732CC[0] + 8372224; v16 = *off_30773338; *(_DWORD *)off_307732D0[0] = (*off_30773338 - 2621437) & 0xFFFFFFFC; v17 = (_DWORD *)off_307732DC[0]; *(_DWORD *)off_307732D8[0] = (v16 - 3) & 0xFFFFFFFC; *v17 = 0x200000; *(_DWORD *)off_307732E0 = 3670016; } else { v2 = *(_DWORD *)off_307732CC[0] + ram_size_alias; v3 = (unsigned int *)off_307732D0[0]; v4 = (_DWORD *)off_307732D4[0]; *(_DWORD *)off_307732D4[0] = *(_DWORD *)off_307732CC[0] + 8372224; *v3 = (v2 - 2621437) & 0xFFFFFFFC; v5 = (_DWORD *)off_307732DC[0]; *(_DWORD *)off_307732D8[0] = (v2 - 3) & 0xFFFFFFFC; *v5 = v1 - 11010048; *(_DWORD *)off_307732E0 = 7798784; } ``` 首先根据SDRAM容量准备布局表,从off_307732CC开始的一个结构体,其中的几个魔数,0x30770000刚好是bootloader装载地址,0x307FC000是bootloader结束地址。发现 0x307FC000刚好是8MB-16KB,又被传入mmu_init(可以看后面的初始化部分分析),这刚好是一级页表的大小!**所以大概可以猜到0x307FC000是TTB地址。** 随后再算出后面装载镜像和解压的可用区上界(off_307732D8) * 8MB:上方 0x30770000 起就是 spiboot 自己,不能碰 → 上界 = spiboot 起点 * 16MB:spiboot 在 8MB 处,其上还有整整 8MB 空闲,物理末尾才是上界 off_307732D0 = 上界 − 2.5MB,应该是一个缓冲区顶 ``` off_307732CC → g_ram_base_ptr //基地址 *→ 0x30000000 off_307732C8 → g_ram_size_ptr //RAM大小,根据read_ram_size返回8M或16M *→ 0x800000 / 0x1000000 off_30773338 → g_spiboot_load_addr //与我们前面分析一致,bootloader装载地址 → 0x30770000 off_307732D4 → g_mmu_ttb_ptr // *→ 0x307FC000 off_307732D8 → g_usable_top_ptr // *→ 0x3076FFFC / 0x30FFFFFC off_307732D0 → g_topbuf_base_ptr // *→ 0x304F0000 / 0x30D80000 off_307732DC → g_pool_size_ptr // *→ 0x200000 / 0x580000 off_307732E0 → g_max_image_size // → 0x380000 / 0x770000 ``` 现在拿着布局表再回看load_and_boot_image函数,**关注off_30772C1C函数,这是从flash中搬运压缩后的镜像。**8MBRAM情况下,它是搬到spiboot下面,16MBRAM,它是搬到8MB处。 ``` 8MB: v6 = *g_usable_top = 0x3076FFFC v7 = v6 - len = 0x3076FFFC - 0x2B72AC = 0x304B8D50 v8 = align_up4(v7) = 0x304B8D50 (已对齐,无变化) 占用区间 [0x304B8D50, 0x3076FFFC),上端紧贴 spiboot 代码底部 16MB: v6 = *g_ram_base = 0x30000000 v7 = v6 + 0x800000 = 0x30800000 (硬编码跳过 spiboot 所在的第一个 8MB) v8 = 0x30800000 占用区间 [0x30800000, 0x30AB72AC) // 读 Flash off_30772C1C( a1, // 整个 entry 指针 v8, // 目的地址(RAM 暂存区) *a1 ); // 字节数 = entry->len ``` **而开头的*a1 > *off_30772C08,检查镜像长度,与off_30772C08(=off_307732DC g_pool_size_ptr)进行比较,如果池区装不下,他就拒绝复制。而off_307732E0 → g_max_image_size则是允许解压出的最大大小,如果超过了就会踩踏池区或者spiboot。** 那个2.5MB的保留区标记还是不知道用来装什么的。 内存布局如下: ``` 16MB: 0x30000000 ┬──────────────────────────────┐ │ 解压输出区 上限 0x770000 │ ← off_307732E0,等于到 RAM起点到spiboot 的距离 0x30770000 ├──────────────────────────────┤ ← *off_30773338 (spiboot 链接地址) │ spiboot 代码/数据/bss/栈 │ 0x8C000 0x307FC000 ├──────────────────────────────┤ ← off_307732D4,16KB 对齐 │ 16KB 页表 (推断 MMU TTB) │ 0x30800000 ├──────────────────────────────┤ ← 压缩数据暂存区起点(硬编码 BASE+8MB) │ 暂存区 上限 0x580000 │ ← off_307732DC 0x30D80000 ├──────────────────────────────┤ ← off_307732D0 │ 顶部保留区 0x280000 (2.5M) │ 0x31000000 ┴──────────────────────────────┘ off_307732D8 = 0x30FFFFFC 8MB: 0x30000000 ┬──────────────────────────────┐ │ 解压输出区 上限 0x380000 │ ← off_307732E0 0x30380000 ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤ │ 空隙 0x170000 │ (保守余量?) 0x304F0000 ├──────────────────────────────┤ ← off_307732D0 │ 顶部保留区 2.5M │ │ 启动期借用为压缩数据暂存区 │ 压缩镜像长度上限 0x200000 0x30770000 ├──────────────────────────────┤ off_307732D8 = 0x3076FFFC │ spiboot(与 16MB 同一链接地址)│ 0x307FC000 ├──────────────────────────────┤ ← off_307732D4(两档恒定) │ 16KB 页表 │ 0x30800000 ┴──────────────────────────────┘ ``` ```c spi_boot_main 52:62 v6 = mmu_init(*v4); MEMORY[0x8000004] = MEMORY[0x8000004] & 0xFC012E00 | 0xD00B; while ( (MEMORY[0x8000004] & 0x1000) == 1 ) ; v7 = off_307732E8(v6); off_307732EC(v7); off_307732F0(0); sdram_set_timing(140000000); v8 = off_307732F4(); uart_init_alias(*((_DWORD *)off_307732C0 + 1), 115200, v8); init_something(); ``` read_ram_size这个函数,实际上在读0x2002D004附近读一个值,sdram_set_timing也是在附近写,**猜测这附近是SDRAM控制器的相关寄存器**。0x08000004写入后等待置位,很可能是PLL,等待锁定,uart_init也是写0x08000000附近,**几乎可以确定0x08000000附近是外设控制器的寄存器**。 在无法获得官方datasheet的情况下,也许可以据此逆向出此SOC的寄存器映射,为此SOC的程序魔改或同厂芯片(如AK3918)提供逆向思路。 最后是其核心,位于63到85行。 ```c spi_boot_main 63:85 memset_alias(boot_info, 0, 32); v9 = (int (__fastcall *)(char *, _BYTE *))debug_printf_alias; debug_printf_alias(off_3077330C, off_30773308[0]); spi_flash_init_alias(); if ( (unsigned __int8)read_image_burn_info(boot_info, v20) ) load_and_boot_image(boot_info); v10 = v9(off_30773314, v20); v11 = sub_30772D9C(v10); v12 = off_30773318(v11); v13 = off_3077331C(v12); v14 = off_30773320(v13); for ( i = off_30773324(v14); ; i = off_30773334() ) { v16 = off_30773328(i); off_3077332C(v16); if ( (unsigned __int8)off_30773330() == 1 ) { if ( (unsigned __int8)read_image_burn_info(boot_info, v20) ) load_and_boot_image(boot_info); else debug_printf_alias(off_30773314, v20); } } ``` **核心启动链其实是:初始化布局表->初始化mmu->提高主频->初始化外设和用于Flash的SPI->读burninfo->装载镜像->解压镜像->跳转。** 还有一条recovery路径,第一次读不到burninfo,会从其他地方搞来一个镜像,然后再读,sub_30772D9C-off_30773320这几个函数控制的,根据函数内容大致猜测是一套轻量的TCP协议栈,获取镜像并解析,因为这条路径在笔者的设备并未用到,这里不做详细分析。 # 镜像信息读取函数read_image_burn_info分析与镜像分区表结构 进入read_image_burn_info函数看一下 ```c int __fastcall read_image_burn_info(int a1, int a2) { int result; // r0 int v5; // r4 unsigned int v6; // r3 int v7; // r7 int v8; // r4 unsigned int v9; // r5 bool v10; // cf int v11; // r2 unsigned int v12[9]; // [sp+0h] [bp-24h] BYREF if ( !a2 ) return 0; v5 = *(_DWORD *)off_30772A70; result = (unsigned __int8)read_page_from_flash_alias(257, *(_DWORD *)off_30772A70, 1); if ( result ) { memcpy_alias1(v12, v5, 4); v6 = v12[0]; if ( v12[0] <= 3 ) { v7 = 0; v8 = v5 + 4; v9 = 0; while ( 1 ) { v10 = v9 >= v6; v11 = 16; ++v9; if ( v10 ) break; v7 = 0; if ( !strncmp_alias(v8 + 16, a2, 16) ) { memcpy_alias1(a1, v8, 32); v7 = 1; break; } v6 = v12[0]; v8 += 32; } debug_printf_alias1(off_30772A84, v7, v11); return v7; } else { return 0; } } return result; } } ``` **可以看到一个关键的魔数257,其传入的函数调用了SPI操作来读flash,这里将其命名为read_page_from_flash_alias,第一个参数是起始页,第三个参数和要读的页数。**burn_info在257页的位置,编译时硬编码进去的。 **这里应该有一个burn_info表,储存了一个(或多个)表项信息,由表的第一个字给出,存于v12,每个表项长32字节。被strncmp比对的,表项结构第16字节位置,等于a2,也就是等于"BIOS",以此来选中某一特定类型的表项(镜像信息)。同时看到memcpy把从表项起始的32字节拷贝出去,这就是镜像元数据。** 直接去dump下来的flash内容中找257页,地址0x10100。  再对照load_and_boot所用的数据就能看出来各个表项的含义, ``` Flash第257页,256字节 ┌───────────────────────────────┐ 0x10100 +0x00 │ count = 2 │ u32,条目数(校验 ≤ 3) ├───────────────────────────────┤ 0x10104 +0x04 │ │ │ entry[0] "BIOS" │ 32 字节 │ │ 0x10124 +0x24 ├───────────────────────────────┤ │ │ │ entry[1] "MACADDR" │ 32 字节 │ │ 0x10144 +0x44 ├───────────────────────────────┤ │ │ │ (未用,页内剩余 188 字节) │ 最多还能再放 1 条 │ │ 0x10200 └───────────────────────────────┘ 每个表项: 偏移 大小 字段 ──────────────────────────────────────────────── +0x00 4 len 数据字节数 +0x04 4 run_addr 解压目标地址 = 入口(数据分区为 0) +0x08 4 page Flash 起始页号(× 0x100 = 字节偏移) +0x0C 4 reserved 两条均为 0xFFFFFFFF +0x10 16 name[16] ASCII,NUL 结尾,strncmp 比较键 ──────────────────────────────────────────────── entry[0] "BIOS" 0x30780104 AC 72 2B 00 len = 0x002B72AC 2,847,404 B (2.72 MB) 0x30780108 00 00 00 30 run_addr = 0x30000000 0x3078010C 02 01 00 00 page = 0x00000102 258 → Flash 0x010200 0x30780110 FF FF FF FF reserved 0x30780114 42 49 4F 53 name "BIOS" entry[1] — MACADDR 0x30780124 1E 00 00 00 len = 0x0000001E 30 B 0x30780128 00 00 00 00 run_addr = 0 ← 数据分区,不可引导 0x3078012C 00 41 00 00 page = 0x00004100 16640 → Flash 0x410000 0x30780130 FF FF FF FF reserved 0x30780134 4D 41 43 41 44 44 52 name "MACADDR" ``` # 解压函数与解压算法分析 load_and_boot_image的关键操作是对镜像进行解压,如果我们想从flash提取或写入镜像,我们首先需要搞清楚的是它用来什么解压算法。 直接看一下解压函数off_30772C24,看到一个传入压缩镜像起始位置,压缩镜像长度和主镜像入口点地址的函数,可以确定他就是解压器。  **它的子函数中出现了经典的14case分发器和各种错误信息,LLM识别到这是zlib的inflate循环。**同时从另一处常量池中发现它是1.1.3版本,(P.S.这是一个经典的有漏洞版本,详见CVE-2002-0059),不过这种固件中用问题不大。  off_30772C24的的函数签名也就明确了: ```c //zlib的解压函数 int uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen); //off_30772C24重排了参数顺序 decompress_image_zlib(src, srclen, dst, &destLen) ``` destLen传入了g_max_image_size,前面布局计算时算出的最大允许解压出的镜像大小,防止解压后产生踩踏。同时返回实际解压出的镜像大小用于解压成功验证。 然后去看一眼258页压缩镜像的位置,找那个魔数(其实这里直接找镜像的魔数也行,因为已经知道是压缩文件了,不过正好要分析一下bootloader流程所以详细分析了)  **78 9C:zlib的常规压缩魔数,zlib实锤了** # 主镜像提取 现在我们知道了主镜像的解压手段,可以直接从flash中提取压缩后的镜像然后解压就是主镜像了。但是即使是压缩的镜像也有近3MB,从串口控制台一个WORD一个WORD读太慢了,需要一天一夜。 又因为我手里只有这一块板子,要是flash焊下来的时候拆废了板子就废了,**因此夹上逻辑分析仪抓off_30772C1C这个函数从flash中读镜像的过程,我们已经看过bootloader,没有加密镜像。** 这片flash,型号XM25QH64,最高可以跑到133Mhz,因此将采样设为500Mhz进行总线频率探测。分析发现实际读取的频率仅10Mhz,因此最终逻辑分析仪频率设为50Mhz。  **观察到是SPI方式操作的flash,因此解码器设置为SPI,MODE0,线序是CS=6,MOSI=1,MISO=2,SCK=0。解码发现镜像的加载使用03命令连续慢读。** 从大致看起来: * 先是一次500kHz的读flash最开始的头 ANVKS3C那附近,低速试着读 ->BootROM行为 * 然后一次10Mhz读flash的头 ->BootROM行为 * 然后是一次从0地址读,数十KB,是我们刚刚看的BL2 ->Bootloader行为 * 然后BL2读0x101页!Burninfo ->Bootloader行为 * 然后是一次从102页开始连续数MB读,随后释放CS ->Bootloader行为 传输大约在3.5s结束,后面是主镜像中的逻辑访问flash。 因为一个命令发送之后,要发大片的FF来读,**所以写一个脚本识别每次连续读的起点,如果是03命令就直接解析并保存,如果不是就标记一下**(3.5s之后就没用了,因为那已经进app逻辑了),下面简单给出分帧函数: ```python def split_transfers(rows): """ 扫描所有采样,识别命令帧。 命令帧 = MOSI 上一段连续的非 0xFF 字节。 返回传输列表,每个传输 = { 'cmd_bytes': 命令帧的 MOSI 字节列表(如 [0x03, addr2, addr1, addr0]), 'cmd_start_i': 命令帧起始行索引, 'data_start_i':数据开始行索引(命令帧结束后的第一行), } 数据结束由下一个传输的 cmd_start_i 界定(最后一个到文件末尾)。 """ n = len(rows) transfers = [] i = 0 while i < n: _, _, mosi = rows[i] if mosi != 0xFF: # 命令帧开始,收集连续的非 FF 字节 cmd_bytes = [] cmd_start_i = i while i < n and rows[i][2] != 0xFF: cmd_bytes.append(rows[i][2]) i += 1 data_start_i = i # 命令帧结束后,MOSI 回到 FF,这里起是数据 transfers.append({ "cmd_bytes": cmd_bytes, "cmd_start_i": cmd_start_i, "data_start_i": data_start_i, }) else: i += 1 # 用下一段的命令起点,界定上一段的数据终点 for idx, t in enumerate(transfers): if idx + 1 < len(transfers): t["data_end_i"] = transfers[idx + 1]["cmd_start_i"] else: t["data_end_i"] = len(rows) return transfers ``` 下面给出了几个3.5s以内或附近的03慢读,注意只有3.5s之前的是在启动过程中。 ``` addr=0x000000 64 字节 起始 1175408300 ns -> read_0x000000.bin addr=0x000000 220 字节 起始 1176617680 ns -> read_0x000000_#1.bin addr=0x000200 63484 字节 起始 1177061860 ns -> read_0x000200.bin addr=0x010100 256 字节 起始 1270143000 ns -> read_0x010100.bin addr=0x010100 256 字节 起始 4638866140 ns -> read_0x010100_#1.bin addr=0x010200 2847488 字节 起始 1277728440 ns -> read_0x010200.bin ``` 1,2次是试读和读信息,3是读BL2,4是读Burninfo表 最后6,读整个压缩主镜像,与我们之前对于bootloader的分析完全一致!顺带一提,4.6s的那条读取很可能是在读MACADDR,但是与启动无关。 read_0x010200.bin这个提取出来的就是压缩主镜像!使用标准的zlib函数解压他。 ```python print("[+] 尝试 1: 标准 zlib 格式解压...") decompressed = zlib.decompress(data) print("[v] 成功! 该文件是标准的 zlib 压缩文件(通过内部 Adler-32 校验)。") return save_output(output_path, decompressed) ``` ``` [*] 当前文件大小: 2847488 字节 [*] 前 4 字节魔数 (Hex): 78 9C B4 BD [+] 尝试 1: 标准 zlib 格式解压... [v] 成功! 该文件是标准的 zlib 压缩文件(通过内部 Adler-32 校验)。 [v] 成功恢复数据,原始未压缩大小: 6654908 字节 [v] 解压后的文件已保存至: read_0x010200_extracted.bin ``` # **将read_0x010200_extracted.bin与使用控制台reg命令读出的内存0x30000000数据进行比较,确定了这就是主镜像**  很标准的aarch32向量表,NOP刚好在Reserved那里。到这里主镜像的提取完成,用于后续分析。 # 总结 感谢您看到这里,如果觉得有意思或者有帮助请点个赞。笔者第一次在论坛上发文,文章有什么不对的地方或者有什么想法欢迎在评论区一起讨论一下。
登录后可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-7-31 06:41 被MicroOrange编辑 ,原因:
#PCB分析
#固件分析
#家用设备
收藏
・
6
点赞
・
12
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
AcheronHana
+1
为你点赞!
2026-9-2 18:10
Dy1an
非常支持你的观点!
2026-8-7 11:19
0bsidian
+1
谢谢你的细致分析,受益匪浅!
2026-8-3 10:29
wx_比安
这个讨论对我很有帮助,谢谢!
2026-7-29 10:31
马来
为你点赞!
2026-7-27 20:10
iCookiie
为你点赞!
2026-7-27 16:33
莲子荷花
你的帖子非常有用,感谢分享!
2026-7-27 11:53
深山樵夫
你的帖子非常有用,感谢分享!
2026-7-27 09:55
Mezone0
期待更多优质内容的分享,论坛有你更精彩!
2026-7-27 02:21
MochaCo
谢谢你的细致分析,受益匪浅!
2026-7-27 00:24
yichen115
感谢你的贡献,论坛因你而更加精彩!
2026-7-24 17:34
AnDii
感谢你分享这么好的资源!
2026-7-24 17:01
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
5
)
胡一米
雪 币:
26403
活跃值:
(9290)
能力值:
(RANK:445 )
在线值:
发帖
5
回帖
87
粉丝
122
关注
私信
胡一米
3
2
楼
很不错。期待第二篇。
2026-7-24 16:53
2
Zepp7289
雪 币:
200
能力值:
( LV1,RANK:0 )
在线值:
发帖
1
回帖
138
粉丝
0
关注
私信
Zepp7289
3
楼
AI文看多了来上一篇好文真是让人神清气爽
2026-7-25 13:19
1
YoungBai
雪 币:
1308
活跃值:
(1660)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
145
粉丝
2
关注
私信
YoungBai
4
楼
666
2026-7-27 10:18
0
wx_WantC
雪 币:
749
活跃值:
(558)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
42
粉丝
1
关注
私信
wx_WantC
5
楼
很详细,学习了
2026-7-29 13:33
0
Thehepta
雪 币:
3596
活跃值:
(7395)
能力值:
( LV11,RANK:185 )
在线值:
发帖
23
回帖
235
粉丝
283
关注
私信
Thehepta
3
6
楼
强,真的强
2026-8-3 15:24
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
MicroOrange
1
发帖
0
回帖
10
RANK
关注
私信
他的文章
[原创]某Arm9裸机门铃黑盒逆向(一)bootloader分析与主镜像提取
4333
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部