该技术主要是将一块可控的内存精心构造(fake chunk),以欺骗free通过其检查,令堆管理器将我们构造的内存块视作堆的chunk,进入bins中。
house of spirit attack常常需要搭配其他攻击手段,也常常是攻击链条中的一环。我们以2014 hack.lu oreo作为例子展示一种house of spirit的攻击场景。
在此之前我们先通过how2heap的例子来说明应该如何精心构造内存才能通过free检查进入bins。
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
int
main()
{
setbuf(stdout, NULL);
puts(
"This file demonstrates the house of spirit attack."
);
puts(
"This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write."
);
puts(
"Required primitives: known target address, ability to set up the start/end of the target memory"
);
puts(
"\nStep 1: Allocate 7 chunks and free them to fill up tcache"
);
void
*
chunks[
7
];
for
(
int
i
=
0
; i<
7
; i
+
+
) {
chunks[i]
=
malloc(
0x30
);
}
for
(
int
i
=
0
; i<
7
; i
+
+
) {
free(chunks[i]);
}
puts(
"\nStep 2: Prepare the fake chunk"
);
/
/
This has nothing to do with fastbinsY (do
not
be fooled by the
10
)
-
fake_chunks
is
just a piece of memory to fulfil allocations (pointed to
from
fastbinsY)
long
fake_chunks[
10
] __attribute__ ((aligned (
0x10
)));
printf(
"The target fake chunk is at %p\n"
, fake_chunks);
printf(
"It contains two chunks. The first starts at %p and the second at %p.\n"
, &fake_chunks[
1
], &fake_chunks[
9
]);
printf(
"This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n"
);
puts(
"... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end."
);
printf(
"Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n"
, &fake_chunks[
1
]);
fake_chunks[
1
]
=
0x40
;
/
/
this
is
the size
printf(
"The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n"
);
printf(
"Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n"
, &fake_chunks[
9
]);
fake_chunks[
9
]
=
0x1234
;
/
/
nextsize
puts(
"\nStep 3: Free the first fake chunk"
);
puts(
"Note that the address of the fake chunk must be 16-byte aligned.\n"
);
void
*
victim
=
&fake_chunks[
2
];
free(victim);
puts(
"\nStep 4: Take out the fake chunk"
);
printf(
"Now the next calloc will return our fake chunk at %p!\n"
, &fake_chunks[
2
]);
printf(
"malloc can do the trick as well, you just need to do it for 8 times."
);
void
*
allocated
=
calloc(
1
,
0x30
);
printf(
"malloc(0x30): %p, fake chunk: %p\n"
, allocated, victim);
assert
(allocated
=
=
victim);
}
这个例子展示了如何绕过free的检查使得我们构造的fake chunk能够进入bins,大部分都是构造一个fast chunk,这里也不例外。需要注意的是long fake_chunks[10] __attribute__ ((aligned (0x10)));
写法是告诉编译器给我的内存地址要是0x10对齐的。
下图展示了chunk块free后想要进入fastbins的流程,以及所经历的检查。
我们发现house of spirit的核心在于控制fake chunk和next chunk的size字段,而不在乎两个字段之间夹杂的内存。也就是说,只要我们可以任意写两块不相邻的内存,可以分别在两块内存构造size字段的值,在free后堆管理器会将这两段内存及其中间的内存视作一个chunk,而我们若可以通过malloc将这个伪造的chunk拿到,我们也就拿到了一块更大的连续内存。
程序是一个文字界面的购物菜单。Add可以增加步枪商品(创建和写堆块),show可以打印商品信息(读并打印堆块),Order函数可以清空购物订单(销毁所有堆块,free),Leave可以添加一个订单备注(往bss段写120字节),Stats函数可以打印本次购物的总体信息(读并打印几个bss段变量)。
程序的自定义结构体非常简单,可以很轻松的逆向出来,下面我主要对关键内存读写的部分进行追踪和分析。
下面结构体的每一个字段都有说明,主要是关于其读写说明,然后我也可以看到所有的订单都一个单链表的结构。
在add中可以创建Rifile结构体,其中desc和name字段在创建时有一次写入的机会,而在show函数中可以答应两个字段;在order函数中,会沿着next便利单链表逐一的free(next)。
下面是几个bss段变量。其中order_count在order函数中每free一个订单就会增加1,在stats中会被打印输出,即order_count是记录多少订单结算了。
obj_count每次add会增加1,调用order会清0,即obj_count是记录当前有多少订单,每次结算订单该变量都会清0。
order_note_ptr是一个指向bss段bss_write的指针,在leave中可以先bss_write写入最多120个字节,在stats中作为字符串指针打印。
漏洞在Add函数中。对Rifile结构体的name和desc字段都可以写入56字节,而两个字段的长度分别是27和25,已经发生了越界写,造成了堆溢出。这次越界写可以覆盖劫持next指针,劫持该指针可以通过调用show函数实现任意地址读。利用任意地址读,我们可以读got表泄露libc基地址。
显然这个时候若,我们想要getshell,必须要实现任意地址写。我们知道next指针指向的地方会视作Rifile结构体,在调用order函数时会尝试free我们劫持的next指针。如果我们劫持next指针至一块我们精心构造的fake chunk,那么我们就能立刻add对构造的fake chunk写入原本我们没办法写入的地方。
那么显然我们只能在bss段去寻找目标了。我们仍然记得bss段有一个指针order_note_ptr,这是一个可以方便的进行读写的指针(只需调用stats和leave函数)。如果我们能够劫持就能实现任意地址,于是我们想当然是观察order_note_ptr指针的前后,看有没有我们可控的内存块,可供我们构造fake chunk。
幸运的是,order_note_ptr上方四字节有一个obj_count变量,而这个变量我们可以通过调用add使其自增,而Rifle结构体的chunk是0x40,因此fake chunk size的值是0x40(实际应该是0x41,因为PREV_INUSE此时是1)。但是我们要观察一下,以order_note_ptr做为mem指针向上偏移8字节是chunk指针,是否是8字节对齐的。我们发现order_note_ptr的地址是0x0804A2A8,减去8字节是0x0804A2A0,是8字节对齐的,因此可以通过地址对齐检查。
而我们可以往bss_write处开始写120个字节,所幸order_note_ptr距离bss_write并不远,两者相距0x18个字节,在fake chunk size的范围内。那么next chunk size的值比2 * SIZE_SZ大一些即可。
我们构造完成fake chunk后调用order函数,全部free,此时fastbin 0x40第一个chunk就是fake chunk,我们直接add出来,然后就可以劫持order_onte_ptr指针实现任意地址写了。
现在我们实现任意地址写,并泄露了libc,我们利用house of spirit技术构造了fake chunk,并劫持了order_note_ptr指针实现了任意地址写,现在我们可以利用任意地址写劫持got表指针为system。并且代码中有一个绝佳的地方,是一个天然的shell。
如下图,如果我们劫持__isoc99_sscanf函数为system,那么我们可以很方便的控制变量s,进行RCE。