动态链接库工作原理 PLT GOT
PLT,内部函数表,表中的内容是静态链接系统添加的。
GOT,全局函数表,表中的内容:
- 初始时:静态链接系统添加的
- 运行时:动态链接器更新的
我们用一个非常简单的例子来讲解,代码如下:
void win() {printf("you win\n");
}int main() {char a[100];gets(a);printf("you lose\n");return 0;
}
依赖的动态库
编译后要依赖的动态库如下:
zuanmin.lzm@localhost:~/test/pltgot$ ldd ./a.outlinux-vdso.so.1 (0x00007ffcbf9c1000)libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f05bc8ab000)/lib64/ld-linux-x86-64.so.2 (0x00007f05bcec4000)
PLT 和 GOT 段
生成的 ELF 里面的 “段” 如下:
zuanmin.lzm@localhost:~/test/pltgot$ readelf -S ./a.out
There are 34 section headers, starting at offset 0x1d08:节头:[号] 名称 类型 地址 偏移量大小 全体大小 旗标 链接 信息 对齐[ 0] NULL 0000000000000000 000000000000000000000000 0000000000000000 0 0 0[ 1] .interp PROGBITS 0000000000000238 00000238000000000000001c 0000000000000000 A 0 0 1[ 5] .dynsym DYNSYM 00000000000002b8 000002b800000000000000d8 0000000000000018 A 6 1 8[ 6] .dynstr STRTAB 0000000000000390 0000039000000000000000a2 0000000000000000 A 0 0 1[ 9] .rela.dyn RELA 0000000000000478 0000047800000000000000c0 0000000000000018 A 5 0 8[10] .rela.plt RELA 0000000000000538 000005380000000000000048 0000000000000018 AI 5 22 8[11] .init PROGBITS 0000000000000580 000005800000000000000017 0000000000000000 AX 0 0 4[12] .plt PROGBITS 00000000000005a0 000005a00000000000000040 0000000000000010 AX 0 0 16[13] .plt.got PROGBITS 00000000000005e0 000005e00000000000000008 0000000000000008 AX 0 0 8[14] .text PROGBITS 00000000000005f0 000005f000000000000001e2 0000000000000000 AX 0 0 16[15] .fini PROGBITS 00000000000007d4 000007d40000000000000009 0000000000000000 AX 0 0 4[16] .rodata PROGBITS 00000000000007e0 000007e00000000000000015 0000000000000000 A 0 0 4[17] .eh_frame_hdr PROGBITS 00000000000007f8 000007f80000000000000044 0000000000000000 A 0 0 4[18] .eh_frame PROGBITS 0000000000000840 000008400000000000000128 0000000000000000 A 0 0 8[19] .init_array INIT_ARRAY 0000000000200da8 00000da80000000000000008 0000000000000008 WA 0 0 8[20] .fini_array FINI_ARRAY 0000000000200db0 00000db00000000000000008 0000000000000008 WA 0 0 8[21] .dynamic DYNAMIC 0000000000200db8 00000db800000000000001f0 0000000000000010 WA 6 0 8[22] .got PROGBITS 0000000000200fa8 00000fa80000000000000058 0000000000000008 WA 0 0 8[23] .data PROGBITS 0000000000201000 000010000000000000000010 0000000000000000 WA 0 0 8[24] .bss NOBITS 0000000000201010 000010100000000000000008 0000000000000000 WA 0 0 1[31] .symtab SYMTAB 0000000000000000 000012e800000000000006a8 0000000000000018 32 48 8[32] .strtab STRTAB 0000000000000000 000019900000000000000234 0000000000000000 0 0 1[33] .shstrtab STRTAB 0000000000000000 00001bc4000000000000013e 0000000000000000 0 0 1
PLT 表
从上面的“段”看出,PLT 的地址是0x5a0,可以明显看出共有4个表项。
- PLT[0],公共 plt 代码,会跳转到 GOT[2]
- PLT[1],puts@plt,第一句跳转 0x200fc0,位于GOT表中
- PLT[2],__stack_chk_fail&plt,第一句跳转 0x200fc8,位于GOT表中
- PLT[3],gets@plt,第一句跳转 0x200fd0,位于GOT表中
gdb-peda$ x /56i 0x5a00x5a0: push QWORD PTR [rip+0x200a0a] # 0x200fb00x5a6: jmp QWORD PTR [rip+0x200a0c] # 0x200fb80x5ac: nop DWORD PTR [rax+0x0]0x5b0 <puts@plt>: jmp QWORD PTR [rip+0x200a0a] # 0x200fc00x5b6 <puts@plt+6>: push 0x00x5bb <puts@plt+11>: jmp 0x5a00x5c0 <__stack_chk_fail@plt>: jmp QWORD PTR [rip+0x200a02] # 0x200fc80x5c6 <__stack_chk_fail@plt+6>: push 0x10x5cb <__stack_chk_fail@plt+11>: jmp 0x5a00x5d0 <gets@plt>: jmp QWORD PTR [rip+0x2009fa] # 0x200fd00x5d6 <gets@plt+6>: push 0x20x5db <gets@plt+11>: jmp 0x5a0
更直观的 PLT 表:
zuanmin.lzm@localhost:~/test/pltgot$ objdump -D -S ./a.outDisassembly of section .plt:00000000000005a0 <.plt>:5a0: ff 35 0a 0a 20 00 pushq 0x200a0a(%rip) # 200fb0 <_GLOBAL_OFFSET_TABLE_+0x8>5a6: ff 25 0c 0a 20 00 jmpq *0x200a0c(%rip) # 200fb8 <_GLOBAL_OFFSET_TABLE_+0x10>5ac: 0f 1f 40 00 nopl 0x0(%rax)00000000000005b0 <puts@plt>:5b0: ff 25 0a 0a 20 00 jmpq *0x200a0a(%rip) # 200fc0 <puts@GLIBC_2.2.5>5b6: 68 00 00 00 00 pushq $0x05bb: e9 e0 ff ff ff jmpq 5a0 <.plt>00000000000005c0 <__stack_chk_fail@plt>:5c0: ff 25 02 0a 20 00 jmpq *0x200a02(%rip) # 200fc8 <__stack_chk_fail@GLIBC_2.4>5c6: 68 01 00 00 00 pushq $0x15cb: e9 d0 ff ff ff jmpq 5a0 <.plt>00000000000005d0 <gets@plt>:5d0: ff 25 fa 09 20 00 jmpq *0x2009fa(%rip) # 200fd0 <gets@GLIBC_2.2.5>5d6: 68 02 00 00 00 pushq $0x25db: e9 c0 ff ff ff jmpq 5a0 <.plt>
GOT 表
从上面的“段”看出,GOT 的地址是0x200fa8,仔细看:
- GOT[0],dynamic 段地址,fill by 静态链接器,如下 0x200db8
- GOT[1],link_map,fill by 动态链接器
- GOT[2],__dl_runtime_resolve,fill by 动态链接器
- GOT[3],0x05b6,为PLT[1]的第二行语句
- GOT[4],0x05c6,为PLT[2]的第二行语句
- GOT[5],0x05d6,为PLT[3]的第二行语句
gdb-peda$ x /72x 0x200fa8
0x200fa8: 0xb8 0x0d 0x20 0x00 0x00 0x00 0x00 0x00
0x200fb0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x200fb8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x200fc0: 0xb6 0x05 0x00 0x00 0x00 0x00 0x00 0x00
0x200fc8: 0xc6 0x05 0x00 0x00 0x00 0x00 0x00 0x00
0x200fd0: 0xd6 0x05 0x00 0x00 0x00 0x00 0x00 0x00
0x200fd8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x200fe0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x200fe8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
使用 xx@plt
编译后通过 disas main
函数中的反编译代码如下:
000000000000070d <main>:int main() {70d: 55 push %rbp70e: 48 89 e5 mov %rsp,%rbp711: 48 83 ec 70 sub $0x70,%rsp715: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax71c: 00 0071e: 48 89 45 f8 mov %rax,-0x8(%rbp)722: 31 c0 xor %eax,%eaxchar a[100];gets(a);724: 48 8d 45 90 lea -0x70(%rbp),%rax728: 48 89 c7 mov %rax,%rdi72b: b8 00 00 00 00 mov $0x0,%eax730: e8 9b fe ff ff callq 5d0 <gets@plt>printf("you lose\n");735: 48 8d 3d b0 00 00 00 lea 0xb0(%rip),%rdi # 7ec <_IO_stdin_used+0xc>73c: e8 6f fe ff ff callq 5b0 <puts@plt>return 0;741: b8 00 00 00 00 mov $0x0,%eax
}746: 48 8b 55 f8 mov -0x8(%rbp),%rdx74a: 64 48 33 14 25 28 00 xor %fs:0x28,%rdx751: 00 00753: 74 05 je 75a <main+0x4d>755: e8 66 fe ff ff callq 5c0 <__stack_chk_fail@plt>75a: c9 leaveq75b: c3 retq75c: 0f 1f 40 00 nopl 0x0(%rax)
我们可以观察到 gets@plt 和 puts@plt,这两个函数,为什么后面加了个@plt,因为这个为 PLT 表中的数据的地址。那为什么反编译中的代码地址为 PLT 表中的地址呢。
原因
运行时,PLT表在代码段,不允许修改,GOT表在数据段,可以修改。PLT 表可以称为内部函数表,GOT 表为全局函数表(也可以说是动态函数表这是个人自称),这两个表是相对应的,什么叫做相对应呢,PLT 表中的数据就是 GOT 表中的一个地址,可以理解为一定是一一对应的,如下图:
PLT 表中的每一项的数据内容都是对应的GOT 表中一项的地址这个是固定不变的,到这里大家也知道了 PLT 表中的数据根本不是函数的真实地址,而是 GOT 表项的地址。
其实在大家进入带有 @plt 标志的函数时,这个函数其实就是个过渡作用,因为 GOT 表项中的数据才是函数最终的地址,而 PLT 表中的数据又是 GOT 表项的地址,我们就可以通过 PLT 表跳转到 GOT表来得到函数真正的地址。那问题来了,这个 @plt 函数是怎么来的,这个函数是编译系统自己加的(静态链接阶段),大家可以通过disas gets看看里面的代码,如下:
gdb-peda$ disas gets
Dump of assembler code for function gets@plt:0x00000000000005d0 <+0>: jmp QWORD PTR [rip+0x2009fa] # 0x200fd00x00000000000005d6 <+6>: push 0x20x00000000000005db <+11>: jmp 0x5a0
大家可以发现,这个函数只有三行代码,
- 第一行跳转,
- 第二行压栈,
- 第三行又是跳转,
解释:
第一行跳转,它的作用是通过 PLT 表跳转到 GOT 表,而在第一次运行某一个函数之前,这个函数PLT 表对应的 GOT 表中的数据为 @plt 函数中第二行指令的地址,针对PLT[3]来说步骤如下:
- jmp 指令跳转到 GOT 表 0x200fd0,
- GOT 表中的数据为 0x5d6,
- 跳转到指令地址为0x5d6,
- 执行push 0x2 #这个为.rela.plt 段中的项偏移,参照下面的.rela.plt代码,为 __dl_runtime_resolve准备参数
- 再执行jmp 0x5a0,
- 而 0x5a0 为 PLT[0] 的地址,
- 执行push [0x200fb0] # 为GOT[1]中存储的数据,即link_map的地址, 为 __dl_runtime_resolve 准备函数参数
- PLT[0] 的指令会进入 GOT 0x200fb8 # 即 PLT[2]
- GOT 0x200fb8 存储 __dl_runtime_resolve 动态链接器的地址,
- __dl_runtime_resolve根据 4 和 7 中的 参数,查询相应的符号对应的地址,
- 执行查询到的函数,并且将真正的函数地址覆盖到 GOT 表中,
__dl_runtime_resolve 函数:
_dl_runtime_resolve(link_map_obj, reloc_index)
重定位节 '.rela.plt' at offset 0x538 contains 3 entries:偏移量 信息 类型 符号值 符号名称 + 加数
000000200fc0 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000200fc8 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __stack_chk_fail@GLIBC_2.4 + 0
000000200fd0 000600000007 R_X86_64_JUMP_SLO 0000000000000000 gets@GLIBC_2.2.5 + 0
几个问题:
- PLT[0] 处到底做了什么,按照我们之前的思路它不是应该跳转到 GOT[0] 吗?
- 为什么中间要进行 push 压栈操作?
- 压入的序号为什么为 0x2,不是最开始应该为 0x0 吗?
解决问题
问题1
看下面代码:
gdb-peda$ x /40 0x5a0
0x5a0: 0xa0a35ff 0x25ff0020 0x200a0c 0x401f0f
0x5b0 <puts@plt>: 0xa0a25ff 0x680020 0xe9000000 0xffffffe0
0x5c0 <__stack_chk_fail@plt>: 0xa0225ff 0x1680020 0xe9000000 0xffffffd0
0x5d0 <gets@plt>: 0x9fa25ff 0x2680020 0xe9000000 0xffffffc0
我们尝试着查看0x5a0地址的数据内容发现一个问题,从0x5a0 - 0x5af之间的数据完全不知道是什么,而真正的PLT[x]中的数据是从 0x5b0 开始的,从这里才有了@plt为后缀的地址,但是我们disas puts 看代码的时候是从0x5a0开始的,我们可以通过x /5i 0x5a0 查看0x5a0 处的代码,如下“代码7”:
gdb-peda$ x /40i 0x5a00x5a0: push QWORD PTR [rip+0x200a0a] # 0x200fb00x5a6: jmp QWORD PTR [rip+0x200a0c] # 0x200fb80x5ac: nop DWORD PTR [rax+0x0]
我们看到了后面的#之后又一个16进制数,一看便可以知道是GOT表的地址,为什么这么肯定呢,因为我们可以通过objdump -R ./a.out查看一个程序的GOT函数的地址,如下图:
图8
zuanmin.lzm@localhost:~/test/pltgot$ objdump -R a.outa.out: 文件格式 elf64-x86-64DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0000000000200da8 R_X86_64_RELATIVE *ABS*+0x00000000000006f0
0000000000200db0 R_X86_64_RELATIVE *ABS*+0x00000000000006b0
0000000000201008 R_X86_64_RELATIVE *ABS*+0x0000000000201008
0000000000200fd8 R_X86_64_GLOB_DAT _ITM_deregisterTMCloneTable
0000000000200fe0 R_X86_64_GLOB_DAT __libc_start_main@GLIBC_2.2.5
0000000000200fe8 R_X86_64_GLOB_DAT __gmon_start__
0000000000200ff0 R_X86_64_GLOB_DAT _ITM_registerTMCloneTable
0000000000200ff8 R_X86_64_GLOB_DAT __cxa_finalize@GLIBC_2.2.5
0000000000200fc0 R_X86_64_JUMP_SLOT puts@GLIBC_2.2.5
0000000000200fc8 R_X86_64_JUMP_SLOT __stack_chk_fail@GLIBC_2.4
0000000000200fd0 R_X86_64_JUMP_SLOT gets@GLIBC_2.2.5
这里都是些GOT地址,我们发现都是0x200… 这些,所以可以断定 上面“代码7”中的也是GOT地址,那么我们可以猜想出,在正式存储一个函数的GOT地址前,我们的PLT表前面有一项进行一些处理,我们暂且不具体深入剖析这些代码有什么用,但是我们可以肯定puts@plt前面那16个字节也算是PLT表中的内容,这其实就是我们的PLT[0],正如我们之前问题提到的那样,我们的PLT[0]根本没有跳转到GOT[0],它不像我们的PLT[1]这些存储的是GOT表项的地址,它是一些代码指令,换句话说,PLT[0]
是一个函数,这个函数的作用是通过GOT[1] 和 GOT[2] 来正确绑定一个函数的正式地址到GOT表中来。
咦,这里问题好像又产生了,本来按照最开始的思路PLT[1]也是跳转到GOT[1]的,GOT[2]同理,但是这两个数据好像被PLT[0] 利用了,同时GOT[0]好像消失了,这里GOT[0]暂且不说它的作用是什么,针对GOT[1]和GOT[2]被PLT[0]利用,所以我们程序中真实情况其实是从PLT[1]到GOT[3],PLT[2]到GOT[4],所以我们推翻了我们的图4,建立一张新的处理表
而plt[0]代码做的事情则是:由于GOT[2]中存储的是动态链接器的入口地址,所以通过GOT[1]中的数据作为参数,跳转到GOT[2]所对应的函数入口地址,这个动态链接器会将一个函数的真正地址绑定到相应的GOT[x] 中。这就是PLT表和GOT表,总而言之,我们调用一个函数的时候有两种方法,一个是通过PLT表调用,一个则是通过GOT表调用,因为PLT表最终也是跳转GOT表,GOT表中则是一个函数真正的地址,这里需要注意的是,在一个函数没有运行一次之前,GOT表中的数据为@plt函数中下一条指令的地址。
问题2
中间进行的压栈是为了确定PLT对应的GOT,根据压栈的数字,找到对应的 .rela.plt 项,该项的 offset 即是GOT项的地址,比如 push 0x0 对应的 offset 为0x200fc0,也就是GOT 的 第3项,所以PLT[1]−>GOT[3],0x3就是GOT的下标3,也就是说压栈后我们跳转到PLT[0],接着PLT[0]中的指令会通过这次压栈的序号来确定操作的GOT表项为多少。
问题3
好像都在第一个问题都已经解决了,这里压入0x2的原因是因为:该gets&plt项对应的 .rela.plt 里的项索引。
000000200fd0 000600000007 R_X86_64_JUMP_SLO 0000000000000000 gets@GLIBC_2.2.5 + 0
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_18661257/article/details/54694748
其它参考:
https://blog.csdn.net/linyt/article/details/51635768
https://blog.csdn.net/linyt/article/details/51637832
https://blog.csdn.net/linyt/article/details/51893258
https://blog.werner.wiki/elf-plt-got-static-analysis/