关于 ELF 可执行文件的 Reloction 的笔记 2/2

装载时重定位的实际运作

通过创建一个简单的动态链接库载入程序,这样一来可以观察装载时重定位的实际的运作过程。但值得注意的是,由于Linux启用了地址空间配置随机加载(Address space layout randomization, ASLR)技术,从而导致重定位相对难以观察,因为每当我们运行程序时,libmlreloc.so 都会被载入到不同的内存空间(Virtual Address)中。

但是不必担心,即便如此我们也有方法能够观测到重定位过程。但首先,让我们谈一谈动态链接库中包含的段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
readelf --segments libmlreloc.so 

Elf file type is DYN (Shared object file)
Entry point 0x1030
There are 9 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x00000000 0x00000000 0x00304 0x00304 R 0x1000
LOAD 0x001000 0x00001000 0x00001000 0x00168 0x00168 R E 0x1000
LOAD 0x002000 0x00002000 0x00002000 0x00050 0x00050 R 0x1000
LOAD 0x002f20 0x00003f20 0x00003f20 0x000f4 0x000f8 RW 0x1000
DYNAMIC 0x002f28 0x00003f28 0x00003f28 0x000c8 0x000c8 RW 0x4
NOTE 0x000154 0x00000154 0x00000154 0x00024 0x00024 R 0x4
GNU_EH_FRAME 0x002000 0x00002000 0x00002000 0x00014 0x00014 R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x002f20 0x00003f20 0x00003f20 0x000e0 0x000e0 R 0x1

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn
01 .init .text .fini
02 .eh_frame_hdr .eh_frame
03 .init_array .fini_array .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .dynamic .got

于前文中提到, myglob 存储于 .data 段中,在 Section to Segment mapping 中发现 .data 被映射在 03 段中,可以发现 03 段 虚拟地址起始于 0x3f20,其内存中的大小为 0xf8 ,这意味着 03 段是从 0x3f20 开始, 到 0x4018 结束,正好包含 myglobmyglob 位于 0x4010 )。

现在,让我们用一个 Linux 给我们的便利工具, dl_iterate_phdr 函数, 来检验载入时的过程。这个函数允许我们的应用程序获取实际载入的动态链接库。

所以,让我们将如下代码保存到 driver.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#define _GNU_SOURCE
#include <link.h>
#include <stdlib.h>
#include <stdio.h>


static int header_handler(struct dl_phdr_info* info, size_t size, void* data)
{
printf("name=%s (%d segments) address=%p\n",
info->dlpi_name, info->dlpi_phnum, (void*)info->dlpi_addr);
for (int j = 0; j < info->dlpi_phnum; j++) {
printf("\t\t header %2d: address=%10p\n", j,
(void*) (info->dlpi_addr + info->dlpi_phdr[j].p_vaddr));
printf("\t\t\t type=%u, flags=0x%X\n",
info->dlpi_phdr[j].p_type, info->dlpi_phdr[j].p_flags);
}
printf("\n");
return 0;
}


extern int ml_func(int, int);


int main(int argc, const char* argv[])
{
dl_iterate_phdr(header_handler, NULL);

int t = ml_func(argc, argc);
return t;
}

其中 header_handler 函数是供给 dl_iterate_phdr 回调用,它会在所有动态链接库都报告其名字、装载的实际地址和其所有的段地址后被调用。 在此之后将会执行 ml_func ,该函数则是从 libmlreloc.so 中导出的。

编译该程序并链接动态链接库,需要如下命令:

1
2
$ gcc -m32 -g -c driver.c -o driver.o -fno-pic
$ gcc -m32 -o driver driver.o -L. -lmlreloc -fno-pic

为了使程序能够正常载入当前目录下的动态链接库,还需要一条额外的命令:

1
export LD_LIBRARY_PATH=./

该命令告诉系统查找动态连接库时,还应当在程序所在的目录查找。该设置退出当前终端后失效,所以不会污染环境。

运行 driver 后我们就能得到载入到 driver 程序中的所有动态链接库的信息。可是每次我们得到的信息都是不同的,所以我们需要将其用 gdb 调试,看看程序输出,同时用 gdb 获取相应内存空间的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gdb -q driver
Reading symbols from driver...
(gdb) b driver.c:31
Breakpoint 1 at 0x12c4: file driver.c, line 31.
(gdb) r
Starting program: /home/tricks/Documents/Projects/test/driver
[...] 省略部分输出
name = ./libmlreloc.so (9 segments) address = 0xf7fc8000
header 0: address = 0xf7fc8000
type = 1, flags = 0x4
header 1: address = 0xf7fc9000
type = 1, flags = 0x5
header 2: address = 0xf7fca000
type = 1, flags = 0x4
header 3: address = 0xf7fcbf20
type = 1, flags = 0x6
[...] 省略部分输出
Breakpoint 1, main (argc=1, argv=0xffffc6e4) at driver.c:31
31 }
(gdb)

程序在断点暂停后,我们观察其输出会发现, libmlreloc.so 有9个段,这和之前 readelf 程序报告的数量相同,同时这次不同点在于该库已经被实际载入一个确定的内存空间中了。

让我们做一些计算,包含有 myglob 的 03 段,该段被载入到了 0xf7fca000 处。回顾本文前半段 readelf 的输出「 myglob 位于 0x4010 处, 03 段的起始地址为 0x3f20 」,则 myglob 相对于 03 段段首的偏移量为 0x4010 - 0x 3f20 = 0xf0 , 再将这个偏移量加上 03 段实际载入的内存地址 0xf7fcbf20 , 则计算出 myglob 在载入后的实际内存地址为 0xf7fcbf20 + 0xf0 = 0xf7fcc010 。

让我们在 gdb 中验证下这个计算是否正确:

1
2
(gdb) p &myglob
$1 = (int *) 0xf7fcc010 <myglob>

完美!同时另一个疑惑又来了,在库函数 ml_func 中是如何访问 myglob 的呢?不妨再一次问问 gdb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb) set disassembly-flavor intel
(gdb) disas ml_func
Dump of assembler code for function ml_func:
0xf7fc912d <+0>: push ebp
0xf7fc912e <+1>: mov ebp,esp
0xf7fc9130 <+3>: mov edx,DWORD PTR ds:0xf7fcc010
0xf7fc9136 <+9>: mov eax,DWORD PTR [ebp+0x8]
0xf7fc9139 <+12>: add eax,edx
0xf7fc913b <+14>: mov ds:0xf7fcc010,eax
0xf7fc9140 <+19>: mov edx,DWORD PTR ds:0xf7fcc010
0xf7fc9146 <+25>: mov eax,DWORD PTR [ebp+0xc]
0xf7fc9149 <+28>: add eax,edx
0xf7fc914b <+30>: pop ebp
0xf7fc914c <+31>: ret
End of assembler dump.

正如我们所预料的, myglob 的真实地址被写入了 MOV 指令的操作数。这样一来,对于内存操作数,装载时重定位机制的运作就非常明晰了。

装载时重定位 —— 对于函数而言

有了前面的基础,后面函数的重定位就显得比较简单了。在 objdump 的输出中找到未载入内存时的函数调用指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ objdump -d -Mintel driver

[...] 忽略部分输出

000011cd <header_handler>:
11cd: 55 push ebp
11ce: 89 e5 mov ebp,esp
[...] 忽略部分输出

00001289 <main>:
1289: 8d 4c 24 04 lea ecx,[esp+0x4]
128d: 83 e4 f0 and esp,0xfffffff0
1290: ff 71 fc push DWORD PTR [ecx-0x4]
1293: 55 push ebp
1294: 89 e5 mov ebp,esp
1296: 53 push ebx
1297: 51 push ecx
1298: 83 ec 10 sub esp,0x10
129b: 89 cb mov ebx,ecx
129d: 83 ec 08 sub esp,0x8
12a0: 6a 00 push 0x0
12a2: 68 cd 11 00 00 push 0x11cd
12a7: e8 fc ff ff ff call 12a8 <main+0x1f>
12ac: 83 c4 10 add esp,0x10
12af: 83 ec 08 sub esp,0x8
12b2: ff 33 push DWORD PTR [ebx]
12b4: ff 33 push DWORD PTR [ebx]
12b6: e8 fc ff ff ff call 12b7 <main+0x2e>
12bb: 83 c4 10 add esp,0x10

动态连接库提供的函数

首先从动态链接库提供的函数 ml_func 开始:

对比源文件,很容易就能认出调用函数 ml_func 的指令在 RVA 的 0x12b6 处。 e8CALL 指令的 OPCODE ,所以CALL的地址为 fc ff ff ff ,即 0xfffffffc,或者说 -4

确认 .reloc 表的内容:

1
2
3
4
5
6
7
readelf -r driver                       

Relocation section '.rel.dyn' at offset 0x3ec contains 18 entries:
Offset Info Type Sym.Value Sym. Name
[...] 忽略部分输出
000012b7 00000402 R_386_PC32 00000000 ml_func
[...] 忽略部分输出

这次的 Offset 为 0x12b7 和我们预想的一样,可是 Type 却为 R_386_PC32 而非之前的 R_386_32 ,同时 Sym.Value 为 0 。很明显,这应该比之前的要难上一点,不过也就仅仅只难一点而已。简单地说就是:于 .rel.dyn (即动态链接库提供函数的)表中的 Offset 值, Offset + Base 即为需要替换的量的内存地址(记为 VA_SOURCE );在动态链接库载入后,根据 Sym.Name 确认的实际地址(记为 $VA_{DEST}$ ),相对位置则为 $VA_{DEST} - VA_{SOURCE}$ 。因为 CALL 指令执行时 EIP 并不位于 $VA_{SOURCE}$ ,而是下一条指令处。下一条指令和 $VA_{SOURCE}$ 的位置差为 4 (这就是之前 0xfffffffc 的来源),则实际运算公式为:

$$ REPLACEMENT = VA_{DEST} - VA_{SOURCE} - 4 $$

局部函数

对于局部函数,问题就更简单了,观察 RVA 0x12a2 处,那是我们传入的回调函数的地址,其内容为 0x11cd ,是不是很眼熟?这就是 header_handler 的 RVA 地址呀!

让我们再确认下 reloc 表中的记录:

1
2
3
4
5
6
7
readelf -r driver                       

Relocation section '.rel.dyn' at offset 0x3ec contains 18 entries:
Offset Info Type Sym.Value Sym. Name
[...] 忽略部分输出
000012a3 00000008 R_386_RELATIVE
[...] 忽略部分输出

没有 Sym.Value ,没有 Sym.Name , 只有 Offset 和 Type, Offset 指出了何处需要修改,其 Type 为 R_386_RELATIV ,其意思就是,若基址变换了,基址增加1,这个值也相应增加1就可以了。

结语:

关于 Linux 上 ELF 文件的重定位就写到这里了,言不达意,翻译起来痛恨自己中文水平太低。若有错误,欢迎指正。