ELF文件解析

本文详细介绍了ELF(Executable and Linkable Format)文件的三种类型:可重定位文件、可执行文件和共享目标文件。重点讲解了ELF文件的结构,包括文件头、程序头表、节区头表和节区,以及解析工具objdump和readelf的使用。通过实例解析了ELF文件中的节区类型、重定位表和符号表,帮助读者深入理解ELF文件的工作原理。

ELF文件格式

ELF (Executable and Linkable Format)是 UNIX 类操作系统中普遍采用的目标文件格式, 分为三种类型:

1、可重定位文件 (Relocatable File)包含适合于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。

这是由汇编器汇编生成的 .o 文件。后面的链接器(link editor)拿一个或一些 Relocatable object files 作为输入,经链接处理后,生成一个可执行的对象文件 (Executable file) 或者一个可被共享的对象文件(Shared object file)。我们可以使用 ar 工具将众多的 .o Relocatable object files 归档(archive)成 .a 静态库文件。如何产生 Relocatable file,你应该很熟悉了,请参见我们相关的基本概念文章和JulWiki。另外,可以预先告诉大家的是我们的内核可加载模块 .ko 文件也是 Relocatable object file。

2、可执行文件 (Executable File)包含适合于执行的一个程序,此文件规定了 exec() 如何创建一个程序的进程映像。

这我们见的多了。文本编辑器vi、调式用的工具gdb、播放mp3歌曲的软件mplayer等等都是Executable object file。你应该已经知道,在我们的 Linux 系统里面,存在两种可执行的东西。除了这里说的 Executable object file,另外一种就是可执行的脚本(如shell脚本)。注意这些脚本不是 Executable object file,它们只是文本文件,但是执行这些脚本所用的解释器就是 Executable object file,比如 bash shell 程序。

3、共享目标文件(Shared Object File)包含可在两种上下文中链接的代码和数据。首先链接编辑器可以将它和其它可重定位文件和共享目标文件一起处理, 生成另外一个目标文件。其次,动态链接器(Dynamic Linker)可能将它与某个可执行文件以及其它共享目标一起组合,创建进程映像。

这些就是所谓的动态库文件,也即 .so 文件。这也是我们研究的重点,因为大多数apk 中包含的elf 就是so格式的这一类文件。是如果拿前面的静态库来生成可执行程序,那每个生成的可执行程序中都会有一份库代码的拷贝。如果在磁盘中存储这些可执行程序,那就会占用额外的磁盘空 间;另外如果拿它们放到Linux系统上一起运行,也会浪费掉宝贵的物理内存。如果将静态库换成动态库,那么这些问题都不会出现。动态库在发挥作用的过程 中,必须经过两个步骤:

a) 链接编辑器(link editor)拿它和其他Relocatable object file以及其他shared object file作为输入,经链接处理后,生存另外的 shared object file 或者 executable file。
b)在运行时,动态链接器(dynamic linker)拿它和一个Executable file以及另外一些 Shared object file 来一起处理,在Linux系统里面创建一个进程映像。

目标文件全部是程序的二进制表示,目的是直接在某种处理器上直接执行。

文章介绍过程中会使用到两个elf 的解析工具,objdump readelf,并在文章中穿插出现。

ELF 文件在不同的视图下有不同的文件结构,共有两种视图: 链接视图和执行视图。

链接视图就是在链接时用到的视图,而执行视图则是在执行时用到的视图。上图左侧的视角是从链接来看的,右侧的视角是执行来看的。什么意思呢?就是同样的一份文件,段中会包含一个或者多个节区。节区的概念是在链接的过程中使用到的,但是在程序运行的时候,是关注整个段的,而不是节区的内容。

整个文件大致可以分为下面几个部分:

1、文件头:

ELF header: 描述整个文件的组织。

2、程序头表:

Program Header Table: 描述文件中的各种segments,用来告诉系统如何创建进程映像的。在程序运行的时候程序头表示需要关注的,反倒节区头表不是必须的。

3、节区头表:

Section Header Table: 包含了文件各个segction的属性信息,我们都将结合例子来解释。程序链接的时候节区头表是需要关注的,但是程序头表却是非必须。节区头部表包含了文件中所有节区的基本信息, 每一个节区在表中都有一个项, 描述了该节区的名字、大小、 偏移位置等关键信息。 节区头部表在链接的过程中指导链接器将相同属性(比如flg)的节区合并成为一个段,因此需要进行链接的文件必须包含节区头部表。

4、节区/段:

sections 或者 segments:segments是从运行的角度来描述elf文件,sections是从链接的角度来描述elf文件,也就是说,在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序(所以很多加固手段删除了section header table)。

从图中我们也可以看出,segments与sections是包含的关系,一个segment包含若干个section。sections按照一定规则映射到segment。那么为什么需要区分两种不同视图?

当ELF文件被加载到内存中后,系统会将多个具有相同权限(flg值)section合并一个segment。操作系统往往以页为基本单位来管理内存分配,一般页的大小为4096B,即4KB的大小。同时,内存的权限管理的粒度也是以页为单位,页内的内存是具有同样的权限等属性,并且操作系统对内存的管理往往追求高效和高利用率这样的目标。ELF文件在被映射时,是以系统的页长度为单位的,那么每个section在映射时的长度都是系统页长度的整数倍,如果section的长度不是其整数倍,则导致多余部分也将占用一个页。而我们从上面的例子中知道,一个ELF文件具有很多的section,那么会导致内存浪费严重。这样可以减少页面内部的碎片,节省了空间,显著提高内存利用率。
需要注意地是:尽管图中显示的各个组成部分是有顺序的,实际上除了 ELF 头部表以外,其他节区和段都没有规定的顺序。

 

文件头

 

首先,我们先来看下32位ELF文件中常用的数据格式:

名称大小对齐目的
Elf32_Addr44无符号程序地址
Elf32_Half22无符号中等整数
Elf32_Off44无符号文件偏移
Elf32_SWord44有符号大整数
Elf32_Word44无符号大整数
unsigned char11无符号小整数

然后我们来观察一下ELF Header的结构体:


#define EI_NIDENT 16
typedef struct {
       unsigned char e_ident[EI_NIDENT];
       ELF32_Half e_type;
       ELF32_Half e_machine;
       ELF32_Word e_version;
       ELF32__Addr e_entry;
       ELF32_Off e_phoff;
       ELF32_Off e_shoff;
       ELF32_Word e_flags;
       ELF32_Half e_ehsize;
       ELF32_Half e_phentsize;
       ELF32_Half e_phnum;
       ELF32_Half e_shentsize;
       ELF32_Half e_shnum;
       ELF32_Half e_shstrndx;
}Elf32_Ehdr;

e_ident : ELF的一些标识信息,前四位为.ELF,其他的信息比如大小端等
e_machine : 文件的目标体系架构,比如ARM
e_version : 0为非法版本,1为当前版本
e_entry : 程序入口的虚拟地址
e_phoff : 程序头部表偏移地址
e_shoff : 节区头部表偏移地址
e_flags :保存与文件相关的,特定于处理器的标志
e_ehsize :ELF头的大小
e_phentsize : 每个程序头部表的大小
e_phnum :程序头部表的数量
e_shentsize:每个节区头部表的大小
e_shnum : 节区头部表的数量
e_shstrndx:节区字符串表位置

 

或者使用010Editor的ELF模板也可以看到ELF Header结构。对比以下三类ELF文件,我们得到了以下结论:
1、e_type标识了文件类型
2、Relocatable File(.o文件)不需要执行,因此e_entry字段为0,且没有Program Header Table等执行视图
3、不同类型的ELF文件的Section也有较大区别,比如只有Relocatable File有.strtab节。
这里写图片描述
Shared Object File(.so文件)

这里写图片描述
Executable File(可执行文件android_server)

这里写图片描述
Relocatable File(.o文件)
详细字段含义介绍:

1 e_ident

其中需要注意地是e_ident是一个16字节的数组,这个数组按位置从左到右都是有特定含义,每个数组元素的下标在标准中还存在别称,如byte0的下标0别名为EI_MAG0,具体如下:

名称元素下标值含义
EI_MAG00文件标识
EI_MAG11文件标识
EI_MAG22文件标识
EI_MAG33文件标识
EI_CLASS4文件类
EI_DATA5数据编码
EI_VERSION6文件版本
EI_PAD7补齐字节开始处
EI_NIDENT16e_ident[]大小

e_ident[EI_MAG0]e_ident[EI_MAG3]即e_ident[0]e_ident[3]被称为魔数(Magic Number),其值一般为0x7f,‘E’,‘L’,‘F’。
e_ident[EI_CLASS](即e_ident[4])识别目标文件运行在目标机器的类别,取值可为三种值:ELFCLASSNONE(0)非法类别;ELFCLASS32(1)32位目标;ELFCLASS64(2)64位目标。

e_ident[EI_DATA](即e_ident[5]):给出处理器特定数据的数据编码方式。即大端还是小端方式。取值可为3种:ELFDATANONE(0)非法数据编码;ELFDATA2LSB(1)高位在前;ELFDATA2MSB(2)低位在前。

2 e_type

e_type表示elf文件的类型,如下定义:

名称取值含义
ET_NONE0未知目标文件格式
ET_REL1可重定位文件
ET_EXEC2可执行文件
ET_DYN3共享目标文件
ET_CORE4Core 文件(转储格式)
ET_LOPROC0xff00特定处理器文件
ET_HIPROC0xffff特定处理器文件
ET_LOPROC~ET_HIPROC0xff00~0xffff特定处理器文件

3 e_machine

e_machine表示目标体系结构类型:

名称取值含义
EM_NONE0未指定
EM_M321AT&T WE 32100
EM_SPARC2SPARC
EM_3863Intel 80386
EM_68K4Motorola 68000
EM_88K5Motorola 88000
EM_8607Intel 80860
EM_MIPS8MIPS RS3000
others9~预留

4 e_entry

e_entry表示程序入口地址
这 个sum.o的进入点是0x0(e_entry),这表面Relocatable objects不会有程序进入点。所谓程序进入点是指当程序真正执行起来的时候,其第一条要运行的指令的运行时地址。因为Relocatable objects file只是供再链接而已,所以它不存在进入点。而可执行文件test和动态库.so都存在所谓的进入点,且可执行文件的e_entry指向C库中的_start,而动态库.so中的进入点指向 call_gmon_start。
如上图中e_entry = 0xD8B0,我们用ida打开该文件看到确实是_start()函数的地址。
这里写图片描述

5 常用字段

在ELF Header中我们需要重点关注以下几个字段:
1、e_entry表示程序入口地址

2、e_ehsize:ELF Header结构大小

3、e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、大小、结构。

4、e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、大小、结构。

5、 e_shstrndx:这一项描述的是字符串表在Section Header Table中的索引,值25表示的是Section Header Table中第25项是字符串表(String Table)。

6、编译后比较固定的字段:e_ident 、 e_machine 、e_version 、e_entry 、e_flags 、e_ehsize

7、目前e_ehsize = 52字节,e_shentsize = 40字节,e_phentsize = 32字节,这些值都是固定值,某些加固会修改这些值造成静态解析失败,可以修改回这些固定值

8、整个so的大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1

9、e_shstrndx一般等于e_shnum - 1

10、e_phoff = ELF头的大小

 

节区头表(Section Header Table)

一个ELF文件中到底有哪些具体的 sections,由包含在这个ELF文件中的 section head table(SHT)决定。在SHT中,针对每一个section,都设置有一个条目(entry),用来描述对应的这个section,其内容主要包括该 section 的名称、类型、大小以及在整个ELF文件中的字节偏移位置等等。我们也可以在TISCv1.2规范中找到SHT表中条目的C结构定义:

typedef struct{
    Elf32_Word sh_name;   //节区名,是节区头部字符串表节区(Section Header String Table Section)的索引。名字是一个 NULL 结尾的字符串。
    Elf32_Word sh_type;    //为节区类型
    Elf32_Word sh_flags;    //节区标志
    Elf32_Addr sh_addr;    //如果节区将出现在进程的内存映像中,此成员给出节区的第一个字节应处的位置。否则,此字段为 0。
    Elf32_Off sh_offset;    //此成员的取值给出节区的第一个字节与文件头之间的偏移。
    Elf32_Word sh_size;   //此 成 员 给 出 节 区 的 长 度 ( 字 节 数 )。
    Elf32_Word sh_link;   //此成员给出节区头部表索引链接。其具体的解释依赖于节区类型。
    Elf32_Word sh_info;       //此成员给出附加信息,其解释依赖于节区类型。
    Elf32_Word sh_addralign;    //某些节区带有地址对齐约束.
    Elf32_Word sh_entsize;    //某些节区中包含固定大小的项目,如符号表。对于这类节区,此成员给出每个表项的长度字节数。
}Elf32_Shdr;

1 sh_type

sh_type的取值如下:

名称取值说明
SHT_NULL0此值标志节区头部是非活动的,没有对应的节区。此节区头部中的其他成员取值无意义。
SHT_PROGBITS1此节区包含程序定义的信息,其格式和含义都由程序来解释。
SHT_SYMTAB2此节区包含一个符号表。目前目标文件对每种类型的节区都只能包含一个,不过这个限制将来可能发生变化。一般,SHT_SYMTAB 节区提供用于链接编辑(指 ld 而言)的符号,尽管也可用来实现动态链接。
SHT_STRTAB3此节区包含字符串表。目标文件可能包含多个字符串表节区。
SHT_RELA4此节区包含重定位表项,其中可能会有补齐内容(addend),例如 32 位目标文件中的 Elf32_Rela 类型。目标文件可能拥有多个重定位节区。
SHT_HASH5此节区包含符号哈希表。所有参与动态链接的目标都必须包含一个符号哈希表。目前,一个目标文件只能包含一个哈希表,不过此限制将来可能会解除。
SHT_DYNAMIC6此节区包含动态链接的信息。目前一个目标文件中只能包含一个动态节区,将来可能会取消这一限制。
SHT_NOTE7此节区包含以某种方式来标记文件的信息。
SHT_NOBITS8这 种 类 型 的 节 区 不 占 用 文 件 中 的 空 间 , 其 他 方 面 和SHT_PROGBITS 相似。尽管此节区不包含任何字节,成员sh_offset 中还是会包含概念性的文件偏移
SHT_REL9此节区包含重定位表项,其中没有补齐(addends),例如 32 位目标文件中的 Elf32_rel 类型。目标文件中可以拥有多个重定位节区。
SHT_SHLIB10此节区被保留,不过其语义是未规定的。包含此类型节区的程序与 ABI 不兼容。
SHT_DYNSYM11作为一个完整的符号表,它可能包含很多对动态链接而言不必要的符号。因此,目标文件也可以包含一个 SHT_DYNSYM 节区,其中保存动态链接符号的一个最小集合,以节省空间。
SHT_LOPROC0X70000000这一段(包括两个边界),是保留给处理器专用语义的。
SHT_HIPROCOX7FFFFFFF这一段(包括两个边界),是保留给处理器专用语义的。
SHT_LOUSER0X80000000此值给出保留给应用程序的索引下界。
SHT_HIUSER0X8FFFFFFF此值给出保留给应用程序的索引上界。

2 sh_flag

sh_flag标志着此节区是否可以修改,是否可以执行,如下定义:

名称取值含义
SHF_WRITE0x1节区包含进程执行过程中将可写的数据。
SHF_ALLOC0x2此节区在进程执行过程中占用内存。某些控制节区并不出现于目标文件的内存映像中,对于那些节区,此位应设置为 0。
SHF_EXECINSTR0x4节区包含可执行的机器指令。
SHF_MASKPROC0xF0000000所有包含于此掩码中的四位都用于处理器专用的语义。

3 sh_link 和 sh_info 字段

sh_link和sh_info字段的具体含义依赖于sh_type的值:

sh_typesh_linksh_info
SHT_DYNAMIC此节区中条目所用到的字符串表格的节区头部索引0
SHT_HASH此哈希表所适用的符号表的节区头部索引0
SHT_REL  
SHT_RELA相关符号表的节区头部索引重定位所适用的节区的节区头部索引
SHT_SYMTAB  
SHT_DYNSYM相关联的字符串表的节区头部索引最后一个局部符号(绑定 STB_LOCAL)的符号表索引值加一
其它SHN_UNDEF0

解析android_server 可执行ELF文件,我们可以看到Section Header Table中确实有23(17h)个条目,且索引为22(16h)确实为section header section string table。
这里写图片描述

打开条目,我们可以看到每个entry的具体字段,与上图的Elf32_Shdr结构一致。
这里写图片描述

需要注意的是,sh_name值实际上是.shstrtab中的索引,该string table中存储着所有section的名字。下图中蓝色部分是.shstrtab的数据,我们可以看到,sh_name实际上是从索引1开始的”.shstrtab”字符串,因此这里的sh_name值为1h。
这里写图片描述

 

节区(section)

 

有些节区是系统预订的,一般以点开头号,因此,我们有必要了解一些常用到的系统节区。

名称类型属性含义
.bssSHT_NOBITSSHF_ALLOC +
SHF_WRITE
包含将出现在程序的内存映像中的为初始化数据。根据定义,当程序开始执行,系统将把这些数据初始化为 0。此节区不占用文件空间。
.commentSHT_PROGBITS(无)包含版本控制信息。
.dataSHT_PROGBITSSHF_ALLOC +
SHF_WRITE
这些节区包含初始化了的数据,将出现在程序的内存映像中。
.data1SHT_PROGBITSSHF_ALLOC +
SHF_WRITE
这些节区包含初始化了的数据,将出现在程序的内存映像中。
.debugSHT_PROGBITS(无)此节区包含用于符号调试的信息。
.dynamicSHT_DYNAMIC 此节区包含动态链接信息。节区的属性将包含 SHF_ALLOC 位。是否 SHF_WRITE 位被设置取决于处理器。
.dynstrSHT_STRTABSHF_ALLOC此节区包含用于动态链接的字符串,大多数情况下这些字符串代表了与符号表项相关的名称。
.dynsymSHT_DYNSYMSHF_ALLOC此节区包含了动态链接符号表。
.finiSHT_PROGBITSSHF_ALLOC +
SHF_EXECINSTR
此节区包含了可执行的指令,是进程终止代码的一部分。程序正常退出时,系统将安排执行这里的代码。
.gotSHT_PROGBITS 此节区包含全局偏移表。
.hashSHT_HASHSHF_ALLOC此节区包含了一个符号哈希表。
.initSHT_PROGBITSSHF_ALLOC +
SHF_EXECINSTR
此节区包含了可执行指令,是进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前(通常指 C 语言的 main 函数)执行这些代码。
.interpSHT_PROGBITS 此节区包含程序解释器的路径名。如果程序包含一个可加载的段,段中包含此节区,那么节区的属性将包含 SHF_ALLOC 位,否则该位为 0。
.lineSHT_PROGBITS(无)此节区包含符号调试的行号信息,其中描述了源程序与机器指令之间的对应关系。其内容是未定义的。
.noteSHT_NOTE(无)此节区中包含注释信息,有独立的格式。
.pltSHT_PROGBITS 此节区包含过程链接表(procedure linkage table)。
.relname
.relaname
SHT_REL
SHT_RELA
 这些节区中包含了重定位信息。如果文件中包含可加载的段,段中有重定位内容,节区的属性将包含 SHF_ALLOC 位,否则该位置 0。传统上 name 根据重定位所适用的节区给定。例如 .text 节区的重定位节区名字将是:.rel.text 或者 .rela.text。
.rodata
.rodata1
SHT_PROGBITSSHF_ALLOC这些节区包含只读数据,这些数据通常参与进程映像的不可写段。
.shstrtabSHT_STRTAB 此节区包含节区名称。
.strtabSHT_STRTAB 此节区包含字符串,通常是代表与符号表项相关的名称。如果文件拥有一个可加载的段,段中包含符号串表,节区的属性将包含SHF_ALLOC 位,否则该位为 0。
.symtabSHT_SYMTAB 此节区包含一个符号表。如果文件中包含一个可加载的段,并且该段中包含符号表,那么节区的属性中包含SHF_ALLOC 位,否则该位置为 0。
.textSHT_PROGBITSSHF_ALLOC +
SHF_EXECINSTR
此节区包含程序的可执行指令。

下面我们分析一些so文件中重要的Section,包括
符号表、重定位表、GOT表等。

-符号表(.dynsym)

符号表包含用来定位、重定位程序中符号定义和引用的信息,简单的理解就是符号表记录了该文件中的所有符号,所谓的符号就是经过修饰了的函数名或者变量名,不同的编译器有不同的修饰规则。例如符号_ZL15global_static_a,就是由global_static_a变量名经过修饰而来。

符号表项的格式如下:

typedef struct {  
     Elf32_Word st_name;      //符号表项名称。如果该值非0,则表示符号名的字符串表索引(offset),否则符号表项没有名称。
     Elf32_Addr st_value;       //符号的取值。依赖于具体的上下文,可能是一个绝对值、一个地址等等。
     Elf32_Word st_size;         //符号的尺寸大小。例如一个数据对象的大小是对象中包含的字节数。
     unsigned char st_info;    //符号的类型和绑定属性。
     unsigned char st_other;  //该成员当前包含 0,其含义没有定义。
     Elf32_Half st_shndx;        //每个符号表项都以和其他节区的关系的方式给出定义。此成员给出相关的节区头部表索引。
} Elf32_sym;

下面是通过010Editor解析出的符号表.dynsym的section header表项:
这里写图片描述

再来看一下符号表的具体内容:
这里写图片描述

-字符串表(.dynstr)

上面我们提到,符号表的st_name是符号名的字符串表中的索引,那么字符串表中肯定存放着所有符号的名称字符串。下面,我们先来看一看字符串表的section header表项:
这里写图片描述

再看一下下图中字符串表的具体内容,我们可以看出,.dynstr和.shstrtab结构完全相同,不过一个存储的是符号名称的字符串,而另一个是Section名称的字符串。
这里写图片描述

-重定位表

重定位表在ELF文件中扮演很重要的角色,首先我们得理解重定位的概念,程序从代码到可执行文件这个过程中,要经历编译器,汇编器和链接器对代码的处理。然而编译器和汇编器通常为每个文件创建程序地址从0开始的目标代码,但是几乎没有计算机会允许从地址0加载你的程序。如果一个程序是由多个子程序组成的,那么所有的子程序必需要加载到互不重叠的地址上。_重定位就是为程序不同部分分配加载地址,调整程序中的数据和代码以反映所分配地址的过程。_简单的言之,则是将程序中的各个部分映射到合理的地址上来。
换句话来说,重定位是将符号引用与符号定义进行连接的过程。例如,当程序调用了一个函数时,相关的调用指令必须把控制传输到适当的目标执行地址。
具体来说,就是把符号的value进行重新定位。

可重定位文件必须包含如何修改其节区内容的信息,从而允许可执行文件和共享目标文件保存进程的程序映象的正确信息。这就是重定位表项做的工作。重定位表项的格式如下:

typedef struct {  
    Elf32_Addr r_offset;     //重定位动作所适用的位置(受影响的存储单位的第一个字节的偏移或者虚拟地址)
    Elf32_Word r_info;       //要进行重定位的符号表索引,以及将实施的重定位类型(哪些位需要修改,以及如何计算它们的取值)
                                         //其中 .rel.dyn 重定位类型一般为R_386_GLOB_DAT和R_386_COPY;.rel.plt为R_386_JUMP_SLOT
} Elf32_Rel; 
typedef struct {  
   Elf32_Addr r_offset;  
   Elf32_Word r_info;  
   Elf32_Word r_addend;    //给出一个常量补齐,用来计算将被填充到可重定位字段的数值。
} Elf32_Rela;

对 r_info 成员使用 ELF32_R_TYPE 宏运算可得到重定位类型,使用 ELF32_R_SYM 宏运算可得到符号在符号表里的索引值。 三种宏的具体定义如下:

#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))

再看一下重定位表中的内容。

这里写图片描述

以下是.rel.plt表的具体内容:
这里写图片描述

我们可以看到,每8个字节(s_entsize)一个表项。第一个表项中的r_offset值为0xc7660,r_info为0xa16。其中r_offset指向下图中GOT表中第一项__imp_clock_gettime外部函数地址。那么我们如何利用r_offset值来找到其对应的符号呢?如上所述,进行 ELF32_R_SYM宏运算实际上就是将r_info右移8位,0xa16右移8位得到0xa,因此这就是其在符号表中的索引。

这里写图片描述

从下图中可以看见符号表的s_entsize值为10h,即16个字节每条目。因此我们可以找到其索引为0xa的条目的st_name值为0x9ea。那么怎么证明我们确实找到的是clock_gettime函数的符号呢?我们再来看一下st_name值是不是正确的。

这里写图片描述

st_name值表示的是符号名字符串中的第一个字符在字符串表中的偏移量,因此我们用0x9ea加上符号表的起始位置(0x7548)就能得到该字符串在‭0x7F32位置。如下图所示。
这里写图片描述

-常见的重定位表类型:

  • .rel.text:重定位的地方在.text段内,以offset指定具体要定位位置。在链接时候由链接器完成。.rel.text属于普通重定位辅助段 ,他由编译器编译产生,存在于obj文件内。连接器连接时,他用于最终可执行文件或者动态库的重定位。通过它修改原obj文件的.text段后,合并到最终可执行文件或者动态文件的.text段。其类型一般为R_386_32和R_386_PC32。

  • .rel.dyn:重定位的地方在.got段内。主要是针对外部数据变量符号。例如全局数据。重定位在程序运行时定位,一般是在.init段内。定位过程:获得符号对应value后,根据rel.dyn表中对应的offset,修改.got表对应位置的value。另外,.rel.dyn 含义是指和dyn有关,一般是指在程序运行时候,动态加载。区别于rel.plt,rel.plt是指和plt相关,具体是指在某个函数被调用时候加载。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。

.rel.dyn和.rel.plt是动态定位辅助段。由连接器产生,存在于可执行文件或者动态库文件内。借助这两个辅助段可以动态修改对应.got和.got.plt段,从而实现运行时重定位。

  • .rel.plt:重定位的地方在.got.plt段内(注意也是.got内,具体区分而已)。 主要是针对外部函数符号。一般是函数首次被调用时候重定位。首次调用时会重定位函数地址,把最终函数地址放到.got内,以后读取该.got就直接得到最终函数地址。我个人理解这个Section的作用是,在重定位过程中,动态链接器根据r_offset找到.got对应表项,来完成对.got表项值的修改。

  • .plt段(过程链接表):所有外部函数调用都是经过一个对应桩函数,这些桩函数都在.plt段内。具体调用外部函数过程是:
    调用对应桩函数—>桩函数取出.got表表内地址—>然后跳转到这个地址.如果是第一次,这个跳转地址默认是桩函数本身跳转处地址的下一个指令地址(目的是通过桩函数统一集中取地址和加载地址),后续接着把对应函数的真实地址加载进来放到.got表对应处,同时跳转执行该地址指令.以后桩函数从.got取得地址都是真实函数地址了。
    下图是.plt某表项,它包含了取.got表地址和跳转执行两条指令。
    这里写图片描述

  • .got(全局偏移表)

链接视图下保留的节区有.text(代码段), .data(初始化数据段), .bss(未初始化数据段), .symtab(符号表, 包含用来定位、重定位程序中符号定义和引用的信息), .strtab(字符串表, 包括头部表或程序中字符串的引用映射)等等, 非保留的节区中包括用户自定义的函数等。 用户可以在编译的时候增加选项,根据需求指定这些特殊节区的起始位置、对齐方式等。
值得注意的是, 符号表中的所包含的通常是一些静态变量或者函数的定义位置,但是由于通常文件与文件之间存在函数和变量的调用关系, 在文件进行链接生成可执行文件之前, 符号表结构体中的值是未定义的,必须通过链接器确定函数和变量在输出可执行文件中的确切偏移位置。

上面有一张图,左边是我们的c源码,右边是被编译后的so文件。我们可以从中看到一些对应关系。

首先是文件头,elf文件头的位置是固定的。

后面是.text(代码段),保存我们对C代码编译后的结果。

跟着是 .data(初始化数据段),保存一些程序中已经初始化的变量。

最后是 .bss(未初始化数据段),保存在程序中做了声明,但是却没有初始化,赋值的变量。

于是乎结构就很清晰了。如果我们要改动一个已经初始化的变量,那么,我们直接去找.data段进行修改就行,若要是想对指令进行修改,那自然就要找到.text区了。当然,这里不意味着一个elf只有这么3个节区,只是这3个节区是必须的,在程序上有直接对应关系而已。

这里我们可以使用ida 来查看,打开ida 中的program segment 视图,可以看到程序中远远不止我们上面列举的几个节区。

另外,我们也可以看看elf 的节头表(Section Header table) ,节头表描述的节区信息。

我们使用readelf 工具来查看

E:\android\reverse\tools>readelf -S C:\Users\Administrator\Desktop\learn\hello\l
ibs\armeabi-v7a\libhello-jni

E:\android\reverse\tools>readelf -S C:\Users\Administrator\Desktop\learn\hello\l
ibs\armeabi-v7a\libhello-jni
There are 27 section headers, starting at offset 0x21e4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

  [ 0]                   NULL            00000000 000000 000000 00      0   0  0

  [ 1] .interp           PROGBITS        00000154 000154 000013 00   A  0   0  1

  [ 2] .note.android.ide NOTE            00000168 000168 000098 00   A  0   0  4

  [ 3] .note.gnu.build-i NOTE            00000200 000200 000024 00   A  0   0  4

  [ 4] .dynsym           DYNSYM          00000224 000224 0000f0 10   A  5   1  4

  [ 5] .dynstr           STRTAB          00000314 000314 0000cf 00   A  0   0  1

  [ 6] .hash             HASH            000003e4 0003e4 000050 04   A  4   0  4

  [ 7] .gnu.version      VERSYM          00000434 000434 00001e 02   A  4   0  2

  [ 8] .gnu.version_r    VERNEED         00000454 000454 000020 00   A  5   1  4

  [ 9] .rel.dyn          REL             00000474 000474 000060 08   A  4   0  4

  [10] .rel.plt          REL             000004d4 0004d4 000050 08  AI  4  11  4

  [11] .plt              PROGBITS        00000524 000524 00008c 00  AX  0   0  4

  [12] .text             PROGBITS        000005b0 0005b0 001640 00  AX  0   0  4

  [13] .ARM.extab        PROGBITS        00001bf0 001bf0 00003c 00   A  0   0  4

  [14] .ARM.exidx        ARM_EXIDX       00001c2c 001c2c 000100 08  AL 12   0  4

  [15] .rodata           PROGBITS        00001d30 001d30 00003a 01 AMS  0   0 16

  [16] .fini_array       FINI_ARRAY      00002e64 001e64 000008 00  WA  0   0  4

  [17] .init_array       INIT_ARRAY      00002e6c 001e6c 000010 00  WA  0   0  4

  [18] .preinit_array    PREINIT_ARRAY   00002e7c 001e7c 000008 00  WA  0   0  4

  [19] .dynamic          DYNAMIC         00002e84 001e84 000118 08  WA  5   0  4

  [20] .got              PROGBITS        00002f9c 001f9c 000064 00  WA  0   0  4

  [21] .data             PROGBITS        00003000 002000 000014 00  WA  0   0  1

  [22] .bss              NOBITS          00003014 002014 000018 00  WA  0   0  4

  [23] .comment          PROGBITS        00000000 002014 000065 01  MS  0   0  1

  [24] .note.gnu.gold-ve NOTE            00000000 00207c 00001c 00      0   0  4

  [25] .ARM.attributes   ARM_ATTRIBUTES  00000000 002098 000038 00      0   0  1

  [26] .shstrtab         STRTAB          00000000 0020d0 000112 00      0   0  1

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

E:\android\reverse\tools>

 这里可以看出节区的信息,包括起始地址,结束地址,节区大小等等。

我们可以看下Flg值,其实是权限值。这个权限值会影响段的划分。我们先留意下

 

 

程序头表(Program Header Table)

程序头部(Program Header)描述与程序执行直接相关的目标文件结构信息。用来在文件中定位各个段的映像。同时包含其他一些用来为程序创建映像所必须的信息。
可执行文件或者共享目标文件的程序头部是一个结构数组,每个结构描述了一个段或者系统准备程序执行所必须的其他信息。目标文件的“段”包含一个或者多个“节区”,也就是“段内容(Segment Contents)”。程序头部仅对可执行文件和共享目标文件有意义。

程序头部的数据结构如下:

typedef struct {  
    Elf32_Word p_type;           //此数组元素描述的段的类型,或者如何解释此数组元素的信息。 
    Elf32_Off  p_offset;           //此成员给出从文件头到该段第一个字节的偏移
    Elf32_Addr p_vaddr;         //此成员给出段的第一个字节将被放到内存中的虚拟地址
    Elf32_Addr p_paddr;        //此成员仅用于与物理地址相关的系统中。System V忽略所有应用程序的物理地址信息。
    Elf32_Word p_filesz;         //此成员给出段在文件映像中所占的字节数。可以为0。
    Elf32_Word p_memsz;     //此成员给出段在内存映像中占用的字节数。可以为0。
    Elf32_Word p_flags;         //此成员给出与段相关的标志。
    Elf32_Word p_align;        //此成员给出段在文件中和内存中如何对齐。
} Elf32_phdr;

p_type

名称取值说明
PT_NULL0此数组元素未用。结构中其他成员都是未定义的。
PT_LOAD1此数组元素给出一个可加载的段,段的大小由 p_filesz 和 p_memsz描述。文件中的字节被映射到内存段开始处。如果 p_memsz 大于p_filesz,“剩余”的字节要清零。p_filesz 不能大于 p_memsz。可加载的段在程序头部表格中根据 p_vaddr 成员按升序排列。
PT_DYNAMIC2数组元素给出动态链接信息。
PT_INTERP3数组元素给出一个 NULL 结尾的字符串的位置和长度,该字符串将被当作解释器调用。这种段类型仅对与可执行文件有意义(尽管也可能在共享目标文件上发生)。在一个文件中不能出现一次以上。如果存在这种类型的段,它必须在所有可加载段项目的前面。
PT_NOTE4此数组元素给出附加信息的位置和大小。
PT_SHLIB5此段类型被保留,不过语义未指定。包含这种类型的段的程序与 ABI不符。
PT_PHDR6此类型的数组元素如果存在,则给出了程序头部表自身的大小和位置,既包括在文件中也包括在内存中的信息。此类型的段在文件中不能出现一次以上。并且只有程序头部表是程序的内存映像的一部分时才起作用。如果存在此类型段,则必须在所有可加载段项目的前面。
PT_LOPROC~
PT_HIPROC
0x70000000~
0x7fffffff
此范围的类型保留给处理器专用语义。

简单说程序头(Segment Header Table) ,描述的就是段的相关信息。一个段包含一个或多个节。从程序头表中可以看出这种映射关系。

比如我们用readelf 的-l 参数来看程序头

E:\android\reverse\tools>readelf -l C:\Users\Administrator\Desktop\learn\hello\l
ibs\armeabi-v7a\libhello-jni

这里INTERP 映射第01行(.interp节区),LOAD对应于多个节区(02,03行 .init_array,.dynamic 这些都是)

这里要注意一下,在程序运行的时候,只有LOAD段才会加载到内存,其他段是不会加载到内存的。

也就是这里红框中标注的2行,其中一个load 段有读和执行权限,另一个load 段有读写权限。然后看下面对应节区的映射关系

按照行号,对应的是第2,第3行。

我们可以看到这里段跟节的映射,那到底怎么划分段到节之间的映射关系,跟权限有关,我们找回前面

E:\android\reverse\tools>readelf -S C:\Users\Administrator\Desktop\learn\hello\l
ibs\armeabi-v7a\libhello-jni 

E:\android\reverse\tools>readelf -S C:\Users\Administrator\Desktop\learn\hello\l
ibs\armeabi-v7a\libhello-jni
There are 27 section headers, starting at offset 0x21e4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

  [ 0]                   NULL            00000000 000000 000000 00      0   0  0

  [ 1] .interp           PROGBITS        00000154 000154 000013 00   A  0   0  1

  [ 2] .note.android.ide NOTE            00000168 000168 000098 00   A  0   0  4

  [ 3] .note.gnu.build-i NOTE            00000200 000200 000024 00   A  0   0  4

  [ 4] .dynsym           DYNSYM          00000224 000224 0000f0 10   A  5   1  4

  [ 5] .dynstr           STRTAB          00000314 000314 0000cf 00   A  0   0  1

  [ 6] .hash             HASH            000003e4 0003e4 000050 04   A  4   0  4

  [ 7] .gnu.version      VERSYM          00000434 000434 00001e 02   A  4   0  2

  [ 8] .gnu.version_r    VERNEED         00000454 000454 000020 00   A  5   1  4

  [ 9] .rel.dyn          REL             00000474 000474 000060 08   A  4   0  4

  [10] .rel.plt          REL             000004d4 0004d4 000050 08  AI  4  11  4

  [11] .plt              PROGBITS        00000524 000524 00008c 00  AX  0   0  4

  [12] .text             PROGBITS        000005b0 0005b0 001640 00  AX  0   0  4

  [13] .ARM.extab        PROGBITS        00001bf0 001bf0 00003c 00   A  0   0  4

  [14] .ARM.exidx        ARM_EXIDX       00001c2c 001c2c 000100 08  AL 12   0  4

  [15] .rodata           PROGBITS        00001d30 001d30 00003a 01 AMS  0   0 16

  [16] .fini_array       FINI_ARRAY      00002e64 001e64 000008 00  WA  0   0  4

  [17] .init_array       INIT_ARRAY      00002e6c 001e6c 000010 00  WA  0   0  4

  [18] .preinit_array    PREINIT_ARRAY   00002e7c 001e7c 000008 00  WA  0   0  4

  [19] .dynamic          DYNAMIC         00002e84 001e84 000118 08  WA  5   0  4

  [20] .got              PROGBITS        00002f9c 001f9c 000064 00  WA  0   0  4

  [21] .data             PROGBITS        00003000 002000 000014 00  WA  0   0  1

  [22] .bss              NOBITS          00003014 002014 000018 00  WA  0   0  4

  [23] .comment          PROGBITS        00000000 002014 000065 01  MS  0   0  1

  [24] .note.gnu.gold-ve NOTE            00000000 00207c 00001c 00      0   0  4

  [25] .ARM.attributes   ARM_ATTRIBUTES  00000000 002098 000038 00      0   0  1

  [26] .shstrtab         STRTAB          00000000 0020d0 000112 00      0   0  1

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

E:\android\reverse\tools>

的运行结果看,就可以发现

节[0] 是空的

节[1]~[15] 是权限A,划分到01,02 行,包含INTERP 段和LOAD段

节[16]~[22]是权限w, 划分到03 行,包含 LOAD段

 

 


objdump


objdump 是以一种可阅读的格式让你更多地了解二进制文件带有的信息的工具。这个工具在ndk中有提供。我的在

E:\android\android-ndk-r14b-windows-x86_64\android-ndk-r14b\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-objdump.exe 

这个路径下,版本不同可能略有不同,大致没错就是了。

 objdump 借助BFD, 更加通用一些, 可以应付不同文件格式,它提供反汇编的功能。
由于本项目中所要解析的是在 ARM 平台下可执行的 ELF 文件, gcc 自带的 objdump 工具不支持arm 平台下的编译文件。 因此需要构建 linux 下交叉编译环境, 在基于 ARM 的嵌入式系统开发中,常常用到交叉编译的 GCC 工具链有两种: arm-linux-*和 arm-elf-*,两者区别主要在于使用不同的 C 库文件。 arm-linux-*使用 GNU 的 Glibc,而 arm-elf-*一般使用 uClibc/uC-libc 或者使用REDHAT 专门为嵌入式系统的开发的 C 库 newlib。 两类交叉编译环境的构建比较繁琐, 一般用户可以下载构建好的交叉编译工具,这样用户可以直接使用对应的 arm-linux-objdump 或 armelf-objdump 查看相应 ELF 文件的可读反汇编结果。 使用者可以通过 arm-*-objdump –D *.elf 直接查看反汇编的结果:

我们不加任何参数,可以看到命令的提示。通过参数来指定解析返回的内容。具体参数功能如下:

这里简单介绍下这些参数选项

参数选项:

--archive-headers 
-a 
显示档案库的成员信息,类似ls -l将lib*.a的信息列出。

-b bfdname 
--target=bfdname 
指定目标码格式。这不是必须的,objdump能自动识别许多格式,比如:

objdump -b oasys -m vax -h fu.o 
显示fu.o的头部摘要信息,明确指出该文件是Vax系统下用Oasys编译器生成的目标文件。objdump -i将给出这里可以指定的目标码格式列表。

-C 
--demangle 
将底层的符号名解码成用户级名字,除了去掉所开头的下划线之外,还使得C++函数名以可理解的方式显示出来。

--debugging 
-g 
显示调试信息。企图解析保存在文件中的调试信息并以C语言的语法显示出来。仅仅支持某些类型的调试信息。有些其他的格式被readelf -w支持。

-e 
--debugging-tags 
类似-g选项,但是生成的信息是和ctags工具相兼容的格式。

--disassemble 
-d 
从objfile中反汇编那些特定指令机器码的section。

-D 
--disassemble-all 
与 -d 类似,但反汇编所有section.

--prefix-addresses 
反汇编的时候,显示每一行的完整地址。这是一种比较老的反汇编格式。

-EB 
-EL 
--endian={big|little} 
指定目标文件的小端。这个项将影响反汇编出来的指令。在反汇编的文件没描述小端信息的时候用。例如S-records.

-f 
--file-headers 
显示objfile中每个文件的整体头部摘要信息。

-h 
--section-headers 
--headers 
显示目标文件各个section的头部摘要信息。

-H 
--help 
简短的帮助信息。

-i 
--info 
显示对于 -b 或者 -m 选项可用的架构和目标格式列表。

-j name
--section=name 
仅仅显示指定名称为name的section的信息

-l
--line-numbers 
用文件名和行号标注相应的目标代码,仅仅和-d、-D或者-r一起使用使用-ld和使用-d的区别不是很大,在源码级调试的时候有用,要求编译时使用了-g之类的调试编译选项。

-m machine 
--architecture=machine 
指定反汇编目标文件时使用的架构,当待反汇编文件本身没描述架构信息的时候(比如S-records),这个选项很有用。可以用-i选项列出这里能够指定的架构.

--reloc 
-r 
显示文件的重定位入口。如果和-d或者-D一起使用,重定位部分以反汇编后的格式显示出来。

--dynamic-reloc 
-R 
显示文件的动态重定位入口,仅仅对于动态目标文件意义,比如某些共享库。

-s 
--full-contents 
显示指定section的完整内容。默认所有的非空section都会被显示。

-S 
--source 
尽可能反汇编出源代码,尤其当编译的时候指定了-g这种调试参数时,效果比较明显。隐含了-d参数。

--show-raw-insn 
反汇编的时候,显示每条汇编指令对应的机器码,如不指定--prefix-addresses,这将是缺省选项。

--no-show-raw-insn 
反汇编时,不显示汇编指令的机器码,如不指定--prefix-addresses,这将是缺省选项。

--start-address=address 
从指定地址开始显示数据,该选项影响-d、-r和-s选项的输出。

--stop-address=address 
显示数据直到指定地址为止,该项影响-d、-r和-s选项的输出。

-t 
--syms 
显示文件的符号表入口。类似于nm -s提供的信息

-T 
--dynamic-syms 
显示文件的动态符号表入口,仅仅对动态目标文件意义,比如某些共享库。它显示的信息类似于 nm -D|--dynamic 显示的信息。

-V 
--version 
版本信息

--all-headers 
-x 
显示所可用的头信息,包括符号表、重定位入口。-x 等价于-a -f -h -r -t 同时指定。

-z 
--disassemble-zeroes 
一般反汇编输出将省略大块的零,该选项使得这些零块也被反汇编。

(参数内容 摘自 https://www.cnblogs.com/tongongV/p/10745040.html )

 

上面解释了一些参数下面来一些示例,比如:

E:\android\reverse\tools>objdump -f C:\Users\Administrator\Desktop\learn\arm_assembly\demo1

这里就是查看文件头信息

这里显示的是文件头信息,我们可以看到入口地址,0x418,可以使用ida对比下是不是一致。

再比如:

我们使用 -p参数

objdump -p C:\Users\Administrator\Desktop\learn\hello\obj\local\armeabi-v7a\libhello-jni
 

E:\android\reverse\tools>objdump -p C:\Users\Administrator\Desktop\learn\hello\o
bj\local\armeabi-v7a\libhello-jni

C:\Users\Administrator\Desktop\learn\hello\obj\local\armeabi-v7a\libhello-jni:
   file format elf32-littlearm

Program Header:
    PHDR off    0x00000034 vaddr 0x00000034 paddr 0x00000034 align 2**2
         filesz 0x00000120 memsz 0x00000120 flags r--
  INTERP off    0x00000154 vaddr 0x00000154 paddr 0x00000154 align 2**0
         filesz 0x00000013 memsz 0x00000013 flags r--
    LOAD off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**12
         filesz 0x00001d6a memsz 0x00001d6a flags r-x
    LOAD off    0x00001e64 vaddr 0x00002e64 paddr 0x00002e64 align 2**12
         filesz 0x000001b0 memsz 0x000001c8 flags rw-
 DYNAMIC off    0x00001e84 vaddr 0x00002e84 paddr 0x00002e84 align 2**2
         filesz 0x00000118 memsz 0x00000118 flags rw-
    NOTE off    0x00000168 vaddr 0x00000168 paddr 0x00000168 align 2**2
         filesz 0x000000bc memsz 0x000000bc flags r--
   STACK off    0x00000000 vaddr 0x00000000 paddr 0x00000000 align 2**0
         filesz 0x00000000 memsz 0x00000000 flags rw-
0x70000001 off    0x00001c2c vaddr 0x00001c2c paddr 0x00001c2c align 2**2
         filesz 0x00000100 memsz 0x00000100 flags r--
   RELRO off    0x00001e64 vaddr 0x00002e64 paddr 0x00002e64 align 2**2
         filesz 0x0000019c memsz 0x0000019c flags rw-

Dynamic Section:
  PLTGOT               0x00002fcc
  PLTRELSZ             0x00000050
  JMPREL               0x000004d4
  PLTREL               0x00000011
  REL                  0x00000474
  RELSZ                0x00000060
  RELENT               0x00000008
  RELCOUNT             0x0000000a
  DEBUG                0x00000000
  SYMTAB               0x00000224
  SYMENT               0x00000010
  STRTAB               0x00000314
  STRSZ                0x000000cf
  HASH                 0x000003e4
  NEEDED               libc.so
  NEEDED               libm.so
  NEEDED               libstdc++.so
  NEEDED               libdl.so
  FINI_ARRAY           0x00002e64
  FINI_ARRAYSZ         0x00000008
  INIT_ARRAY           0x00002e6c
  INIT_ARRAYSZ         0x00000010
  PREINIT_ARRAY        0x00002e7c
  PREINIT_ARRAYSZ      0x00000008
  FLAGS                0x00000008
  FLAGS_1              0x00000001
  VERSYM               0x00000434
  VERNEED              0x00000454
  VERNEEDNUM           0x00000001

Version References:
  required from libc.so:
    0x00050d63 0x00 02 LIBC
private flags = 5000200: [Version5 EABI] [soft-float ABI]

这里我们注意到在动态节区中包含了几个so

 NEEDED               libc.so
 NEEDED               libm.so
 NEEDED               libstdc++.so
 NEEDED               libdl.so

这些so其实包含了我们程序中使用到的一些函数。这些函数并不一定是我们自己写的,也可能是系统带的一些api,比如一些str 的操作,输入输出库函数等。我们可以使用ida 打开这个elf 文件,然后查看导入函数列表

明显的会有一些函数是系统的api。比如这里的strcmp 就是字符串的比较函数,这个函数并不是我们实现的。通过查找我们可以发现这个函数在so 的plt 段中发现了这个函数的声明。当然这个函数还调用了一个 叫 __imp_strcmp 的更底层的函数。我们可以从__ (两个下划线)看出来

__imp_strcmp 这个更加底层的函数,在静态分析中我们找不到它的具体实现,无法继续深入下去分析。因为它不是在这个so中实现的,这就很可能在前面我们提到的其他几个引用到的so中。比如:

 NEEDED               libc.so
 NEEDED               libm.so
 NEEDED               libstdc++.so
 NEEDED               libdl.so

这些so中,这个想法我们可以后面使用动态调试的方法来验证。因为动态调试我们是可以跟进去看下它的具体实现的。

最后再介绍一个 -S 选项,这是尽可能反汇编出源码

E:\android\reverse\tools>objdump -S C:\Users\Administrator\Desktop\learn\arm_ass
embly\demo1 > C:\Users\Administrator\Desktop\test.txt

我们可以使用这个命令来显示所有内容,因为内容较多所有后面加上" > file.txt "其实就是将输出保存到文件中的意思,这一招很经常用,尤其是需要保存返回数据或者是返回内容较多的时候。

另外,还有-s参数也是很常会使用到的。这是显示section的完整内容

E:\android\reverse\tools>objdump -s C:\Users\Administrator\Desktop\learn\arm_ass
embly\demo1 > C:\Users\Administrator\Desktop\test.txt

保存的文件内容如下所示,跟我们在ida 中看到的差不多。

 

readelf

这里我们介绍下一款elf 解析工具,同样的这个工具也在ndk中可以找到,我呢

E:\android\android-ndk-r14b-windows-x86_64\android-ndk-r14b\toolchains\arm-linux-androideabi-4.9\prebuilt\windows-x86_64\bin\arm-linux-androideabi-readelf.exe

在ndk中,路径如上。

elf 工具不借助BFD,而是直接兑取ELF格式文件的信息,得到的结果也会更加细致一些。

下面是readelf 的一些参数

E:\android\reverse\tools>readelf
Usage: readelf <option(s)> elf-file(s)
 Display information about the contents of ELF format files
 Options are:
  -a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
  -h --file-header       Display the ELF file header
  -l --program-headers   Display the program headers
     --segments          An alias for --program-headers
  -S --section-headers   Display the sections' header
     --sections          An alias for --section-headers
  -g --section-groups    Display the section groups
  -t --section-details   Display the section details
  -e --headers           Equivalent to: -h -l -S
  -s --syms              Display the symbol table
     --symbols           An alias for --syms
  --dyn-syms             Display the dynamic symbol table
  -n --notes             Display the core notes (if present)
  -r --relocs            Display the relocations (if present)
  -u --unwind            Display the unwind info (if present)
  -d --dynamic           Display the dynamic section (if present)
  -V --version-info      Display the version sections (if present)
  -A --arch-specific     Display architecture specific information (if any)
  -c --archive-index     Display the symbol/file index in an archive
  -D --use-dynamic       Use the dynamic section info when displaying symbols
  -x --hex-dump=<number|name>
                         Dump the contents of section <number|name> as bytes
  -p --string-dump=<number|name>
                         Dump the contents of section <number|name> as strings
  -R --relocated-dump=<number|name>
                         Dump the contents of section <number|name> as relocated
 bytes
  -w[lLiaprmfFsoRt] or
  --debug-dump[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=fr
ames,
               =frames-interp,=str,=loc,=Ranges,=pubtypes,
               =gdb_index,=trace_info,=trace_abbrev,=trace_aranges,
               =addr,=cu_index]
                         Display the contents of DWARF2 debug sections
  --dwarf-depth=N        Do not display DIEs at depth N or greater
  --dwarf-start=N        Display DIEs starting with N, at the same depth
                         or deeper
  -I --histogram         Display histogram of bucket list lengths
  -W --wide              Allow output width to exceed 80 characters
  @<file>                Read options from <file>
  -H --help              Display this information
  -v --version           Display the version number of readelf

E:\android\reverse\tools>

这个工具的使用示范在前面已经说过了,这里就不重复提及。

另外,还推荐一款工具,叫

010Editor

这个工具可以用来直接编辑2进制,当然很多其他工具比如带上HEX插件的notpad++ ,winhex 都是可以的。

这里推荐这款010Editor 是因为有一个Elf 插件,使用插件可以更好的查看我们的elf 结构。

 

 

参考:

https://blog.youkuaiyun.com/mergerly/article/details/94585901

### ELF 文件格式解析 ELF(Executable and Linkable Format)是一种常见的文件格式,广泛用于 Unix 和类 Unix 系统中。它不仅被用来存储可执行文件,还常用于目标文件和共享库。 #### 使用 `hexdump` 查看特定部分的数据 可以通过命令行工具 `hexdump` 来查看 ELF 文件的某些区域数据。例如,如果要查看从某个偏移量开始的一段数据,可以使用以下命令: ```bash hexdump -s 偏移地址 -n 数据长度 -C 文件名 ``` 这会以十六进制和ASCII的形式展示指定范围内的内容[^1]。 #### 实现简单的 ELF 文件解析器 为了模仿 Linux 下的 `readelf` 工具的功能,可以从头开始编写一个基本的解析器来处理 `.text`, `.data`, 或者符号表等内容。这种解析器应至少支持 `-h` (头部信息), `-S` (节区信息), 和 `-s` (符号表) 这些选项[^2]。 下面是一个 Python 脚本的例子,演示如何读取并打印 ELF 头部的一些基本信息: ```python import struct def read_elf_header(file_path): with open(file_path, 'rb') as f: # 读取前几个字节判断是否为 ELF 文件 ident = f.read(16) if not ident.startswith(b'\x7fELF'): raise ValueError('Not an ELF file') ei_class = ord(ident[4:5]) is_32bit = True if ei_class == 1 else False format_str = '<HHIIQQQI' if not is_32bit else '<HHIIIII' header_data = f.read(struct.calcsize(format_str)) elf_header = list(struct.unpack(format_str, header_data)) print(f"Type: {elf_header[0]}") print(f"Machine architecture: {elf_header[1]}") print(f"Entry point address: {'%#x' % elf_header[2]}") if __name__ == "__main__": import sys try: read_elf_header(sys.argv[1]) except IndexError: print("Usage: python script.py <path_to_elf>") ``` 此脚本仅作为基础示例提供,实际应用可能需要考虑更多细节以及错误处理逻辑。 #### 利用现有工具进行深入分析 除了自己开发解析器外,还可以利用现有的多种工具来进行更加详细的分析工作。比如通过运行 `readelf -h` 可获得关于整个 ELF 文件结构的重要概览;而像 `objdump`, `nm` 等其他工具也能补充不同方面的洞察力[^4]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值