堆

​

基础

glibc堆管理器:ptmalloc2。

堆由低地址向高地址方向增长。

如果申请了很多的堆块,不考虑释放堆块,会按申请的顺序依次从低地址到高地址排列。

堆空间申请

申请到的一块堆内存起始地址 ≠ 可以写入数据的起始地址,因为堆块头部会记录一些信息。

申请的大小 ≠ 实际申请的大小,会有一个取整的步骤。

​

Top Chunk / Allocated Chunk

当程序第一次进行malloc的时候,heap会被分为两块,一块给用户(allocated chunk),剩下的那块就是top chunk,再次申请堆块要是没有合适的空间便会使用top chunk的空间。

​

常见函数

malloc()

分配所需的内存空间,并返回一个指向它的指针。

1
2
3
4
5
6
7
8
9
void *malloc(size_t size);

/***
参数 size:
要分配的字节大小,是无符号数,如果输入-1,理论上会有一个很大很大的堆,实际上会报错。

返回值:
如果分配成功,则返回指向分配内存的指针;如果分配失败,则返回NULL。
***/

calloc()

分配所需的内存空间,并返回一个(一组)指向它(它们)的指针。

malloc() 与 calloc() 之间的不同点是,malloc() 不会设置内存为零,而 calloc() 会设置分配的内存为零。

1
2
3
4
5
6
7
8
9
void *calloc(size_t nitems, size_t size);

/***
参数 nitems:
需要的堆块数量,也是无符号数。

返回值:
如果分配成功,则返回一个(一组)指向分配的堆块(堆块们)的指针;如果分配失败,则返回NULL。
***/

realloc()

更改已经配置的内存空间,即更改由 malloc() 函数分配的内存空间的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *realloc(void *ptr, size_t size);

/***
参数 ptr:
一个指针,指向堆块或者为空。

返回值:
(1)如果 重新申请的大小 > 申请内存的大小,且当前内存段后面有需要的内存空间,则直接扩展这段内存空间,realloc() 将返回原指针。
(2)如果 重新申请的大小 > 申请内存的大小,且当前内存段后面的空闲字段不够,那么就使用堆中的第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,并将原来的数据块释放掉,返回新的内存块位置,相当于 free() + malloc()。
(3)如果 重新申请的大小 < 申请内存的大小,堆块会直接缩小,被削减的内存会释放。
(4)如果传入了一个空的堆块地址,但是 size ≠ 0,那么就相当于 malloc()。
(5)如果传入了一个正常的堆块地址,但是 size = 0,那么就相当于 free()。
(6)如果申请失败,将返回NULL,此时,原来的指针仍然有效。
***/

free()

释放之前调用 calloc / malloc / realloc 所分配的内存空间。

1
2
3
4
5
6
void free(void *ptr);

/***
参数 ptr:
指针指向一个要释放内存的内存块。如果传递的参数是一个空指针,则不会执行任何动作,**注意 free() 不会清除内存块的数据。**
***/

​

结构

Chunk

Allocated Chunk
  • prev_size/data:临近的上一个Chunk的size或data
  • size:此Chunk的size
  • A(NON_MAIN_ARENA bit):是否由其他的arena管理,而非main_arena
  • M(IS_MMAPPED bit):是否由mmap创出来的
  • P(PREV_INUSE bit):临近的上一个Chunk是否正在使用

image-20240627092215559

Free Chunk
  • Free掉后的Chunk会根据Size而进到不同的Bins中
  • fd:Forward Pointer,指向下一块Free的Chunk
  • bk:Backward Pointer,指向上一块Free的Chunk
  • 以fd/bk将各个Free Chunk串联起来

image-20240627093101297

Bin

Fastbin
  • Free掉 Chunk Size <= global_max_fast 的Chunk,会回收至Fastbin
  • 预设 global_max_fast = 0x80
  • Fastbin共有7个,分别为 [0x20,0x30,0x40,…,0x80]
  • 为单向链表,LIFO,fd指向下一块Free Chunk的Chunk Header
  • 例如:Free掉 Chunk Size = 0x20 的Chunk,会进到代表0x20 Fastbin的链表
  • Free这类Chunk时,不会清除临近的下一块Chunk的P bit
  • 在Free时,如果下一块是Top Chunk,并不会被合并进去
Tcache
  • 从libc 2.26开始使用
  • 为了再加速程序效率而诞生
  • Tcache有许多,分别为 [0x20,0x30,0x40,…,0x410]
  • 为单向链表,LIFO,fd指向下一块Free Chunk的Chunk Data
  • 每个Tcache最多收7个Chunks
  • Free这类Chunk时,不会清除临近的下一块Chunk的P bit
  • 用结构 tcache_perthread_struct 管理 Tcache(指向此结构的指针存在于TLS中),0x291大小的Chunk
Smallbin

每个chunk的大小与其所在的bin的index关系:chunk_size=2*SIZE_SZ*index。

  • 共有62个循环链表,每个链表中存储的chunk大小都一致
  • 每个bin对应的链表采用FIFO的规则,同一个链表中先被释放的chunk会被分配出去

注:

在smallbin中有很大一部分大小与fastbin重合,在后期触发某些操作时,会导致分配器刷新fastbin,将chunk清理到smallbin里。

Largebin
  • 共有63个bin,每个bin中chunk的大小不一致,而是处于一定区间范围内
  • 分为6组,每组bin中chunk大小之间的公差一致
Unsortedbin

可以视为空闲chunk回归其所属bin之前的缓冲区。

  • 处于bin数组下标1处,只有一个链表,采用FIFO的规则
  • bin中的空闲chunk处于乱序状态
  • 两个来源
    • 当一个较大的chunk被分割成两半后,如果剩下的部分(last reminder)大于MINSIZE,就会被放到unsortedbin中(且可被多次切分);如果小于MINSIZE,就会直接返回
    • 释放一个不属于fastbin的chunk,并且该chunk不和top chunk紧邻时,该chunk会被首先放到unsortedbin中;如果该chunk和top chunk紧邻,则优先和top chunk合并

​

Hooks

glibc < 2.34

在 malloc/free/calloc/realloc 进到主要分配算法之前,若有设定hook函数,则会先执行hook函数。

在写exploit时,是个很好的利用对象。朝hook函数中写入,就能控制执行流程。写入One Gadget就能得到shell。

​

UAF

全名 Use After Free。把一个指针当做参数传给free,会释放指针指向的Chunk,此时指针变成悬空指针,后续程序又从此指针写/读数据,就是UAF。

​

Double Free

Free同一块Chunk两次。

若成功,则链表上会出现此Chunk两次。再一次malloc,得到此Chunk,此时这个Chunk在链表中还是存在。

​

bin攻击

Fastbin Attack

基于fastbin机制的漏洞利用方法,利用前提:

  • 存在堆溢出、UAF等能控制chunk内容的漏洞
  • 漏洞发生于fastbin类型的chunk中

常见分类:

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

其中,前两种主要漏洞侧重于利用free函数释放真的chunk或伪造的chunk,然后再次申请chunk进行攻击;后两种侧重于故意修改fd指针,直接利用malloc申请指定位置的chunk进行攻击。

后面几种在新版本利用比较困难,glibc加入了更多的保护。

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

House of Spirit

主要思想是改写在free状态的fast chunk的fd指针,在下一次分配时拿到目标地址的读写权。

步骤:

  1. 释放一个fast chunk,并且不会被tcache回收或干扰,最好禁用tcache或绕过
  2. fast chunk被放进了fastbin,假设此时该chunk位第一个被放进bin的chunk,那么ptmalloc为其fd赋值为零
  3. 利用漏洞(如UAF)改写fd指针为目标地址(如 __free_hook)
  4. 申请一个被释放大小堆块一样的chunk

​

Tcache Attack

源代码中tcache机制的实现在 _int_malloc 前,导致 _int_malloc 的很多安全检查被忽略。

主要分配和释放函数:tcache_get(),tcache_put()

操作过程

  • 第一次malloc时,会先malloc一块内存用来存放tcache_perthread_struct
  • free内存,且size小于smallbin size时
  • tcache之前会放到fastbin或者unsortedbin中
  • tcache后:

    • 先放到对应的tcache中,直到tcache被填满(默认是7个)
    • tcache被填满之后,再次free的内存和之前一样被放到fastbin或者unsortedbin中
    • tcache中的chunk不会合并(不取消inuse bit)
  • malloc内存,且size在tcache范围内

  • 先从tcache取chunk,直到tcache为空
  • tcache为空后,从bin中找
  • tcache为空时,如果fastbin/smallbin/unsortedbin中有size符合的chunk,会先把fastbin/smallbin/unsortedbin中的chunk放到tcache中,直到填满。之后再从tcache中取;因此chunk在bin中和tcache中的顺序会反过来

另外,在tcache中,大部分指针操作都是依靠用户指针进行的。

Tcache Dup

​

Unsortedbin Attack

(预留)

​

Largebin Attack

(预留)

​

off by攻击

off by one

(预留)

​

off by null

(预留)

​

_IO_FILE结构体

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。

FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中,然后以链表的形式串联起来,但系统一开始会自动创建的三个文件即 stdin、stdout、stderr,它们是在libc上。

_IO_FILE_plus 结构体

一个定义引用:

1
extern struct _IO_FILE_plus *_IO_list_all;

_IO_list_all 是一个 _IO_FILE_plus 结构体定义的一个指针,它存在在符号表内。

所有 FILE 文件结构都是这样的一个结构体,结构体内部:

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno; // stderr:2, stdout:1, stdin:0

#if 0
int _blksize;
#else

int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

#ifndef __cplusplus
typedef struct _IO_FILE _IO_FILE;
#endif

struct _IO_FILE_plus;

extern struct _IO_FILE_plus *_IO_list_all;
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;
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
struct _IO_jump_t
{
0x0:JUMP_FIELD(size_t, __dummy);
0x8:JUMP_FIELD(size_t, __dummy2);
0x10:JUMP_FIELD(_IO_finish_t, __finish);
0x18:JUMP_FIELD(_IO_overflow_t, __overflow);
0x20:JUMP_FIELD(_IO_underflow_t, __underflow);
0x28:JUMP_FIELD(_IO_underflow_t, __uflow);
0x30:JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
0x38:JUMP_FIELD(_IO_xsputn_t, __xsputn);
0x40:JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
0x48:JUMP_FIELD(_IO_seekoff_t, __seekoff);
0x50:JUMP_FIELD(_IO_seekpos_t, __seekpos);
0x58:JUMP_FIELD(_IO_setbuf_t, __setbuf);
0x60:JUMP_FIELD(_IO_sync_t, __sync);
0x68:JUMP_FIELD(_IO_doallocate_t, __doallocate);
0x70:JUMP_FIELD(_IO_read_t, __read);
0x78:JUMP_FIELD(_IO_write_t, __write);
0x80:JUMP_FIELD(_IO_seek_t, __seek);
0x88:JUMP_FIELD(_IO_close_t, __close);
0x90:JUMP_FIELD(_IO_stat_t, __stat);
0x98:JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
0xa0:JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif

};

IO_FILE_1

​

用来将一个双向链表(只存储空闲的chunk)中的一个元素取出来,可能在以下地方使用:

malloc

从恰好大小合适的largebin中获取chunk。

注:fastbin没有使用unlink,这就是为什么漏洞会经常出现在这里的原因。

从比请求的chunk所在的bin大的bin中取出chunk。

free

后向合并,合并物理相邻低地址空闲的chunk。

前向合并,合并物理相邻高地址空闲的chunk(除top chunk)。

malloc_consolidate

后向合并,合并物理相邻低地址空闲的chunk。

前向合并,合并物理相邻高地址空闲的chunk(除top chunk)。

realloc

前向扩展,合并物理相邻高地址空闲chunk(除top chunk)。

由于unlink使用非常频繁,所以unlink被实现为一个宏。

在新版本libc中,unlink操作已经被更改为函数定义,且防护力加强。简单unlink攻击只能存在于低版本的libc中)(如libc-2.23)。

​

house of系列

house of force

(预留)

​

house of spirit

(预留)

​

house of einherjar

(预留)

​

house of roman

(预留)

​

house of orange

(预留)

​

house of lore

(预留)

​

house of rabbit

(预留)

​

house of kiwi

(预留)

​

house of banana

(预留)

​

house of apple

(预留)

​

house of cat

(预留)

​

house of emma

(预留)

​