ELF解析04 - 字符串表/导入库表/符号表/导入表

本文主要讨论 Dynamic Segment 里面的几个子表。

总体结构介绍

我们先总体看下 Dynamic Segment :

这里我们只需要关心图中两个画圆圈的地方。

第一个是虚拟地址

由于 Dynamic Segment 的子表内容都在可加载段的范围里面,所以对这些子表的访问都是在虚拟内存里面进行的。

p_offset_FROM_FILE_BEGIN 这个值我们可以自己静态分析使用,但是解析的时候不要用这个地址。包括 p_filesz_SEGMENT_FILE_LENGTH  p_memsz_SEGMENT_RAM_LENGTH,这些值都没用。

有一个需要注意的地方,就是p_offset_FROM_FILE_BEGIN 的值要比 p_vaddr_VIRTUAL_ADDRESS 的值要大一个 PAGE 值。这是为啥呢?

是因为第二个可加载段的映射偏差:

可以看到在第二个可加载段在做内存映射的时候就有一个 PAGE 值的偏差,由于只要是放在第二个段里面的内容,就会有一个 PAGE 的偏差。

第二个是子表

第二个画圈的地方说明了,子表有 39 项。但是实际上最后几项都是 NULL:

所以解析子表的时候,不会去先计算子表的大小,然后用 大小/sizeof(xxx) 这样的方式,而是直接从头开始解析,直到遇到 DT_NULL。

子表的结构体是 struct Elf64_Dyn :

typedef struct
{
  Elf64_Sxword d_tag;   /* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;  /* Integer value */
      Elf64_Addr d_ptr;   /* Address value */
    } d_un;
} Elf64_Dyn;

一共16个字节,前8个字节表示类型,后8个字节表示地址(注意表示的是虚拟地址)或者整数值。

我们先研究字符串表,其 type 为 5:

字符串表

我们找到字符串表的虚拟地址位置 0x2328,它是在第一个段里面。其实这个地址也能在 section header 里面找到:

只不过 section header 里面的这个地址容易被篡改而已。

根据第一个段的mmap映射关系,我们知道,其文件位置也是 0x2328,我们跳过去看看:

明显看出,这里确实是字符串表。

这个表要如何访问呢?使用相对于字符串表的偏移。一个访问规律就是尽可能使用较小的相对偏移,我猜测是为了减少数据大小。像数组表,就使用数组索引来访问,而字符串元素大小不固定,所以就使用偏移。

我们以导入库表为例,顺便介绍对字符串表的访问。

导入库表

在子表里面,type 为 1 的,就是导入库表。这里有8项,说明这个 ELF 依赖了其他8个 so。以第一项为例:

这里的 d_ptr 的值是 0xD1B,这个值就是字符串表的相对偏移,我们算一下其真实虚拟地址:

0xD1B + 0x2328 = 0x3043

可以看到,其 so 的名字为 liblog.so:

符号表

对于符号表来说,就是描述的一个映射关系:一个符号对应着一个地址。

符号表 type 为 6:

其数据结构为:

typedef struct
{
  Elf64_Word st_name;  /* Symbol name (string tbl index) */
  unsigned char st_info;  /* Symbol type and binding */
  unsigned char st_other;  /* Symbol visibility */
  Elf64_Section st_shndx;  /* Section index */
  Elf64_Addr st_value;  /* Symbol value */
  Elf64_Xword st_size;  /* Symbol size */
} Elf64_Sym;

里面的字段就不解释了,我还记得之前的笔记里面做过详细的解释,但是现在也忘记的差不过了,只要有个印象就行,后面真的遇到了再回过头看或者查资料。

该数据结构一共占24个字节:

typedef struct {
 Elf64_Word st_name; // 4 B (B for bytes)
 unsigned char st_info; // 1 B
 unsigned char st_other; // 1 B
 Elf64_Half st_shndx; // 2 B
 Elf64_Addr st_value; // 8 B
 Elf64_Xword st_size; // 8 B
} Elf64_Sym; // total size = 24 B

符号表的第一项全是0值,我们不管。

我们以linker中的某一项为例:

前4个字节表示字符串的偏移,值为 rtld_db_dlactivity 。再看其 st_value,是 0232D8,到 IDA 中看看这个地址里面是什么东西:

我们发现,字符串名字与 IDA 中导出符号的名字是一样的。

但是,同一个地址可以对应多个不同的字符串,地址可以有多个名字,不是一对一的关系。

ELF文件的最后也详细的解释了这个动态符号表:

重定位表(rela.plt)

重定位表起始就是对需要重定位符号的描述:

  • 其真实储存地址

  • 其类型与符号索引

  • attend

其 type 为 23。

请参见过程链接表(特定于处理器)。此元素要求同时存在 DT_PLTRELSZ 和 DT_PLTREL 元素。

  • DT_PLTRELSZ:PLT类型重定位的大小

  • DT_PLTREL:指明PLT重定位的类型,这一项的value就高级了,value的值是DT_REL(17)或DT_RELA(7)宏的值。

其结构体如下,24个字节:

typedef struct
{
  Elf64_Addr r_offset;  /* Address */
  Elf64_Xword r_info;   /* Relocation type and symbol index */
  Elf64_Sxword r_addend;  /* Addend */
} Elf64_Rela;

从 r_info 的解释可知,重定位表里面的符号集合是符号表的子表。

从 section 里面还可以看出,它是对应的这个:

跳转到 9C10 位置,分析第一项:

第一项是地址,我们去 IDA 里面看+7一下:

.got.plt:000000000005F658 68 59 06 00 00 00 00 00       off_5F658 DCQ __libc_init               ; DATA XREF: .__libc_init↑o
.got.plt:000000000005F658                                                                       ; .__libc_init+4↑r
.got.plt:000000000005F658                                                                       ; .__libc_init+8↑o

这个地址里面储存的值是68 59 06 00 00 00 00 00 ,而这个值代表了一个函数(__libc_init)的地址,当然现在是没有值的:

extern:0000000000065968 00 00 00 00 00 00 00 00       IMPORT __libc_init                      ; CODE XREF: .__libc_init+C↑j
extern:0000000000065968                                                                       ; DATA XREF: .got.plt:off_5F658↑o

需要等程序链接后,将 __libc_init 的真实地址给填入到5F658的位置,这个过程其实就是 plt 的过程。

后面的8个字节分为两部分:

  • 0402,这个是重定位类型,具体为 #define R_AARCH64_JUMP_SLOT 1026,详细看源码:https://cs.android.com/android/platform/superproject/main/+/main:external/musl/include/elf.h;l=2527?q=R_ARM_JUMP_SLOT&sq=&ss=android

  • 01 ,这个是符号表的索引,我们找到符号表,然后看其第一项

这就是说,对于导入表,linker 需要先加载该 ELF 所依赖的 so,然后再找到这个符号,最后将这个符号的真实地址填入到 5F658这个位置。

有一个地方需要注意,就是linker在寻找符号的时候,它不知道这个符号存放在哪个so里面。我们可以从 IDA 中得到验证:

这里的 Library 写的是 .dynsym,但是屁用没有。

所以,当两个 so 出现了相同的符号时,那么就以先找到的为准,可以理解为 linker 会去遍历所依赖的 so,在里面找导出符号,找到匹配的了就直接返回。

其实这个过程,看过 csapp 的应该好理解,而且 010 中体现的也很明显:

由于 0x05F658 的大小已经超过了第一个可加载段的长度,所以这个地址在第二个可加载段中,算一下偏移,它在文件中的地址为 0x05E658 处:

这里有很多的 F0 B8 值,这因为 plt 懒加载的原因,plt 项全部都指向一个特定的地址,然后从这个地址开始执行,解析符号,得到地址后,再回填到这个地址,后面就不用在走这个过程了。

最后

由于64位 so 的重定位类型相当之多,所以想模拟一下程序的加载还是很有难度的,需要写很多个 switch case,暂时就不写了。我贴一下 linker 源码:

case R_ARM_JUMP_SLOT:
    COUNT_RELOC(RELOC_ABSOLUTE);
    MARK(rel->r_offset);
    TRACE_TYPE(RELO, "%5d RELO JMP_SLOT %08x <- %08x %s\n", pid,
               reloc, sym_addr, sym_name);
    *((unsigned*)reloc) = sym_addr;
    break;
case R_ARM_GLOB_DAT:
    COUNT_RELOC(RELOC_ABSOLUTE);
    MARK(rel->r_offset);
    TRACE_TYPE(RELO, "%5d RELO GLOB_DAT %08x <- %08x %s\n", pid,
               reloc, sym_addr, sym_name);
    *((unsigned*)reloc) = sym_addr;
    break;
case R_ARM_ABS32:
    COUNT_RELOC(RELOC_ABSOLUTE);
    MARK(rel->r_offset);
    TRACE_TYPE(RELO, "%5d RELO ABS %08x <- %08x %s\n", pid,
               reloc, sym_addr, sym_name);
    *((unsigned*)reloc) += sym_addr;
    break;

在 2.x 版本的时候,switch case 还不多,对于理解核心逻辑非常有帮助。核心代码就一行:

    *((unsigned*)reloc) = sym_addr;
    *((unsigned*)reloc) += sym_addr;

不过是,不同的重定位类型有不同的处理。

对于像 R_ARM_GLOB_DAT与 R_ARM_JUMP_SLOT ,就是直接赋值符号真实地址就好了。

对于像 R_ARM_ABS32 之类的,需要加上符号的真实地址,这个有可能是因为访问了结构体或者数组元素,编译器在编译文件时就写入了一个结构偏移量,而符号的地址是结构的起始地址,所以最终需要加上这个地址。

当然要完全的理解这些东西,需要去看文档。不同的符号,编译器在生成时会有不同的处理方式,给符号赋值不同的类型与重定位方式,然后 linker 会根据一定的规则对这些符号进行重定位,最后将值写入导入表地址中。

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值