largebin attack
在64位下,申请大于0x400chunk最终会进入largebin。
Large bins 一共包括63 个bin,每个bin 中的chunk 大小不是一个固定公差的等差数列,而是分成6 组bin,每组bin 是一个固定公差的等差数列,每组的bin 数量依次为32、16、8、4、2、1,公差依次为64B、512B、4096B、32768B、262144B 等。
根据ctfwiki和师傅们的总结:
- 按照大小从大到小排序
- 若大小相同,按照free时间排序
- 若干个大小相同的堆块,只有首堆块的fd_nextsize和bk_nextsize会指向其他堆块,后面的堆块的fd_nextsize和bk_nextsize均为0
- size最大的chunk的bk_nextsize指向最小的chunk; size最小的chunk的fd_nextsize指向最大的chunk
代码分析
进入largebin的相关代码如下(_int_malloc)for (;; )
{
int iters = 0;
// 先判断下是不是unsorted本身,不是就进入while
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
// victim = 将要入largebin的块
bck = victim->bk;
// 检测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);
/*
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.
*/
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
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;
}
/* remove from unsorted list */
// 这个地方的bck是之前unsortedbin中的第二块,将bin[1]的bk赋值为第二块,开始进行脱链操作
unsorted_chunks (av)->bk = bck;
// 将第二块的fd赋值为bin[1](ub)
bck->fd = unsorted_chunks (av);
/* Take now instead of binning if exact fit */
if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
/* place chunk in bin */
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
// 获得对应largebin的index
victim_index = largebin_index (size);
// bck为bin[对应的index],记为bin1
bck = bin_at (av, victim_index);
// fwd = bin1 -> fd ,即整个链条的尾部
fwd = bck->fd;
/* maintain large bins in sorted order */
// largebin里有就进if
if (fwd != bck)
{
/* 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);
// 如果该chunk的size小于bin1 -> bk -> size, 即小于最前面的那个,就进if
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
// fwd = bin1
fwd = bck;
// bck = bin1 -> bk (第一个chunk)
bck = bck->bk;
// 调整nextsize: fd_nextsize = bin1 -> fd,即指向最后一个chunk
victim->fd_nextsize = fwd->fd;
// 调整nextsize: bk_nextsize = bin1 -> fd -> bk_nextsize,即指向第一个chunk(该chunk插入后将会成为第一个chunk)
victim->bk_nextsize = fwd->fd->bk_nextsize;
// 调整相邻两块的nextsize,第一块chunk的fd_nextsize将成为victim,最后一块的bk_nextsize将会成为victim(此时nextsize已完成链表的插入操作)
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
// 该chunk大于前面那个chunk的size就进else
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
// 直到该size大于等于链表中的某个size(从尾部开始遍历,即size最大的地方向小的地方遍历)
while ((unsigned long) size < fwd->size)
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}
// 判断是否和size相等,相等的话就链到前面
if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
// 不相等
else
{
// 调整nextsize,这个时候是以fwd为索引,因为进入了while循环,不确定fwd是否发生了改变,bck索引不再准确
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
// 调整相邻chunk的nextsize
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
// 对应的情况就是largebin中只有这一个,也就是该chunk是第一个进入largebin的块,那么其两个nextsize位都是其本身
victim->fd_nextsize = victim->bk_nextsize = victim;
}
// 最后都进行链表的插入
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
if (++iters >= MAX_ITERS)
break;
}
图示
网络上有一张比较经典的largebin的图示,我再稍微补充一下index64的fd和bk,大概长这样
how2heap
用libc-2.23.so
|
输出:
This technique only works with disabled tcache-option for glibc, see glibc_build.sh for build instructions. |
分析
关键代码如下:
p2[-1] = 0x3f1; |
我们改掉size使得p2变小,并且改掉bk和bk_nextsize为栈地址
当我们malloc(0x90)的时候,位于ub的p3会执行如下代码
// ...... |
借用A1ex师傅博客总结如下:
victim
(也即unsorted chunk
)的size
必须大于largebin chunk
,这样才能够绕过else
之前的检测;- 控制
fwd->bk_nextsize
为target_addr1
,那么执行victim->bk_nextsize = fwd->bk_nextsize
时,就能控制victim->bk_nextsize
为traget_addr1
;- 接着就能控制
victim->bk_nextsize->fd_nextsize = victim
,也就相当于target_addr1->fd_nextsize = victim
,我们在*(taget_addr1+0x20)=victim
写入了unsorted chunk
的地址;- 然后,我们修改
fwd->bk
为target_addr2
,就能控制bck
;- 最后,通过
bck->fd=victim
,就能在*(target_addr2+0x10)=victim
,又写入了unsortedbin chunk
的地址总结就是,如果能够控制已经在
largebin
中的chunk的bk、bk_nextsize
字段,那么就能实现往任意地址写入待插入largebin
的chunk的地址。一般待插入的chunk
地址为堆地址,所以通过largebin attack
可以实现往任意地址写入堆地址的目的
利用手法
我认为,利用手法很简要,控制largebin中chunk的bk字段为(&target - 2)或者bk_nextsize字段为(&target - 4),那么target里面将会被写入堆地址,注意size不能相同,fd等位置置0避免进入unlink
例题
让我先找找看2333