Linux二进制ELF程序查找symbol过程分析【转】

转自:https://blog.csdn.net/weixin_46222091/article/details/108668735?spm=1001.2101.3001.6650.3&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7EHighlightScore-3.queryctrv2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7EHighlightScore-3.queryctrv2&utm_relevant_index=6

文章目录
0.相关section的简介
.got
.plt
.got.plt
.plt.got
1.关于Partial RELRO下的外部函数延迟/惰性(lazily)调用的过程分析
1.1例子
1.2静态编译结果初步分析
1.3GOTPLT_ADDR是什么地址?jmp到该地址将会完成那些功能?
1.4对比Partial Full下的符号查找过程
1.5总结一下
2.参考
[Runtime Dynamic Linking](http://users.eecs.northwestern.edu/~kch479/docs/notes/linking.html)(害怕万一这个外网网站访问不到了,copy下来(^__^) ):

Linux二进制ELF程序查找symbol过程分析涉及到的段有.got/.got.plt/.plt/.plt.got,今天我们将一起深入探讨其原理和过程。
0.相关section的简介
.got
称为全局偏移表,链接器填充外部符号实际地址的表。全局偏移表(或GOT)是程序内部的一个部分,其中包含动态链接的函数的地址。

.got的前三项存放着特殊的地址引用:

GOT[0]:保存.dynamic段的地址,动态链接器利用该地址提取动态链接相关的信息。 GOT[0] is the address of the program’s .dynamic segment. This segment holds a lot of pointers to other parts of the ELF. It basically serves as a guide for the dynamic linker to navigate the ELF.

GOT[1]:保存动态链接程序管理的数据结构的指针。该数据结构是与程序链接的每个共享库的符号表相对应的节点的链接列表。GOT[1] is the pointer to a data structure that the dynamic linker manages. This data structure is a linked list of nodes corresponding to the symbol tables for each shared library linked with the program. When a symbol is to be resolved by the linker, this list is traversed to find the appropriate symbol. Using the LD_PRELOAD environment variable basically ensures that your preload library will be the first node on this list.

GOT[2]:存放了指向动态链接器_dl_runtime_resolve函数的地址,该函数用来解析共享库函数的实际符号地址。GOT[2] is the address osf the symbol resolution function within the dynamic linker. In ld.so, it contains the address of the function named _dl_runtime_resolve, which is basically an assembly stub that does some register/stack setup and calls into a C function called dl_fixup(). dl_fixup is the workhorse that actually resolves the symbol in question. Once the symbol’s address is found, the program’s GOT entry for it must be patched. This is also the job of dl_fixup(). Once dl_fixup() patches the correct GOT entry, the next time the function is called, it will again jump to the PLT entry, but this time the indirect jump there will go to the symbol’s address instead of the following instruction.

.plt
称为过程链接表,通过这个表程序将跳转到符号的正确地址,或者跳转到.got.plt段中寻找正确的符号地址。The PLT holds an entry for each external function reference.

.got.plt
为.plt的.got。它包含返回.plt去触发查找的地址,或者是一个经过查找后填充的正确符号地址。

注:

如果Partial RELRO下产生的程序,.got.plt段中一开始并未填充正确的符号地址。在程序运行过程中,当发现.got.plt段中符号地址未填充时,将使用动态链接器_dl_runtime_resolve函数来解析共享库函数的实际符号地址,并填充到.got.plt段中相应的位置。当程序第二次使用该符号地址时,由于.got.plt段中已经有值了,将直接跳转到正确的符号地址。在Partial RELRO下.got.plt段是可读/写的。

.plt.got
暂且不知道其用处。

1.关于Partial RELRO下的外部函数延迟/惰性(lazily)调用的过程分析
1.1例子
一小段C代码

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv) {
puts("Hello world1!");
puts("Hello world2!");
exit(0);
}

编译构建

$ gcc -no-pie -g -o plt plt.c
1
1.2静态编译结果初步分析
使用以下命令查看Section Header

$ readelf -W -S plt
1
得到如下结果:

There are 34 section headers, starting at offset 0x21c0:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[12] .plt PROGBITS 0000000000400420 000420 000030 10 AX 0 0 16
[13] .text PROGBITS 0000000000400450 000450 000192 00 AX 0 0 16
[21] .got PROGBITS 0000000000600ff0 000ff0 000010 08 WA 0 0 8
[22] .got.plt PROGBITS 0000000000601000 001000 000028 08 WA 0 0 8
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)

总共有34个section,如上所示。我仅保留了我们接下来讨论将重点讨论的几个section,.plt/.text/.got/.got.plt。

查看plt的反汇编代码,在命令行中使用了-M intel选项来指定使用intel汇编格式而不是AT&T。

main函数的反汇编结果如下:

0000000000400537 <main>:
400537: 55 push rbp
400538: 48 89 e5 mov rbp,rsp
40053b: 48 83 ec 10 sub rsp,0x10
40053f: 89 7d fc mov DWORD PTR [rbp-0x4],edi
400542: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi
400546: 48 8d 3d a7 00 00 00 lea rdi,[rip+0xa7] # 4005f4 <_IO_stdin_used+0x4>
40054d: e8 de fe ff ff call 400430 <puts@plt>
400552: 48 8d 3d a9 00 00 00 lea rdi,[rip+0xa9] # 400602 <_IO_stdin_used+0x12>
400559: e8 d2 fe ff ff call 400430 <puts@plt>
40055e: bf 00 00 00 00 mov edi,0x0
400563: e8 d8 fe ff ff call 400440 <exit@plt>
400568: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
40056f: 00

如上所示,在0x40054d调用了0x400430 <puts@plt>,当rip到0x400430时,其将要执行的汇编代码如下:

0000000000400430 <puts@plt>:
400430: ff 25 e2 0b 20 00 jmp QWORD PTR [rip+0x200be2] # 601018 <puts@GLIBC_2.2.5>
400436: 68 00 00 00 00 push 0x0
40043b: e9 e0 ff ff ff jmp 400420 <.plt>
1
2
3
4
执行的指令是jmp QWORD PTR [rip+0x200be2],其中rip+0x200be2=0x601018,地址0x601018指向哪里呢?

当再一次回头看一下之前查看的Section Header信息,你会发现0x601018正好落在.got.plt段中,如下:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[22] .got.plt PROGBITS 0000000000601000 001000 000028 08 WA 0 0 8
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)

根据我们现有的知识可知:

如果ELF程序是Full RELRO的,那么.got.plt中将在链接时,填充上正确的符号地址。
如果ELF程序是Partial RELRO的,那么.got.plt中填充的是返回.plt去触发查找的地址,将使用动态链接器_dl_runtime_resolve函数来解析共享库函数的实际符号地址,并填充到.got.plt段中相应的位置。
从Section Headers表中可以发现.got.plt的Flg是WA,.got.plt具有可写权限,说明ELF程序是Partial RELRO的。同时再利用checksec工具来看一下ELF程序是Partial RELRO,应证我们得到的结论。

cmp@U1804:~/work_dir/how_to_exploit/ELF/test2$ gdb plt
GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git
...略...
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from plt...done.
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
gdb-peda$

结果如上RELRO : Partial。

回到当前将要执行的指令jmp QWORD PTR [rip+0x200be2],其中rip+0x200be2=0x601018,地址0x601018指向.got.plt段中的一个地址,0x601018指向的内存区域中存放的数据我们暂且称之为GOTPLT_ADDR。

GOTPLT_ADDR是什么地址?通过jmp到GOTPLT_ADDR,将会完成那些功能?

接下来将一一解惑。

1.3GOTPLT_ADDR是什么地址?jmp到该地址将会完成那些功能?
使用gdb调试,使用以下命令在0x40054d指令处下断点:

gdb-peda$ b *0x40054d
1
运行到0x40054d处,调试结果如下:

[----------------------------------registers-----------------------------------]
RAX: 0x400537 (<main>: push rbp)
RBX: 0x0
RCX: 0x400570 (<__libc_csu_init>: push r15)
RDX: 0x7fffffffdec8 --> 0x7fffffffe26d ("CLUTTER_IM_MODULE=xim")
RSI: 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
RDI: 0x4005f4 ("Hello world1!")
RBP: 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
RIP: 0x40054d (<main+22>: call 0x400430 <puts@plt>)
R8 : 0x7ffff7dd0d80 --> 0x0
R9 : 0x7ffff7dd0d80 --> 0x0
R10: 0x2
R11: 0x7
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x40053f <main+8>: mov DWORD PTR [rbp-0x4],edi
0x400542 <main+11>: mov QWORD PTR [rbp-0x10],rsi
0x400546 <main+15>: lea rdi,[rip+0xa7] # 0x4005f4
=> 0x40054d <main+22>: call 0x400430 <puts@plt>
0x400552 <main+27>: lea rdi,[rip+0xa9] # 0x400602
0x400559 <main+34>: call 0x400430 <puts@plt>
0x40055e <main+39>: mov edi,0x0
0x400563 <main+44>: call 0x400440 <exit@plt>
Guessed arguments:
arg[0]: 0x4005f4 ("Hello world1!")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0008| 0x7fffffffddc8 --> 0x100000000
0016| 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
0024| 0x7fffffffddd8 --> 0x7ffff7a05b97 (<__libc_start_main+231>: mov edi,eax)
0032| 0x7fffffffdde0 --> 0x1
0040| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0048| 0x7fffffffddf0 --> 0x100008000
0056| 0x7fffffffddf8 --> 0x400537 (<main>: push rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 1, 0x000000000040054d in main (argc=0x1, argv=0x7fffffffdeb8) at plt.c:5
5 puts("Hello world1!");
gdb-peda$

使用gdb的si命令单指令调试,将跳转到0x400430 <puts@plt>处,结果如下:

[----------------------------------registers-----------------------------------]
RAX: 0x400537 (<main>: push rbp)
RBX: 0x0
RCX: 0x400570 (<__libc_csu_init>: push r15)
RDX: 0x7fffffffdec8 --> 0x7fffffffe26d ("CLUTTER_IM_MODULE=xim")
RSI: 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
RDI: 0x4005f4 ("Hello world1!")
RBP: 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffddb8 --> 0x400552 (<main+27>: lea rdi,[rip+0xa9] # 0x400602)
RIP: 0x400430 (<puts@plt>: jmp QWORD PTR [rip+0x200be2] # 0x601018)
R8 : 0x7ffff7dd0d80 --> 0x0
R9 : 0x7ffff7dd0d80 --> 0x0
R10: 0x2
R11: 0x7
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400421: xor eax,0x200be2
0x400426: jmp QWORD PTR [rip+0x200be4] # 0x601010
0x40042c: nop DWORD PTR [rax+0x0]
=> 0x400430 <puts@plt>: jmp QWORD PTR [rip+0x200be2] # 0x601018
| 0x400436 <puts@plt+6>: push 0x0
| 0x40043b <puts@plt+11>: jmp 0x400420
| 0x400440 <exit@plt>: jmp QWORD PTR [rip+0x200bda] # 0x601020
| 0x400446 <exit@plt+6>: push 0x1
|-> 0x400436 <puts@plt+6>: push 0x0
0x40043b <puts@plt+11>: jmp 0x400420
0x400440 <exit@plt>: jmp QWORD PTR [rip+0x200bda] # 0x601020
0x400446 <exit@plt+6>: push 0x1
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddb8 --> 0x400552 (<main+27>: lea rdi,[rip+0xa9] # 0x400602)
0008| 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0016| 0x7fffffffddc8 --> 0x100000000
0024| 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
0032| 0x7fffffffddd8 --> 0x7ffff7a05b97 (<__libc_start_main+231>: mov edi,eax)
0040| 0x7fffffffdde0 --> 0x1
0048| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0056| 0x7fffffffddf0 --> 0x100008000
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400430 in puts@plt ()
gdb-peda$

注:

细心的人会发现以下的一个问题。jmp [rip+0x200be2]中 [rip+0x200be2]应该等于0x601012 。使用gdb打印其结果,如下:

gdb-peda$ p $rip
$1 = (void (*)()) 0x400430 <puts@plt>
gdb-peda$ p $rip+0x200be2
$2 = (void (*)()) 0x601012

为什么 jmp [rip+0x200be2]实际跳转到了0x601018呢?

使用以下命令查看.got.plt段中0x601018位置存放的GOTPLT_ADDR是什么?

gdb-peda$ x 0x601018
0x601018: 0x0000000000400436

jmp QWORD PTR [rip+0x200be2]指令将变成jmp 0x400436,并沿着以下命令执行到0x40043b <puts@plt+11>: jmp 0x400420

| 0x400436 <puts@plt+6>: push 0x0
| 0x40043b <puts@plt+11>: jmp 0x400420

jmp 0x400420命令执行后将跳转到.plt段的开头,将要执行的指令如下:

Disassembly of section .plt:

0000000000400420 <.plt>:
400420: ff 35 e2 0b 20 00 push QWORD PTR [rip+0x200be2] # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400426: ff 25 e4 0b 20 00 jmp QWORD PTR [rip+0x200be4] # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40042c: 0f 1f 40 00 nop DWORD PTR [rax+0x0]

push QWORD PTR [rip+0x200be2] # 601008命令存入栈的是什么呢?有什么作用?

使用gdb打印0x601008存放的数据,如果如下:

gdb-peda$ x 0x601008
0x601008: 0x00007ffff7ffe170
gdb-peda$ x 0x00007ffff7ffe170
0x7ffff7ffe170: 0x0000000000000000

根据Runtime Dynamic Linking文章中所说:“The next instruction pushes some info on the stack (the PLT offset) and jumps to the very first entry into the PLT, which calls into the dynamic linker’s resolution function (_dl_runtime_resolve for ld.so)”。push进栈的是一个存放在.got.plt段中表示外部函数在.plt中的偏移值的指针。

本例中中put@plt在.plt中的便宜为0。故0x7ffff7ffe170指向的数据为0。

查看进程映射信息:

gdb-peda$ i proc mappings
process 6427
Mapped address spaces:

Start Addr End Addr Size Offset objfile
..........
0x7ffff7ffe000 0x7ffff7fff000 0x1000 0x0
1
2
3
4
5
6
7
查看/proc/6427/maps中该区域的权限有rw-p:

7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0

接着看这一条指令400426: ff 25 e4 0b 20 00 jmp QWORD PTR [rip+0x200be4] # 601010。指令将首先取出.got.plt段中0x601010处的数据。使用gdb打印0x601010处的数据,如下:

gdb-peda$ x 0x601010
0x601010: 0x00007ffff7dec7a0

通过查看进程映射信息,可以看到0x7ffff7dec7a0位于/lib/x86_64-linux-gnu/ld-2.27.so动态链接库中。

gdb-peda$ i proc mappings
process 6427
Mapped address spaces:

Start Addr End Addr Size Offset objfile
0x7ffff7dd5000 0x7ffff7dfc000 0x27000 0x0 /lib/x86_64-linux-gnu/ld-2.27.so

使用gdb反汇编0x7ffff7dec7a0处的指令,如下:

gdb-peda$ disas 0x00007ffff7dec7a0
Dump of assembler code for function _dl_runtime_resolve_xsavec:
0x00007ffff7dec7a0 <+0>: push rbx
0x00007ffff7dec7a1 <+1>: mov rbx,rsp
0x00007ffff7dec7a4 <+4>: and rsp,0xffffffffffffffc0
.....
0x00007ffff7dec815 <+117>: call 0x7ffff7de4e40 <_dl_fixup> ; 调用dl_fixup函数完成对未定义符号的修复操作。
.....
0x00007ffff7dec84e <+174>: mov rbx,QWORD PTR [rsp]
0x00007ffff7dec852 <+178>: add rsp,0x18
0x00007ffff7dec856 <+182>: bnd jmp r11
End of assembler dump.

汇编结果如上所示,这段是_dl_runtime_resolve_xsavec函数的反汇编。该函数的主要作用是:调用dl_fixup函数完成对未定义符号的修复操作。

使用gdb命令完成当前puts@plt的调用,将返回到main函数。当前rip指向0x400552 (<main+27>: lea rdi,[rip+0xa9] # 0x400602),如下所示:

[----------------------------------registers-----------------------------------]
RAX: 0xe
RBX: 0x0
RCX: 0x7ffff7af4264 (<__GI___libc_write+20>: cmp rax,0xfffffffffffff000)
RDX: 0x7ffff7dd18c0 --> 0x0
RSI: 0x602260 ("Hello world1!\n")
RDI: 0x1
RBP: 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
RIP: 0x400552 (<main+27>: lea rdi,[rip+0xa9] # 0x400602)
R8 : 0x0
R9 : 0x0
R10: 0x602010 --> 0x0
R11: 0x246
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400542 <main+11>: mov QWORD PTR [rbp-0x10],rsi
0x400546 <main+15>: lea rdi,[rip+0xa7] # 0x4005f4
0x40054d <main+22>: call 0x400430 <puts@plt>
=> 0x400552 <main+27>: lea rdi,[rip+0xa9] # 0x400602
0x400559 <main+34>: call 0x400430 <puts@plt>
0x40055e <main+39>: mov edi,0x0
0x400563 <main+44>: call 0x400440 <exit@plt>
0x400568: nop DWORD PTR [rax+rax*1+0x0]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0008| 0x7fffffffddc8 --> 0x100000000
0016| 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
0024| 0x7fffffffddd8 --> 0x7ffff7a05b97 (<__libc_start_main+231>: mov edi,eax)
0032| 0x7fffffffdde0 --> 0x1
0040| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0048| 0x7fffffffddf0 --> 0x100008000
0056| 0x7fffffffddf8 --> 0x400537 (<main>: push rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
main (argc=0x1, argv=0x7fffffffdeb8) at plt.c:6
6 puts("Hello world2!");

继续使用si命令,rip将指向0x400559 <main+34>: call 0x400430 <puts@plt>,即将再次调用puts@plt函数。

继续使用si命令,rip将指向0x400430 <puts@plt>: jmp QWORD PTR [rip+0x200be2] # 0x601018。如下所示:

[----------------------------------registers-----------------------------------]
RAX: 0xe
RBX: 0x0
RCX: 0x7ffff7af4264 (<__GI___libc_write+20>: cmp rax,0xfffffffffffff000)
RDX: 0x7ffff7dd18c0 --> 0x0
RSI: 0x602260 ("Hello world1!\n")
RDI: 0x400602 ("Hello world2!")
RBP: 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffddb8 --> 0x40055e (<main+39>: mov edi,0x0)
RIP: 0x400430 (<puts@plt>: jmp QWORD PTR [rip+0x200be2] # 0x601018)
R8 : 0x0
R9 : 0x0
R10: 0x602010 --> 0x0
R11: 0x246
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400421: xor eax,0x200be2
0x400426: jmp QWORD PTR [rip+0x200be4] # 0x601010
0x40042c: nop DWORD PTR [rax+0x0]
=> 0x400430 <puts@plt>: jmp QWORD PTR [rip+0x200be2] # 0x601018
| 0x400436 <puts@plt+6>: push 0x0
| 0x40043b <puts@plt+11>: jmp 0x400420
| 0x400440 <exit@plt>: jmp QWORD PTR [rip+0x200bda] # 0x601020
| 0x400446 <exit@plt+6>: push 0x1
|-> 0x7ffff7a64a30 <_IO_puts>: push r13
0x7ffff7a64a32 <_IO_puts+2>: push r12
0x7ffff7a64a34 <_IO_puts+4>: mov r12,rdi
0x7ffff7a64a37 <_IO_puts+7>: push rbp
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddb8 --> 0x40055e (<main+39>: mov edi,0x0)
0008| 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0016| 0x7fffffffddc8 --> 0x100000000
0024| 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
0032| 0x7fffffffddd8 --> 0x7ffff7a05b97 (<__libc_start_main+231>: mov edi,eax)
0040| 0x7fffffffdde0 --> 0x1
0048| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe23d ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt")
0056| 0x7fffffffddf0 --> 0x100008000
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400430 in puts@plt ()

使用gdb查看.got.plt段中0x601018处的值,如下所示:

gdb-peda$ x 0x601018
0x601018: 0x00007ffff7a64a30
1
2
反汇编0x00007ffff7a64a30处的代码,可以发现此处已经是_IO_puts函数的反汇编代码,这是_IO_puts函数的地址。说明经过第一次调用puts@plt后,puts正确的函数地址已经通过动态链接器中_dl_runtime_resolve_xsavec函数解析并且填充到了.got.plt段中。

gdb-peda$ disas 0x00007ffff7a64a30
Dump of assembler code for function _IO_puts:
0x00007ffff7a64a30 <+0>: push r13
0x00007ffff7a64a32 <+2>: push r12
0x00007ffff7a64a34 <+4>: mov r12,rdi
......
0x00007ffff7a64c21 <+497>: add rsp,0x80
0x00007ffff7a64c28 <+504>: mov rdi,rsi
0x00007ffff7a64c2b <+507>: call 0x7ffff7a05e70 <_Unwind_Resume>
End of assembler dump.
gdb-peda$

到此外部函数的延迟调用时,外部函数符号的解析过程已经完成。

1.4对比Partial Full下的符号查找过程
同样使用1.1中的小程序,使用如下命令进行编译

$ gcc -z relro -z now -no-pie -g -o plt_full plt.c
1
使用checksec工具查看一下Partial Full模式是开启状态。

gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : FULL

使用一下命令查看Section Header信息:

There are 33 section headers, starting at offset 0x2178:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[12] .plt PROGBITS 0000000000400420 000420 000030 10 AX 0 0 16
[13] .text PROGBITS 0000000000400450 000450 000192 00 AX 0 0 16
[21] .got PROGBITS 0000000000600fc8 000fc8 000038 08 WA 0 0 8
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)

结果如下:

There are 33 section headers, starting at offset 0x2178:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[12] .plt PROGBITS 0000000000400420 000420 000030 10 AX 0 0 16
[13] .text PROGBITS 0000000000400450 000450 000192 00 AX 0 0 16
[21] .got PROGBITS 0000000000600fc8 000fc8 000038 08 WA 0 0 8
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)

main函数的反汇编结果如下:

0000000000400537 <main>:
400537: 55 push rbp
400538: 48 89 e5 mov rbp,rsp
40053b: 48 83 ec 10 sub rsp,0x10
40053f: 89 7d fc mov DWORD PTR [rbp-0x4],edi
400542: 48 89 75 f0 mov QWORD PTR [rbp-0x10],rsi
400546: 48 8d 3d a7 00 00 00 lea rdi,[rip+0xa7] # 4005f4 <_IO_stdin_used+0x4>
40054d: e8 de fe ff ff call 400430 <puts@plt>
400552: 48 8d 3d a9 00 00 00 lea rdi,[rip+0xa9] # 400602 <_IO_stdin_used+0x12>
400559: e8 d2 fe ff ff call 400430 <puts@plt>
40055e: bf 00 00 00 00 mov edi,0x0
400563: e8 d8 fe ff ff call 400440 <exit@plt>
400568: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0]
40056f: 00

gdb调试运行plt_full,在0x40054d处下断点。程序运行到断点处的调试结果如下图所示:

[----------------------------------registers-----------------------------------]
RAX: 0x400537 (<main>: push rbp)
RBX: 0x0
RCX: 0x400570 (<__libc_csu_init>: push r15)
RDX: 0x7fffffffdec8 --> 0x7fffffffe268 ("CLUTTER_IM_MODULE=xim")
RSI: 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
RDI: 0x4005f4 ("Hello world1!")
RBP: 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
RIP: 0x40054d (<main+22>: call 0x400430 <puts@plt>)
R8 : 0x7ffff7dd0d80 --> 0x0
R9 : 0x7ffff7dd0d80 --> 0x0
R10: 0x2
R11: 0x7
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x40053f <main+8>: mov DWORD PTR [rbp-0x4],edi
0x400542 <main+11>: mov QWORD PTR [rbp-0x10],rsi
0x400546 <main+15>: lea rdi,[rip+0xa7] # 0x4005f4
=> 0x40054d <main+22>: call 0x400430 <puts@plt>
0x400552 <main+27>: lea rdi,[rip+0xa9] # 0x400602
0x400559 <main+34>: call 0x400430 <puts@plt>
0x40055e <main+39>: mov edi,0x0
0x400563 <main+44>: call 0x400440 <exit@plt>
Guessed arguments:
arg[0]: 0x4005f4 ("Hello world1!")
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
0008| 0x7fffffffddc8 --> 0x100000000
0016| 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
0024| 0x7fffffffddd8 --> 0x7ffff7a05b97 (<__libc_start_main+231>: mov edi,eax)
0032| 0x7fffffffdde0 --> 0x1
0040| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
0048| 0x7fffffffddf0 --> 0x100008000
0056| 0x7fffffffddf8 --> 0x400537 (<main>: push rbp)
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x000000000040054d 5 puts("Hello world1!");
gdb-peda$

使用gdb命令si进入puts@plt,如果如下:

[----------------------------------registers-----------------------------------]
RAX: 0x400537 (<main>: push rbp)
RBX: 0x0
RCX: 0x400570 (<__libc_csu_init>: push r15)
RDX: 0x7fffffffdec8 --> 0x7fffffffe268 ("CLUTTER_IM_MODULE=xim")
RSI: 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
RDI: 0x4005f4 ("Hello world1!")
RBP: 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffddb8 --> 0x400552 (<main+27>: lea rdi,[rip+0xa9] # 0x400602)
RIP: 0x400430 (<puts@plt>: jmp QWORD PTR [rip+0x200baa] # 0x600fe0)
R8 : 0x7ffff7dd0d80 --> 0x0
R9 : 0x7ffff7dd0d80 --> 0x0
R10: 0x2
R11: 0x7
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400421: xor eax,0x200baa
0x400426: jmp QWORD PTR [rip+0x200bac] # 0x600fd8
0x40042c: nop DWORD PTR [rax+0x0]
=> 0x400430 <puts@plt>: jmp QWORD PTR [rip+0x200baa] # 0x600fe0
| 0x400436 <puts@plt+6>: push 0x0
| 0x40043b <puts@plt+11>: jmp 0x400420
| 0x400440 <exit@plt>: jmp QWORD PTR [rip+0x200ba2] # 0x600fe8
| 0x400446 <exit@plt+6>: push 0x1
|-> 0x7ffff7a64a30 <_IO_puts>: push r13
0x7ffff7a64a32 <_IO_puts+2>: push r12
0x7ffff7a64a34 <_IO_puts+4>: mov r12,rdi
0x7ffff7a64a37 <_IO_puts+7>: push rbp
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffddb8 --> 0x400552 (<main+27>: lea rdi,[rip+0xa9] # 0x400602)
0008| 0x7fffffffddc0 --> 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
0016| 0x7fffffffddc8 --> 0x100000000
0024| 0x7fffffffddd0 --> 0x400570 (<__libc_csu_init>: push r15)
0032| 0x7fffffffddd8 --> 0x7ffff7a05b97 (<__libc_start_main+231>: mov edi,eax)
0040| 0x7fffffffdde0 --> 0x1
0048| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
0056| 0x7fffffffddf0 --> 0x100008000
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000000000400430 in puts@plt ()

查看rip指向的指令0x400430 <puts@plt>: jmp QWORD PTR [rip+0x200baa] # 0x600fe0,将要跳转到0x600fe0中存储的数据地址。

注意:

回过头看Section Header中的信息发现:0x600fe0地址在.got段中。而Partial RELRO模式下的.got.plt段却消失了。

查看0x600fe0处的数据

gdb-peda$ x 0x600fe0
0x600fe0: 0x00007ffff7a64a30
1
2
反汇编0x00007ffff7a64a30处的代码,可以发现已经是_IO_puts函数的代码了。

gdb-peda$ disas 0x00007ffff7a64a30
Dump of assembler code for function _IO_puts:
0x00007ffff7a64a30 <+0>: push r13
0x00007ffff7a64a32 <+2>: push r12
0x00007ffff7a64a34 <+4>: mov r12,rdi
..........
0x00007ffff7a64c28 <+504>: mov rdi,rsi
0x00007ffff7a64c2b <+507>: call 0x7ffff7a05e70 <_Unwind_Resume>
End of assembler dump.
gdb-peda$

疑问???

以上例子,位于.got段中0x600fe0地址处的正确的_IO_puts函数地址是编译生成的?还是在加载程序的时候由动态链接器填写进去的呢?

查看Section Header中.got段的信息,如下:

There are 33 section headers, starting at offset 0x2178:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[21] .got PROGBITS 0000000000600fc8 000fc8 000038 08 WA 0 0 8
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)

.got段的Flg为WA,该段是可写的。

其次读取程序静态时的.got段的数据,如下图所示:

$ readelf -x 21 plt_full

Hex dump of section '.got':
NOTE: This section has relocations against it, but these have NOT been applied to this dump.
0x00600fc8 d80d6000 00000000 00000000 00000000 ..`.............
0x00600fd8 00000000 00000000 36044000 00000000 ........6.@.....
0x00600fe8 46044000 00000000 00000000 00000000 F.@.............
0x00600ff8 00000000 00000000 ........

由于ELF是小端字节序(little endian),可以发现0x600fe0地址处的数据是0x0000000036044000,并不是正确的_IO_puts函数地址0x00007ffff7a64a30。

然后调试程序,在_start程序入口处下断点。结果如下:

[----------------------------------registers-----------------------------------]
RAX: 0x1c
RBX: 0x0
RCX: 0x4
RDX: 0x7ffff7de59f0 (<_dl_fini>: push rbp)
RSI: 0x7ffff7ffe700 --> 0x0
RDI: 0x8
RBP: 0x0
RSP: 0x7fffffffdeb0 --> 0x1
RIP: 0x400450 (<_start>: xor ebp,ebp)
R8 : 0x2
R9 : 0x0
R10: 0x2
R11: 0x7
R12: 0x400450 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdeb0 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400440 <exit@plt>: jmp QWORD PTR [rip+0x200ba2] # 0x600fe8
0x400446 <exit@plt+6>: push 0x1
0x40044b <exit@plt+11>: jmp 0x400420
=> 0x400450 <_start>: xor ebp,ebp
0x400452 <_start+2>: mov r9,rdx
0x400455 <_start+5>: pop rsi
0x400456 <_start+6>: mov rdx,rsp
0x400459 <_start+9>: and rsp,0xfffffffffffffff0
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdeb0 --> 0x1
0008| 0x7fffffffdeb8 --> 0x7fffffffe233 ("/home/cmp/work_dir/how_to_exploit/ELF/test2/plt_full")
0016| 0x7fffffffdec0 --> 0x0
0024| 0x7fffffffdec8 --> 0x7fffffffe268 ("CLUTTER_IM_MODULE=xim")
0032| 0x7fffffffded0 --> 0x7fffffffe27e ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
0040| 0x7fffffffded8 --> 0x7fffffffe86a ("LC_MEASUREMENT=zh_CN.UTF-8")
0048| 0x7fffffffdee0 --> 0x7fffffffe885 ("LESSCLOSE=/usr/bin/lesspipe %s %s")
0056| 0x7fffffffdee8 --> 0x7fffffffe8a7 ("LC_PAPER=zh_CN.UTF-8")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 2, 0x0000000000400450 in _start ()

使用如下命令查看.got段中的数据,如下:

gdb-peda$ x /14wx 0x00600fc8
0x600fc8: 0x00600dd8 0x00000000 0x00000000 0x00000000
0x600fd8: 0x00000000 0x00000000 0xf7a64a30 0x00007fff
0x600fe8: 0xf7a271d0 0x00007fff 0xf7a05ab0 0x00007fff
0x600ff8: 0x00000000 0x00000000

由于ELF是小端字节序(little endian),0x600fe0地址处的数据是0x00007ffff7a64a30,在程序入口处已经是正确的_IO_puts函数地址0x00007ffff7a64a30。

Full RELRO模式下,ELF文件中不会出现.got.plt段。所有的外部函数的符号,均在程序载入时被动态链接器解释后填充到.got段(Flg为WA,具有可写权限)中的相应位置。

1.5总结一下
在开启Partial RELRO时,外部函数的符号地址并不会事先填入.got.plt段中。
当第一次调用外部函数时,将利用碰床机制,通过.got.plt使rip,再次跳转.plt的头部触发动态链接器_dl_runtime_resolve_xsavec解析符号地址。
当完成解析后,正确的符号地址将填入.got.plt对应的位置中。
再次调用该外部函数时,由于.got.plt中已经正确的地址,而不需要重复解析了。
Full RELRO模式下,ELF文件中不会出现.got.plt段。所有的外部函数的符号,均在程序载入时被动态链接器解释后填充到.got段(Flg为WA,具有可写权限)中的相应位置。
注:

延迟符号解析可以避免对没有调用的函数进行昂贵的查找。可以在编译选项中添加-z relro -z lazy开启Partial RELRO。已经编译好的Partial RELRO程序,您也可以通过设置LD_BIND_NOW环境变量来强制链接器在程序启动时对所有符号进行解析。
在编译选项中添加-z relro -z now开启Full RELRO。
2.参考
Runtime Dynamic Linking(害怕万一这个外网网站访问不到了,copy下来(__) ):
Dynamically linked binaries (usually) resolve external function calls lazily through what’s called the Procedure Linkage Table (PLT). The PLT holds an entry for each external function reference. When the function, say printf, is first called, it jumps to a known offset within the PLT corresponding to that function. This location contains a few instructions. The first performs an indirect jump into an entry of the Global Offset Table (GOT). At first, this entry contains the address of the instruction following the previous jump. This method is commonly known as trampolining. The next instruction pushes some info on the stack (the PLT offset) and jumps to the very first entry into the PLT, which calls into the dynamic linker’s resolution function (_dl_runtime_resolve for ld.so).

This call is simply another indirect jump into the GOT. The first three entries of the GOT are reserved, and are filled in by the dynamic linker on program startup. GOT[0] is the address of the program’s .dynamic segment. This segment holds a lot of pointers to other parts of the ELF. It basically serves as a guide for the dynamic linker to navigate the ELF.

GOT[1] is the pointer to a data structure that the dynamic linker manages. This data structure is a linked list of nodes corresponding to the symbol tables for each shared library linked with the program. When a symbol is to be resolved by the linker, this list is traversed to find the appropriate symbol. Using the LD_PRELOAD environment variable basically ensures that your preload library will be the first node on this list.

Finally, GOT[2] is the address osf the symbol resolution function within the dynamic linker. In ld.so, it contains the address of the function named _dl_runtime_resolve, which is basically an assembly stub that does some register/stack setup and calls into a C function called dl_fixup(). dl_fixup is the workhorse that actually resolves the symbol in question. Once the symbol’s address is found, the program’s GOT entry for it must be patched. This is also the job of dl_fixup(). Once dl_fixup() patches the correct GOT entry, the next time the function is called, it will again jump to the PLT entry, but this time the indirect jump there will go to the symbol’s address instead of the following instruction.

This method of lazy symbol resolution avoids costly lookups for functions that aren’t even called. You can force the linker to eagerly resolve symbols on program startup by setting the LD_BIND_NOW environment variable.
————————————————
版权声明:本文为CSDN博主「ronnie88597」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_46222091/article/details/108668735

posted @ 2022-02-05 09:23  Sky&Zhang  阅读(214)  评论(0编辑  收藏  举报