关于glibc2.32引入的safe-linking的一些总结
关于glibc2.32引入的safe-linking的一些总结
首先看一下glibc2.32的部分源码
1 |
|
在之前的glibc版本没有引入safe-linking时,对于进入 tcache 的 chunk,free 掉之后用户区前 8 字节会直接存下一个空闲 chunk 的地址,也就是 fd
1 | freed_chunk: |
这样如果存在UAF漏洞,就可以直接改fd指针为 _free_hook,下一次malloc就会分配到 __free_hook
而在2.32之后,对 tcache 单链表里的 next 指针不再直接存真实地址,而是按“存放这个指针的位置”做异或编码,可以写成encoded_fd = real_fd ^ (store_addr >> 12)
也就是说:
1 | freed_chunk: |
这里的 store_addr 可以理解为当前这个 freed chunk 用户区里存 fd 的那个位置;为什么要 >> 12:因为地址低 12 位通常是页内偏移,ASLR 主要随机化页基址,右移 12 位相当于拿这个存储位置所在页的高位参与异或
接下来我们用一道题来详细说明:
通过网盘分享的文件:pwn.rar
链接: https://pan.baidu.com/s/18aNC15HMvRdz33khARf9PA 提取码: e6v4

看下版本,这题用的是 glibc 2.32,所以 tcache 有 safe-linking 保护。safe-linking 的作用是:free 掉 chunk 后,tcache 里不会直接保存下一个 chunk 的真实地址,而是保存一个按存储位置异或编码后的值。它本质上是一种指针混淆,不是真正意义上的加密。

可以看出这是一道经典菜单题,分析各个函数可以看到del函数存在UAF漏洞,没有把noteList[idx] = NULL

safe-linking 后,fd 不再直接等于目标地址,而是:
1 | encoded_fd = target ^ (store_addr >> 12) |
所以如果我们想把 tcache 的 fd 改成 __free_hook,不能直接写p64(free_hook),而要写:p64(free_hook ^ (heap_chunk >> 12))
也就是脚本里的这一句:
1 | edit(p64(free_hook^(heap_chunk>>12))+p64(0)) |
这里的 heap_chunk 就是当前被我们 UAF 控制的那个 freed chunk 用户区地址,也就是存放 fd 的位置。glibc 后面 malloc 取 tcache chunk 的时候,会再异或一次:
1 | encoded_fd ^ (heap_chunk >> 12) |
这样才能还原出真正的 _free_hook
利用思路大概是:
先申请 16 个 0x78 chunk。0x78 是用户传给 malloc 的大小,加上 glibc chunk header 后进入 0x80 tcache bin;如果直接传 0x80,程序会因为 size > 0x7e 把它改成 0x7f,最终进入 0x90 bin,所以这里用 0x78 更稳。申请 16 个填满 noteList,因为 noteList 只有 16 个槽,填满后后续 add 虽然还能 malloc,但不会再更新 idx,这样 idx 会一直指向最后一个 chunk。
然后 delete() 释放最后一个 chunk。这里程序只 free,没有把 noteList[idx] 清零,所以 idx 对应的旧指针还在,后面 show/edit/delete 都还能继续操作这个已经 free 的 chunk,这就是 UAF。
接着用隐藏的 edit(5) 改 freed chunk。chunk 进入 tcache 后,用户区前 16 字节变成 tcache 管理数据:前 8 字节是 fd,后 8 字节是 key。glibc 会用 key 检测 double free,所以要再次 free 同一个 chunk 前,先用 edit(p64(0)+p64(0)) 把 key 清掉。其中第一个 p64(0) 覆盖 fd,第二个 p64(0) 覆盖 key,重点是第二个。
清掉 key 后再次 delete(),同一个 chunk 就会被放进 tcache 两次,形成 tcache double free。然后用 show() 读 freed chunk 的前 8 字节,拿到 safe-linking 编码后的 fd。glibc 2.32 不直接保存 fd,而是保存 fd ^ (store_addr >> 12),所以需要用 dec() 把泄露值还原成 heap_chunk 地址。这里之所以能还原,是因为这一题里我们既能读到编码值,又能利用堆地址按页异或的关系逐步把它解出来。
拿到 heap_chunk 后,第二次 edit() 做 tcache poisoning,把 fd 改成 _free_hook 的编码值,也就是 free_hook ^ (heap_chunk >> 12)。这样下一次 malloc 解码 fd 时,得到的目标地址就是 __free_hook。
后面两次 add():第一次 malloc 拿回原来的 heap chunk,往里面写 /bin/sh\x00;第二次 malloc 会被 tcache poisoning 引到 _free_hook,往 __free_hook 写 system 地址。因为前面 noteList 已经满了,这两次 add 不会更新 idx,所以 idx 仍然指向写着 /bin/sh 的原 chunk。
最后 delete() 释放 idx 指向的 chunk。此时 __free_hook 已经被改成 system,所以 free(“/bin/sh”) 实际变成 system(“/bin/sh”),最终拿到 shell。也可以看出safe-linking 主要是在阻止你盲改 fd,一旦能泄露并恢复相关堆地址,还是可以继续完成 tcache poisoning。
最终exp
1 | from pwn import * |
