堆相关漏洞与利用
堆溢出
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数 (注意:是写入的字节数超过了堆块本身可使用的字节数,而不是用户申请的字节数,因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),并覆盖到物理相邻的高地址的下一个堆块,轻则可以使得程序崩溃,重则可以使得攻击者控制程序执行流程
一般来说,堆溢出漏洞需要两个前提:
- 程序向堆上写入数据
- 写入的数据大小没有被良好地控制
参考文章:
与栈溢出不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此堆溢出通常无法像栈溢出一样直接控制 EIP
一般来说,我们利用堆溢出的策略是:
覆盖与其物理相邻的下一个
chunk的内容prev_sizesize,主要有三个比特位,以及该堆块真正的大小NON_MAIN_ARENAIS_MAPPEDPREV_INUSEthe True chunk size
chunk content,从而改变程序固有的执行流
利用堆中的机制(如
unlink等 )来实现任意地址写入或控制堆块中的内容等效果,从而来控制程序的执行流
堆溢出通常需要配合其他的方法来实现漏洞的利用,比较常用的方法有:Chunk Extend and Overlap 等
关键步骤
寻找堆分配函数
通常来说堆是通过调用 Glibc 函数
malloc进行分配的,在某些情况下会使用calloc分配,realloc同样也可以达到类似的效果因此,常用的堆分配函数有:
malloccallocrealloc
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 0x000000000000000016 字节的空间是如何装得下 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 - 12BK = P -> bk = expect valueFD -> bk = BK,即:*(target addr - 12 + 12) = BK = expect valueBK -> 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) == PfakeBK -> fd == P,即:*(fakeBK + 8) == P
当满足上述两式时,可以进入 unlink 的环节,进行如下操作:
fakeFD -> bk = fakeBK,即:*(fakeFD + 12) = fakeBKfakeBK -> 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: 15935728House 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_MASKfake chunk的size大小需要满足对应的fast bin的需求,同时也得对齐fake chunk的next chunk的大小不能小于2 * SIZE_SZ,同时也不能大于av->system_memfake 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"] + 0x10Unsorted 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;


















































































