更多安全资讯和分析文章请关注启明星辰ADLab微信公众号及官方网站(adlab.venustech.com.cn)
近日,Qualys公司Threat Research Unit披露了一个Glibc漏洞,Glibc库在处理环境变量的时候存在缓冲区溢出漏洞,可导致本地权限提升。该漏洞影响各种Linux 发行版,包括 Fedora、Ubuntu、Debian 等。
0****1
漏洞分析
根据披露的信息,漏洞存在于ld.so动态链接器对环境变量的处理过程中。使用ldd命令查看系统程序的加载器,例如:ldd /bin/ls,可以看到实际加载器为/lib64/ld-linux-x86-64.so.2。
`$ ldd /bin/ls` `linux-vdso.so.1 (0x00007ffe2935d000)` `libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f088ec45000)` `libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f088ea1d000)` `libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f088e986000)` `/lib64/ld-linux-x86-64.so.2 (0x00007f088eca8000)`
漏洞存在于加载器的parse_tunables函数中,该函数由tunables_init函数调用,tunables_init函数负责处理 GLIBC_TUNABLES 环境变量,使开发人员能够动态调整运行时库的行为。
`void __tunables_init (char **envp)``{` `char *envname = NULL;` `char *envval = NULL;` `size_t len = 0;` `char **prev_envp = envp;` ` maybe_enable_malloc_check ();`` ` `while ((envp = get_next_env (envp, &envname, &len, &envval,` `&prev_envp)) != NULL) #获取环境变量` `{``#if TUNABLES_FRONTEND == TUNABLES_FRONTEND_valstring` `if (tunable_is_name (GLIBC_TUNABLES, envname))` `{` `char *new_env = tunables_strdup (envname);` `if (new_env != NULL)` `parse_tunables (new_env + len + 1, envval); #漏洞程序` `/* Put in the updated envval. */` `*prev_envp = new_env;` `continue;` `}`
代码中,get_next_env函数从保存的环境变量中逐个提取环境变量信息。tunable_is_name (GLIBC_TUNABLES, envname)函数负责查找“GLIBC_TUNABLES”的环境变量,找到该变量后将其保存到tunables_strdup函数申请的空间中,并返回缓冲区地址保存到new_env指针。由于此时malloc程序还没初始化,所以tunables_strdup调用__minimal_malloc分配地址,minimal_malloc() 实际上调用 mmap() 来获取内存。
`static char *``tunables_strdup (const char *in)``{` `size_t i = 0;` `while (in[i++] != '\0');` `char *out = __minimal_malloc (i + 1);`` ` `/* For most of the tunables code, we ignore user errors. However,` `this is a system error - and running out of memory at program` `startup should be reported, so we do. */` `if (out == NULL)` `_dl_fatal_printf ("failed to allocate memory to process tunables\n");`` ` `while (i-- > 0)` `out[i] = in[i];`` ` `return out;``}``#endif`
随后调用parse_tunables方法处理 new_env 中的数据,下面对代码进行详细分析。以“tunable1=tunable2=AAA”参数为例。进入第一个while(true),首先找到第一个"="之后的参数,然后将p指向第一个参数的值“tunable2=AAA”。
`while (p[len] != '=' && p[len] != ':' && p[len] != '\0')` `len++;` `...``p += len + 1;``len=0;`
然后,开始第二次循环检索,此时没有对错误格式输入的第二个等号进行检索,直接定位到参数的结尾,这时len的长度为"tunable2=AAA"的长度。
`while (p[len] != ':' && p[len] != '\0')` `len++;`
随后在for循环中将tunable1后面所有的数据全部拷贝到tunestr,此时缓冲区已经被占满。
`for (size_t i = 0; i < sizeof (tunable_list) / sizeof (tunable_t); i++)` `{` `tunable_t *cur = &tunable_list[i];`` ` `if (tunable_is_name (cur->name, name))` `{` `/* If we are in a secure context (AT_SECURE) then ignore the` `tunable unless it is explicitly marked as secure. Tunable` `values take precedence over their envvar aliases. We write` `the tunables that are not SXID_ERASE back to TUNESTR, thus` `dropping all SXID_ERASE tunables and any invalid or` `unrecognized tunables. */` `if (__libc_enable_secure)` `{` `if (cur->security_level != TUNABLE_SECLEVEL_SXID_ERASE)` `{` `if (off > 0)` `tunestr[off++] = ':';`` ` `const char *n = cur->name;`` ` `while (*n != '\0')` `tunestr[off++] = *n++;`` ` `tunestr[off++] = '=';`` ` `for (size_t j = 0; j < len; j++)` `tunestr[off++] = value[j];` `}`` ` `if (cur->security_level != TUNABLE_SECLEVEL_NONE)` `break;` `}`` ` `value[len] = '\0';` `tunable_initialize (cur, value);` `break;` `}` `}`
最后一个判断,如果p[len]!='\0',则将p指向下一个参数。但是由上文可知,此时p[len]=='\0',所以进入第二个循环,此时p指向第二个参数的值“tunable2=AAA“。再重复上面的拷贝过程中会造成缓冲区溢出,溢出字节为“AAA”。
`if (p[len] != '\0')` `p += len + 1;`
0****2
权限提升
下面介绍如何劫持程序的环境变量,修改glibc动态链接库路径,并且使其加载修改过的libc.so.6文件,达到提权的目的。
首先,看一下这部分程序申请空间的过程,根据调试,tunables_init初始化中第一次获取GLIBC_TUNABLES环境变量会调用minimal_malloc来申请内存。申请内存的位置0x7f8b545cd2e0 位于/usr/local/lib/ld-linux-x86-64.so.2缓冲区中,此时距离ld-linux-x86-64.so.2程序空间末尾距离为0xd20。
`pwndbg> b __GI___tunables_init``pwndbg> b *0x7f8b545aad5d #通过计算得到``Breakpoint 4 at 0x7f8b545aad5d: file dl-tunables.c, line 52.``pwndbg> c``Continuing.``Thread 3.1 "test" hit Breakpoint 4, 0x00007f8b545aad5d in tunables_strdup (in=<optimized out>) at dl-tunables.c:52``52 char *out = __minimal_malloc (i + 1);``pwndbg> ni``pwndbg> i r``rax 0x7f8b545cd2e0` `pwndbg> vmmap` `0x7f8b54595000 0x7f8b54597000 r--p 2000 0 /usr/local/lib/ld-linux-x86-64.so.2` `0x7f8b54597000 0x7f8b545be000 r-xp 27000 2000 /usr/local/lib/ld-linux-x86-64.so.2` `0x7f8b545be000 0x7f8b545c9000 r--p b000 29000 /usr/local/lib/ld-linux-x86-64.so.2` `0x7f8b545ca000 0x7f8b545ce000 rw-p 4000 34000 /usr/local/lib/ld-linux-x86-64.so.2` `0x7ffca3e3e000 0x7ffca4440000 rw-p 602000 0 [stack]``0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]``pwndbg> hex(0x7f8b545ce000-0x7f8b545cd2e0)` `0x000d20`
第一次申请空间会分配到0xd20这部分空间,但是如果在申请0xd00大小的空间之后。程序再次调用minimal_malloc函数来申请空间,将会调用mmap()程序从内核申请可用空间。这里以申请0x200大小的空间为例,可以看到内核分配了大小为0x2000的空间。经过调试后可知,后续使用minimal_malloc申请的空间也会从这一块空间中分配,这也就让利用该漏洞有了可能。
`pwndbg> c``Continuing.``Thread 3.1 "test" hit Breakpoint 4, 0x00007f8b545aad5d in tunables_strdup (in=<optimized out>) at dl-tunables.c:52``52 char *out = __minimal_malloc (i + 1);``pwndbg> ni``0x00007f8b545aad62 52 char *out = __minimal_malloc (i + 1);``pwndbg> i r``rax 0x7f8b5458d000` `pwndbg> vmmap``LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA` `Start End Perm Size Offset File` `0x403000 0x405000 rw-p 2000 4000 /home/kpy/test` `0x7f8b5458d000 0x7f8b5458f000 rw-p 2000 0 [anon_7f8b5458d]`
在tunables_init初始化完成后紧接着会在dl-object.c 的__dl_new_object函数中申请缓冲区来存储struct link_map结构体,由于此时glibc的calloc的函数还未初始化,所以此时还是调用minimal_malloc函数来申请空间。
`new = (struct link_map *) calloc (sizeof (*new) + audit_space` `+ sizeof (struct link_map *)` `+ sizeof (*newname) + libname_len, 1);`
根据调试信息,此时申请的空间位于GLIBC_TUNABLES环境变量后面,也就是说,溢出刚好能覆盖struct link_map结构体的内容。
`pwndbg> c``Continuing.``Thread 3.1 "test" hit Breakpoint 4, 0x00007f8b545aad5d in tunables_strdup (in=<optimized out>) at dl-tunables.c:52``52 char *out = __minimal_malloc (i + 1);``pwndbg> ni` `0x00007f8b545aad62 52 char *out = __minimal_malloc (i + 1);``pwndbg> i r``rax 0x7f8b5458d210 140236392288784``pwndbg> vmmap``0x403000 0x405000 rw-p 2000 4000 /home/kpy/test``0x7f8b5458d000 0x7f8b5458f000 rw-p 2000 0 [anon_7f8b5458d]`
接下来,考虑需要覆盖结构体的哪个成员变量。根据link_map结构体信息,发现一个非常有意思的成员变量link_map->l_info[DT_RPATH],这是一个指向小型 (16B) Elf64_Dyn 结构的指针。
`pwndbg> p *((struct link_map *) $rax)``$1 = {` `l_addr = 4774451407232463713,` `l_name = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>,` `l_ld = 0x4242424242424242,` `l_next = 0x4242424242424242,` `l_prev = 0x4242424242424242,` `l_real = 0x4242424242424242,` `l_ns = 4774451407313060418,` `l_libname = 0x4242424242424242,` `l_info = {0x4242424242424242 <repeats 49 times>, 0x696c673a42424242, 0x6f6c6c616d2e6362, 0x74736166786d2e63, 0x3d, 0x0 <repeats 24 times>},` `l_phdr = 0x7ffcfffff010,` `l_entry = 0,` `l_phnum = 0,` `l_ldnum = 0,` `l_searchlist = {` `r_list = 0x0,` `r_nlist = 0` `}` `l_local_scope = {0x0, 0x2e6362696c673a00},` `l_file_id = {` `dev = 7867334929274397037,` `ino = 67570361263736` `},` `...` `l_relro_addr = 0,` `l_relro_size = 0,` `l_serial = 0``}`
控制该指针变量即可以控制用户程序的动态链接库路径。具体代码在_dl_init_paths(elf/dl-load.c)函数中,当动态链接器加载共享库时会执行该部分代码。代码首先检查 DT_RPATH 成员变量是否存在,如果存在,则从该节中读取 RPATH 信息,并将其解析为一组目录路径,存储在l->l_rpath_dirs.dirs 中。如果 RPATH 为空,则设置 l->l_rpath_dirs.dirs = (void*)-1,表示路径查找失败。
`if (l->l_info[DT_RPATH])` `{` `/* Allocate room for the search path and fill in information` `from RPATH. */` `decompose_rpath (&l->l_rpath_dirs,` `(const void *) (D_PTR (l, l_info[DT_STRTAB])` `+ l->l_info[DT_RPATH]->d_un.d_val),` `l, "RPATH");` `/* During rtld init the memory is allocated by the stub` `malloc, prevent any attempt to free it by the normal` `malloc. */` `l->l_rpath_dirs.malloced = 0;` `}` `else` `l->l_rpath_dirs.dirs = (void *) -1;` `}`
在上面代码调用decompose_rpath时,代码对 l->l_rpath_dirs 进行了内存分配和初始化,其中l->l_info[DT_STRTAB] 和l->l_info[DT_RPATH]->d_un.d_val 分别指向DT_STRTAB表和偏移。
DT_STRTAB表地址在实际程序su的0xFF0处,通过该地址加上偏移,就能得到程序调用的动态链接库路径。
一般在suid的程序中DT_STRTAB表附近都会有下图中类似的字符,以引号字符“为例,也就是如果将 l->l_info[DT_RPATH]->d_un.d_val 设置为-0x14,就能计算出目录为引号字符“的路径,只要在引号字符“目录中设置修改过的libc.so.6,就能让被攻击的程序调用错误的动态链接库,获取root权限。
在实际开发时,如何将l_info[DT_RPATH]设置为指向0x14的地址?在上文中有提到,最开始的环境变量保存在堆栈中,所以这里将l_info[DT_RPATH]地址覆盖为栈地址。但是通常拥有SUID权限的程序都开启了PIE保护,堆栈中没有稳定可用的地址。但是由于漏洞可以反复触发,所以使用Stack Spray。在 Linux 上,堆栈会在 16GB 区域中随机化,环境变量字符串最多可以占用 6MB。假如我们填充6M大小的环境变量,在 最多16GB / 6MB = 2730 次尝试后,就很有可能列举出指向0x14的地址。经过2000多次的尝试,提权成功。
0****3
补丁分析
下面是ubuntu对该漏洞的修复代码,可以看到在代码后面增加了一条判断语句if (p[len] == '\0'),如果p[len]==\0,则执行break,跳出循环,不会继续复制,防止了缓冲区溢出。
补丁链接 https://ubuntu.com/security/notices/USN-6409-1
`+-static void``++__attribute__ ((noinline)) static void``+ parse_tunables (char *tunestr, char *valstring)``+ {``+ if (tunestr == NULL || *tunestr == '\0')``+@@ -187,11 +187,7 @@ parse_tunables (char *tunestr, char *val``+ /* If we reach the end of the string before getting a valid name-value``+ pair, bail out. */``+ if (p[len] == '\0')``+- {``+- if (__libc_enable_secure)``+- tunestr[off] = '\0';``+- return;``+- }``++ break;``+` `+ /* We did not find a valid name-value pair before encountering the``+ colon. */``+@@ -251,9 +247,16 @@ parse_tunables (char *tunestr, char *val``+ }``+ }``+` `+- if (p[len] != '\0')``+- p += len + 1;``++ /* We reached the end while processing the tunable string. */``++ if (p[len] == '\0')``++ break;``++``++ p += len + 1;``+ }``++``++ /* Terminate tunestr before we leave. */``++ if (__libc_enable_secure)``++ tunestr[off] = '\0';``+ }``+ #endif`
0****4
修复建议
在ubuntu系统中可以运行下面的命令进行升级,提高系统安全性。
`# apt-get update``# apt-get upgrade libc6`
参考链接:
[2]https://paper.seebug.org/3090/
[3]https://www.uptycs.com/blog/cve-2023-4911-looney-tunables-glibc-exploit
[4]https://blog.csdn.net/canpool/article/details/121942562
[5]https://github.com/leesh3288/CVE-2023-4911
启明星辰积极防御实验室(ADLab)
ADLab成立于1999年,是中国安全行业最早成立的攻防技术研究实验室之一,微软MAPP计划核心成员,“黑雀攻击”概念首推者。截止目前,ADLab已通过CVE累计发布安全漏洞近1200个,通过 CNVD/CNNVD/NVDB累计发布安全漏洞4000余个,持续保持国际网络安全领域一流水准。实验室研究方向涵盖基础安全研究、5G安全研究、人工智能安全研究、移动与物联网安全研究、工控安全研究、信创安全研究、云安全研究、无线安全研究、高级威胁研究、攻防体系建设。研究成果应用于产品核心技术研究、国家重点科技项目攻关、专业安全服务等。