看了ctfwiki上堆相关的部分内容所写笔记。
32位解释一下:
0x00000000-0xc0000000是供各个进程使用,称为用户空间,而0xc0000000-0xffffffff是kernel space也就是内核空间,用户无法访问。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。
由malloc函数申请的内存为chunk,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;
};
struct malloc_chunk*
是定义指针,定义的变量大小在32位系统占4个字节,在64位占8个字节。
INTERNAL_SIZE_T:被定义为size_t,在32位系统上是32位无符号整数(4bytes),在64位系统上是64位无符号整数(8bytes)
prev_size:前一个chunk空闲则记录了前一个chunk的大小,如果前一个chunk不空闲,那么这里存储的就是前一个chunk最后的数据.
这里的前一个chunk指较低地址的的chunk
prev_size如何得到前一个chunk的大小呢?
用当前chunk的地址指针减去前一个地址指针值就得到了。
- SIZE_SZ的定义:
#define SIZE_SZ (sizeof (INTERNAL_SIZE_T))
也就是说,SIZE_SZ在32位系统下是4个字节大小,在64位系统下是8个字节大小- 接上面,由于每个chunk的大小必须是2*SIZE_SZ的整数倍,所以在32位系统下最小的chunk是16个字节大小,64位系统下最小的chunk是24个字节或32个字节大小。8个字节=>8的整数倍是的用二进制表示是 1000*n,所以低三位始终为0,对堆块大小没影响。于是设计人员选择拿低三位当标志位使用,从高到低分别表示:
- NON_MAIN_ARENA:记录当前chunk是否不属于主线程,1代表不属于,0代表属于
- IS_MAPPED:记录当前chunk是否由mmap分配
- PREV_INUSE:记录前一个chunk块是否被分配,1代表被分配,0代表没有被分配。当一个 chunk 的 size 的 P 位为 0 时,说明前一个chunk空闲,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
堆中第一个被分配的chunk的size字段的PREV_INUSE都会被置为1,以便于禁止访问前面的非法内存
fd、bk
fd_nextsize, bk_nextsize
只有在chunk空闲时才是用,只用于large chunk
chunk指的是chunk的首地址,mem指的是用户数据的首地址,中间差了一个chunk header(也就是PREV_SIZE和SIZE,大小是2*SIZE_SZ)
/* conversion from malloc headers to user pointers, and back */
#define chunk2mem(p) ((void *) ((char *) (p) + 2 * SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char *) (mem) -2 * SIZE_SZ))
/* The smallest possible chunk */
#define MIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))
这里求了chunk结构体开头到fd_nextsize的偏移,也就是说,最小chunk包含2个INTERNAL_SIZE_T和2个struct malloc_chunk*,一共4*SIZE_SZ,所以32位占16字节大小,64位占32字节大小,用代码测试一下
#include
#include
int main()
{
void* p1,*p2,*p3,*p4;
p1=malloc(0);
p2=malloc(0);
p3=malloc(0);
p4=malloc(0);
int a;
printf("%p\n",p1);
printf("%p\n",p2);
printf("%p\n",p3);
printf("%p\n",p4);
return 0;
}
32位运行结果
0x883b008
0x883b018
0x883b028
0x883b038
说明32位下最小的chunk大小是16字节
64位运行结果是
0x1c14010
0x1c14030
0x1c14050
0x1c14070
说明64位最小的chunk大小是0x20也就是32个字节
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))
/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)
也就是取结构体中的size然后与上0b000,把低三位清0
ptmalloc把空闲的堆分成四种,分别是fast bins,small bins,large bins,unsorted bin
相似大小的 chunk 会用双向链表链接起来。也就是说,在每类 bin 的内部仍然会有多个互不相关的链表来保存不同大小的 chunk。
对于 small bins,large bins,unsorted bin 来说,ptmalloc 将它们维护在同一个数组中。这些 bin 对应的数据结构在 malloc_state 中,如下
#define NBINS 128
/* Normal bins packed as described above */
mchunkptr bins[ NBINS * 2 - 2 ];
当释放较小或较大的chunk的时候,如果系统没有将它们添加到对应的bins中,系统就将这些chunk添加到unsorted bin中。
组 | 数量 | 公差 |
---|---|---|
1 | 32 | 64B |
2 | 16 | 512B |
3 | 8 | 4096B |
4 | 4 | 32768B |
5 | 2 | 262144B |
6 | 1 | 不限制 |
unlink用来从空闲链表管理器ptmalloc2中取出一个元素(chunk),在以下函数中会用到。
注意fastbin和smallbin没有使用unlink,这里我的理解是unlink是从链表中间取使用的(如果不对希望师傅在评论区指点一下)?small bin和fast bin每个索引位置对应的链表上的chunk的大小是固定的,只需要定位到索引的位置然后从链表尾部取一个(因为这个索引上的一条链表上的元素一样大),而large bin同一个索引上的一排链表中的chunk在一个大小范围内浮动,并不完全相同,为了取一个跟所请求的size一模一样大小的chunk,可能需要从中间unlink一个chunk,所以用到了unlink函数。
unlink被实现为宏,把\去掉写成函数并代码格式化一下好看一点
/* Take a chunk off a bin list */
// unlink p
#define unlink(AV, P, BK, FD) {
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
// 防止攻击者简单篡改空闲的 chunk 的 fd 与 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;
// 下面主要考虑 P 对应的 nextsize 双向链表的修改
if (!in_smallbin_range (chunksize_nomask (P))
// 如果P->fd_nextsize为 NULL,表明 P 未插入到 nextsize 链表中。
// 那么其实也就没有必要对 nextsize 字段进行修改了。
// 这里没有去判断 bk_nextsize 字段,可能会出问题。
&& __builtin_expect (P->fd_nextsize != NULL, 0))
{
// 类似于小的 chunk 的检查思路
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);
// 这里说明 P 已经在 nextsize 链表中了。
// 如果 FD 没有在 nextsize 链表中
if (FD->fd_nextsize == NULL)
{
// 如果 nextsize 串起来的双链表只有 P 本身,那就直接拿走 P
// 令 FD 为 nextsize 串起来的
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else
{
// 否则我们需要将 FD 插入到 nextsize 形成的双链表中
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;
}
}
}
}
可以看出, P 最后的 fd 和 bk 指针并没有发生变化,但是当我们去遍历整个双向链表时,已经遍历不到对应的链表了。这一点没有变化还是很有用处的,因为我们有时候可以使用这个方法来泄漏地址