玩 中国科学技术大学第五届信息安全大赛


在某 Telegram 群看见了这个来自 USTC 的 CTF 比赛,就玩了一下。

https://hack.ustclug.org/

这是我第一次打 CTF(虽然这个和大佬们的 CTF 并不一样),啥经验都没有。也发现自己基础很差,真正感受到了知识储备的重要性,我的知识储备对我解题速度的降低起了很大作用呢。

不过这个还是很好玩的!感谢 USTC 的大佬们!

这里列出的题解都是我在实际答题过程中打通一个写一个这样写下来的,所以还有一些我没做过的题目请移步官方的 writeup

最后得分: 4150

最后排名: 全场第8,“其他”组第7


0x00 索引

正在加载…

0x01 签到题

只要修改maxlength然后提交就好啦。


0x02 猫咪问答

真·搜索就行了


0x03 游园会的集章卡片

我真的是拼出来的。

Snipaste_2018-10-09_21-37-23.png


0x04 Word 文档

提到了 Office 07 引入的船新格式。这个格式是以 zip 包的形式储存数据。解压可见 flag.txt,然后去掉所有换行就拿到了flag。


0x05 黑曜石浏览器

在页面上找了半天没找到相关的资料。后来利用搜索引擎找到了 https://heicore.com ,可以在页面中找到一个判断黑曜石浏览器的 UA,用它就对了。

Snipaste_2018-10-09_20-57-28.png


0x06 我是谁.哲学思考

一直开着开发者工具的我一眼就看出了 Network 里面的不对劲。

Snipaste_2018-10-09_20-59-16.png

填入 teapot 搞定。


0x07 我是谁."Can I help me?"

根据第一根 flag 提供的 url,建议我们使用别的 method 来请求。既然说到 brew tea,说到 teapot,那肯定是 RFC 7168 啦。( HTCPCP-TEA 真好玩!)

读一读 RFC 7168 就可以写出下面这个 payload:

BREW /the_super_great_hidden_url_for_brewing_tea/ HTTP/1.1
Content-Type: message/teapot

Snipaste_2018-10-09_21-03-31.png

然后接着请求 Alternates 里面提供的 url。

BREW /the_super_great_hidden_url_for_brewing_tea/black_tea HTTP/1.1
Content-Type: message/teapot

Snipaste_2018-10-09_21-04-39.png

嗯……给大佬递茶……


0x08 猫咪克星

用人的手速肯定不行(但我还是天真的用手来了几次),所以我专门写了个 Python 程序来做题。刚开始觉得很简单就是个eval,后来发现太狡猾了他们,还有 exit()sleep(100) 这种万恶的语句来让我们的 eval 爆炸。

机智的我使用了替换大法!最后的程序是这样的

from socket import *
tcpClient=socket(AF_INET,SOCK_STREAM)
tcpClient.connect(("202.38.95.46",12009))

while True:
    data=tcpClient.recv(1024).decode(encoding="utf-8") 
    print(data)
    try:
        if "flag{" in data:
            exit()
        result=eval(data.replace("exit()","0").replace("sleep(100)","sleep(0)"))
        rtn=str(result)+"\n"
        print(rtn)
        tcpClient.send(rtn.encode(encoding="utf-8"))
    except Exception as e:
        pass

tcpClient.close()

拿到了!

Snipaste_2018-10-09_21-25-05.png


0x09 回到过去

就是把下载下来的 input_sequence 在 ed 里面敲了一遍…… (大力出奇迹)

Snipaste_2018-10-09_21-55-13.png


0x0a 猫咪电路

就是一个逻辑电路分析。只要最左边输出1就好了。

photo_2018-10-10_00-10-14.jpg


0x0b 猫咪和键盘

这是一个将程序源码纵向切割后打乱形成的文件。我也写了个程序来帮我恢复原状。

with open("typed_printf.cpp","r") as f:
    lines=f.readlines()
    for line in lines:
        seg1=line[0:1]
        seg2=line[1:7]
        seg3=line[8:20]
        seg4=line[20:22]
        seg5=line[22:32]
        seg6=line[32:39]
        seg7=line[39:-1]
        print((seg1+seg6+seg2+seg4+seg3+seg5+seg7).strip())

恢复原样后按照注释里的指令编译并没有通过。后来查了一下是因为我的 g++ 是 6.3.0,而如果想要成功编译则需要 7+ 的 g++。于是我打开了洛谷的在线IDE。

Snipaste_2018-10-10_00-56-59.png

又拿到一个flag。


0x0c 她的诗

的确可以一眼看出来这个 uuencode 编码的诗。但是 Python 解出来的就是正常的诗。这里面肯定有什么玄机,我就把整个诗再给逐行转回去了,发现二次转换并没有还原成最初的文本。那么这里面肯定有什么奇奇怪怪的套路了。后来找来找去,发现每一行的第一个字符被 -1 了。所以原本放在行尾的 flag 被“隐藏”起来了。把它给倒腾回去,然后就发现 flag 都藏在行尾。

后来了解到第一个字符是标示长度的,所以用 -1 可以隐藏最后一个字符。


0x0d 猫咪遥控器

下载 seq.txt 之后是一堆由 U/D/L/R 组成的字符串,四个字符分别代表上下左右。把它画出来就好了。

我选择的是将它转成了一个 LOGO 程序然后画出来了。

Snipaste_2018-10-10_10-00-25.png


0x0e 她的礼物

首先下载 gift 文件。执行后随便传个参数进去发现会出现这个消息:

Error code [2333]
Running this program for too long is unhealthy for your computer.
Please contact your administrator for details.

我直接反编译找到了相关的内容。

Snipaste_2018-10-10_13-55-48.png

直接对程序进行了一波魔改,删掉了 too long 那个 alarm,以及干掉了没用的 printf。然后坐等233333次程序执行完成拿到flag。

Snipaste_2018-10-10_16-52-21.png


0x0f 秘籍残篇.滑稽Art

打开 malbolge.txt 后首先感觉这个空格有点儿小优雅。于是我不断缩小……

flag get!

Snipaste_2018-10-10_17-28-21.png


0x10 猫咪银行

这居然是这么后面解出来的。

当时看到 PHP 还以为是弱类型坑啊什么的研究了半天,今天上课老师讲了溢出我才意识到还有这一招。

一个大数字就让取出时间变成了公元前。

Snipaste_2018-10-11_19-19-21.png

然后直接取出买 flag 就行啦!要是现实当中也有这么多钱就好了。

Snipaste_2018-10-11_19-21-24.png


0x11 数理基础扎实的发烧友

拿到压缩包打开发现是张图片和一个隐写程序。bmp图片上大大的DSD,一看就是图片给藏了一个音频文件了。又用 Ps 拉了一下曲线,很明显有数据藏着嘛。

Snipaste_2018-10-12_01-39-57.png

我又反编译了一下 stegan.exe 看了两眼也大概了解了隐写过程。我比较菜,并不知道到 bmp 图片上那个就是 Delta-Sigma Modulate,这是后来才发现的。

Snipaste_2018-10-12_01-43-41.png

那么就从这里入手对图片进行处理,剥离出数据部分然后就得到了 DSD 的二进制数据。

Snipaste_2018-10-12_01-49-46.png

网上搜了一份比较常用的 DSD 音频格式 DSF 的文档,给这些二进制流数据加上文件头使之成为一个成熟的音频。

Snipaste_2018-10-12_01-59-33.png

然后用 ffmpeg 给他转换回 wav 格式的文件。怎么从音频文件中解出数据是个关键。我在这里卡了俩小时。因为最初的没有把采样频率转到 44.1kHz 的时候,在 70kHz 的频谱上有些空格有音频有些空格是空的,所以我最初以为是一串二进制什么的然后通过 ASCII 拿到 flag。然后就这么闷头撞了一个多小时的墙。

后来转了采样频率之后才发现 600-1.5kHz 这里别有洞天啊!不得不说先入为主思想的确不是个好东西。它让我在“猫咪银行”卡了好久,现在又让我在这里卡了半个小时。为什么这么说呢?因为之前我以为是二进制串,我脑子没转过来,继续尝试在 0.6-1.5kHz 种找出二进制串,找了半天规律。听着听着,发现这个音频里的声音怎么这么耳熟呢?每一块怎么就这么恰巧是两个音呢?我一拍大腿,妈呀,这是 DTMF (Wikipedia) 啊!然后就只要根据每一块的音翻译出来就好了。

Snipaste_2018-10-12_02-05-55.png

根据 DTMF 信号翻译出来的是这个东西。

102#108#97#103#123#102#105#114#101#95#119#97#116#101#114#95#110#117#99#108#101#97#114#125

刚翻译到第四个 # 的时候,我就觉得这是 ASCII 的十进制表示。也的确是这样,按 # 分割然后用 ASCII 翻译出来了这个玩意儿:

flag{fire_water_nuclear}

火电、水电、核电,给你不一样的 Hi-Fi 体验。


0x12 加密算法和解密算法

玩了一天的《中国式家长》之后

解压后阅读了 html 文件中的内容,大概意思是,加密算法的核心是用 BrainFuck 写的。加密过程是,先把字符串按每个字符换成其在 base64 中的顺序,然后丢给 BrainFuck 作处理后再丢回来,再重新通过 base64 折成字符串。

我读完 BrainFuck 之后首先先将加密程序用 js 改写了一次,方便我愚笨的脑子理解。

let stringMap='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
let magicThings=[
    [ 23, 46, 21, 16, 35, 17, 34, 19, 12, 38 ],
    [ 05, 03, 16, 27, 22, 00, 15, 38, 55, 14 ],
    [ 40, 40, 63, 05, 02, 51, 10, 52, 41, 43 ],
    [ 61, 54, 33, 53, 43, 46, 52, 08, 04, 59 ],
    [ 47, 31, 60, 37, 04, 37, 27, 49, 39, 55 ],
    [ 21, 23, 26, 17, 36, 44, 19, 07, 62, 10 ],
    [ 62, 54, 39, 24, 03, 11, 38, 36, 48, 50 ],
    [ 09, 11, 32, 61, 22, 13, 15, 40, 01, 18 ],
    [ 18, 00, 48, 23, 58, 07, 30, 60, 21, 36 ],
    [ 17, 05, 39, 50, 37, 18, 04, 45, 02, 13 ]
];
let finalAddition=[ 2, 6, 8, 8, 3, 5, 5, 7, 4, 9 ];
let str2num=str40=>str40.match(/.{1,10}/g).map(chunk=>chunk.split('').map(c=>stringMap.indexOf(c)));
let num2str=arr_4=>arr_4.map(a=>a.map(v=>stringMap[v]).join('')).join('');

let encode=function (data){ //data=[16, 20, 8, 2, 10, 63, 1, 17, 14, 22]
    let encoded=[0,0,0,0,0,0,0,0,0,0];
    for(let i=0;i<10;i++){
        for(let j=0;j<10;j++){
            encoded[j]+=data[i]*magicThings[i][j];
        }
    }
    return encoded.map((v,i)=>(v+finalAddition[i])%64);
};

console.log(num2str(str2num("QUICK_BROWN_FOXES_JUMP_OVER_THE_LAZY_DOG").map(v=>encode(v))));

因为我几乎没有任何密码学知识,我写出这段 js 后并不知道这是个啥算法。我只根据这个分析出来,解这个需要线代知识。刚上大学,之前也没学过相关知识,于是我在网上查有关矩阵知识的时候发现了 Hill cipher (希尔密码) 这个玩意儿。一看定义,这不就是这个加密算法的基础!magicThings 里面存储的就是密钥矩阵!可惜我因为没有背景知识,并看不出来这些,还把密钥矩阵叫成了“magicThings”,倘若我有这些基础知识铺垫的话,我在题面提到“将原文分为四段,每段长度为十”的时候应该就能猜出这是希尔密码了。

为了方便我默默拿出 CrypTools,输入密钥矩阵解密。CrypTools 似乎有个 bug,就是不管你选的是 {row vector} * {matrix} 还是 {matrix} * {column vector},它使用的都是后者。因此我在输入密钥的时候自己来了个行列转置。

咦?怎么解密出来的东西奇奇怪怪的?原来是我忘了还有个 finalAddition。将密文预处理后重新解密,就获得了正确的 flag。

Snipaste_2018-10-13_02-18-59.png


0x13 王的特权

从题面我还真没看出啥。直接打开 IDA。

Snipaste_2018-10-13_13-17-14.png

一眼看到有个 StrSearcher,上面参数是 unk_51A64

.rodata:0000000000051A64 unk_51A64       db  73h ; s             ; DATA XREF: b::main::h68e9c4d0c5168d89+66↑o
.rodata:0000000000051A65                 db  75h ; u
.rodata:0000000000051A66                 db  64h ; d
.rodata:0000000000051A67                 db  6Fh ; o

找“sudo”字符串?当时就想到了重命名。

Snipaste_2018-10-13_13-19-20.png

还以为是啥高端玩法,所以一直放着没做。没想到其实这么简单。这道题我是第十个过的,为什么没人过大概也是因为没人觉得这道题目会这么简单?


0x14 "C 语言作业"

下载 calc,这个肯定是反编译了。打开 IDA 发现 main 函数十分正常,各种输入也安全。

Snipaste_2018-10-14_00-09-56.png

看函数列表发现有个 __init ,于是点进去看了,发现注册了一系列信号触发事件,触发的是 __err ,这个函数是这样的。

Snipaste_2018-10-14_00-12-13.png

触发事件很简单,只要让计算崩溃就好了。于是我选择了 -2147483648/-1

Snipaste_2018-10-14_00-19-53.png

嗯……这里提供了一个运行外部程序的功能,但是 sh 被屏蔽了,而且用了 execlp 所以不能传参数,也没有可以缓冲区溢出的漏洞。于是中午我试了一下 vi emacs 等编辑器,发现它们都不存在。于是一时解题陷入了僵局。

于是我的一整个下午和晚上都在各种查资料各种测试,尝试从 calc 层面搞定(没有切入点,这当然是徒劳的)。

直到我洗完澡后不死心又试了一个 vim ……

居然还真有……

我tm浪费了大半天的时间,就因为我中午偷懒了没试 vim 存不存在???

行吧栽在了自己手里。

现在手速有点不够用了,于是我拿出了 py。

from socket import *
tcpClient=socket(AF_INET,SOCK_STREAM)
tcpClient.connect(("202.38.95.46",12008))

while True:
    data=tcpClient.recv(1024)
    if len(data) is 0:
        exit()
    data=data.decode(encoding="ascii") 
    print data
    try:
        if 'flag{' in data:
            exit()
        if ">>>" in data:
            tcpClient.send("-2147483648/-1\n".encode(encoding="ascii"))
        if "examine" in data:
            tcpClient.send("vim\n".encode(encoding="ascii"))
            tcpClient.send(":!cat /flag\n".encode(encoding="ascii"))
            tcpClient.send(":!cat /flag\n".encode(encoding="ascii"))
            tcpClient.send(":!cat /flag\n".encode(encoding="ascii"))
            
    except Exception as e:
        pass

tcpClient.close()

然后通过 vim 里敲的 :!cat /flag ,这坑爹玩意儿居然说 The real flag is in the file "-"

行吧,把 /flag 改成 /- 再来一次。解决。

Snipaste_2018-10-14_00-21-02.png

看了官方题解后:我居然没意识到进入 vim 以后就没有 5s 的时限了!


0x15 "FLXG 的秘密".来自未来的漂流瓶

文中说了“他们再以四千年前的伏羲先天六十四卦将程序编码”,看到 64 就想到了 base64。所以按照 Wikipedia 里面从左到右的顺序(这里我还专门问了一下我爹相关知识),依次替换成 base64 的字符。我使用了下面的 js 来帮我:

let fs=require('fs');
let xianTian64Gua=[
    "坤","剥","比","观","豫","晋","萃","否",
    "谦","艮","蹇","渐","小过","旅","咸","遁",
    "师","蒙","坎","涣","解","未济","困","讼",
    "升","蛊","井","巽","恒","鼎","大过","姤",
    "复","颐","屯","益","震","噬嗑","随","无妄",
    "明夷","贲","既济","家人","丰","离","革","同人",
    "临","损","节","中孚","归妹","睽","兑","履",
    "泰","大畜","需","小畜","大壮","大有","夬","乾"
],base64Map='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';

let flxg=fs.readFileSync('flxg.txt').toString();
xianTian64Gua.forEach((v,i)=>flxg=flxg.replace(new RegExp(v,'g'),base64Map.charAt(i)));

fs.writeFileSync('newFlxg.bin',Buffer.from(flxg,'base64'));

习惯性看了一眼 hex 的头和尾。flag 清晰可见。

Snipaste_2018-10-15_13-43-37.png


0x16 "困惑的 flxg 小程序"

打开程序不管输什么看起来都没做过任何事情的样子。

Snipaste_2018-10-15_22-38-23.png

直接开 IDA 上反编译!

Snipaste_2018-10-15_22-43-51.png

通过查找 flxg 找到了这里,似乎在参数格式刚好等于 60 个的时候会触发一个 Exception……难道是这里?试一下!

Snipaste_2018-10-15_22-51-28.png

的确!就是这里!找到这部分代码,然后让 IDA 帮我们生成伪代码。

Snipaste_2018-10-15_22-53-58.png

可以看到这里对用户输入转为 base64 后再进行反转了以后做了一波异或,再和系统里面存储的一个字符串进行比对。因此用 s[i] = i^s[i]unk_7FF75AA154D8 这里存储的长度为 56 的字符串转回 base64。就可以得到 flag 啦!

Snipaste_2018-10-15_22-56-37.png

验证一下。

Snipaste_2018-10-15_23-02-01.png

开心!


0xff 放一下马后炮

这里记录一下我因为某些原因没解出来,但是思路是可以解出来的题目。

CWK的试炼."神庙设计图, Get!"

这题我是在上课的时候看的。当时用 Stegsolve 扒通道已经发现了神庙的地方和右下角了。

Snipaste_2018-10-16_13-14-46.png

加上 nc 的提示。

Snipaste_2018-10-16_13-10-37.png

我就知道东西肯定藏在神庙那块和周围有异的地方。hex 中看到了很像 base64 的字符串,当时估计这大概就是解题关键了。当时刚好快下课了,距离比赛结束还有十来分钟,想想算了,就放下了手中提取 base64 的任务,吃饭去了。

那又能怎样,世上没有后悔药。

心疼 250 分。

秘籍残篇.天书易解

当时直接用 gdb 调试了那个 C 写的 malbolge 的解释器并在程序退出前 dump 了内存。

大概是没仔细看?

看到群里有人的确用 dump 内存的方式解出来了,明文存在内存里。

那又能怎样,世上没有后悔药。

心疼 400 分。