ELF文件格式解析


参考资料

  上边的资料对ELF文件格式的分析非常详细,这边主要记录一下自己感觉比较重要和掌握的不是很清楚的部分,增加一些自己的理解,也方便后续查阅。

ELF文件整体组成

  在《Android软件安全权威指南》中提到一个ELF文件可以按内容划分为三部分,将Sections归到了Program Header TableSection Header Table中,这边我更倾向于将Sections独立出来,于是ELF被分为如下四部分:

  • ELF Header:文件头
  • Program Header Table:程序头表,包含多个Program Header
  • Sections:程序节区,ELF文件的主要内容存放与此
  • Section Header Table:节区头表,包含多个Section Header

ELF文件

  如图展示了ELF文件四个部分的空间上的组成,同时展示了ELF的两个视图:链接视图和执行视图。链接视图只在链接中起作用,而执行视图只在加载并执行时起作用。

  为什么需要区分两种不同视图?(即为什么要区分段和节?)内存分配和权限管理以页为单位,一节太小浪费空间,所以把相同权限的节放到一起管理。可以减少页面内部的碎片,节省了空间,显著提高内存利用率。另外将Section Header放到Sections下面的好处是,程序在运行时只需加载整个ELF文件的上面三个部分即可,而最后的Section Header可以忽略。不过即便如此,经过实测发现在实际加载过程中还是会把Section Header给加载进去。下面会对每个组成部分来单独讲解,最好配合010Editor打开一个so文件对照着看。

ELF Header

  所有ELF文件首先都以一个64字节大小的ELF头作为开头,其包含内容可以用readelf -h xxx.so或者拖入010Editor中用模板查看,如下图所示:
ELF文件头 010Editor
ELF文件头 Readelf

其中比较重要的就是它记录了Program Header Table以及Section Header Table的起始位置、数量和大小,还有Section Header String Table的index,这个Section Header String Table马上就会讲到,详情见Section Header Table,这几个可能在逆向和防护上用得上,其他的用处不大。

所以综上所述,整个SO文件的大小= e_shoff + e_shnum * sizeof(e_shentsize) + 1

有兴趣可以参考这篇文章来了解更多:ELF文件格式解析,下面很多部分都借鉴了这篇文章的内容。

Section Header Table

  为方便理解还是先讲Section Header Table在讲Program Header Table,同理对于SectionSegment的概念也是先讲前者,因为在了解Section Header Table之前,位于Section Header TableProgram Header Table中间的一大片数据都是Sections,所以首先我们要了解什么是Section Header Table

  Section Header Table顾名思义,是由一个个Section Header组成的Table,其第一个Section Header的地址可以在ELF Header中找到。每一个都有一下十个属性

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;

sh_name & String Table

  首先由于每个Section Header的大小都是固定的且相等的,所以sh_name就不能直接硬编码写在Header里,所以这里的sh_name其实是一个偏移,它的基址是Section Header String Table的起始地址。既然讲到这个Section Header String Table,就先解释一下它是啥。在ELF Header的部分就提到了这个玩意,ELF Header记录了这个Section Header String Table在所有Sections中的index,也就是说它其实也是众多Section中的其中一个,只不过它比较特殊所以ELF Header给他特殊标记出来了,它内部记录了一些字符串,所以这个节的sh_type值为SHT_STRTAB,代表它是一个字符串表。当然一个ELF文件中可能不止一个字符串表,但是由于这个节被ELF Header明确标记为Section Header String Table,说明它是一个专门给所有Section记录名字的字符串表。另外还有一个重要的字符串表是.dynsym它的sh_type值也为SHT_STRTAB但是它的作用不一样,之后再讲。

sh_type

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

关于每个类型不会一一详细介绍,在Section部分会挑出几个重要的单独介绍。

sh_flag

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

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

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

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

Section

字符串表与符号表

  一个ELF文件中包含三张字符串表

  • .dynstr
  • .shstrtab
  • .strtab

.shstrtab

  .shstrtab节之前已经提过了,是用来记录每个Section的名称的,每个Section Header Tablesh_name字段记录的就是这个Section的名称在.shstrtab节中的偏移。

.dynstr

  .dynstr.shstrtab一样存放的都是字符串,只不过字符串的用途不一样,后者是每个Section的名称,而前者则是程序中符号定义和引用的名称,通俗讲就是函数名和变量名,还有依赖的其他So的名称等。而访问这个表的则是另一个节区.dynsym

.dynsym

dynsym内容如下

dynsym1

乍一看也是莫名其妙不知所云,其实.dynsym也是像Section Header Table一样有自己的组成格式

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里解析完成后这部分的内容就是dynamic_symbol_table
dynsym2

其中的symname值就是在.dynstr的偏移了。

.strtab

  .strtab表里面存放的是ELF中引用的字符串信息,例如ELF中导出的函数名、编译器添加的调试符号名称与源代码文件名等。暂略。

.dynamic节区

  由于.dynamic节区在010Editor中不会和其他Header Table一样有模板来解析各个组成部分的含义,而是只能看到如下图中的内容,因此需要人工的理解一下各个部分的意义。参考elf文件类型六 Dynamic Section(动态section),文中介绍了.dynamic节区的数据结构和特定值的含义

typedef struct {
      Elf32_Sword d_tag;
      union {
          Elf32_Sword d_val;
          Elf32_Addr d_ptr;
      } d_un;
  } Elf32_Dyn;

  只有两个属性d_tagd_un,其中d_un的值的含义取决于d_tag的值,不同情况下含义不同,可能是代表size,也可能是offset等等,offset也分不同情况,可能是基于ELF文件基址的offset,也可能是基于字符串表地址的offset。如图是32位ELF的.dynamic节区,所以一个条目是d_tag(四字节) + d_un(四字节)共八字节。若在64位下则是一共十六字节。由于使用了union共用体所以d_vald_ptr两者在不同情况下只有最多其中一个有意义。

dynamic section

d_tag

以下的表格总结了对可执行和共享object文件需要的tag。假如tag被标为mandatory,ABI-conforming文件的动态连接数组必须有一个那样的入口。同样的,optional意味着一个可能出现tag的入口,但是不是必须的。

Name Value d_un Executable Shared Object description
DT_NULL 0 ignored mandatory mandatory 一个DT_NULL标记的入口表示了_DYNAMIC数组的结束。
DT_NEEDED 1 d_val optional optional 这个元素保存着以NULL结尾的字符串表的偏移量,那些字符串是所需库的名字。该偏移量是以DT_STRTAB 为入口的表的索引。
DT_PLTRELSZ 2 d_val optional optional 该元素保存着跟PLT关联的重定位入口的总共字节大小。假如一个入口类型DT_JMPREL存在,那么DT_PLTRELSZ也必须存在。
DT_PLTGOT 3 d_ptr optional optional 该元素保存着跟PLT关联的地址和(或者)是GOT。
DT_HASH 4 d_ptr mandatory mandatory 该元素保存着符号哈希表的地址,该哈希表指向被DT_SYMTAB元素引用的符号表。
DT_STRTAB 5 d_ptr mandatory mandatory 该元素保存着字符串表地址,包括了符号名,库名,和一些其他的在该表中的字符串。
DT_SYMTAB 6 d_ptr mandatory mandatory 该元素保存着符号表的地址,对32-bit类型的文件来说,关联着一个Elf32_Sym入口。
DT_RELA 7 d_ptr mandatory optional 该元素保存着重定位表的地址,一个object文件可能好多个重定位section。当为一个可执行和共享文件建立重定位表的时候,连接编辑器连接 那些section到一个单一的表。尽管在object文件中那些section是保持独立的。动态连接器只看成是一个简单的表。当动态连接器为一个可执行文件创建一个进程映象或者是加一个共享object到进程映象中,它读重定位表和执行相关的动作。假如该元素存在,动态结构必须也要有DT_RELASZ和DT_RELAENT元素。当文件的重定位是mandatory,DT_RELA 或者DT_REL可能出现(同时出现是允许的,但是不必要的)。
DT_RELASZ 8 d_val mandatory optional 该元素保存着DT_RELA重定位表总的字节大小。
DT_RELAENT 9 d_val mandatory optional 该元素保存着DT_RELA重定位入口的字节大小。
DT_STRSZ 10 d_val mandatory mandatory 该元素保存着字符串表的字节大小。
DT_SYMENT 11 d_val mandatory mandatory 该元素保存着符号表入口的字节大小。
DT_INIT 12 d_ptr optional optional 该元素保存着初始化函数的地址。
DT_FINI 13 d_ptr optional optional 该元素保存着终止函数的地址。
DT_SONAME 14 d_val ignored optional 该元素保存着以NULL结尾的字符串的字符串表偏移量,那些名字是共享object的名字。偏移量是在DT_STRTAB入口记录的表的索引。
DT_RPATH 15 d_val optional ignored 该元素保存着以NULL结尾的搜索库的搜索目录字符串的字符串表偏移量。
DT_SYMBOLIC 16 ignored ignored optional 在共享object库中出现的该元素为在库中的引用改变动态连接器符号解析的算法。替代在可执行文件中的符号搜索,动态连接器从它自己的共享object开始。假如一个共享的object提供引用参考失败,那么动态连接器再照常的搜索可执行文件和其他的共享object。
DT_REL 17 d_ptr mandatory optional 该元素相似于DT_RELA。假如这个元素存在,它的动态结构必须也同时要有DT_RELSZ和DT_RELENT的元素。
DT_RELSZ 18 d_val mandatory optional 该元素保存着DT_REL重定位表的总字节大小。
DT_RELENT 19 d_val mandatory optional 该元素保存着DT_RELENT重定为入口的字节大小。
DT_PLTREL 20 d_val optional optional 该成员指明了PLT指向的重定位入口的类型。适当地,d_val成员保存着 DT_REL或DT_RELA。在一个PLT中的所有重定位必须使用相同的转换。
DT_DEBUG 21 d_ptr optional ignored 该成员被调试使用。它的内容没有被ABI指定;访问该入口的程序不是ABI-conforming的。
DT_TEXTREL 22 ignored optional optional 如在程序头表中段许可所指出的那样,这个成员的缺乏代表没有重置入口会引起非写段的修改。假如该成员存在,一个或多个重定位入口可能请求修改一个非写段,并且动态连接器能因此有准备。
DT_JMPREL 23 d_ptr optional optional 假如存在,它的入口d_ptr成员保存着重定位入口(该入口单独关联着PLT)的地址。假如lazy方式打开,那么分离它们的重定位入口让动态连接器在进程初始化时忽略它们。假如该入口存在,相关联的类型入口DT_PLTRELSZ和DT_PLTREL一定要存在。
DT_LOPROC 0x70000000 unspecified unspecified unspecified 在该范围内的变量为特殊的处理器语义保留。
DT_HIPROC 0x7fffffff unspecified unspecified unspecified 在该范围内的变量为特殊的处理器语义保留。

暂时总结这么多,这部分内容可能要单独一篇文章。

重定位表

水很深,另开新坑介绍

Program Header Table

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

ELF加载

  经过测试,ELF文件被加载时会每个LOAD段的记录将文件进行加载,具体来说,就是将ELF文件从LOAD段的p_offset位置读取p_filesz大小的数据,映射到内存中ELF基址+p_vaddr偏移的位置(描述可能不够准确)。会被加载的就只有LOAD段而已,不要误解以为其他类型的段也是从文件中加载进去的,实际上其他类型的段只是将被加载到内存中的LOAD段中的某些重要数据单独指出来而已。

program_header_table

  如上图所示,DYNAMIC段的内存范围实际上是位于第二个LOAD段内部的。其他的非LOAD段也是同样的道理。

  然后上图的LOAD有两个主要是为了将相同权限的节合并到一起管理,这些节在文件中的位置基本上也是按照顺序排下来的,可以具体某一个节映射到哪个位置则是看Section Header Table中的sh_addr值。这边可以看下一般情况下第一个Section Header应该是空的,原因不明。第二个Section Header开始就有用了,从上图可以知道我这边的情况,第一个Header应该是.note.gnu.build-id,然后它的sh_addr必定是0x200,因为在第一个节往上的空间被ELF HeaderProgram Header给占据了他们的大小是0x40+0x1C0=0x200

  此外,值得注意的是ELF加载时并没有读取所有ELF文件中的内容去加载到内存中,实际上我这个ELF文件还有个.comment.shstrtab节,并没有在上图中看到被加载到哪个段了,原因是这个节的sh_addr0x0000。而且Section Header Table也不会被加载到内存中。所以其实可以把这两个节给删除,完全不影响加载。

一些重要结论

如何定位.dynstr

  通过Program Header Table定位,首先找到dynamic段,然后读取遍历dynamic段的条目,找到DT_STRTAB,后边的值即为.dynstr的偏移。如图

dynamic_segment

init函数定位

so 介绍
IDA调试android so的.init_array数组

ELF删除或添加一个Section

以删除.comment为例

  1. 直接删除
  2. 修复受影响的节的偏移(shstrtab应该会受影响,要求修改对应Header中的偏移值)
  3. 将Section Header Table以16字节对齐
  4. 删除Section Header Table中的.comment节的Header
  5. 修改ELF Header中Section Header Table的偏移,并将Header数量以及shstrtab的index都-1

文章作者: 大A
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 大A !
评论
  目录