阅读本文前需要阅读 https://www.zhihu.com/question/21249496 和回答后面的四个链接。
plt && got 表
之前提到有些函数实现的代码是在 libc 中,但是编译时和运行时,libc 可能是不同的版本,函数的偏移位置也不一样,这种情况下,操作系统是怎么找到对应的函数的呢?
以下面的代码为例
#include <stdio.h>
#include <stdlib.h>
void world() {
printf("World");
}
int main(int argc, char* argv[]) {
printf("Hello ");
world();
return 0;
}
main
函数的汇编代码可以看到
gdb-peda$ pdisas main
Dump of assembler code for function main:
0x000000000040053c <+0>: push rbp
0x000000000040053d <+1>: mov rbp,rsp
0x0000000000400540 <+4>: sub rsp,0x10
0x0000000000400544 <+8>: mov DWORD PTR [rbp-0x4],edi
0x0000000000400547 <+11>: mov QWORD PTR [rbp-0x10],rsi
0x000000000040054b <+15>: mov edi,0x4005fa
0x0000000000400550 <+20>: mov eax,0x0
0x0000000000400555 <+25>: call 0x400400 <printf@plt>
0x000000000040055a <+30>: mov eax,0x0
0x000000000040055f <+35>: call 0x400526 <world>
0x0000000000400564 <+40>: mov eax,0x0
0x0000000000400569 <+45>: leave
0x000000000040056a <+46>: ret
在两个 printf
那里下断点,然后 si
跟进第一个 printf@plt
看下。
=> 0x400400 <printf@plt>: jmp QWORD PTR [rip+0x200c12] # 0x601018
| 0x400406 <printf@plt+6>: push 0x0
| 0x40040b <printf@plt+11>: jmp 0x4003f0
| 0x400410 <__libc_start_main@plt>: jmp QWORD PTR [rip+0x200c0a] # 0x601020
| 0x400416 <__libc_start_main@plt+6>: push 0x1
|-> 0x400406 <printf@plt+6>: push 0x0
0x40040b <printf@plt+11>: jmp 0x4003f0
0x400410 <__libc_start_main@plt>: jmp QWORD PTR [rip+0x200c0a] # 0x601020
0x400416 <__libc_start_main@plt+6>: push 0x1
这时候 0x601018
存储的地址就是 0x400406
,对应的是 printf@plt+6
,跳转过去之后,接下来又是 jmp 0x4003f0
,之后就是 _dl_runtime_resolve_avx
函数了。
0x4003f0: push QWORD PTR [rip+0x200c12] # 0x601008
=> 0x4003f6: jmp QWORD PTR [rip+0x200c14] # 0x601010
| 0x4003fc: nop DWORD PTR [rax+0x0]
| 0x400400 <printf@plt>: jmp QWORD PTR [rip+0x200c12] # 0x601018
| 0x400406 <printf@plt+6>: push 0x0
| 0x40040b <printf@plt+11>: jmp 0x4003f0
|-> 0x7ffff7dee870 <_dl_runtime_resolve_avx>: push rbx
0x7ffff7dee871 <_dl_runtime_resolve_avx+1>: mov rbx,rsp
0x7ffff7dee874 <_dl_runtime_resolve_avx+4>: and rsp,0xffffffffffffffe0
0x7ffff7dee878 <_dl_runtime_resolve_avx+8>: sub rsp,0x180
接下来就是一大堆指令,没耐心看了,直接按 c
到了第二个 printf@plt
那里,这时候发现第一个 jmp
的行为已经不一样了。
=> 0x400400 <printf@plt>: jmp QWORD PTR [rip+0x200c12] # 0x601018
| 0x400406 <printf@plt+6>: push 0x0
| 0x40040b <printf@plt+11>: jmp 0x4003f0
| 0x400410 <__libc_start_main@plt>: jmp QWORD PTR [rip+0x200c0a] # 0x601020
| 0x400416 <__libc_start_main@plt+6>: push 0x1
|-> 0x7ffff7a62800 <__printf>: sub rsp,0xd8
0x7ffff7a62807 <__printf+7>: test al,al
0x7ffff7a62809 <__printf+9>: mov QWORD PTR [rsp+0x28],rsi
0x7ffff7a6280e <__printf+14>: mov QWORD PTR [rsp+0x30],rdx
这时候 0x601018
指向的地址已经是 0x7ffff7a62800
,在 __printf
中,而之前是在 printf@plt
中。
上面的流程可以看出,对于动态库来说,可执行文件在运行前并不知道它的真实地址,而是需要运行时动态的去解析才能知道,专业的说法是运行时重定位。
上面已经有 plt 的概念了
PLT(Procedure Linkage Table)作用是将位置无关的符号转移到绝对地址。当一个外部符号被调用时,PLT 去引用 GOT 中的其符号对应的绝对地址,然后转入并执行。
GOT(Global Offset Table)用于记录在 ELF 文件中所用到的共享库中符号的绝对地址。在程序刚开始运行时,GOT 表项是空的,当符号第一次被调用时会动态解析符号的绝对地址然后转去执行,并将被解析符号的绝对地址记录在 GOT 中,第二次调用同一符号时,由于 GOT 中已经记录了其绝对地址,直接转去执行即可(不用重新解析)。
plt 表的信息也可以通过 readelf -r exe_file
看到
Relocation section '.rela.plt' at offset 0x398 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
利用
- 如果代码存在任意写,那就可以把某些函数的 got 表改写为 shellcode 的地址,和覆盖返回地址的原理是一样的。
- 如果代码存在任意读,那就可以读取某些函数的 got 表中的地址,然后结合 libc 推算出 libc 的加载基址。
## 练习题