TSCTF2019 薛定谔的堆块-HeapSpray
heapspray有很多的应用场景,但大多都是windows下的漏洞应用,关于Glibc的比较少,至今只看见两题:
这里参考第二篇文章针对第二题做个复现,理解下堆喷的思想。
1.函数理解
这里分析起来比较麻烦,最好就调试,直接给出相关的功能:
(1)Create函数:
创建chunk,但每次Create会创建0x10个相同大小的chunk,且大小为输入size+4。比如输入size为0xc,那么创建的chunk就是0x10个0x18大小的Chunk。同时每0x10个小chunk在宏观意义上组成一个大chunk,这里用SmallChunk和BigChunk区分一下。
chunk的索引在全局数组dword_4060,chunkList中随机排列,比如idx为0的chunk不一定是第一个创建的。
这点在后面堆喷会用到,无法简单地通过打印值来判断heap地址,只能判断出在哪个BigChunk中,还得判断出某个SmallChunk在BigChunk中的位置才能泄露出堆地址。
创建chunk读取数据时read_str函数里有\x00截断,所以Display在没有UAF的情况下难以泄露出地址,这里也不存在堆溢出。
选择chunk类型时会给chunk_addr+size处赋值,这里就是之前申请size+4的原因。
这个赋予的值是一个ELF上的data数据地址,没啥用,迷惑用的,同时如果选择的选项不为1-4的话,就会不赋值,这个在后面很有用。
(2)Display函数:
比较常规,输出给定index范围的SmallChunk的内容
(3)Delete函数:
删除最后一次Create的BigChunk的所有SmallChunk,free数据且置指针NULL,没啥漏洞。但是这里删除是依据chunkList的顺序索引删除,而chunkList又是被打乱的,所以删除之后的顺序其实不是我们最开始输入数据的顺序,这个在后面unsortedbin泄露数据的时候需要注意一下。
(4)Modify函数:
编辑指定index的Small Block的内容,这里没啥用
(5)CallFunction函数:
根据Create时的最后那4byte的数值来决定执不执行某个函数指针(这个函数指针就是最开始创建的时候赋值的ELF上的数据)。
*(chunk_addr+size) != 0,则set ((chunk_addr+size)) -= 1
*(chunk_addr+size) == 0,则jmp ((chunk_addr+size))+0x4
这里调用CallFunction函数之后就可以调用到0xf7e87401,这里的*0x57d1ab8c是我们在堆上设置好的内容。
2.漏洞发现:
这里就结合Create函数,利用先申请填充内容之后再释放,使得*(chunk_addr+size)
可控,从而能够调用任意函数。但是在保护全开的情况下想要调用函数,必须需要泄露地址,而地址在没有漏洞的情况下又没办法泄露。
堆喷原理:https://www.cnblogs.com/Fang3s/articles/3911561.html
(1)堆喷结合CallFunction函数的-1泄露地址:
假设某个堆地址:magic_addr。由于这里可以Display,所以如果*magic_addr= magic_addr-1
,而利用堆喷使得一定范围内的堆内容都为magic_addr,打印内容之后,就可以依据打印的内容,能够从中筛选出magic_addr,获取其索引,再经过我们制造堆喷过程中运算就能得到开始堆喷的地址start_addr。
比如:magic_addr = 0x58585858,申请了0x100个0x20000大小的Chunk,那么得到索引为0x58,且magic_addr 也是一个0x20000的chunk,就可求得start_addr为0x58585858-0x58*0x20000。当然这是理论上的,实际还得一系列的判断运算。
同理,在当我们释放堆块进入unsortedbin之后,踩下main_arena地址再申请回来,由于\x00截断很难泄露出地址,这里也是采用这个方法,使得\x00-1成为0xff来把\x00截断给抹杀。
(2)getshell原理
有了地址之后就可调用libc上任意的函数了,这里的one_gadget都用不了,在没办法往栈上输入数据的情况下就需要栈劫持了,这里找两个gadget,原题给的是:
1 | #注释头 |
我用我自己编译的Libc是:
1 | #注释头 |
一样的,没啥区别,得自己找去。ROPgadget。
在调用jmp *(*(chunk_addr+size))+0x4
时,看到context为
这里的ecx就保存这一个堆地址,那么我们就利用ecx和eax结合这两个gdaget来进行栈劫持,从而getshell。
3.exp编写:
(1)堆喷堆布局
填充数据在堆上,满足*magic_addr=magic_addr
,且其他chunk的所有数据也为magic_addr
1 | #注释头 |
(2)填充需要触发的chunk数据
满足*chunk_addr + size = magic_addr
,然后调用callfuc函数使得*magic_addr= magic_addr-1
,打印数据之后即可判断。
1 | #----------------------------------------------- |
(3)判断chunk基于的BigChunk索引:
1 | #注释头 |
(4)计算chunk在BigChunk中的位置:
1 | #注释头 |
同样的方法,依据地址顺序遍历BigChunk中的0x1-0x10的所有可能范围,对于修改*chunk_addr= magic_addr-1,然后打印判断得到各个索引对应的地址。由于创建的时候是random函数,所以也可以用爆破的方式解决,概率为1/16。
(5)获取libc地址
方法是释放之后使之进入unsortedbin踩下地址,利用callfuc函数和字节错位的方法对抗\x00截断从而泄露出地址:
1 | #注释头 |
这里的main_arena变化是因为0xedb7ab000变为了0xedb7afff,导致字节错位变化的,具体调试一下就知道
(6)劫持栈
结合gadget来getshell。
1 | #注释头 |
这里关于最后堆上数据的布局需要调试才能知道,建议先随便写几个,然后调试的时候在写数据。
▲题外话:这里其实并没有用到常规意义上的通过堆喷滑板0x0c,0x58之类的滑板指令来执行shellcode或者ROP,所以其实这里的magic_addr换成0x57575757,0x56565656也是一样可以的,只不过成功率可能会小不少,毕竟这里还最开始申请了一个随机大小的堆块,而且PIE堆的随机化程度也大多在0x56到0x58之间。
4.总结:
堆喷思想:其实就是多级指针的思想,通过劫持指针来滑动程序流或者泄露地址。
调试:汇编指令一定要熟悉,像劫持栈常用的xchg eax,esp等。