该文章主要借鉴了yichen师傅的博客,加上自己的一些实操与理解。主要是为了更好理解一些原理。

Linux动态链接

PLT与GOT

在Linux中,动态链接主要通过PLT与GOT来实现的。实验以下源代码来理解一下

#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}

使用gcc -o test test.o -m32与gcc -Wall -g -o test.o -c test.c -m32进行编译,可以同时获得一个.o文件与可执行文件,接着使用objdump看一下反汇编

test.o:     file format elf32-i386


Disassembly of section .text:

00000000 <print_banner>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 53 push %ebx
4: 83 ec 04 sub $0x4,%esp
7: e8 fc ff ff ff call 8 <print_banner+0x8>
c: 05 01 00 00 00 add $0x1,%eax
11: 83 ec 0c sub $0xc,%esp
14: 8d 90 00 00 00 00 lea 0x0(%eax),%edx
1a: 52 push %edx
1b: 89 c3 mov %eax,%ebx
1d: e8 fc ff ff ff call 1e <print_banner+0x1e>
22: 83 c4 10 add $0x10,%esp
25: 90 nop
26: 8b 5d fc mov -0x4(%ebp),%ebx
29: c9 leave
2a: c3 ret

0000002b <main>:
2b: 55 push %ebp
2c: 89 e5 mov %esp,%ebp
2e: 83 e4 f0 and $0xfffffff0,%esp
31: e8 fc ff ff ff call 32 <main+0x7>
36: 05 01 00 00 00 add $0x1,%eax
3b: e8 fc ff ff ff call 3c <main+0x11>
40: b8 00 00 00 00 mov $0x0,%eax
45: c9 leave
46: c3 ret

Disassembly of section .text.__x86.get_pc_thunk.ax:

00000000 <__x86.get_pc_thunk.ax>:
0: 8b 04 24 mov (%esp),%eax
3: c3 ret

函数都在glibc库中,当程序运行时才会确定地址,因此此时看到的函数是用fc ff ff ff也就是-4代替。又由于运行时,重定位是无法修改代码段的,只能将函数地址重定位到数据段,因此当程序运行时,链接器会额外生成一段代码,通过这段代码来获取相应函数的地址,如:

.text
...

// 调用printf的call指令
call printf_stub
...
printf_stub:
mov rax, [printf函数的储存地址] // 获取printf重定位之后的地址
jmp rax // 跳过去执行printf函数

.data
...
printf函数的储存地址,这里储存printf函数重定位后的地址

因此可以得出,每一个函数在动态链接时需要两个东西:存放函数地址的数据段获取该地址的代码,这也就是我们所熟知的GOT表PLT表。可执行文件里面存放的是PLT表的地址,对应PLT表指向的是GOT表的地址,GOT表指向glibc中的地址。于是我们可以通过PLT表来获取函数的地址,但是这就需要GOT表以及获取了正确的地址,也就是已经重定位到了数据段上,但是若一开始就将所有函数都重定位的话,过于麻烦,于是就有了延迟绑定

延迟绑定

延迟绑定,只有函数在被调用时才会地址解析与重定位,如下代码:

//一开始没有重定位的时候将 printf@got 填成 lookup_printf 的地址
void printf@plt()
{
address_good:
jmp *printf@got
lookup_printf:
调用重定位函数查找 printf 地址,并写到 printf@got
goto address_good;//再返回去执行address_good
}

一开始,第一次调用printf()时,printf@got是lookup_printf函数的地址,该函数用来寻找printf()地址并写入到printf@got,该函数执行完后再回到address_good,最后再跳转到printf执行。当第二次调用时,已经直到了printf的地址了,就直接执行printf()了。接下来具体看一下是怎么找的。

没关pie保护
00001040 <puts@plt>:
1040: ff a3 10 00 00 00 jmp *0x10(%ebx)
1046: 68 08 00 00 00 push $0x8
104b: e9 d0 ff ff ff jmp 1020 <_init+0x20>
关了pie保护
Disassembly of section .plt:

08049020 <__libc_start_main@plt-0x10>:
8049020: ff 35 f8 bf 04 08 push 0x804bff8
8049026: ff 25 fc bf 04 08 jmp *0x804bffc
804902c: 00 00 add %al,(%eax)
...

08049030 <__libc_start_main@plt>:
8049030: ff 25 00 c0 04 08 jmp *0x804c000
8049036: 68 00 00 00 00 push $0x0
804903b: e9 e0 ff ff ff jmp 8049020 <_init+0x20>

08049040 <puts@plt>:
8049040: ff 25 04 c0 04 08 jmp *0x804c004
8049046: 68 08 00 00 00 push $0x8
804904b: e9 d0 ff ff ff jmp 8049020 <_init+0x20>

为什么这里是puts@plt,因为当printf的参数只是一个字符串并且没有额外的格式参数时,编译器会将printf优化成puts。

我们发现除了第一个之外,其余plt表的第一条都是跳转到某一地址(即got表),来看一下该表的内容:

pwndbg> x/x 0x804c004
0x804c004 <puts@got[plt]>: 0x08049046

我们可以发现该地址存放的是plt表第二条指令的地址,为什么?因为printf函数还没有被调用,got表中是没有该函数的实际地址,因此要先寻找地址。

接下来要做的就是

8049046:	68 08 00 00 00       	push   $0x8
804904b: e9 d0 ff ff ff jmp 8049020 <_init+0x20>
8049020: ff 35 f8 bf 04 08 push 0x804bff8
8049026: ff 25 fc bf 04 08 jmp *0x804bffc

那来看一下程序还没运行时,0x804bffc里面是什么?运行后又是什么?

运行前
pwndbg> x/x 0x804bffc
0x804bffc: 0x00000000
运行后
pwndbg> x/x 0x804bffc
0x804bffc: 0xf7fdaba0

发现多了一个地址,而这个值对应的函数为**_dl_runtime_resolve**,通过该函数寻找到所需函数的地址并保存到对应的got表中。

OK,先来小总结一下当调用一个没有调用过的函数时的流程:

xxx@plt -> xxx@got -> xxx@plt -> 公共@plt -> _dl_runtime_resolve

接下来就是对_dl_runtime_resolve这个函数进行研究了。

_dl_runtime_resolve

dl_runtime_resolve函数负责程序运行时解析和绑定共享库中函数地址。

接下来说一下_dl_runtime_resolve(link_map_obj,reloc_index)函数如何使程序第一次调用一个函数

  1. 首先用link_map访问.dynamic,分别取出.dynstr、 .dynsym、 .rel.plt的地址;
  2. .rel.plt + 参数reloc_index,求出当前函数的重定位表项Elf32_Rel的指针,记作rel;
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym;
  4. .dynstr + sym->st_name得出符号名字符串指针;
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表;
  6. 最后调用这个函数;

接下来用一道例题看一下:

#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";

setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}

源码如上,使用gdb调一下

0x8049254 <main+120>    call   strlen@plt                  <strlen@plt>
将断点下在此处并进入看一下
 0x8049066  <strlen@plt+6>              push   0x18
0x804906b <strlen@plt+11> jmp 0x8049020 <0x8049020>
这就是之前说的,第一次调用函数,先将参数压入栈用于寻找函数,再跳转到公共plt表项
0x8049020 push 0x804bff8
0x8049026 jmp *0x804bffc
再将参数压入栈中并跳转到_dl_runtime_resolve函数,刚好两个参数,0x18为reloc_index(第二个参数),0x804bff8为link_map指针(第一个参数)。

接着来看是如何通过这两个参数找到所需函数地址的:

pwndbg> x/x 0x804bff8
0x804bff8: 0xf7ffda20
找到link_map地址0xf7ffda20,再通过该地址找到.dynamic地址

pwndbg> x/10x 0xf7ffda20
0xf7ffda20: 0x00000000 0xf7ffdd28 0x0804bf00 0xf7fc1000
0xf7ffda30: 0x00000000 0xf7ffda20 0x00000000 0xf7ffdd1c
0xf7ffda40: 0x00000000 0x0804bf00 //0x0804bf00为.dynamic地址

pwndbg> x/40x 0x0804bf00
0x804bf00: 0x00000001 0x00000048 0x0000000c 0x08049000
0x804bf10: 0x0000000d 0x08049284 0x00000019 0x0804bef8
0x804bf20: 0x0000001b 0x00000004 0x0000001a 0x0804befc
0x804bf30: 0x0000001c 0x00000004 0x6ffffef5 0x080481ec
0x804bf40: 0x00000005 0x080482ac 0x00000006 0x0804820c
0x804bf50: 0x0000000a 0x00000076 0x0000000b 0x00000010
0x804bf60: 0x00000015 0xf7ffd93c 0x00000003 0x0804bff4
0x804bf70: 0x00000002 0x00000028 0x00000014 0x00000011
0x804bf80: 0x00000017 0x08048380 0x00000011 0x08048368
0x804bf90: 0x00000012 0x00000018 0x00000013 0x00000008 //0x080482ac为.dynstr地址,0x0804820c为.dynsym地址,0x08048380为.rel.plt地址

pwndbg> x/10x 0x08048380
0x8048380: 0x0804c000 0x00000107 0x0804c004 0x00000207
0x8048390: 0x0804c008 0x00000307 0x0804c00c 0x00000507
0x80483a0: 0x0804c010 0x00000607 //0x0804c00c为r_offest,0x00000507为r_info,5为.dynsym的下标

pwndbg> x/30x 0x0804820c
0x804820c: 0x00000000 0x00000000 0x00000000 0x00000000 [0]
0x804821c: 0x00000016 0x00000000 0x00000000 0x00000012 [1]
0x804822c: 0x00000030 0x00000000 0x00000000 0x00000012 [2]
0x804823c: 0x00000024 0x00000000 0x00000000 0x00000012 [3]
0x804824c: 0x00000067 0x00000000 0x00000000 0x00000020 [4]
0x804825c: 0x0000001d 0x00000000 0x00000000 0x00000012 [5]
0x804826c: 0x00000042 0x00000000 0x00000000 0x00000012 [6]
0x804827c: 0x00000010 0x00000000 //name_offest=0x0000001d

pwndbg> x/s 0x080482c9
0x80482c9: "strlen" //.dynstr + name_offset

第三个地址为.dynamic的地址,即0x0804bf00,再通过该地址获取.dynstr、 .dynsym、 .rel.plt的地址。
即.dynamic的地址加0x44的位置是.dynstr,即0x080482ac;.dynamic的地址加0x4c的位置是.dynsym,即0x0804820c;.dynamic的地址加0x84的位置是.rel.plt,即0x08048380。
.rel.plt 的地址加上参数 reloc_index,找到的就是函数的重定位表项 Elf32_Rel 的指针,通过这个可以得到r_offest与r_info。将r_info>>8作为.dynsym中的下标,再来到.dynsym的地址,在该下标的地方为函数名称的偏移:name_offest,.dynstr + name_offset 就是这个函数的符号名字符串 st_name最后在动态链接库查找这个函数的地址,并且把地址赋值给 *rel -> r_offset,即 GOT 表就可以了。

大致可以看出_dl_runtime_resolve函数最后是通过st_name来确定执行某一个函数,因此我们可以通过修改该内容从而执行任意函数。而reloc_index是我们控制的,那么可以通过一系列操作来修改就行了(也就是ret2dlresolve攻击手法了)。

SROP