写了一段main函数:
#include
#include
int main(int argc, char **argv)
{
printf("hello world\n");
printf("12345678\n");
_exit(0);
}
很简单,两条打印。但是printf属于glibc动态链接库,是在程序运行的时候再加载链接进来的,那么这里的main函数是如何确定printf的地址的呢?
先看下main函数的反汇编:
0000000000400566 :
400566: 55 push %rbp
400567: 48 89 e5 mov %rsp,%rbp
40056a: 48 83 ec 10 sub $0x10,%rsp
40056e: 89 7d fc mov %edi,-0x4(%rbp)
400571: 48 89 75 f0 mov %rsi,-0x10(%rbp)
400575: bf 24 06 40 00 mov $0x400624,%edi
40057a: e8 c1 fe ff ff callq 400440
40057f: bf 30 06 40 00 mov $0x400630,%edi
400584: e8 b7 fe ff ff callq 400440
400589: bf 00 00 00 00 mov $0x0,%edi
40058e: e8 9d fe ff ff callq 400430 <_exit@plt>
400593: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40059a: 00 00 00
40059d: 0f 1f 00 nopl (%rax)
很明显“callq 400440 puts@plt”表示要跳转执行printf函数,于是我们拉出来相关的反汇编:
0000000000400440 :
400440: ff 25 da 0b 20 00 jmpq *0x200bda(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
400446: 68 01 00 00 00 pushq $0x1
40044b: e9 d0 ff ff ff jmpq 400420 <_init+0x20>
首先跳转到_GLOBAL_OFFSET_TABLE_+0x20存放的地址,而此处存放的地址是0x400446,我们用gdb确认下:
(gdb) b *0x40057a
(gdb) run
Starting program: /home/liuht/workspace/elf/a.out
Breakpoint 1, 0x000000000040057a in main (argc=1, argv=0x7fffffffe488) at hello.c:5
5 printf("hello world\n");
(gdb) si
0x0000000000400440 in puts@plt ()
(gdb) p/x *0x601020
$1 = 0x400446
确实如此,但是这里并没有调用printf,而是继续做了一次跳转,跳转到0x400420 地址,我们拉出相关的反汇编:
0000000000400420 <_exit@plt-0x10>:
400420: ff 35 e2 0b 20 00 pushq 0x200be2(%rip) # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
400426: ff 25 e4 0b 20 00 jmpq *0x200be4(%rip) # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
40042c: 0f 1f 40 00 nopl 0x0(%rax)
这里继续做了一次跳转,跳转到了601010 <GLOBAL_OFFSET_TABLE+0x10>存放的地址,我们用gdb看下:
(gdb) p/x *(unsigned long long *)0x601010
$3 = 0x7ffff7deee40
很明显,这不是我们当前程序的地址,而应该是一块动态载入的代码分配的地址,这难道就是printf?我们跳进去看下:
(gdb) p/x *(unsigned long long *)0x601010
$3 = 0x7ffff7deee40
(gdb) si
_dl_runtime_resolve_xsave () at ../sysdeps/x86_64/dl-trampoline.h:71
71 ../sysdeps/x86_64/dl-trampoline.h: No such file or directory.
很明显,这不是printf,而函数名可以看出来,这是动态链接器里面的函数。动态链接器用于动态链接,所以printf的地址应该是通过_dl_runtime_resolve_xsave确定的,也就是说_dl_runtime_resolve_xsave执行完了以后,printf的地址应该才能确定。那么我们如何知道printf的地址呢?这个其实也很简单,我们直接跟踪第二次printf执行应该就可以了:
(gdb) b *0x400584
Breakpoint 4 at 0x400584: file hello.c, line 6.
(gdb) c
Continuing.
hello world
Breakpoint 4, 0x0000000000400584 in main (argc=1, argv=0x7fffffffe488) at hello.c:6
6 printf("12345678\n");
(gdb) si
0x0000000000400440 in puts@plt ()
(gdb) si
_IO_puts (str=0x400630 "12345678") at ioputs.c:33
33 ioputs.c: No such file or directory.
printf的地址放在了<GLOBAL_OFFSET_TABLE+0x20>这里,GDB确认下:
(gdb) p/x *(unsigned long long *)0x601020
$8 = 0x7ffff7a7c6a0
果然这个位置的内容被改了,而且可以确定是被动态链接器改了。
Disassembly of section .got.plt:
0000000000601000 <_GLOBAL_OFFSET_TABLE_>:
601000: 28 0e sub %cl,(%rsi)
601002: 60 (bad)
...
601017: 00 36 add %dh,(%rsi)
601019: 04 40 add $0x40,%al
60101b: 00 00 add %al,(%rax)
60101d: 00 00 add %al,(%rax)
60101f: 00 46 04 add %al,0x4(%rsi)
601022: 40 00 00 add %al,(%rax)
601025: 00 00 add %al,(%rax)
601027: 00 56 04 add %dl,0x4(%rsi)
60102a: 40 00 00 add %al,(%rax)
60102d: 00 00 add %al,(%rax)
上面的section存放动态函数的地址,但是在开始的时候并不是动态函数的实际地址,因为实际地址只有在链接器链接后才能确认,所以初始地址是跳转到动态链接器,动态链接器修改该地址,第二次执行时,就直接跳转到动态链接器修改后的地址。
这里还有一个疑问,动态链接器怎么知道要修改0x601020这个地址的内容呢?这里开始我也很不解,后来我查看了一下对应的elf section,发现了一个很可疑的section:
Relocation section '.rela.plt' at offset 0x3b8 contains 3 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 _exit@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601028 000300000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
这里指明了printf地址的位置,如果动态链接器知道printf对应该section的哪个entry,问题就解决了。为此我们再来看下:
0000000000400440 :
400440: ff 25 da 0b 20 00 jmpq *0x200bda(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
400446: 68 01 00 00 00 pushq $0x1
40044b: e9 d0 ff ff ff jmpq 400420 <_init+0x20>
这里有个1的压栈,很可能对应.rela.plt’的第一个entry,如果是的话,那就完全对的上了。为此我们旁路验证下:
0000000000400430 <_exit@plt>:
400430: ff 25 e2 0b 20 00 jmpq *0x200be2(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
400436: 68 00 00 00 00 pushq $0x0
40043b: e9 e0 ff ff ff jmpq 400420 <_init+0x20>
0000000000400440 :
400440: ff 25 da 0b 20 00 jmpq *0x200bda(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
400446: 68 01 00 00 00 pushq $0x1
40044b: e9 d0 ff ff ff jmpq 400420 <_init+0x20>
0000000000400450 <__libc_start_main@plt>:
400450: ff 25 d2 0b 20 00 jmpq *0x200bd2(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
400456: 68 02 00 00 00 pushq $0x2
40045b: e9 c0 ff ff ff jmpq 400420 <_init+0x20>
_exit和__libc_start_main分别压栈0和2,他们在.rela.plt中的entry也是0和2,那么基本上可以确定,动态链接器是通过知道printf在.rela.plt的哪个entry,从而得知printf的地址应该存放咋哪个位置。