Linux逆向---ELF符号和重定位

1.ELF符号

符号是对某些类型的数据或者代码(全局变量、函数等)的符号引用,例如printf函数会在动态符号表.dynsym中存有一个指向该函数的符号条目。

.dynsym保存了引用来自外部文件符号的全局符号,.symtab中还保存了可执行文件的本地符号,如全局变量等,.dynsym保存的符号是.symtab保存的符号的子集。

.dynsym在程序运行时会被分配并装载进内存,主要用于动态链接可执行文件的执行。而.symtab则不会,它主要用来进行调试和链接的。

ELF文件符号项的结构体如下:

typedef struct {
	uint32_t      st_name;	//保存了指向符号表中字符串表的偏移地址(.dynstr或.strtab)
	unsigned char st_info;	//制定符号类型及绑定属性
	unsigned char st_other;	//定义符号的可见性
	uint16_t      st_shndx;	//每个符号表的条目的定义都与某些节对应,该变量保存了相关节头表的索引
	Elf64_Addr    st_value;	//存放符号的值(地址或者位置偏移量)
	uint64_t      st_size;	//存放符号的大小
} Elf64_Sym;

这里我们可以观察一个实例:

1.1.symtab符号表分析

首先用readelf -S查看节信息:

  [29] .symtab           SYMTAB           0000000000000000  00001070
       0000000000000648  0000000000000018          30    47     8
  [30] .strtab           STRTAB           0000000000000000  000016b8
       0000000000000216  0000000000000000           0     0     1

然后再用hexedit来查看程序代码:

.symtab节:(这里没有从0x1070顶头截取,0x1310是第28项的起始)

00001310   01 00 00 00  04 00 F1 FF  00 00 00 00  00 00 00 00  ................
00001320   00 00 00 00  00 00 00 00  0C 00 00 00  01 00 15 00  ................
00001330   20 0E 60 00  00 00 00 00  00 00 00 00  00 00 00 00   .`.............
00001340   19 00 00 00  02 00 0E 00  60 04 40 00  00 00 00 00  ........`.@.....
00001350   00 00 00 00  00 00 00 00  1B 00 00 00  02 00 0E 00  ................
00001360   A0 04 40 00  00 00 00 00  00 00 00 00  00 00 00 00  ..@.............
00001370   2E 00 00 00  02 00 0E 00  E0 04 40 00  00 00 00 00  ..........@.....

.strtab节:

									 00 63 72 74  73 74 75 66  .........crtstuf
000016C0   66 2E 63 00  5F 5F 4A 43  52 5F 4C 49  53 54 5F 5F  f.c.__JCR_LIST__
000016D0   00 64 65 72  65 67 69 73  74 65 72 5F  74 6D 5F 63  .deregister_tm_c
000016E0   6C 6F 6E 65  73 00 5F 5F  64 6F 5F 67  6C 6F 62 61  lones.__do_globa
000016F0   6C 5F 64 74  6F 72 73 5F  61 75 78 00  63 6F 6D 70  l_dtors_aux.comp
00001700   6C 65 74 65  64 2E 37 35  39 34 00 5F  5F 64 6F 5F  leted.7594.__do_
00001710   67 6C 6F 62  61 6C 5F 64  74 6F 72 73  5F 61 75 78  global_dtors_aux

我们可以计算一下:

每一个符号项的大小为:4+1+1+2+8+8=24

则这一段代码中包含的符号项数量为:0x648/24=67项,然后我们用如下命令查看一波符号表:

readelf -s hello.out

输出为:

Symbol table '.symtab' contains 67 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000400238     0 SECTION LOCAL  DEFAULT    1 
     2: 0000000000400254     0 SECTION LOCAL  DEFAULT    2 

证明我们的计算是正确的。

拿.dynsym中的第三个符号项为例子分析一下:

19 00 00 00  02 00 0E 00  60 04 40 00  00 00 00 00   00 00 00 00  00 00 00 00

首先知道偏移量为0x19,这样的话它的字符串在.strtab中的位置就是:0x19+0x16B8=0x16D1,结束位置为下一个0x00所在位置,即0x16E5

即:“deregister_tm_clones”,它的st_info字段的值为0x02,st_other字段的值为0x00,st_shndex的值为0x0E,st_value的值为0x400460,st_size的值为0x00。接下来我们再打印一波符号表然后看看第31项:

    30: 0000000000400460     0 FUNC    LOCAL  DEFAULT   14 deregister_tm_clones

看来与我们的计算也是相同的。

1.2.dyntab符号表分析

刚才分析的表的地址已经超出了数据段和代码段的地址了,这也说明程序运行时并不会把他们加载进内存,所以如果我们要分析与程序运行相关的符号表就要看.dyntab表,首先查看节信息:

  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400318  00000318
       000000000000003f  0000000000000000   A       0     0     1

然后找出对应的内容:

.dyntab:

									 00 00 00 00  00 00 00 00  ................
000002C0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
000002D0   0B 00 00 00  12 00 00 00  00 00 00 00  00 00 00 00  ................
000002E0   00 00 00 00  00 00 00 00  12 00 00 00  12 00 00 00  ................
000002F0   00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  ................
00000300   24 00 00 00  20 00 00 00  00 00 00 00  00 00 00 00  $... ...........
00000310   00 00 00 00  00 00 00 00

.dynstr:

									 00 6C 69 62  63 2E 73 6F  .........libc.so
00000320   2E 36 00 70  72 69 6E 74  66 00 5F 5F  6C 69 62 63  .6.printf.__libc
00000330   5F 73 74 61  72 74 5F 6D  61 69 6E 00  5F 5F 67 6D  _start_main.__gm
00000340   6F 6E 5F 73  74 61 72 74  5F 5F 00 47  4C 49 42 43  on_start__.GLIBC
00000350   5F 32 2E 32  2E 35 00 00  00 00 02 00  02 00 00 00  _2.2.5..........

这里首先也简单计算一下吧,一共长度为0x60字节,每项24字节大小,共有0x60/24=4项,可以打印一下符号表来验证一下:

Symbol table '.dynsym' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__

说明计算是正确的。

这里还是用第三项来分析,第三项的代码为:

12 00 00 00  12 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00

偏移量为0x12,即strtab中的0x32a,一直到0x33B,也就是如下字符:"__libc_start_main",它的 st_info字段为0x12,其余的字段都是0。

但是现在可以发现一个有趣的现象,那就是符号表打印的并不是"__libc_start_main",而是是"__libc_start_main@GLIBC_2.2.5 (2)",

我想这也与st_info字段的取值有关,这个字段决定了打印结果中的两列的取值—type与bind。

符号类型type有如下几种:

  • STT_NOTYPE 符号未定义

  • STT_FUNC 表示该符号与函数或者其他可执行代码关联

  • STT_OBJECT 表明该符号与数据目标文件关联

符号绑定bind有如下几种:

  • STB_LOCAL 本地符号,在目标文件之外都是不可见的,如一个声明为static的函数

  • STB_GLOBAL 全局符号,对于所有要合并的目标文件来说都是可见的

  • STB_WEAK 与全局绑定类似,不过比STB_GLOBAL优先级低,甚至可能会被同名的未标记为STB_WEAK的符号覆盖

所以我觉得应该是因为这两个符号是全局符号的原因,所以需要在后缀添加上这些信息。

2.ELF重定位

重定位就是将符号定义和符号引用进行连接的过程,包括描述如何修改节内容的相关信息,从而使得可执行文件和共享目标文件能够保存进程的程序镜像所需的正确信息。重定位条目就是我们上面说的相关信息。

重定位记录保存了如何对给定的符号的对应代码进行补充的相关信息,重定位实际上是一种给二进制文件打补丁的机制。

简单点来说,就是两个目标文件输出可执行文件之前,是无法确定各自符号和代码在内存中的位置的,而重定位之后,目标文件中的代码会被重定位到可执行文件段中的一个给定的地址。

看一下64位的重定位条目:

typedef struct {
	Elf64_Addr r_offset;
	uint64_t   r_info;
} Elf64

有的条目还需要append字段:

typedef struct {
	Elf64_Addr r_offset;	//指向需要进行重定位操作的位置
	uint64_t   r_info;	//指定必须对其进行重定位符号表索引以及要应用的重定位类型
	int64_t    r_addend;	//制定常量加数,用于计算存储在可重定位字段中的值
} Elf64_Rela;

这里我突然想起来之前分析节的时候看见的.rela.dyn、.rela.plt节:

  [ 9] .rela.dyn         RELA             0000000000400380  00000380
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400398  00000398
       0000000000000030  0000000000000018  AI       5    24     8

我想这些部分与重定位密切相关。

到这里我参考的书提到了隐式加数的概念,不过鉴于64位往往采用显式存储,所以这里就不去探究了。

2.1.重定位项查看

重定位项是可以直接查看的,用如下命令:

readelf -r hello.out

得到如下的输出:

重定位节 '.rela.dyn' 位于偏移量 0x380 含有 1 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000600ff8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0

这里也可以简单的进行一个计算:

每个重定位条目大小为3*8=24=0x18,所以一个条目是0x18,两个条目就是0x30,我们再去分析一下对应地址的二进制部分:

.dela.dyn对应的:

00000380   F8 0F 60 00  00 00 00 00  06 00 00 00  03 00 00 00  ..`.............
00000390   00 00 00 00  00 00 00 00

从这里,我们可以看出0~8字节为偏移量r_offset=0x600FF8,8 ~16字节为信息r_info=0x300000006,加数r_append=0x0,也正好与我们的条目相对应。

2.2.偏移计算

为了说明这个计算,首先我们需要生成一个目标文件,源代码依然用最简单的hello world:

gcc -c hello.c

然后我们目录下会出现这个hello.o,然后我们查看一下它的指令部分:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	b8 00 00 00 00       	mov    $0x0,%eax
   e:	e8 00 00 00 00       	callq  13 <main+0x13>
  13:	b8 00 00 00 00       	mov    $0x0,%eax
  18:	5d                   	pop    %rbp
  19:	c3                   	retq   

只有这么一小部分,但是可以看到它的函数调用部分是callq 0x13,按我参考书上的说法,这是因为此时目标文件并没有printf函数的地址,而当生成可执行文件时,链接器会对该位置进行修改,在printf函数被包含进可执行文件时,链接器会通过偏移补齐4个字节,这样也就相当于存储了foo的实际偏移地址,这里再打印一下重定位表:

重定位节 '.rela.text' 位于偏移量 0x1f8 含有 2 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000000005  00050000000a R_X86_64_32       0000000000000000 .rodata + 0
00000000000f  000a00000002 R_X86_64_PC32     0000000000000000 printf - 4

重定位节 '.rela.eh_frame' 位于偏移量 0x228 含有 1 个条目:
  偏移量          信息           类型           符号值        符号名称 + 加数
000000000020  000200000002 R_X86_64_PC32     0000000000000000 .text + 0

这里可以通过简单计算:0x13-4=0xF,也就是说第二个偏移条目printf的函数的地址会被添加到这个位置,也就是callq指令的部分,而这个等待被补全的地址刚好是四个字节,也可以解释加数4了。

接下来便是生成可执行文件,然后看一下它重定位后的结果:

gcc hello.o -o hello.out
objdump -d hello.out

可以看到如下输出:

0000000000400400 <printf@plt>:
  400400:	ff 25 12 0c 20 00    	jmpq   *0x200c12(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  400406:	68 00 00 00 00       	pushq  $0x0
  40040b:	e9 e0 ff ff ff       	jmpq   4003f0 <_init+0x28>
........
0000000000400526 <main>:
  400526:	55                   	push   %rbp
  400527:	48 89 e5             	mov    %rsp,%rbp
  40052a:	bf c4 05 40 00       	mov    $0x4005c4,%edi
  40052f:	b8 00 00 00 00       	mov    $0x0,%eax
  400534:	e8 c7 fe ff ff       	callq  400400 <printf@plt>
  400539:	b8 00 00 00 00       	mov    $0x0,%eax
  40053e:	5d                   	pop    %rbp
  40053f:	c3                   	retq   

可以看到,callq这里的地址的确被替换掉了。

每种类型都有各自的计算方式,比如这个R_X86_64_PC32,被替换的值符合S+A-P的方式。

这里S是调用printf函数地址指令所在的地址,即0x400535,A为.o文件时打印重定位项的加数-4,P则是要进行重定位的存储单元的地址,在这里是printf函数所在的地址,即0x400400,所以偏移量就是 0x400535-4+0x400400=-0x139=0xFFFFFEC7。

所以这也是我之前做注入的小实验时那个讲究的4的解释了。

猜你喜欢

转载自blog.csdn.net/zekdot/article/details/84714673