ELF 文件 PLT 和 GOT 静态分析
摘要
本文将对一个特意构造的、十分简单的、64 位的 ELF 文件的 PLT(Procedure Linkage Table)和 GOT(Global Offset Table)进行静态分析。目的是
- 验证所学的关于 PLT 和 GOT 的相关知识
- 加深对所学知识的理解和记忆
- 记录分析时用到的命令以备忘
背景知识
关于什么是 PLT 和 GOT,可阅读海枫发表于 2016 年 6~7 月的系列文章
- 海枫.《聊聊Linux动态链接中的PLT和GOT(1)——何谓PLT与GOT》.CSDN 博客.2016-06.11
- 海枫.《聊聊Linux动态链接中的PLT和GOT(2)——延迟重定位》.CSDN 博客.2016-06.11
- 海枫.《聊聊Linux动态链接中的PLT和GOT(3)——公共GOT表项》.CSDN 博客.2016-06.11
- 海枫.《聊聊Linux动态链接中的PLT和GOT(4)—— 穿针引线》.CSDN 博客.2016-07.13
准备 ELF 文件
准备一个简单的 ELF 文件,源码如下所示
/* test.c */
#include <stdio.h>
int main() {
int integer;
printf("Enter an integer: ");
scanf("%d", &integer);
printf("Number = %d\n", integer);
return 0;
}
这段代码中 printf 和 scranf 这两个函数需要在运行时确定函数地址,即需用到 PLT 和 GOT。
用如下命令编译
gcc test.c -z norelro -fno-stack-protector -o test
简单起见,使用 gcc 选项 -z norelro
关闭了 RELRO,-fno-stack-protector
关闭了 CANNARY。
查看编译出的可执行文件
$ file test
test: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=779ce5dad37fc44d6106c16adae2c7557d775101, not stripped
试运行
$ ./test
Enter an integer: 1
Number = 1
查看 ELF 所有段
使用 readelf 命令可例出一个 ELF 文件的所有段。选项 --section-headers
(可简写为 -S
)的含义是 Display the sections' header
,--wide
(可简写为 -W
)的含义是 Allow output width to exceed 80 characters
。
$ readelf --section-headers --wide test
There are 30 section headers, starting at offset 0x1490:
Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 0000000000000200 000200 00001c 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000000021c 00021c 000020 00 A 0 0 4
[ 3] .note.gnu.build-id NOTE 000000000000023c 00023c 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000000260 000260 00001c 00 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000000280 000280 0000c0 18 A 6 1 8
[ 6] .dynstr STRTAB 0000000000000340 000340 00009d 00 A 0 0 1
[ 7] .gnu.version VERSYM 00000000000003de 0003de 000010 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 00000000000003f0 0003f0 000030 00 A 6 1 8
[ 9] .rela.dyn RELA 0000000000000420 000420 0000c0 18 A 5 0 8
[10] .rela.plt RELA 00000000000004e0 0004e0 000030 18 AI 5 23 8
[11] .init PROGBITS 0000000000000510 000510 000017 00 AX 0 0 4
[12] .plt PROGBITS 0000000000000530 000530 000030 10 AX 0 0 16
[13] .plt.got PROGBITS 0000000000000560 000560 000008 08 AX 0 0 8
[14] .text PROGBITS 0000000000000570 000570 0001d2 00 AX 0 0 16
[15] .fini PROGBITS 0000000000000744 000744 000009 00 AX 0 0 4
[16] .rodata PROGBITS 0000000000000750 000750 000027 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000000778 000778 00003c 00 A 0 0 4
[18] .eh_frame PROGBITS 00000000000007b8 0007b8 000108 00 A 0 0 8
[19] .init_array INIT_ARRAY 00000000002008c0 0008c0 000008 08 WA 0 0 8
[20] .fini_array FINI_ARRAY 00000000002008c8 0008c8 000008 08 WA 0 0 8
[21] .dynamic DYNAMIC 00000000002008d0 0008d0 0001f0 10 WA 6 0 8
[22] .got PROGBITS 0000000000200ac0 000ac0 000028 08 WA 0 0 8
[23] .got.plt PROGBITS 0000000000200ae8 000ae8 000028 08 WA 0 0 8
[24] .data PROGBITS 0000000000200b10 000b10 000010 00 WA 0 0 8
[25] .bss NOBITS 0000000000200b20 000b20 000008 00 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 000b20 000029 01 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 000b50 000618 18 28 44 8
[28] .strtab STRTAB 0000000000000000 001168 00021e 00 0 0 1
[29] .shstrtab STRTAB 0000000000000000 001386 000107 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
其中与 PLT 和 GOT 有关的段是 .plt 和 .got.plt。下面我们将查看并分析这两个段的内容。此外还注意到 .dynamic 段的地址是 0x00000000002008d0,后文有相关内容。
.plt 段
PLT 中的每一项都是一小段代码,所以使用 objdump 命令查看 .plt 段的内容时添加反汇编参数。选项 --disassemble
(可简写为 -d
)的含义是 Display assembler contents of executable sections
,--full-contents
(可简写为 -s
)的含义是 Display the full contents of all sections requested
,--section
(可简写为 -j
)的含义是 Display information only for section name
。
$ objdump --disassemble --full-contents --section=.plt test
test: file format elf64-x86-64
Contents of section .plt:
0530 ff35ba05 2000ff25 bc052000 0f1f4000 .5.. ..%.. ...@.
0540 ff25ba05 20006800 000000e9 e0ffffff .%.. .h.........
0550 ff25b205 20006801 000000e9 d0ffffff .%.. .h.........
Disassembly of section .plt:
0000000000000530 <.plt>:
530: ff 35 ba 05 20 00 pushq 0x2005ba(%rip) # 200af0 <_GLOBAL_OFFSET_TABLE_+0x8>
536: ff 25 bc 05 20 00 jmpq *0x2005bc(%rip) # 200af8 <_GLOBAL_OFFSET_TABLE_+0x10>
53c: 0f 1f 40 00 nopl 0x0(%rax)
0000000000000540 <printf@plt>:
540: ff 25 ba 05 20 00 jmpq *0x2005ba(%rip) # 200b00 <printf@GLIBC_2.2.5>
546: 68 00 00 00 00 pushq $0x0
54b: e9 e0 ff ff ff jmpq 530 <.plt>
0000000000000550 <__isoc99_scanf@plt>:
550: ff 25 b2 05 20 00 jmpq *0x2005b2(%rip) # 200b08 <__isoc99_scanf@GLIBC_2.7>
556: 68 01 00 00 00 pushq $0x1
55b: e9 d0 ff ff ff jmpq 530 <.plt>
可以看到共有 3 个 PLT 表项,第 0 个表项(.plt)是共公 plt 表项,第 1 个表项(printf@plt)是 printf 函数对应的 PLT 表项,第 2 个表项(__isoc99_scanf@plt)是 scanf 函数对应的 PLT 表项。
.got.plt 段
GOT 的每一项都是一个地址,因此不用进行反汇编。同样使用 objdump 命令查看。
$ objdump --full-contents --section=.got.plt test
test: file format elf64-x86-64
Contents of section .got.plt:
200ae8 d0082000 00000000 00000000 00000000 .. .............
200af8 00000000 00000000 46050000 00000000 ........F.......
200b08 56050000 00000000 V.......
64 位系统中地址长度是 64 比特,也就是 8 字节。按 8 字节一项并调整字节序后可得 GOT 的内容是
第几项 | 地址 | 内容 | 备注 |
---|---|---|---|
0 | 0x200ae8 | 0x00000000002008d0 | .dynamic 段地址 |
1 | 0x200af0 | 0x0000000000000000 | 本镜像的link_map数据结构地址,未运行无法确定,故以全 0 填充 |
2 | 0x200af8 | 0x0000000000000000 | _dl_runtime_resolve 函数地址,未运行无法确定,故以全 0 填充 |
3 | 0x200b00 | 0x0000000000000546 | printf 对应的 GOT 表项,内容是 printf 的 PLT 表项地址加 6 |
4 | 0x200b08 | 0x0000000000000556 | scanf 对应的 GOT 表项,内容是 scanf 的 PLT 表项地址加 6 |
分析
以 printf 函数为例,分析 PLT 和 GOT 的工作过程。
反汇编 main 函数(以下命令输出删除了无关内容)
$ objdump --disassemble --full-contents --section=.text test
000000000000067a <main>:
67a: 55 push %rbp
67b: 48 89 e5 mov %rsp,%rbp
67e: 48 83 ec 10 sub $0x10,%rsp
682: 48 8d 3d cb 00 00 00 lea 0xcb(%rip),%rdi # 754 <_IO_stdin_used+0x4>
689: b8 00 00 00 00 mov $0x0,%eax
68e: e8 ad fe ff ff callq 540 <printf@plt>
693: 48 8d 45 fc lea -0x4(%rbp),%rax
697: 48 89 c6 mov %rax,%rsi
69a: 48 8d 3d c6 00 00 00 lea 0xc6(%rip),%rdi # 767 <_IO_stdin_used+0x17>
6a1: b8 00 00 00 00 mov $0x0,%eax
6a6: e8 a5 fe ff ff callq 550 <__isoc99_scanf@plt>
6ab: 8b 45 fc mov -0x4(%rbp),%eax
6ae: 89 c6 mov %eax,%esi
6b0: 48 8d 3d b3 00 00 00 lea 0xb3(%rip),%rdi # 76a <_IO_stdin_used+0x1a>
6b7: b8 00 00 00 00 mov $0x0,%eax
6bc: e8 7f fe ff ff callq 540 <printf@plt>
6c1: b8 00 00 00 00 mov $0x0,%eax
6c6: c9 leaveq
6c7: c3 retq
6c8: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
6cf: 00
看到 main 函数调用 printf 函数的指令是 callq 540
,0x540 正是 printf 函数的 PLT 表项的地址。反汇编结果里的 <printf@plt>
也明确地指出了这一点。
0x540 地址开始的几条指令是
540: ff 25 ba 05 20 00 jmpq *0x2005ba(%rip) # 200b00 <printf@GLIBC_2.2.5>
546: 68 00 00 00 00 pushq $0x0
54b: e9 e0 ff ff ff jmpq 530 <.plt>
看到它跳转到了 0x2005ba(%rip)
指向的地址,0x2005ba(%rip)
的内容在反汇编结果的注释中给出了,是 0x200b00。0x200b00 正是 printf 函数的 GOT 表项的地址,其内容是 0x0000000000000546,这个地址实际上是 printf 的 PLT 表项地址加 6。可见 0x540 处的 jmpq 指令实际上跳到了 0x546 处,相当于没有跳转。0x546 处的 pushq 指令将 0x00 压栈,可以理解为接下来要调用的函数的参数。接着 0x54b 处的 jmpq 指令跳转到了 0x530 即 PLT 表的第 0 项。
0x530 地址开始的几条指令是
530: ff 35 ba 05 20 00 pushq 0x2005ba(%rip) # 200af0 <_GLOBAL_OFFSET_TABLE_+0x8>
536: ff 25 bc 05 20 00 jmpq *0x2005bc(%rip) # 200af8 <_GLOBAL_OFFSET_TABLE_+0x10>
53c: 0f 1f 40 00 nopl 0x0(%rax)
先是把 0x200af0 即 GOT 表的第 1 项压栈,接着跳转到 0x200af8 即 GOT 表的第 2 项亦即 _dl_runtime_resolve 函数,解析 pritnf 函数真正的地址。之后会执行 pritnf,并将 pritnf 函数真正的地址写到 printf 对应的 GOT 表项中。这样下次调用 ptinf 函数时 0x540 处的 jmpq 指令会直接跳转到 pritnf 函数真正的地址,不用再调用 _dl_runtime_resolve。