Safe-Linking 机制的绕过
前言
自从引入了 tcache
机制后,从 2.26
开始 tcache poisoning
就是一种简便的攻击方式,因为它不需要像 fastbin attack
利用那样对 size
检查较为严格(只能申请到 malloc
和 _IO_2_1_stdout_
上方的区域),篡改了 tcache bin
中堆块的 next
指针就相当于可以任意地址申请了
safe-Linking
在 2.32
之前 tcache poisoning
可以说是无往不利,但到了 glibc 2.32
及以后,增加了 safe-Linking
机制,至此篡改 next
指针直接任意地址申请的操作便绝迹在了高版本的 libc
中
safe-Linking
就是对 next
指针进行了一些运算,规则是将 当前 free
后进入 tcache bin
堆块的用户地址 右移 12
位的值和 当前 free
后进入 tcache bin
堆块原本正常的 next
值 进行异或 ,然后将这个值重新写回 next
的位置
#define PROTECT_PTR(pos, ptr) \ |
触发这个 PROTECT_PTR
宏,有两种情况,第一种是当前 free
的堆块是第一个进入 tcache bin
的(此前 tcache bin
中没有堆块),这种情况原本 next
的值就是 0
,第二种情况则是原本的 next
值已经有数据了。如果是第一种情况的话,对于 safe-Linking
机制而言,可能并没有起到预期的作用,因为将当前堆地址右移 12
位和 0
异或,其实值没有改变,如果我们能泄露出这个运算后的结果,再将其左移 12
位就可以反推出来堆地址,如果有了堆地址之后,那我们依然可以篡改 next
指针,达到任意地址申请的效果
恢复 next
的宏为 #define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
,其实这个宏最终还是调用了 PROTECT_PTR
,原理就是 A=B^C ; C=A^B
例题
NCTF2021-ezheap
本题的 libc
版本为 2.32
,因为是本地复现,所以我就随便选了一个 2.32
的小版本来做了
保护策略:
漏洞所在:
在 delete
函数中,发现 free
掉 malloc_store[index]
后将 size_store[index]
给置空了,由于忘记给 malloc_store[index]
造成了 UAF
。
因为本题有 edit
和 show
函数,所以篡改 next
以及泄露堆地址和 libc
地址都较为轻松
利用思路:
edit-after-free
考虑一点就是 delete
函数后会将 size[index]
置空,如果直接 edit
的话,无法往里面写入数据。采取的措施是 先申请 chunk1
然后将其释放,此时它的 size
被置空了,但是地址依然留在了 malloc_store
里面,此时再申请等大的 chunk2
,此时再次释放 chunk1
(因为刚刚的 chunk2
是将原本的 chunk1
申请出来了,所以这里不会造成 double free
),此时 chunk1
和 chunk2
指向的地址是相同的,chunk1
的 size
为 0
, chunk2
的 size
正常,并且编辑 chunk2
就可以篡改已经处于 free
状态的 chunk1
,从而修改其 next
指针。(如下图)
通过下图可以发现,此时 0x000055d5f6e622a0
的位置是有两个,第二个对应的 size
是 0x70
,所以可以在这里篡改 next
指针
泄露堆地址
此时的 tcache bin
中只有一个堆块,执行 show
函数泄露其 next
指针数据,得到了 0x551dcbb2
,我们将其左移 12
位即可得到堆地址(因为 next
原本为 0
,和 0
异或结果不变)
heap_base=u64(p.recv(6).ljust(8,b'\x00'))<<12 |
此时即可得到堆地址(如下)
tcache poisoning
最后一点需要考虑的是如何将 __free_hook
写入到 next
指针上。
因为 safe_Linking
机制会存放 next
指针运算后的结果,因此 tcache poisoning
只需要我们自己将 __free_hook
地址进行同样方法运算写入 next
位置(如下)
value=((heap_base+0x2a0)>>12)^free_hook |
heap_base+0x2a0
是当前 free
后进入 tcache bin
堆块的用户地址
此时 __free_hook
写入 next
后的情况如下
最后将 __free_hook
申请出来写入 system
地址,通过 free
释放掉一个存有 /bin/sh
字符串的堆块,获取 shell
。
注意: 需要提前布局 0x80
这条链的堆块,保证其 counts
在申请 __free_hook
时要大于 0
,否则无法从这条 tcache bin
中申请出来 __free_hook
EXP
from tools import* |
VNCTF2021-ff
题目附件在 BUUCTF 中的 VNCTF2021 比赛中可以找到
保护策略
程序分析
libc
为 2.32-0ubuntu3_amd64
,这个版本存在 safe-Linking
机制
add
函数,对 size
进行了限制,最大能申请 0x7f
,并且申请出来的堆块索引会被赋值为全局变量 idx
,最多申请 0x10
个堆块
delete
函数存在 UAF
漏洞,但是我们无法选择索引释放指定的堆块,只能释放索引为 idx
的堆块(也就是只能释放最近一次申请的堆块)
show
函数也是只能打印出最近一次申请堆块中的八个字节数据,并且 show
函数只有一次执行的机会
edit
函数只能向最近一次堆块中写入 0x10
字节的数据,并且 edit
函数只能执行两次
利用思路
因为本题一个麻烦的点在于 edit
show
delete
函数都只能对最近一个申请出来的堆块操作,所以需要反复调试进行一个布局。
add
函数最大申请 0x80
的堆块,这就导致了泄露 libc
地址泄露不出来(即使填满 tcache bin
因为还需要做一个阻止与 top chunk
合并的堆块,也是无法将 libc
泄露出来的,就算真的泄露出来还要考虑 safe-Linking
)
所以这里最终选择的是泄露 heap
地址,利用 UAF
加上 show
函数即可泄露堆地址(将泄露出来的数据左移 12
位)
需要注意的是 edit
函数可以写入 0x10
个字节的数据,这样可以篡改 free
状态堆块的 key
字段,给了我们 double free
的机会,目的是去将 pthread_tcache_struct
申请出来(此时两次 edit
机会已经用完)
之后泄露 libc
肯定要考虑残留一个 main_arena+96
地址,然后爆破申请 _IO_2_1_stdout_
结构体泄露 libc
。本题堆块即使填满 tcache bin
也会落入 fast bin
中(0x90
虽然落不进去,但产生了 main_arena+96
也没办法改为 _IO_2_1_stdout_
地址)
所以只能将 pthread_tcache_struct
释放掉进入 unsorted bin
,当我们每次去从 unsorted bin
中切割堆块的时候,都会残留 main_arena+96
在 pthread_tcache_struct
中,当 main_arena+96
正好落到 tcache
头指针的位置,我们再切割 unsorted bin
的时候就能篡改 main_arena+96
改为 _IO_2_1_stdout
地址了。
注意:从 tcache bin
中申请堆块出来需要保证 counts > 0
,为了最后还有机会做一个 __free_hook
申请出来,我们必须让申请出来的堆块尽可能小(在后面堆块布局的时候就会发现这点)
调试过程
调试过程主要演示如何将 __IO_2_1_stdout
和 __free_hook
申请出来
下图是申请 pthread_tcache_struct
前的情况,申请出来要写入 b'\x00\x00' * 0x27 + b'\x07\x00'
,这样正好将 0x290
这条链的 counts
设置为 7
,保证了释放掉 pthread_tcache_struct
后可以进入 unsorted bin
下图是 pthread_tcache_struct
进入了 unsorted bin
,接下来我们需要反复从 unsorted bin
里来切堆块
我们第一个要切下来的堆块大小为 0x40
,写入的数据为 '\x00\x00'*3+'\x01\x00'*1+'\x00\x00'*2+'\x01\x00'+'\x00'*0x38
,这样才可以让 0x50
和 0x80
这两条链上的 counts
为 1
(如下图)(这里就是一个布局,为后面申请 _IO_2_1_stdout
和 __free_hook
做准备)
自己做题的时候,这里肯定不是第一次就能写出来的,等调试到后面发现这里需要构造 counts
,才拐回来布置的,包括申请的堆块大小为 0x40
也是反复调试更改后确定的。总结一下就是这些数据都是调试得来的。
再次申请一个 0x30
堆块,这次发送的数据全部填写 \x00
即可,此时 pthread_tcache_struct
中已经残留了被切割后的 main_arena+96
(如下图)
申请一个 0x10
的堆块,写入数据为 \xc0\x16
(这是 _IO_2_1_stdout_
的后两字节,不过第一位需要爆破),写入的数据会正好落在刚刚残留的 main_arena+96
上,从而产生了 _IO_2_1_stdout_
地址,并且 0x50
这条链的 counts
已经被设置为 1
了,所以是可以申请出来的(如下图)
io leak
就不说了 此处exp
的代码为
add(0x40,p64(0xfbad1887)+p64(0)*3+b'\x00') |
具体做法请参考 文章 ,现在我们已经拿到了 libc
地址并且 bins
的情况如下
申请一个 0x10
的堆块,写入 __free_hook
的地址,该地址会正好落在 0x80
的 tcache
头(如下),__free_hook
为什么会正好落在这里? 别问,问就是布局 ◕‿◕
因为之前已经将 0x80
这条链的 counts
设置为 1
,所以可以直接将 __free_hook
申请出来,然后写入 system
地址。然后观察 0x20
这条链是没有任何数据的,我们就可以申请一个 0x20
的堆块存入 /bin/sh
再将其释放,即可获取 shell
本题注意的几点:
- 很多看似顺理成章的布局,其实都是反复调试出来的
counts
为0
的时候,从tcache bin
中申请不出来堆块counts
大于0
的时候,如果里面的值不是一个合法地址,则申请时会报错- 为了打最后的
tcache poisoning
,必须要让每次申请堆块的size
尽可能的小,这样才能让__free_hook
落在0x80
,再往后的话因为对add
函数中对size
检查的原因,就申请不出来了 - 往
pthread_tcache_struct
中写入数据时,尽可能的写入\x00
,不然可能会破坏某些tcache bin
的counts
EXP
from tools import* |