PLT与GOT

0x01  什么是PLT和GOT数据结构

名称:函数

  • PLT : 程序连接表(PLT,Procedure Link Table)
  • GOT : 重局偏移表(GOT, Global Offset Table)

原因:spa

  这缘起于动态连接,动态连接须要考虑的各类因素,但实际总结起来讲两点:调试

  • 须要存放外部函数的数据段  —— PLT
  • 获取数据段存放函数地址的一小段额外代码 —— GOT

 

  若是可执行文件中调用多个动态库函数,那每一个函数都须要这两样东西,这样每样东西就造成一个表,每一个函数使用中的一项。code

  存放函数地址的数据表,称为全局偏移表(GOT, Global Offset Table),而那个额外代码段表,称为程序连接表(PLT,Procedure Link Table)。blog

 

 

内容:进程

  举个例子,对于一个函数,这里命名为common,其plt以下:get

080482a0 <common@plt>:
80482a0: pushl 0x80496f0
80482a6: jmp *0x80496f4
      ...

 


第一句,pushl 0x80496f0,是将地址压到栈上,也即向最终调用的函数传递参数。
第二句,jmp *0x80496f4,这是跳到最终的函数去执行,不过猜猜就能想到,这是跳到能解析动态库函数地址的代码里面执行。it

 

0x80496f4属于GOT表中的一项,进程尚未运行时它的值是0x00000000,当进程运行起来后,它的值变成了0xf7ff06a0。io

若是作更进一步的调试会发现这个地址位于动态连接器内,对应的函数是_dl_runtime_resolve。(相应的过程图在下面贴出)

 

若是将PLT和GOT抽象起来描述,能够写成如下的伪代码:

plt[0]:
   pushl got[1]
   jmp *got[2]

plt[n]:              // n >= 1
   jmp *got[n+2]           

      // GOT前3项为公共项,第3项开始才是函数项,plt[1]对应的GOT[3],依次类推
   push (n-1)*8
   jmp plt[0]

—————————————————————————————————————————

got[0] = address of .dynamic section                    

      //本ELF动态段(.dynamic段)的装载地址 
got[1] = address of link_map object( 编译时填充0)           

      //本ELF的link_map数据结构描述符地址 
got[2] = address of _dl_runtime_resolve function (编译时填充为0)     

      //_dl_runtime_resolve函数的地址
got[n+2] = plt[n] + 6 (即plt[n]代码片断的第二条指令)

 

 

特色:

PLT表结构有如下特色:

PLT表中的第一项为公共表项,剩下的是每一个动态库函数为一项(固然每项是由多条指令组成的,jmp *0xXXXXXXXX这条指令是全部plt的开始指令)

每项PLT都从对应的GOT表项中读取目标函数地址

GOT表结构有如下特色:

GOT表中前3个为特殊项,分别用于保存 .dynamic段地址、本镜像的link_map数据结构地址和_dl_runtime_resolve函数地址;

但在编译时,没法获取知道link_map地址和_dl_runtime_resolve函数地址,因此编译时填零地址,进程启动时由动态连接器进行填充3个特殊项后面依次是每一个动态库函数的GOT表项

 

注意点:

以printf函数为例,三个问题:

  • _dl_runtime_resolve是怎么知要查找printf函数的
  • _dl_runtime_resolve找到printf函数地址以后,它怎么知道回填到哪一个GOT表项
  • 到底_dl_runtime_resolve是何时被写到GOT表的
printf@plt>:
   jmp *0x80496f8
   push $0x00
   jmp common@plt

 

  每一个xxx@plt的第二条指令push的操做数都是不同的,它就至关于函数的id,动态连接器经过它就能够知道是要解析哪一个函数了。

 

它俩的运行关系以下:

 

0x02  重定位

重定位分为如下三种:

  • 连接重定位:将一个或多个中间文件(.o文件)经过连接器将它们连接成一个可执行文件。其中分为两种状况:
  1. 若是是在其余中间文件中已经定义了的函数,连接阶段能够直接重定位到函数地址
  2. 若是是在动态库中定义了的函数,连接阶段没法直接重定位到函数地址,只能生成额外的小片断代码,也就是PLT表,而后重定位到该代码片断
  • 运行重定位:运行后加载动态库,把动态库中的相应函数地址填入GOT表,因为PLT表是跳转到GOT表的,这就构成了运行时重定位
  • 延迟重定位:只有动态库函数在被调用时,才会进行地址解析和重定位工做,这时候动态库函数的地址才会被写入到GOT表项中,过程以下图:

 

 

  PLT属于代码段,在进程加载和运行过程都不会发生改变,PLT指向GOT表的关系在编译时已彻底肯定,惟一能发生变化的是GOT表。

 

示例:

重定位时:

 

 重定位后:

  

 

 

0X03  在PWN中的应用 —— ret2libc

 应用场景:

  在一些提供单独 libc(版本号).so的pwn题中,大部分状况是要在这个so文件中寻找一些函数的偏移地址,并且大部分状况下,为了方便,只会使用已经在程序中出现的函数的got表中的实际地址,咱们能够直接把这个so文件拖进IDA中进行寻找,

  经典的例题如Jarvis oj上面的pwn level3,这里只给出较好的wp地址,能够看到,用read函数写入,而后return已经使用过一次的write函数的plt地址,从而调用这个函数(以后的那个padding是系统call用来跳转执行的下一个地址,能够deadbeef也能够换成想要执行的函数地址),继而是对ret的这个write函数的参数进行输入,其中第二个参数就是要输出在显示屏上的内容,所以咱们填入write的got表地址,输出write的真实地址,进而基地址就能够由  基地址= 真实地址-偏移地址算出。

相关文章
相关标签/搜索