智能门铃设备长期以来都是硬件安全问题的重点关注对象和重灾区。然而许多这类设备为了将成本和功耗压缩到极致,使用裸机或RTOS,配上各种偏门,找不到文档和SDK的国产SOC,使得对其进行的分析变得十分麻烦。同时裸机编程的各种手写原语和未充分测试的边界条件使其安全问题更加严峻。
本文对一台闲置的智能门铃(楼宇对讲系统室内机)进行了硬件分析,bootloader固件提取,启动流程分析。对bootloader固件进行反汇编,分析其运行过程中的内存布局,烧录信息的储存和读取方式,主镜像的装载和解压方式。通过逻辑分析仪监听片外flash的SPI总线,获取并解压得到主镜像供下一步分析。
此室内机设备具有一带触摸的LCD,MIC,扬声器,通过一网口与楼宇系统连接,其功能包括楼门呼叫,可视对讲,紧急呼叫等等功能。拆开其后面板观察其PCB。
首先确定给主电源的供电电压,未查到DCDC1控制器型号,但是考虑到标准POE电源应该是48V,为了保险这里给12V,此设备顺利启动,运行功耗2.5W左右。 将串口引脚焊接并引出,使用USB转串口模块将连接到PC,试一下常见波特率发现波特率是115200,重启设备发现打印出如下的调试信息。
几乎可以确定这个设备跑的不是Linux,而是某种RTOS或者裸机,spiboot是其bootloader,可能是厂商SDK中的。0x30000000是主镜像的地址,也就是RESET向量所在的位置。
向串口发送一个空行,发现控制台没有锁,给出了一张命令表。
发现其中竟然有flash和reg这种操作,可以直接读flash,reg经过测试发现是直接读片内总线上的任意地址的,设备没有隔离手段。
试读一个,直接就是一条跳转指令。最初的想法是编写一个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附近,它自己肯定也在附近,它的基地址应该是多少?
回顾一下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主要行为。发现这些字符串都是这个函数打印的,字符串传入的那个函数就是debug_printf。反编译器面对vargs有点乱套了。aBurnInfo_ptr,这里打出了那个Burn Info:字符串,所以实际上并不是从这个函数获得的,而是作为参数a1传入。
a1[2], *a1, a1[1],就是打印出来的页号,长度和加载地址。注意,v12 = (int (__fastcall *)(_DWORD))a1[1],v12实际是指向0x30000000的指针,return v12(v16),实际上这就是bootloader跳转到主镜像RESET向量! (v16那个参数是反编译器混乱了)。
根据错误处理路径的字符串推断,具体的流程分析如下:
off_30772C1C是是加载镜像一类的,off_30772C24是解压 ,off_30772C30实则是memcpy。
回过头去看main函数,其调用了这个load_and_boot_image。
其中部分操作函数已经根据参数猜测并检查过功能,最后命名,命名只是猜测,不一定准确。
看看spi_boot_main的几个初始化函数
首先根据SDRAM容量准备布局表,从off_307732CC开始的一个结构体,其中的几个魔数,0x30770000刚好是bootloader装载地址,0x307FC000是bootloader结束地址。发现 0x307FC000刚好是8MB-16KB,又被传入mmu_init(可以看后面的初始化部分分析),这刚好是一级页表的大小!所以大概可以猜到0x307FC000是TTB地址。
随后再算出后面装载镜像和解压的可用区上界(off_307732D8)
off_307732D0 = 上界 − 2.5MB,应该是一个缓冲区顶
现在拿着布局表再回看load_and_boot_image函数,**关注off_30772C1C函数,这是从flash中搬运压缩后的镜像。**8MBRAM情况下,它是搬到spiboot下面,16MBRAM,它是搬到8MB处。
*而开头的 a1 > off_30772C08,检查镜像长度,与off_30772C08(=off_307732DC g_pool_size_ptr)进行比较,如果池区装不下,他就拒绝复制。而off_307732E0 → g_max_image_size则是允许解压出的最大大小,如果超过了就会踩踏池区或者spiboot。 * 那个2.5MB的保留区标记还是不知道用来装什么的。
内存布局如下:
read_ram_size这个函数,实际上在读0x2002D004附近读一个值,sdram_set_timing也是在附近写,猜测这附近是SDRAM控制器的相关寄存器 。0x08000004写入后等待置位,很可能是PLL,等待锁定,uart_init也是写0x08000000附近,几乎可以确定0x08000000附近是外设控制器的寄存器 。
在无法获得官方datasheet的情况下,也许可以据此逆向出此SOC的寄存器映射,为此SOC的程序魔改或同厂芯片(如AK3918)提供逆向思路。
最后是其核心,位于63到85行。
核心启动链其实是:初始化布局表->初始化mmu->提高主频->初始化外设和用于Flash的SPI->读burninfo->装载镜像->解压镜像->跳转。
还有一条recovery路径,第一次读不到burninfo,会从其他地方搞来一个镜像,然后再读,sub_30772D9C-off_30773320这几个函数控制的,根据函数内容大致猜测是一套轻量的TCP协议栈,获取镜像并解析,因为这条路径在笔者的设备并未用到,这里不做详细分析。
进入read_image_burn_info函数看一下
**可以看到一个关键的魔数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所用的数据就能看出来各个表项的含义,
load_and_boot_image的关键操作是对镜像进行解压,如果我们想从flash提取或写入镜像,我们首先需要搞清楚的是它用来什么解压算法。
直接看一下解压函数off_30772C24,看到一个传入压缩镜像起始位置,压缩镜像长度和主镜像入口点地址的函数,可以确定他就是解压器。
**它的子函数中出现了经典的14case分发器和各种错误信息,LLM识别到这是zlib的inflate循环。**同时从另一处常量池中发现它是1.1.3版本,(P.S.这是一个经典的有漏洞版本,详见CVE-2002-0059),不过这种固件中用问题不大。
off_30772C24的的函数签名也就明确了:
destLen传入了g_max_image_size,前面布局计算时算出的最大允许解压出的镜像大小,防止解压后产生踩踏。同时返回实际解压出的镜像大小用于解压成功验证。
然后去看一眼258页压缩镜像的位置,找那个魔数(其实这里直接找镜像的魔数也行,因为已经知道是压缩文件了,不过正好要分析一下bootloader流程所以详细分析了)
[内核课程]《Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
上传的附件: