计算机最讲道理。凭啥计算机知道加载地址,而我不知道?答:裸机程序中不必要包含自己的加载地址,如果没有加载地址,就无法对绝对地址的引用有正确的解析。所以分析的固件如果是裸机层面的代码,就需要知道其加载地址。
本文章相关PPT:加载地址相关.pptx
原因分析
平日在做pwn题时,从来没有分析过程序的加载地址因为:
- 如果没有开启PIE编译选项,程序的加载基址是写在ELF文件中的
- 如果开启PIE编译选项,那么程序的加载基址是加载器随机决定的
二种程序都可以被IDA正常的分析,其中的地址解析也不会出现什么问题,因为绝对地址的引用和ELF中保存的程序基址是匹配的。所以我们也从来就没有在分析代码时琢磨过加载地址啥的,那么为啥研究IoT固件的时候就需要知道固件的加载地址呢?让我们来看一下你刚刚按下电源键那一刻,计算机内部的盘古开天地吧!
CPU相关
不过在按下电源之前,先回忆一下CPU这么多年的发展吧:
- 芯片维基百科(非常推荐!!!)
- CPU的历史
- 时间简史——扒一扒那些近代经典CPU(上)
- 时间简史——扒一扒那些近代经典CPU(下)
- CPU 历史上著名的破解有哪些?
- 硅谷历史 Intel的东进与ARM的西征
- Arm公司再次重拳反制RISC-V架构,中国芯片厂商们该何去何从
x86
先说我们一般PC机的CPU架构:x86,其模式包括,实模式,保护模式,虚拟8086模式,IA-32e模式以及系统管理模式:
历史
仍然来看一下x86这么多年来的历史吧:
想要明白x86,看英特尔官方手册是最好的,不过我也没明白总共有几卷,官网注释说总共5卷,下面这就8卷了,还有一个2卷的集合,也不知道咋回事,乱七八糟的。卷123内容分别是:基本架构,指令集参考,系统编程指南
- 英特尔® 64 位和 IA-32 架构开发人员手册:卷 1
- 英特尔® 64 位和 IA-32 架构开发人员手册:卷 2A
- 英特尔® 64 位和 IA-32 架构开发人员手册:卷 2B
- 英特尔® 64 位和 IA-32 架构开发人员手册:卷 2C
- 英特尔® 64 位和 IA-32 架构开发人员手册:卷 3A
- 英特尔® 64 位和 IA-32 架构开发人员手册:卷 3B
- 英特尔® 64 位和 IA-32 架构开发人员手册,卷 3C
- 英特尔® 64 位和 IA-32 架构开发人员手册,卷 3D
- 英特尔® 64 位和 IA-32 软件开发人员手册:文档变更
一个单独的卷2:
可以看到x86架构的CPU实体一般是Intel和AMD两家生产,这两家既是x86架构的设计者,又是CPU的设计者。
启动分析
以《一个64位操作系统的设计与实现》为例,最近正好在跟着这本书学习操作系统相关知识,x86 CPU的启动,当然这个启动过程的例子是古老的:
- CPU上电,CS:IP复位到0xffff0
- 此时天地初开,内存没有开始工作,CPU访问的0xffff0实际上是BIOS的ROM,这个实现的原理应该是在线路上设计好的
- BIOS在0xffff0的指令一般是一个长跳转指令,不过仍然是跳转到BIOS中的代码去执行,初始化各种硬件
- 去读取磁盘上的0磁头0磁道1扇区(引导扇区)内容到内存中0x7c00,然后并跳转
- 引导扇区其实怎么设计都可以,直接放文件系统的第一个扇区格式也可以,总之是存放着boot相关代码
- 在《一个64位操作系统的设计与实现》中,第一个扇区是带FAT12文件系统的boot,整个磁盘被格式化为FAT12文件系统
- Boot去文件系统中搜索loader.bin并加载到0x10000,并跳转过去
- loader去文件系统中搜索kernel.bin并加载到0x100000,并跳过去
代码名称 | 代码基址 | 静态时代码保存位置 | 控制流 | 基址决定因素 |
---|---|---|---|---|
BIOS | 0xf0000 | BIOS ROM | 开机加电跳转到0xffff0 | 主板线路BIOS基址 |
boot | 0x7c00 | 硬盘第一个扇区 | BIOS执行完毕后跳转到0x7c00 | BIOS决定boot基址 |
loader | 0x10000 | 硬盘中的文件系统(扇区随意) | boot执行完毕后跳转到0x10000 | boot决定loader基址 |
kernel | 0x100000 | 硬盘中的文件系统(扇区随意) | loader执行完毕后跳转到0x100000 | loader决定kernel基址 |
- 所以BIOS可以不知道自己的加载地址,因为线路以及决定好了映射,CPU加电就跳过去
- 同理boot可以不知道自己的加载地址,因为BIOS加载的他,并跳过去
- 同理loader也可以不知道自己的加载地址,因为boot加载的他,并跳过去
- 同理kernel也可以不知道自己的加载地址,因为loader加载的他,并跳过去
之后我们一般将boot.bin,loader.bin,kernel.bin,一起打包成一个镜像os.img,也就是我们常见的系统镜像:
所以没法直接用IDA分析融合了三个文件的os.img,需要拆出来分别分析。故拆的方法就是打包的逆方法,镜像打包的方法可能不同,所以拆的方式也不同,本书中是os.img是一个FAT12的文件系统,boot.bin作为FAT12文件系统的第一个扇区,loader.bin与kernel.bin是直接放入文件系统中的文件:
- IDA可以直接识别BIOS固件的加载基址
- 要用IDA分析boot需要设置加载地址0x7c00
- 要用IDA分析loader需要设置加载地址0x10000
- 要用IDA分析kernel需要设置加载地址0x100000
更多关于BIOS与UEFI的知识:
- 不知道哪来的BIOS文档
- MBR与GPT
- UEFI背后的历史
- UEFI架构
- UEFI与硬件初始化
- ACPI与UEFI
- UEFI和UEFI论坛
- UEFI安全启动
- FAT文件系统与UEFI
- 知乎专栏:UEFI和BIOS探秘
ARM
IoT设备的CPU大多是ARM架构,所以ARM才是我们关注的重点,还是从ARM的历史说起把!
历史
可以看出ARM公司不直接设计CPU,和Intel不太一样。所以我们常见的ARM架构的手机的CPU的那些牌子,比如苹果的A系列,高通骁龙,华为海思的麒麟,都是ARM授权才允许制作的。还看到了任天堂,索性查了一下常见的一些设备:
发现的确都是ARM架构的CPU,而且也都是不是ARM公司自己去做的,可以发现一张芯片从指令集到整体设计再到制作,都不是一家完成的。所以虽然说CPU的指令集都是ARM,可是这些CPU却千差万别,比如,以下分析了一些ARM架构的CPU的启动过程:
一张芯片上只有CPU是不合适移动设备如此普及的今天,于是来到了SoC的时代:
SoC的定义多种多样,由于其内涵丰富、应用范围广,很难给出准确定义。一般说来,SoC称为系统级芯片,也有称片上系统,意指它是一个产品,是一个有专用目标的集成电路,其中包含完整系统并有嵌入软件的全部内容。同时它又是一种技术,用以实现从确定系统功能开始,到软/硬件划分,并完成设计的整个过程。
要问到底啥是SoC:
- 知乎回答:soc≈一台电脑的主机,而不是=cpu
- 我的回答:集成了其他部件的CPU就可以算SoC了
移动设备上,寸土寸金,既要考虑成本,又要考虑工艺,还要考虑节能等等。所以集成电路(IC)发展到今天是可以做到把各种东西(CPU,RAM,ROM,GPU,NPU等等)封装到一张芯片里的。这个芯片本身,加上能控制这一套东西正常运行的软件,都应该是SoC的一部分。
启动分析
以一般android手机启动为例:
- CPU上电,位于CPU内部的onChipRom开始执行
- 此时天地初开,外部内存SDRAM没有真正的开始工作,onChipRom进行一些列芯片内部的初始化工作
- onChipRom将flash中的xloader加载到CPU内部的sram中,然后跳转过去执行
- xloader对系统时钟以及外部SDRAM进行初始化,然后将flash芯片中的u-boot(bootloader)加载到外部内存中,然后跳转
- u-boot(bootloader)在进行一系列初始化,并将flash中的kernel加载到外部内存中然后跳转
代码名称 | 代码运行位置 | 静态时代码保存位置 | 控制流 | 运行位置决定因素 |
---|---|---|---|---|
onchiprom | SRAM | CPU chip | CPU加电 | CPU设计 |
xloader | SRAM | flash | onChipRom执行后跳转 | onChipRom决定 |
u-boot | SDRAM | flash | xloader执行后跳转 | xloader决定 |
kernel | SDRAM | flash | u-boot执行后跳转 | uboot决定 |
注:onChipRom,boot rom,rom code,一个意思,都是指SoC内部的ROM,即整个CPU芯片里面的ROM
- on-chip ROM boot的原理分析
- xloader概念
- 关于xloader和uboot的几个初级问题
- uboot如何启动内核
- Uboot和内核到底是什么(从系统启动角度看)
- U-Boot移植——链接地址、运行地址、加载地址、存储地址
- 真假vmlinux–由vmlinux.bin揭开的秘密
- vmlinux,vmlinuz,bzimage,zimage,initrd.img的区别与联系
当然现在的手机启动没有这么简单,在uboot这个层面不仅仅是简单的启动一个linux的kernel,而且有着更复杂的过程,即ARM公司提出的Trust Zone技术,linux kernel已经不再是启动的唯一目标。
影响
还是以以《一个64位操作系统的设计与实现》最开始的一个引导程序为例:
可以看出没有正确的加载地址,将导致分析绝对地址使用时分析出错,如:控制流转移jmp,call,以及数据的访问。我们在分析内存破坏漏洞的时候关心的就是内存,唯一标识一块内存的信息就是内存地址,但是在不同的寻址模式下,内存地址的意义也是不同的,所以分析一段代码,也要关心执行这段代码时,CPU的模式,寻址的模式。
对比平日的用户程序和裸机的二进制:
加载地址 | 加载者 | 地址分类 | |
---|---|---|---|
用户程序 | 程序文件自身包含 | 加载器:如linux的ld | 虚拟地址 |
裸机程序 | 程序文件不必要包含 | 自举:只要能run就可以 | 物理地址 |
总结
所以我们研究的固件研究加载地址是为了:分析那些需要确定加载地址的代码
这句圈话类似于,于丹讲论语,《论语·子罕》:”子曰:知者不惑,仁者不忧,勇者不惧”,知者不惑意为:聪明的人就不困惑。有人抨击于丹,这不是废话么,聪明的就不困惑,困惑的就不聪明。认为此处,知,应该翻译成求知。
哪些代码需要确定加载地址呢?
- 裸机程序:bootloader 这类(物理地址)
- 内核程序:vmlinux 这类(逻辑地址)
比如一个路由器固件(全部存储在flash中)包括: bootloader,linux kernel,以及文件系统。如果我们关注点在文件系统中的可执行程序,那么就完全不用关心什么固件加载地址。
但是如果我们关注一个PLC固件,这个东西实现不是bootloader起linux kernel这套。而是类似裸机程序,即关注的功能实现就是在bootloader和kernel的层面实现的,就需要关注加载地址了。如:工控漏洞挖掘方法之固件逆向分析。
确定地址
发现一篇文章介绍了一个工具可以帮助定位固件的加载地址:
这个工具参考的论文:
这篇文章解决的问题就是:在只有这个二进制文件本身时,利用其自身的一些性质,分析出其加载地址。利用的性质如下:
- 利用序言来识别函数
- 利用了代码开发时的函数表的特征
- 利用了函数指针与函数的关系对应
- 利用了字符串的引用,长度等
其中的第一种方法是:基于函数入口表的装载基址定位,也是刚才的工具的原理方法。在分析大型应用程序的源代码时,经常可以发现这种形式的代码:
//GT-I9500_JB_Opensource\Kernel\net\bluetooth\mgmt.c
struct mgmt_handler {
int (*func) (struct sock *sk, struct hci_dev *hdev, void *data, u16 data_len);
bool var_len;
size_t data_len;
}
mgmt_handlers[] = {
{ NULL }, /* 0x0000 (no command) */
{ read_version,false, MGMT_READ_VERSION_SIZE },
{ read_commands, false, MGMT_READ_COMMANDS_SIZE },
......
{ block_device,false, MGMT_BLOCK_DEVICE_SIZE },
{ unblock_device,false, MGMT_UNBLOCK_DEVICE_SIZE },
};
该代码来自三星手机内核源代码,这类代码的特点是定义了一个结构体数组,并且结构体的成员之一为函数指针。作者定义这些函数指针组成的表为函数入口表。
- 所以想办法分析出二进制中的函数指针后
- 对内存中所有可能的装载基址位置进行枚举,在每一个可能位置根据函数入口表查找每个函数序言
- 如果一个基址使得较多的函数指针指针匹配上了函数
- 那么估计这个地址为装载基址。
剩下三种办法就是根据字符串在代码中的一些使用性质,如引用偏移,长度啥的,进行的分析:
- 基于字符串地址集合的装载基址定位
- 基于文字池匹配的装载基址定位
- 基于字符串存储长度分组匹配的装载基址定位
这个哥们还在Kcon2018做了关于此内容的报告,不过没有找到完整的PPT:
固件练习
IDA加载地址设置
IDA如果ARM,MIPS等处理器,则可以在选择完CPU后设置如下界面,x86只能设置基址偏移:
提取练习
分析练习
哪些binwalk出来就可以直接分析文件系统中的代码的?
哪些是本身加密的,需要解密后才能binwalk的?
哪些不是要研究文件系统的,而是直接研究无文件结构的代码的?