前文:https://blog.joe1sn.top/2022/01/04/CVE-2021-3156/
受到youtuber:LiveOverflow的系列教程的启发,我发现在中文互联网上并没有相关的翻译教程,所以我想以实验报告的形式来创造这个从fuzz到exp的系列图文教程
原始视频合集:https://www.youtube.com/watch?v=uj1FTiczJSE&list=PLhixgUqwRTjy0gMuT4C3bmjeZjuNQyqdx
原始Blog:https://liveoverflow.com/why-pick-sudo-research-target-part-1/
原作者代码仓库:https://github.com/LiveOverflow/pwnedit
My previous blog: https://blog.joe1sn.top/2022/01/04/CVE-2021-3156/
I was inspired by the LiveOverflow’s Sudo Vulnerability Walkthrough on youtube, but i found there’s no Chinese version of this walkthrough tutorial, so i decided to write in experimental report way to create this “from fuzz to exploit” series.
Original Videos: https://www.youtube.com/watch?v=uj1FTiczJSE&list=PLhixgUqwRTjy0gMuT4C3bmjeZjuNQyqdx
Original Blog: https://liveoverflow.com/why-pick-sudo-research-target-part-1/
Source Project Code: https://github.com/LiveOverflow/pwnedit
本节内容:
Troubleshooting AFL Fuzzing Problems | Ep. 03
Finding Buffer Overflow with Fuzzing | Ep. 04
Found a Crash Through Fuzzing? Minimize AFL Testcases! | Ep. 05
Root Cause Analysis With AddressSanitizer (ASan) | Ep. 06
Understanding C Pointer Magic Arithmetic | Ep. 07
C Code Review - Reaching Vulnerable Code in sudo | Ep. 08
解决AFL的小麻烦
因为时间原因,我并不能一直开着电脑跑,不过我翻译一下作者遇到的问题
No more free CPU cores
作者在遇到fuzz很慢的时候,尝试关闭一个fuzz,然后重启
然后使用ps aux
产看全部运行过程,发现afl在尝试fuzz这些奇怪的东西(因为sudo中可能会有exec
之类的)。然后pkill vi
关闭所有vi的进程就短暂的解决了这个问题。
**解决:**彻底解决的话要关闭所有在sudo中的exec
相关函数,然后重新编译
And of Disk Space
作者查看空间使用情况过后发现磁盘空间充足,但是任然不能创建文件
但是使用df -i
查看inode节点,发现被占满了
**inode (index node)**是指在许多“类Unix 文件系统 ”中的一种数据结构 ,用于描述文件系统 对象(包括文件 、目录 、设备文件 、socket 、管道 等)。每个inode保存了文件系统 对象数据的属性和磁盘块位置[1] 。文件系统 对象属性包含了各种元数据 (如:最后修改时间) ,也包含用户组(owner )和权限数据
说明有过多的细小文件使用光了inode节点号,最后在/var/tmp
找到了这些文件,原因是fuzz的时候产生了例如../../
的路径穿越。
**解决:**手动在sudo要创建文件的时候添加上一个crash,这里用空指针引用
1 2 printf ("mk tmp file(%s)\n" ,stuff);*(int *)0 =0 ;
之后开始fuzz
然后分析crash
但是又引入了新的问题:
root和普通用户相同吗?
这里就要说到sudo的原理,sudo是通过在root条件下使用setuid
的方式来让普通用户指令得到root执行。
比如我们在user
下fuzz,但是真实情况会将它变为root
下运行
如果要在fuzz时实现真实情况的效果,那么就要将当前用户uid设置为普通用户的
sudo-1.8.31p2/src/sudo.c get_user_info
1 2 3 4 ud->uid = 1000 ud->euid = geteuid(); ud->gid = 1000 ud->egid = getegid();
忘写分号了
找到缓冲区溢出
作者用上一节的fuzz得到了一些ctash样本,本章内容讲的基本上是分析这些样本
gdb调试
和我预料的一样,这样做会产生大量的非sudo
从而crash的样本,可以用以下命令查看
1 2 grep -R sudoedit file_floder/ grep -R sudo file_floder/
为了方便分析,可以安装一些gdb的插件,如pwndbg
,也在CVE分析的文章 里讲过了该插件的安装(不要放在共享文件夹/pwd
下安装)
有的crash是由于fuzzer的错误引起的,作者使用了这段代码判断
1 2 3 4 5 6 7 #include "argv-fuzz-inl.h" int main (int argc, char *argv[], char *envp[]) { AFL_INIT_ARGV(); execve("/usr/local/bin/sudo" , argv, envp); }
作者遇到的第一个问题是argv-fuzz-inl
中的ret
数组造成的栈溢出,覆写了其他的函数指针造成crash
解决 如果rc
比最大参数数量大时退出循环
更换fuzzer
使用更好的fuzzer:AFL++ 项目地址:https://github.com/AFLplusplus/AFLplusplus
AFL++支持对命令行的fuzz,所以之前的修改要去掉
要新建镜像的话,可以在Dockerfile中加上
1 RUN cd /root/ && git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus && make source-only && make install
1 2 3 4 5 6 7 8 9 10 11 FROM ubuntu:20.04 ENV LC_CTYPE C.UTF-8 ARG DEBIAN_FRONTEND=noninteractiveRUN apt-get update && apt-get install -yq gcc make wget curl git vim gdb clang llvm lld llvm-dev bsdmainutils libstdc++-10-dev python3 python3-pip python3-dev automake flex bison build-essential libglib2.0-dev libpixman-1-dev python3-setuptools RUN cd /root/ && wget https://www.sudo.ws/dist/sudo-1.8.31p2.tar.gz && tar -xvf sudo-1.8.31p2.tar.gz && cd sudo-1.8.31p2 && ./configure && make && make install RUN cd /root/ && git clone https://github.com/AFLplusplus/AFLplusplus && cd AFLplusplus && make source-only && make install RUN useradd -ms /bin/bash user RUN echo 'export PS1="\[\e]0;\u@\h: \w\a\]\[\033[01;31m\]\u\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# "' >> /root/.bashrc RUN echo 'export PS1="\[\e]0;\u@\h: \w\a\]\[\033[01;32m\]\u\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]$ "' >> /home/user/.bashrc USER user WORKDIR /home/user
重新编译
1 2 3 whereis afl-clang-fast ls -lah /usr/local/bin/afl-clang-fast CC=afl-cc ./configure --disable-shared && make -j8
开始fuzz,指令-T
参数可以指定argv[0]
1 afl-fuzz -i /tmp/in/ -o /tmp/out/ -T sudoedit ./src/sudo
我这里故意放了能够引起crash的样本进去只为了加速过程
分析新的crash
判断是否为误报
我直接使用作者的crash文件,你可以在:https://github.com/LiveOverflow/pwnedit/tree/main/episode05 中找到
id_000000,sig_06,src_000083+000451,time_23448104,op_splice,rep_8
检验下在我的环境里面是否会有crash
root
user
gdb调试
原视频里面用的是GEF
,这里我用pwndbg
,新人(没有CTFpwn经验)建议用GEF
程序自动运行后停止了
说明这个错误是被malloc
给抛出的
**这会是一个新的0day吗?**在最新平台上测试后发现并不是
简化crash
其实我做到这一步想到的是用afl-tmin
,后来发现作者尝试其他方案失败后,我就直接用afl-tmin
了
在user
下检验
创建软链接
1 2 ln -s /usr/local/bin/sudo 0edit ls -lah 0edit
运行测试
有趣的发现
结尾是xedit
这种形式就可以调用sudoedit
使用ASAN分析漏洞
asan一直是一个很操蛋的工具,经常报错,作者也在这里报错很多,我也是直接展示正常(正常报错)做法
1 make clean && ./configure CFLAGS="-fsanitize=address,undefined -g" LDFLAGS="-fsanitize=address,undefined" CC=clang --disable-shared && make -j8
送入mini_crash样例检测
如果没有加上--disable-shared
的话,就算有-g
参数,也不会知道具体代码在哪里
现在我们知道漏洞的位置在/plugins/sudoers/./sudoers.c:868
的set_cmnd
函数内
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 static int set_cmnd (void ) { struct sudo_nss *nss ; char *path = user_path; int ret = FOUND; debug_decl(set_cmnd, SUDOERS_DEBUG_PLUGIN) user_stat = calloc (1 , sizeof (struct stat)); if (user_stat == NULL ) { sudo_warnx(U_("%s: %s" ), __func__, U_("unable to allocate memory" )); debug_return_int(NOT_FOUND_ERROR); } if (user_cmnd == NULL ) user_cmnd = NewArgv[0 ]; if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { if (ISSET(sudo_mode, MODE_RUN | MODE_CHECK)) { if (def_secure_path && !user_is_exempt()) path = def_secure_path; if (!set_perms(PERM_RUNAS)) debug_return_int(-1 ); ret = find_path(NewArgv[0 ], &user_cmnd, user_stat, path, def_ignore_dot, NULL ); if (!restore_perms()) debug_return_int(-1 ); if (ret == NOT_FOUND) { if (!set_perms(PERM_USER)) debug_return_int(-1 ); ret = find_path(NewArgv[0 ], &user_cmnd, user_stat, path, def_ignore_dot, NULL ); if (!restore_perms()) debug_return_int(-1 ); } if (ret == NOT_FOUND_ERROR) { if (errno == ENAMETOOLONG) audit_failure(NewArgc, NewArgv, N_("command too long" )); log_warning(0 , "%s" , NewArgv[0 ]); debug_return_int(ret); } } if (NewArgc > 1 ) { char *to, *from, **av; size_t size, n; 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 ); } if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { 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' ; } else { for (to = user_args, av = NewArgv + 1 ; *av; av++) { n = strlcpy(to, *av, size - (to - user_args)); if (n >= size - (to - user_args)) { sudo_warnx(U_("internal error, %s overflow" ), __func__); debug_return_int(-1 ); } to += n; *to++ = ' ' ; } *--to = '\0' ; } } } if ((user_base = strrchr (user_cmnd, '/' )) != NULL ) user_base++; else user_base = user_cmnd; if (ISSET(sudo_mode, MODE_RUN) && strcmp (user_base, "sudoedit" ) == 0 ) { CLR(sudo_mode, MODE_RUN); SET(sudo_mode, MODE_EDIT); sudo_warnx(U_("sudoedit doesn't need to be run via sudo" )); user_base = user_cmnd = "sudoedit" ; } TAILQ_FOREACH(nss, snl, entries) { if (!update_defaults(nss->parse_tree, NULL , SETDEF_CMND, false )) { log_warningx(SLOG_SEND_MAIL|SLOG_NO_STDERR, N_("problem with defaults entries" )); } } debug_return_int(ret); }
漏洞造成的原因在CVE那篇文章 分析过了
简化漏洞模型
这里的视角更像是给CTF出题,我也确实一句这个漏洞出过一道,不过在这里我们后面会完成exp的编写,所以只写c程序分析就行了
精简一下上面的源代码,问题出现在这里
1 2 3 4 5 6 7 8 9 10 11 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { 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' ; }
如果最后一个参数是\
的话,from++
,
然后*to++ = *from++
,此时的from
指针就超出了边界,造成堆溢出
可以写一个小的例子调试一下
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 #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> int main () { char from[100 ]; puts ("please input some data, max is 100" ); read(0 ,from,100 ); int len = strlen (from); char *src = from; char *to = (char *)malloc (len); char *dst = to+1 ; puts ("start copy file" ); while (*src){ if (src[0 ] == '\\' && !isspace ((unsigned char )src[1 ])) src++; *dst++ = *src++; } *to++ = '\n' ; printf ("src> %s" ,from); printf ("dst> %s" ,to); }
构造这样的特殊输入,在输入的时候已经输入0x18个字符串了 ,所以是malloc(0x18)
按照程序的效果,会将下一个chunk
的头部份覆写为0xbbbbbbbb
成功覆盖掉,实现预期堆溢出的目标,说明当结尾的反斜杠后面还有数据的时候会产生堆溢出