Linux栈溢出利用之return to dl-resolve payload 构造原理(一)

0x00. 写在前面

Return to dl-resolve是一种新的rop攻击方式,出自USENIX Security 2015上的一篇论文How the ELF Ruined Christmas。前段时间,学习了return to dl-resolve 方法,并且分别在32位应用和64位应用上实践了一番。网上有很多文章讲解return to dl-resolve的原理,现记录下我的学习心得,如有不对的地方,欢迎斧正。

0x01. dl-resolve解析原理

return to dl-resolve 主要是利用Linux glibc的延迟绑定技术(Lazy binding)。Linux下glibc库函数在第一次被调用的时候,才会去寻找函数的真正地址然后进行绑定。在这一过程中,主要由过程链接表(PLT)提供跳转到解析函数的胶水代码,然后将函数的真正地址回填到函数的全局偏移表中,再将控制权给需要解析的函数。

首先,了解一下在动态链接过程中需要的辅助信息:重定位表(.rel.plt和.rel.dyn)、全局偏移表(.got和.got.plt)、动态链接符号表(.dyn.sym)、动态链接字符串表(.dyn.str)。

  1. 重定位表
    重定位表中.rel.plt用于函数重定位,.rel.dyn用于变量重定位。函数重定位表的主要作用是提供函数在动态链接符号表以及全局偏移表中的位置。具体的,函数重定位表表项为地址解析函数提供一个参数(r_info的前24位),定位需要解析的函数;为重定位入口提供一个偏移地址,定位函数地址的保存位置(r_offset),即函数的全局偏移表值(.got.plt)。重定位表的定义如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typedef struct
    {
    Elf32_Addr r_offset; /* Address */
    Elf32_Word r_info; /* Relocation type and symbol index */
    } Elf32_Rel;

    typedef struct
    {
    Elf64_Addr r_offset; /* Address */
    Elf64_Xword r_info; /* Relocation type and symbol index */
    Elf64_Sxword r_addend; /* Addend */
    } Elf64_Rela;
    2)全局偏移表
    全局偏移表中.got 保存全局变量偏移表,.got.plt 保存全局函数偏移表。全局函数偏移表主要保存了函数在内存中的实际地址,刚开始全局函数便宜表中存放的是:过程链接表PLT中该函数胶水代码中的第二条指令地址。在函数解析之后,才存放了函数的真正地址。以XMAN中32位ELF level4中的read@plt为例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    ~/workspace/pwn/jarvisoj/xman$ objdump -d -j.plt level4
    level4: file format elf32-i386
    Disassembly of section .plt:
    08048300 <read@plt-0x10>:
    8048300: ff 35 04 a0 04 08 pushl 0x804a004
    8048306: ff 25 08 a0 04 08 jmp *0x804a008
    804830c: 00 00 add %al,(%eax)
    ...
    08048310 <read@plt>:
    8048310: ff 25 0c a0 04 08 jmp *0x804a00c
    8048316: 68 00 00 00 00 push $0x0
    804831b: e9 e0 ff ff ff jmp 8048300 <_init+0x30>
    ...

    ~/workspace/pwn/jarvisoj/xman$ objdump -R level4
    level4: file format elf32-i386
    DYNAMIC RELOCATION RECORDS
    OFFSET TYPE VALUE
    08049ffc R_386_GLOB_DAT __gmon_start__
    0804a00c R_386_JUMP_SLOT read
    ...

    ~/workspace/pwn/jarvisoj/xman$ gdb level4
    GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
    Copyright (C) 2014 Free Software Foundation, Inc.

    (gdb) x/wx 0x804a00c
    0x804a00c <read@got.plt>: 0x08048316
    (gdb) x/wi 0x804a00c
    0x804a00c <read@got.plt>: push %ss

    调用函数read时,先调用read@plt(本例是0x8048310)。在read@plt中先跳转到.got.plt,如上图所示,第一次调用时.got.plt值为 read@plt的第二条指令地址。然后reloc_arg参数进栈,跳转到过程链接表开始(PLT[0]),执行胶水代码。
    read@plt - 0x10(本例是0x8048300)是整个过程链接表的开始(PLT[0]),该处的胶水代码是进入_dl_runtime_resolve(link_map, reloc_arg)的入口。pushl 0x804a004,将_dl_runtime_resolve函数的第一个参数 link_map 入栈,该link_map函数地址位于全局函数偏移表(.got.plt) + 4的地方(64位应该是+8)。第二个参数reloc_arg 前面已经通过read@plt 中的第二条指令(本例是0x8048316)入栈。
    在32位应用中reloc_arg为重定位项在重定位表的偏移值,而64位应用中reloc_arg为重定位项在重定位表中的索引,构造payload的时候需要注意一下。下面的宏定义说明了原因。
    1
    2
    3
    4
    #ifndef reloc_offset
    #define reloc_offset reloc_arg
    #define reloc_index reloc_arg / sizeof (PLTREL)
    #endif

3) 动态链接符号表和动态链接字符串表
动态链接符号表是一个结构体数组,保存了每个函数解析是需要的信息,比如函数名在动态链接字符串表中的偏移等等。动态链接字符串表中保存了函数的名称,并且以\x00作为开始和结尾。动态链接符号表的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 * Symbol table entry.  */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;

#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))

#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

注意:32位动态链接符号表和64位动态链接符号表中的顺序。

4)延迟绑定的过程
正如前面提到的,从PLT[0]进入_dl_runtime_resolve函数。
32位应用中: ,该函数位于glibc-2.24/sysdeps/i386/dl-trampoline.S中。用汇编语言写的,先保存寄存器值,然后通过栈传递参数,调用_dl_fixup函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
glibc-2.24/sysdeps/i386/dl-trampoline.S _dl_runtime_resolve(link_map, reloc_arg)函数:

28 .text
29 .globl _dl_runtime_resolve
30 .type _dl_runtime_resolve, @function
31 cfi_startproc
32 .align 16
33 _dl_runtime_resolve:
34 cfi_adjust_cfa_offset (8)
35 pushl %eax # Preserve registers otherwise clobbered.
36 cfi_adjust_cfa_offset (4)
37 pushl %ecx
38 cfi_adjust_cfa_offset (4)
39 pushl %edx
40 cfi_adjust_cfa_offset (4)
41 movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
42 movl 12(%esp), %eax # that `fixup' takes its parameters in regs.
43 call _dl_fixup # Call resolver.
44 popl %edx # Get register content back.
45 cfi_adjust_cfa_offset (-4)
46 movl (%esp), %ecx
47 movl %eax, (%esp) # Store the function address.
48 movl 4(%esp), %eax
49 ret $12 # Jump to function address.
50 cfi_endproc
51 .size _dl_runtime_resolve, .-_dl_runtime_resolve

函数_dl_fixup是在glibc-2.24/elf/dl-runtime.c中实现的,源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
// 分别获取动态链接符号表和动态链接字符串表的基址
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]);

//通过参数reloc_arg计算重定位入口,这里的DT_JMPREL即.rel.plt, reloc_offset即reloc_arg
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

//根据函数重定位表中的动态链接符号表索引,即r_info字段,获取函数在动态链接符号表中对应的条目。
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
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);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
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;
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
//根据strtab+sym->st_name在字符串表中找到函数名,然后进行符号查找获取libc基地址result
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
//将要解析的函数的偏移地址加上libc基址,就可以获取函数的实际地址
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);

//将已经解析完的函数地址写入相应的GOT表中
if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

64位应用中:该函数位于glibc-2.24/sysdeps/x86_64/dl-trampoline.h中。dl_runtime_resolve函数和_dl_fixup函数在有些方面与32位不同,后面payload构造时介绍。

0x03. 参考文献

1. 通过ELF动态装载构造ROP链(Return-to-dl-resolve)