为啥要分析固件的加载地址?

计算机最讲道理。凭啥计算机知道加载地址,而我不知道?答:裸机程序中不必要包含自己的加载地址,如果没有加载地址,就无法对绝对地址的引用有正确的解析。所以分析的固件如果是裸机层面的代码,就需要知道其加载地址。

本文章相关PPT:加载地址相关.pptx

原因分析

平日在做pwn题时,从来没有分析过程序的加载地址因为:

  • 如果没有开启PIE编译选项,程序的加载基址是写在ELF文件中的
  • 如果开启PIE编译选项,那么程序的加载基址是加载器随机决定的

二种程序都可以被IDA正常的分析,其中的地址解析也不会出现什么问题,因为绝对地址的引用和ELF中保存的程序基址是匹配的。所以我们也从来就没有在分析代码时琢磨过加载地址啥的,那么为啥研究IoT固件的时候就需要知道固件的加载地址呢?让我们来看一下你刚刚按下电源键那一刻,计算机内部的盘古开天地吧!

CPU相关

不过在按下电源之前,先回忆一下CPU这么多年的发展吧:

x86

先说我们一般PC机的CPU架构:x86,其模式包括,实模式,保护模式,虚拟8086模式,IA-32e模式以及系统管理模式:

image

历史

仍然来看一下x86这么多年来的历史吧:

想要明白x86,看英特尔官方手册是最好的,不过我也没明白总共有几卷,官网注释说总共5卷,下面这就8卷了,还有一个2卷的集合,也不知道咋回事,乱七八糟的。卷123内容分别是:基本架构,指令集参考,系统编程指南

一个单独的卷2:

可以看到x86架构的CPU实体一般是Intel和AMD两家生产,这两家既是x86架构的设计者,又是CPU的设计者。

启动分析

《一个64位操作系统的设计与实现》为例,最近正好在跟着这本书学习操作系统相关知识,x86 CPU的启动,当然这个启动过程的例子是古老的:

image

  1. CPU上电,CS:IP复位到0xffff0
  2. 此时天地初开,内存没有开始工作,CPU访问的0xffff0实际上是BIOS的ROM,这个实现的原理应该是在线路上设计好的
  3. BIOS在0xffff0的指令一般是一个长跳转指令,不过仍然是跳转到BIOS中的代码去执行,初始化各种硬件
  4. 去读取磁盘上的0磁头0磁道1扇区(引导扇区)内容到内存中0x7c00,然后并跳转
  5. 引导扇区其实怎么设计都可以,直接放文件系统的第一个扇区格式也可以,总之是存放着boot相关代码
  6. 在《一个64位操作系统的设计与实现》中,第一个扇区是带FAT12文件系统的boot,整个磁盘被格式化为FAT12文件系统
  7. Boot去文件系统中搜索loader.bin并加载到0x10000,并跳转过去
  8. 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,也就是我们常见的系统镜像:

image

所以没法直接用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的知识:

ARM

IoT设备的CPU大多是ARM架构,所以ARM才是我们关注的重点,还是从ARM的历史说起把!

历史

可以看出ARM公司不直接设计CPU,和Intel不太一样。所以我们常见的ARM架构的手机的CPU的那些牌子,比如苹果的A系列,高通骁龙,华为海思的麒麟,都是ARM授权才允许制作的。还看到了任天堂,索性查了一下常见的一些设备:

image

发现的确都是ARM架构的CPU,而且也都是不是ARM公司自己去做的,可以发现一张芯片从指令集到整体设计再到制作,都不是一家完成的。所以虽然说CPU的指令集都是ARM,可是这些CPU却千差万别,比如,以下分析了一些ARM架构的CPU的启动过程:

一张芯片上只有CPU是不合适移动设备如此普及的今天,于是来到了SoC的时代:

SoC的定义多种多样,由于其内涵丰富、应用范围广,很难给出准确定义。一般说来,SoC称为系统级芯片,也有称片上系统,意指它是一个产品,是一个有专用目标的集成电路,其中包含完整系统并有嵌入软件的全部内容。同时它又是一种技术,用以实现从确定系统功能开始,到软/硬件划分,并完成设计的整个过程。

要问到底啥是SoC:

移动设备上,寸土寸金,既要考虑成本,又要考虑工艺,还要考虑节能等等。所以集成电路(IC)发展到今天是可以做到把各种东西(CPU,RAM,ROM,GPU,NPU等等)封装到一张芯片里的。这个芯片本身,加上能控制这一套东西正常运行的软件,都应该是SoC的一部分。

启动分析

以一般android手机启动为例:

image

  1. CPU上电,位于CPU内部的onChipRom开始执行
  2. 此时天地初开,外部内存SDRAM没有真正的开始工作,onChipRom进行一些列芯片内部的初始化工作
  3. onChipRom将flash中的xloader加载到CPU内部的sram中,然后跳转过去执行
  4. xloader对系统时钟以及外部SDRAM进行初始化,然后将flash芯片中的u-boot(bootloader)加载到外部内存中,然后跳转
  5. 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决定

image

注:onChipRom,boot rom,rom code,一个意思,都是指SoC内部的ROM,即整个CPU芯片里面的ROM

当然现在的手机启动没有这么简单,在uboot这个层面不仅仅是简单的启动一个linux的kernel,而且有着更复杂的过程,即ARM公司提出的Trust Zone技术,linux kernel已经不再是启动的唯一目标。

image

影响

还是以以《一个64位操作系统的设计与实现》最开始的一个引导程序为例:

image

可以看出没有正确的加载地址,将导致分析绝对地址使用时分析出错,如:控制流转移jmp,call,以及数据的访问。我们在分析内存破坏漏洞的时候关心的就是内存,唯一标识一块内存的信息就是内存地址,但是在不同的寻址模式下,内存地址的意义也是不同的,所以分析一段代码,也要关心执行这段代码时,CPU的模式,寻址的模式。

对比平日的用户程序和裸机的二进制:

  加载地址 加载者 地址分类
用户程序 程序文件自身包含 加载器:如linux的ld 虚拟地址
裸机程序 程序文件不必要包含 自举:只要能run就可以 物理地址

总结

所以我们研究的固件研究加载地址是为了:分析那些需要确定加载地址的代码

这句圈话类似于,于丹讲论语,《论语·子罕》:”子曰:知者不惑,仁者不忧,勇者不惧”,知者不惑意为:聪明的人就不困惑。有人抨击于丹,这不是废话么,聪明的就不困惑,困惑的就不聪明。认为此处,知,应该翻译成求知。

哪些代码需要确定加载地址呢?

  • 裸机程序:bootloader 这类(物理地址)
  • 内核程序:vmlinux 这类(逻辑地址)

比如一个路由器固件(全部存储在flash中)包括: bootloader,linux kernel,以及文件系统。如果我们关注点在文件系统中的可执行程序,那么就完全不用关心什么固件加载地址。

但是如果我们关注一个PLC固件,这个东西实现不是bootloader起linux kernel这套。而是类似裸机程序,即关注的功能实现就是在bootloader和kernel的层面实现的,就需要关注加载地址了。如:工控漏洞挖掘方法之固件逆向分析

确定地址

发现一篇文章介绍了一个工具可以帮助定位固件的加载地址:

这个工具参考的论文:

这篇文章解决的问题就是:在只有这个二进制文件本身时,利用其自身的一些性质,分析出其加载地址。利用的性质如下:

  1. 利用序言来识别函数
  2. 利用了代码开发时的函数表的特征
  3. 利用了函数指针与函数的关系对应
  4. 利用了字符串的引用,长度等

其中的第一种方法是:基于函数入口表的装载基址定位,也是刚才的工具的原理方法。在分析大型应用程序的源代码时,经常可以发现这种形式的代码:

//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:

固件练习

IOT 安全实战资料收集整合

IDA加载地址设置

IDA如果ARM,MIPS等处理器,则可以在选择完CPU后设置如下界面,x86只能设置基址偏移:

image

提取练习

分析练习

哪些binwalk出来就可以直接分析文件系统中的代码的?
哪些是本身加密的,需要解密后才能binwalk的?
哪些不是要研究文件系统的,而是直接研究无文件结构的代码的?