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)。重定位表的定義如下:
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爲例:
~/workspace/pwn/jarvisoj/xman$ objdump -d -j.plt level4
level4: file format elf32-i386
Disassembly of section .plt:
08048300 <read@plt-0x10>:
08048300: ff 35 04 a0 04 08 pushl 0x804a004
08048306: ff 25 08 a0 04 08 jmp *0x804a008
0804830c: 00 00 add %al,(%eax)
...
08048310 <read@plt>:
08048310: ff 25 0c a0 04 08 jmp *0x804a00c
08048316: 68 00 00 00 00 push $0x0
0804831b: 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 <[email protected]>: 0x08048316
(gdb) x/wi 0x804a00c
0x804a00c <[email protected]>: 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的時候需要注意一下。下面的宏定義說明了原因。
#ifndef reloc_offset
#define reloc_offset reloc_arg
#define reloc_index reloc_arg / sizeof (PLTREL)
#endif
3)動態鏈接符號表和動態鏈接字符串表
動態鏈接符號表是一個結構體數組,保存了每個函數解析是需要的信息,比如函數名在動態鏈接字符串表中的偏移等等。動態鏈接字符串表中保存了函數的名稱,並且以\x00作爲開始和結尾。動態鏈接符號表的定義如下:
/* 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函數。
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中實現的,源代碼如下:
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構造時介紹。