基础

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

(预留)