下载源码,源代码下载连接:
1
2
|
$ wget https:
/
/
www.sudo.ws
/
dist
/
sudo
-
1.8
.
21.tar
.gz
$ tar xzf sudo
-
1.8
.
21.tar
.gz
|
须安装AFL++,可以使用官方docker镜像,如果已经在本地安装,也可直接使用
1
2
3
4
5
6
7
|
$ docker pull aflplusplus
/
aflplusplus
# 拉取afl++
$ docker run
-
ti
-
v
/
location
/
of
/
your
/
target:
/
src \
[
-
v
/
location
/
of
/
your
/
afl_src
/
:AFLplusplus \]
aflplusplus
/
aflplusplus
/
bin
/
bash
# 启动docker afl
# 映射代码文件夹,如果本地没有afl++的源代码的话,建议也映射一份,便于后续操作
|
$ cd /src/sudo-1.8.21
查看sudo源代码目录
先不进行插桩编译,使用原版安装,测试一下poc是否符合预期。
1
2
3
4
5
|
$ .
/
configure
-
-
prefix
=
/
src
/
origin_compile
$ make
$ make install
$ cd
/
src
/
origin_compile
/
bin
$ .
/
sudoedit
-
s
'\'
aaaaaaaaaaaaaaa
|
可以看到成功产生了一个崩溃
我们接下来的任务就是将该崩溃用afl复现出来。
sudo程序具有SUID,普通用户通过输入密码,利用sudo执行命令,获得暂时的权力提升。对于sudo的测试,需要通过非预期的输入,致使sudo程序产生崩溃,进而找到漏洞的利用点。sudo的输入有两处,执行参数与密码输入,本文暂不考虑密码输入引发的异常。测试的场景为,非特权用户输入恶意构造程序执行参数,引起sudo程序崩溃。
sudo程序由root用户和其他用户启动的表现是不同的。sudo的所有权是root,但却是由普通用户调用的。然而我们在使用afl模糊测试时,使用的是root身份,这不能完成测试的需求,虽然这并不影响本CVE的复现。因此我们需要使sudo程序即使以root身份运行,但让其认为是普通用户执行的。这可以通过查看sudo调用getuid()的代码来实现,只需将值硬编码为1000,这是一个普通用户的用户ID。
这个补丁,可以通过在源代码文件夹中搜索getuid
,将getuid()
和getgid()
修改为1000即可。
1
2
3
4
5
6
7
8
9
10
11
12
|
-
-
-
.
/
src
/
sudo.c
+
+
+
.
/
src
/
sudo.c
@@
-
522
,
9
+
524
,
9
@@
}
ud
-
>sid
=
getsid(
0
);
-
ud
-
>uid
=
getuid();
+
ud
-
>uid
=
1000
;
ud
-
>euid
=
geteuid();
-
ud
-
>gid
=
getgid();
+
ud
-
>gid
=
1000
;
ud
-
>egid
=
getegid();
|
afl原生并不支持对argv参数进行Fuzzing。afl的fuzzing模式一般是将变异得到的文件,重定向到程序,作为程序的标准输入,然后运行被测程序,等待程序结束、崩溃或超时。注意:afl的启动命令中可以使用 @@ 作为占用符,但其作用并不是对占位符的位置进行fuzzing,@@占位符表示此处应有文件的输入,且这个输入的文件应是fuzzing得到的,由afl自动填入。
为了实现对argv的fuzzing,我们可以将/aflplusplus/utils/argv_fuzzing/argv-fuzz-inl.h复制到sudo的源代码目录../sudo/src下,并对sudo.c进行相应的补丁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
+
+
+
sudo.c
2023
-
01
-
22
01
:
09
:
38.175635142
-
0800
-
-
-
sudo.c_org1
2023
-
01
-
22
01
:
06
:
27.035319663
-
0800
@@
-
14
,
7
+
14
,
6
@@
*
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
/
+
#include "argv-fuzz-inl.h"
#ifdef __TANDEM
# include <floss.h>
#endif
@@
-
134
,
7
+
133
,
6
@@
int
main(
int
argc, char
*
argv[], char
*
envp[])
{
+
AFL_INIT_ARGV();
int
nargc, ok, status
=
0
;
char
*
*
nargv,
*
*
env_add;
char
*
*
user_info,
*
*
command_info,
*
*
argv_out,
*
*
user_env_out;
|
我们看一下argv-fuzz-inl.h。
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
|
#define AFL_INIT_ARGV() \
do { \
\
argv
=
afl_init_argv(&argc); \
\
}
while
(
0
)
······
#define MAX_CMDLINE_LEN 100000
#define MAX_CMDLINE_PAR 50000
static char
*
*
afl_init_argv(
int
*
argc) {
static char in_buf[MAX_CMDLINE_LEN];
static char
*
ret[MAX_CMDLINE_PAR];
char
*
ptr
=
in_buf;
int
rc
=
0
;
if
(read(
0
, in_buf, MAX_CMDLINE_LEN
-
2
) <
0
) {}
while
(
*
ptr && rc < MAX_CMDLINE_PAR) {
ret[rc]
=
ptr;
if
(ret[rc][
0
]
=
=
0x02
&& !ret[rc][
1
]) ret[rc]
+
+
;
rc
+
+
;
while
(
*
ptr)
ptr
+
+
;
ptr
+
+
;
}
*
argc
=
rc;
return
ret;
,
}
|
argv-fuzz-inl.h定义了一个宏函数AFL_INIT_ARGV()
,调用相当于执行argv = afl_init_argv(&argc);
。afl_init_argv
从标准输入中读取输入,以'\0'表示一个参数的结束,以'\0\0'表示输入的结束。argv作为一个指针数组的指针,该指针数组中最后一个指针应为0,其余的每一项为一个字符串指针。'\0'
作为字符串结束的标志,因此参数中'\0'后的字符没有意义,因此以'\0'表示一个参数的结束是一个合适的操作。注意到afl_init_argv函数中,存在对0x02的判断,编写这个文件的作者解释到,以单独一个0x02作为参数表示空参数,因此将其跳过,也就是说,生成的输入文件,如果存在0x...000200...的话,0x02会被删除,该参数直接变为以0x00开始且结束的空字符串。进行这个处理的原因是,默认的方法无法生成空字符串的参数,而0x02很少用,可以用它来表示空参数,影响很小。
在执行sudo程序的时,会从标准输入中读取密码,来进行权限认证。我们已经通过使用标准输入来输入argv参数。当执行到输入密码时,会再次从标准输入读取,sudo程序会一直等待密码输入,因此被测程序就会因超时而被挂起。重复的挂起将会导致测试时间被严重拉长。
我们要明确测试的目的,普通用户以特定的参数打开sudo,导致程序崩溃,因此权限认证不应该被通过,我们试着取消执行sudo时权限认证环节。
首先,我们需要找到权限认证的代码片段,这里可以通过gdb调试,查看sudo运行到输入密码时程序的状态。为了避免docker用户管理与sudo用户管理引起的混乱,我还是建议在本机上编译,调试。注意一定要设置好安装目录,防止破坏主机环境。
1
2
3
4
|
$ make clean
#还原目录
$ .
/
configure
-
-
prefix
=
~
/
sudo_gdb_test_auth
-
-
disable
-
shared
#设置安装路径,采用静态链接
$ make
$ make install
|
安装完成后,找到对应的sudo文件,确保其用户属于root,并设置SUID。
1
2
3
|
$ sudo chown root:root .
/
sudo
$ sudo chmod u
+
s .
/
sudo
$ ls
-
l
|
使用非root用户测试运行$ ./sudo ls
会停留在输入密码处。
1
2
|
$ .
/
sudo ls
Password:(expecting
input
)
|
保留当前窗口,再开一个终端。
1
2
|
$ ps
-
ef |grep sudo
root
206957
206889
0
19
:
15
pts
/
4
00
:
00
:
00
.
/
sudo ls
|
必须使用root权限调试,$ sudo gdb attach <pid>
。如果成功的话,会断在read
处。
我们查看backtrace。显然,可以考虑将verify_user
优化掉。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
pwndbg> bt
#0 0x00007f1421f7dfd2 in __GI___libc_read (fd=fd@entry=5, buf=buf@entry=0x7ffd483eb217, nbytes=nbytes@entry=1) at ../sysdeps/unix/sysv/linux/read.c:26
#1 0x00005622b6cac7af in read (__nbytes=1, __buf=0x7ffd483eb217, __fd=5) at /usr/include/x86_64-linux-gnu/bits/unistd.h:44
#2 getln (fd=fd@entry=5, buf=buf@entry=0x5622b6d0b480 <buf> "", feedback=feedback@entry=0, bufsiz=256) at ./tgetpass.c:311
#3 0x00005622b6cacbd0 in tgetpass (prompt=0x5622b6e87bb0 "Password: ", timeout=300, flags=0, callback=callback@entry=0x7ffd483ebd50) at ./tgetpass.c:178
#4 0x00005622b6c9c81e in sudo_conversation (num_msgs=<optimized out>, msgs=<optimized out>, replies=0x7ffd483eb918, callback=0x7ffd483ebd50) at ./conversation.c:70
#5 0x00005622b6cd8485 in auth_getpass (prompt=0x5622b6e87bb0 "Password: ", timeout=<optimized out>, type=type@entry=1, callback=callback@entry=0x7ffd483ebd50) at ./auth/sudo_auth.c:426
#6 0x00005622b6cd88d6 in verify_user (pw=0x5622b6e7e158, prompt=<optimized out>, prompt@entry=0x5622b6e87bb0 "Password: ", validated=validated@entry=2, callback=callback@entry=0x7ffd483ebd50) at ./auth/sudo_auth.c:282
#7 0x00005622b6cd93f1 in check_user_interactive (auth_pw=0x5622b6e7e158, mode=<optimized out>, validated=2) at ./check.c:149
#8 check_user (validated=validated@entry=2, mode=<optimized out>) at ./check.c:212
#9 0x00005622b6cc2af2 in sudoers_policy_main (argc=argc@entry=1, argv=argv@entry=0x7ffd483ec1a0, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, closure=closure@entry=0x7ffd483ebeb0) at ./sudoers.c:423
#10 0x00005622b6cbcfc4 in sudoers_policy_check (argc=1, argv=0x7ffd483ec1a0, env_add=0x0, command_infop=0x7ffd483ebf28, argv_out=0x7ffd483ebf30, user_env_out=0x7ffd483ebf38) at ./policy.c:775
#11 0x00005622b6c9ae57 in policy_check (plugin=0x5622b6d0c000 <policy_plugin>, user_env_out=0x7ffd483ebf38, argv_out=0x7ffd483ebf30, command_info=0x7ffd483ebf28, env_add=0x0, argv=0x7ffd483ec1a0, argc=1) at ./sudo.c:1149
#12 main (argc=argc@entry=2, argv=argv@entry=0x7ffd483ec198, envp=0x7ffd483ec1b0) at ./sudo.c:247
#13 0x00007f1421e94083 in __libc_start_main (main=0x5622b6c9aa50 <main>, argc=2, argv=0x7ffd483ec198, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffd483ec188) at ../csu/libc-start.c:308
#14 0x00005622b6c9c6be in _start () at ./sudo.c:798
|
我们可以看到,sudoedit只是sudo的一个符号链接。
1
2
3
|
-
rwsr
-
xr
-
x
1
root root
1647712
Jan
23
06
:
50
sudo
lrwxrwxrwx
1
root root
4
Jan
23
06
:
50
sudoedit
-
> sudo
-
rwxr
-
xr
-
x
1
root root
318384
Jan
23
06
:
50
sudoreplay
|
但在测试的过程中发现,如果打开的是sudo,即使在程序起始处将argv[0]
修改为了sudoedit,程序的执行流依旧是sudo代码片段。
1
2
3
4
5
6
7
8
9
|
$ echo
-
ne
"sudoedit\0id\0\0"
| .
/
sudo
usage: sudo
-
h |
-
K |
-
k |
-
V
usage: sudo
-
v [
-
AknS] [
-
g group] [
-
h host] [
-
p prompt] [
-
u user]
usage: sudo
-
l [
-
AknS] [
-
g group] [
-
h host] [
-
p prompt] [
-
U user] [
-
u user] [command]
usage: sudo [
-
AbEHknPS] [
-
C num] [
-
g group] [
-
h host] [
-
p prompt] [
-
T timeout] [
-
u user] [VAR
=
value] [
-
i|
-
s] [<command>]
usage: sudo
-
e [
-
AknS] [
-
C num] [
-
g group] [
-
h host] [
-
p prompt] [
-
T timeout] [
-
u user]
file
...
$ echo
"sudo\0id\0\0"
| .
/
sudoedit
usage: sudoedit [
-
AknS] [
-
C num] [
-
g group] [
-
h host] [
-
p prompt] [
-
T timeout] [
-
u user]
file
...
|
我们可以发现main
函数在开头处调用os_init(argc, argv, envp);
,而os_init
被宏定义为os_init_common
,os_init_common
会调用initprogname
初始化程序名。
1
2
3
4
5
6
7
8
9
10
|
int
os_init_common(
int
argc, char
*
argv[], char
*
envp[])
{
initprogname(argc >
0
? argv[
0
] :
"sudo"
);
#ifdef STATIC_SUDOERS_PLUGIN
preload_static_symbols();
#endif
gc_init();
return
0
;
}
|
但是initprogname
会优先使用__progname
宏获取程序名,而不是传递进来的argv[0]
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
void
initprogname(const char
*
name)
{
# ifdef HAVE___PROGNAME
extern const char
*
__progname;
if
(__progname !
=
NULL &&
*
__progname !
=
'\0'
)
progname
=
__progname;
else
# endif
if
((progname
=
strrchr(name,
'/'
)) !
=
NULL) {
progname
+
+
;
}
else
{
progname
=
name;
}
/
*
Check
for
libtool prefix
and
strip it
if
present.
*
/
if
(progname[
0
]
=
=
'l'
&& progname[
1
]
=
=
't'
&& progname[
2
]
=
=
'-'
&&
progname[
3
] !
=
'\0'
)
progname
+
=
3
;
}
|
因此,为了使被测程序通过argv[0]获取程序名,我们将HAVE___PROGNAME
的部分删除。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-
-
-
..
/
origin_file
/
progname.c
2023
-
01
-
27
01
:
21
:
37.829958000
-
0800
+
+
+
progname.c
2023
-
01
-
27
01
:
23
:
42.824771537
-
0800
@@
-
59
,
13
+
59
,
6
@@
void
initprogname(const char
*
name)
{
-
# ifdef HAVE___PROGNAME
-
extern const char
*
__progname;
-
-
if
(__progname !
=
NULL &&
*
__progname !
=
'\0'
)
-
progname
=
__progname;
-
else
-
# endif
if
((progname
=
strrchr(name,
'/'
)) !
=
NULL) {
progname
+
+
;
}
else
{
|
再完成上述四个补丁后,我们再次编译得到最终文件。
重新开一个容器,将最终代码放入/src,创建/fuzz,同时设置好主机跟踪目录,便于观察fuzz情况及其进展。
1
2
3
4
|
$ sudo docker run
-
it \
-
v path
/
to
/
all_change:
/
src \
-
v path
/
to
/
trace_paper_fuzz:
/
fuzz\
aflplusplus
/
aflplusplus
/
bin
/
bash
|
在docker中添加uid为1000的普通用户
1
|
$ useradd
-
u
1000
aflfuzzer
|
使用afl-clang-fast进行插桩编译,使用afl-gcc编译得到的文件无法运行,相关分析我放到文末。
1
2
3
4
5
|
$ cd
/
src
$ make clean
$ CFLAGS
=
"-g"
LDFLAGS
=
"-g"
CC
=
afl
-
clang
-
fast .
/
configure
-
-
prefix
=
/
fuzz
/
release
-
-
disable
-
shared
$ make
$ make install
|
编译成功后,再次验证poc能够引起崩溃。
1
2
|
echo
-
ne
"sudoedit\x00-s\x00\x5c\x00aaaaaaaaaaaaaaaaaaaaaaaaaa\x00\x00"
| .
/
sudo
# 为啥不用直接用\0,我觉得可以,但是我本机解析经常出问题,因此本文全篇都是用\xHH
|
经历总总曲折,总算可以开始测试了。
1
2
3
4
5
6
7
|
$ cd
/
fuzz
$ mkdir {
input
,output}
$ echo
-
ne
"sudo\x00id\x00\x00"
>
input
/
payload1
$ echo
-
ne
"sudoedit\x00id\x00\x00"
>
input
/
payload2
afl
-
fuzz
-
i
input
/
-
o output
/
-
D
-
M Master
/
fuzz
/
release
/
bin
/
sudo
# 可以创建多个从fuzzer辅助测试
afl
-
fuzz
-
i
input
/
-
o output
/
-
D
-
S slave1
/
fuzz
/
release
/
bin
/
sudo
|
可以发现,很快就能获得一个崩溃。
查看崩溃,符合预期
在进行插桩编译的过程中,笔者一开始使用的是afl-gcc编译,但是编译出来的文件运行会直接崩溃。查找相关资料推荐使用llvm模式编译。为了弄清楚afl-gcc编译的文件失败的原因,笔者对此做出必要探索。
使用afl-gcc编译
1
2
3
4
5
|
$ make clean
$ CFLAGS
=
"-g"
LDFLAGS
=
"-g"
CC
=
afl
-
gcc .
/
configure
-
-
prefix
=
/
src
/
gcc_compile
-
-
disable
-
shared
$ make
$ make install
$ .
/
sudoedit
|
回顾一下插桩程序的运行过程
被测程序会在第一次执行__afl_maybe_log
时进行初始化,第一次调用时共享内存指针__afl_area_ptr
为空,进而调用__afl_setup
初始化forkserver。__afl_setup
首先检查__afl_setup_failure
是否为空,如果不为空代表已经初始化失败过,调用__afl_return
返回,否则调用__afl_setup_first
进行初始化。__afl_setup_first
会保存所有寄存器的值,然后调用getenv
获取SHM_ENV_VAR(fuzz程序保存的共享内存id)
然而跟进getenv
发现,getenv
又调用了__afl_maybe_log
,也就是说getenv
也被插桩了。
然后,本来是调用__afl_maybe_log
进行初始化,但是初始化的过程又调用了__afl_maybe_log
,而此时还未初始化完毕,于是又会进行初始化操作,就导致了程序执行流程的疯狂套娃。由于执行过程中,会保留寄存器到栈上,因此栈资源被疯狂使用,最终进程被操作系统杀掉。
为什么getenv
会被插桩呢,getenv
原本是c库函数,但是在sudo源代码中的env_hook.c
中定义同名的getenv
函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
__dso_public char
*
getenv(const char
*
name)
{
char
*
val
=
NULL;
switch (process_hooks_getenv(name, &val)) {
case SUDO_HOOK_RET_STOP:
return
val;
case SUDO_HOOK_RET_ERROR:
return
NULL;
default:
return
getenv_unhooked(name);
}
}
|
而afl-gcc的插桩是通过解析编译过程中的.s汇编文件,在需要插桩的地方,添加插桩的汇编代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
afl
-
as.h
"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n"
"movq $0x%08x, %%rcx\n"
"call __afl_maybe_log\n"
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n"
"\n"
"/* --- END --- */\n"
"\n"
;
|
因此afl-gcc在编译env_hook.c时,也无可避免的对getenv
进行了插桩。而使用CC=afl-clang-fast
编译,在llvm模式下对编译中间码IR进行插桩,就不会出现这个问题。
更多【通过AFL++复现sudo漏洞的一次尝试】相关视频教程:www.yxfzedu.com