前言
又鸽了好久,抱歉哈~ 总的来说House Of Einherjar这种利用方法还是挺简单的,有点像chunk extend/shrink技术,只不过该技术是后向,并且利用top_chunk合并机制,个人觉得杀伤力比较强大
往期回顾:
(补题)HITCON 2018 PWN baby_tcache超详细讲解
好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc
(补题)LCTF2018 PWN easy_heap超详细讲解
好好说话之Tcache Attack(3):tcache stashing unlink attack
好好说话之Tcache Attack(2):tcache dup与tcache house of spirit
好好说话之Tcache Attack(1):tcache基础与tcache poisoning
好好说话之Large Bin Attack
好好说话之Unsorted Bin Attack
好好说话之Fastbin Attack(4):Arbitrary Alloc
(补题)2015 9447 CTF : Search Engine
好好说话之Fastbin Attack(3):Alloc to Stack
好好说话之Fastbin Attack(2):House Of Spirit
好好说话之Fastbin Attack(1):Fastbin Double Free
好好说话之Use After Free
好好说话之unlink
…
编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks♪(・ω・)ノ
House Of Einherjar
House Of Einherjar这种堆块的利用方式与之前的有些区别,该技术可以强制使得malloc
返回一个几乎任意地址的chunk。和之前的利用技术有一些区别,之前我们都是尽可能的避免释放堆块与top_chunk合并,因为释放之后的堆块可能会进行复用,比如在挂hook的时候我们需要两次对同一个释放堆块进行写操作。但是House Of Einherjar这种技术反而是,当释放堆块下一个块是top_chunk的时候,free
会与相邻后向地址进行合并
涉及的原理
free函数:后向合并
free函数后向合并关键代码如下(glibc/malloc/malloc.c):
解读一下这段代码:
- 4002:判断被释放堆块p的inuse标志位是否为
0
,如果为0则进行if中的内容,相当于一个检查。通过这个点说明我们至少要通过堆溢出去覆盖掉相邻高地址位的inuse
标志位,最常见的方式就是off-by-one
- 4003:记录相邻堆块p的
prev_size
值 - 4004:size为size + prev_size
- 4005:堆块p的指针最后由
chunk_at_offset()
函数决定,chunk_at_offset()函数如下图,作用是将原本p指针位置加上
s偏移后的位置作为合并堆块的新指针。那么带回到free函数中,意思就是原本p指针需要减去
(向后)一个后向堆块size(p->prev_size)大小的偏移后得到合并堆块的新指针 - 4006:unlink检查
free函数:与top_chunk合并
当被释放堆块紧邻top_chunk,那么释放后会与top_chunk进行合并
可以看到执行set_head()函数后,合并堆块的size会变为两个堆块的总和,并且top_chunk的指针会指向被合并的堆块p的位置。就相当于top_chunk把p给吞了,并取代了p的位置
how2heap例子验证
由于wiki上面那个例子讲真没看懂,所以在how2heap里面找了找,发现了有这个例子,下面是我精简之后的程序源码:
1 //gcc -g hollk.c -o hollk
2 //glibc-2.23
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <stdint.h>
8 #include <malloc.h>
9
10 int main()
11 {
12 setbuf(stdin, NULL);
13 setbuf(stdout, NULL);
14
15 uint8_t* a;
16 uint8_t* b;
17 uint8_t* d;
18
19 a = (uint8_t*) malloc(0x38);
20 printf("a: %p\n", a);
21
22 int real_a_size = malloc_usable_size(a);
23 printf("Since we want to overflow 'a', we need the 'real' size of 'a' after rounding:%#x\n", real_a_size);
24
25 size_t fake_chunk[6];
26
27 fake_chunk[0] = 0x100;
28 fake_chunk[1] = 0x100;
29 fake_chunk[2] = (size_t) fake_chunk;
30 fake_chunk[3] = (size_t) fake_chunk;
31 fake_chunk[4] = (size_t) fake_chunk;
32 fake_chunk[5] = (size_t) fake_chunk;
33 printf("Our fake chunk at %p looks like:\n", fake_chunk);
34
35 b = (uint8_t*) malloc(0xf8);
36 int real_b_size = malloc_usable_size(b);
37 printf("b: %p\n", b);
38
39 uint64_t* b_size_ptr = (uint64_t*)(b - 8);
40 printf("\nb.size: %#lx\n", *b_size_ptr);
41 a[real_a_size] = 0;
42 printf("b.size: %#lx\n", *b_size_ptr);
43
44 size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
45 printf("Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
46 *(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;
47
48 fake_chunk[1] = fake_size;
49
50 free(b);
51 printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);
52
53 d = malloc(0x200);
54 printf("Next malloc(0x200) is at %p\n", d);
55 }
简单的描述一下这个程序,首先创建了一个size为0x48大小的chunk_a,并将其malloc指针赋值给指针变量a。接下来创建了一个数组fake_chunk,并在数组0和1下标处赋值0x100、在数组下标2、3、4、5处赋值fake_chunk的起始地址。接下来创建了一个size为0x108大小的chunk_b,并且其malloc指针赋值给变量b。然后将chunk_b的inuse标志位修改成0,将chunk_b的prev_size和fake_chunk的size均设置为chunk_b到fake_chunk的偏移。释放chunk_b后重新申请一个0x300大小的堆块
调试一下
首先在第25行下断点,执行下面的代码:
12 setbuf(stdin, NULL);
13 setbuf(stdout, NULL);
14
15 uint8_t* a;
16 uint8_t* b;
17 uint8_t* d;
18
19 a = (uint8_t*) malloc(0x38);
20 printf("a: %p\n", a);
21
22 int real_a_size = malloc_usable_size(a);
23 printf("Since we want to overflow 'a', we need the 'real' size of 'a' after rou nding: %#x\n", real_a_size);
这段代码我们关注的点其实就是创建了一个size为0x48
(0x38+0x10)大小的chunk_a
接下来我们在第35行下断点,执行下面的代码:
25 size_t fake_chunk[6];
26
27 fake_chunk[0] = 0x100;
28 fake_chunk[1] = 0x100;
29 fake_chunk[2] = (size_t) fake_chunk;
30 fake_chunk[3] = (size_t) fake_chunk;
31 fake_chunk[4] = (size_t) fake_chunk;
32 fake_chunk[5] = (size_t) fake_chunk;
33 printf("Our fake chunk at %p looks like:\n", fake_chunk);
这里创建了一个数组fake_chunk,并将其中填满数据,在地3行会输出fake_chunk的地址,使用gdb看一下这个位置
这里将fake_chunk的prev_size、size部分设置为0x100,fd、bk、fd_nextsize、bk_nextsize设置为fake_chunk自身地址,这样做是为了绕过free()函数后向合并时最后的unlink检查
接下来我们在第39行下断点,执行下面的代码:
35 b = (uint8_t*) malloc(0xf8);
36 int real_b_size = malloc_usable_size(b);
37 printf("b: %p\n", b);
这段代码其实就是创建了一个size为0x108(0xf8+0x10)大小的chunk_b,我们来看一下:
接下来将断点下在第41行,完成uint64_t* b_size_ptr = (uint64_t*)(b - 8);
这段代码:
这里其实就是将chunk_b的malloc指针-0x8的位置,即chunk_b的size值放在了b_size_ptr变量中。这一步是为了更好的演示接下来溢出后的对比
那么接下来将断点下载第44行,我们看一下a[real_a_size] = 0;
这段diamante运行之后的结果
a[real_a_size] = 0
这段代码中的a[n]是以chunk_a
的malloc指针为起始的指针数组
,那么数组下标n指向的就是第n+1
个字节的地址,也就是说a[real_a_size]其实指向的是以chunk_a的malloc指针为起始,第real_a_size + 1个字节的位置等于0。其实这里模拟的就是off-by-one的过程。那么这样一来chunk_b的inuse标志位就被覆盖成了0
接下来我们在第46行下断点,执行size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
,分析一下这个计算过程:
fake_size是由b-sizeof(size_t)*2和(uint8_t*)fake_chunk相减得到的:
- b-sizeof(size_t)*2:chunk_b的malloc指针减去两个地址位宽,也就是chunk_b的头指针
- (uint8_t*)fake_chunk:即是伪造堆块的头指针
那么这样一来就可以很明显的看出fake_size,即是chunk_b头指针距离fake_chunk头指针的偏移,需要注意的是我们看到的偏移为0xffffd5555575a140
这代表着偏移其实是一个负数
接下来我们将断点下在第50行,执行下面的部分代码:
46 *(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;
47
48 fake_chunk[1] = fake_size;
real_a_size-sizeof(size_t)
的位置其实就是chunk_a与chunk_b公用的chunk_b的prev_size位置,也就是说这一步模拟的是通过对chunk_a的data赋值后,影响chunk_b的prev_size,根据第46行的代码,我们可以知道chunk_b的prev_size背负上了fake_size。接下来又将fake_chunk的size部分也修改成了fake_size
将chunk_b与fake_chunk部署完成后,像上图一样摆在一起,这看起来就有那味儿了
。chunk_b的prev_size等于fake_chunk的size
,这个size恰巧又是chunk_b到fake_chunk的偏移
,更巧的是chunk_b的inuse标志位为0
那么如果chunk_b被释放掉,首先会去检查其inuse标志位,发现为0
,这就意味着存在一个相邻地址的堆块也是处于释放状态
的,那么就会根据chunk_b的prev_size先前找是否存在一个大小为0xffffd5555575a140
大小的堆块,结果根据chunk_b的头指针+0xffffd5555575a140
处找到了fake_chunk
,fake_chunk的size
正是我们部署的0xffffd5555575a140
根据free()函数后向合并机制,由于我们部署了fake_chunk的fd
、bk
、fd_nextsize
、bk_nextsize
,所以可以绕过unlink
检查,那么chunk_b与fake_chunk就被合并称为一个大小为fake_size + b_size
的大堆块,并且合并大堆块的头指针
即是fake_chunk的头指针0x00007fffffffdf00
这还不算完,根据top_chunk合并机制,由于chunk_b是紧邻top_chunk
的,那么在chunk_b与fake_chunk合并之后top_chunk会将合并后的大堆块整个“吞掉”
。新的top_chunk的size变成了old_top_size + fake_size + b_size
。并且top_chunk的头指针会变成合并堆块的头指针,即fake_chunk的头指针0x00007fffffffdf00
接下来我们将断点下在第54行,执行free(b)
和malloc(0x200)
这两步操作。free(b)会完成上述的执行过程,而因为bin中没有
能够满足malloc(0x200)的空闲块,所以会向top_chunk
申请一个size为0x210
(0x200+0x10)大小的堆块。由于此时top_chunk的头指针是fake_chunk0x00007fffffffdf00
,所以最后被启用的堆块即是以fake_chunk为头指针0x00007fffffffdf00
,size为0x210大小的堆块
这里由于pwndbg插件的heap指令无法识别heap段意外的区域,所以我们直接运行结束,打印出这个从top_chunk申请的堆块的头指针:
可以看到,我们伪造的fake_chunk就会被正式以堆块的形式被启用了
总结
利用该方法需要注意的三点
- 需要有溢出漏洞可以写物理相邻的高地址的 prev_size 与 PREV_INUSE 部分
- 需要计算目的 fake_chunk 与 chunk_b 地址之间的差,所以需要泄漏地址
- 需要在目的 chunk 附近构造相应的 fake chunk,从而绕过 unlink 的检测