通过GDB调试理解GOT表和PLT表

(清楚下述动态解析的过程,有助于理解GOT覆写利用)

关于Linux中ELF文件格式可以参考文档《ELF_Format》
文档链接:http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf
GOT(Global Offset Table): 全局偏移表用于记录在ELF文件中所用到的共享库的绝对地址。在程序刚开始运行时,GOT表项是空的,当符号第一次被调用时会动态解析符号的绝对地址然后转去执行,并将被解析符号的绝对地址记录在GOT表中,第二次调用同一符号时,由于GOT表中已经记录了其绝对地址,直接转去执行即可(不用重新解析)。

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

GOT位于.got.plt的section中,而PLT位于.plt的section中。下面给出一示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib.h>
int main(int argc,char* argv[]){
if(argc<2)
{
printf("argv[1] required!\n");
exit(0);
}
printf("You input:");
printf("argv[1]);
printf("Down\n")
return 0;
}

编译该程序

1
gcc -o format format.c

然后我们通过readelf命令来查看format程序的section信息,并检查GOT:
1.png
从上图可看到,该ELF文件共包含29个section,有关GOT的重定向:
2.png
.rel.dyn记录了加载时需要重定位的变量,.rel.plt记录的是需要重定位的函数。

接下来,我们使用GDB来对程序进行调试,观察程序在调用printf()函数时,GOT的变化情况。

因为程序逻辑需要输入参数,设置好参数后,在主函数处下断点,然后运行,单步调试来到printf()函数调用的地方:
3.png
这里可以看到在0x080484ab处指令为:

1
call 0x8048330 <printf@plt>

然后查看一下0x8048330处的代码:
4.png
可以看到流程会跳转到ds[0x804a00c]处,而0x804a00c是printf()重定位偏移(查看上面GOT信息图),接着看一下后面的流程都做了什么:
5.png
根据上面的流程分析,进行单步调试,当动态解析(_dl_runtime_resolve)完成后,流程会直接跳转到printf()函数主体:
6.png
上面我们说过,当第一次调用动态符号时会动态解析其绝对地址并写到GOT中,下次调用的时候就不用再次解析了,我们来看看这个时候原先0x804a00c处的指向情况:
7.png
其所指向的地址正好为第一次解析后得到的printf()函数的入口地址。
程序中,printf()函数调用过程可以总结为:
8.png
总结来说就是,GOT保存了程序中所要调用的函数的地址,运行一开时其表项为空,会在运行时实时的更新表项。一个符号调用在第一次时会解析出绝对地址更新到GOT中,第二次调用时就直接找到了GOT表项所存储的函数地址直接调用了。

(清楚上述动态解析过程,有助于理解GOT的覆写利用)