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

2022-06-13 8,928

前言

本文旨在讲述在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)


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

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


我们可以找到很多关于如何绕过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的链表。伪造整个链表,进行劫持。相关的全局变量是可以写的。后面会解释这个变量的用处。


另外一个与之相比破坏性比较小,更容易成功。由于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 的数据


以及一个不为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


一些注意点:

因为_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 师傅


本文作者:i春秋聚集地

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/180765.html

Tags:
评论  (0)
快来写下你的想法吧!

i春秋聚集地

文章数:24 积分: 225

i春秋聚集地旨在为信息安全爱好者提供及时有效的信息渠道和体验平台,欢迎关注公众号(icqedu),和“i春秋学院”微博了解更多网络安全新知识~

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号