本博客为 SpiritCTF 2021(吉林大学 CTF 校赛)Misc 部分(及一道Reverse)的官方题解。本次比赛共放出 Misc 题目 8 道,题解按照题目难度从低至高排序。
弄脏的PDF
题目
校赛前的某个下午,小k同学惬意的提着他喜欢的○○咖啡坐在电脑面前,准备开始出题。正当他准备打开咖啡袋,开始享用他的下午茶时,突然他手中咖啡杯的盖子一松,咖啡杯顺势躺在了笔记本上。小k急忙抓起纸巾擦拭,但是一切都已经太晚了……
题解
本题考查的内容为PDF DRM。PDF文件格式虽然支持设置复制、编辑密码,但是由于此类密码并不会对PDF本身的文件数据进行加密,因此实际上该密码只是需要PDF阅读器遵守的“君子协定”。不过,由于PDF格式的版权曾经属于Adobe公司,开发商业软件需要得到Adobe的授权,因此较早的PDF阅读器一般都会遵守规范以规避潜在的法律问题。但随着Adobe开放PDF格式、PDF格式的标准化(ISO 32000-1:2008),如今遵守相关规范就更需要靠开发者自觉了。
因此,本题只需要选择不遵守规范的PDF阅读器、编辑工具就可以了。出题者设想并验证过的解题方式如下:
- 使用不遵守或难以遵守规范的PDF阅读器。比如PDF.js,它会将PDF渲染为Html DOM,因此直接使用开发者工具就可以获得Flag
- 使用在线PDF格式转换器转为Word等格式。此类转换器通常使用开源软件,因此并不一定遵守相关约定
- 使用工具。如:PDF Restrictions Remover
- 利用PDF阅读器的辅助功能。本题的PDF事实上并没有禁用权限“辅助功能的内容复制”,因此可以通过辅助功能获取内容,比如Edge浏览器的朗读功能 听力题石锤
One more thing…
- 本题共有38个队伍解出
- 由于本题Flag位于一个无序列表,所以很多同学提交Flag时都把无序列表复制进去了
- 虽然我验证过Edge浏览器的朗读功能可以解题,但是没有想到真的有队伍是靠此解出的,十分佩服他们的毅听力
- 关于“使用辅助功能”的解法,队伍“数码白细胞”的解法很有趣:使用搜索功能搜索每个可能出现的字母,然后根据高亮确定字母位置
真假Flag
题目
本题已由出题人承保正品Flag险,Flag假1赔49
题解
本题附件中有50个二维码,扫描后可得类似Flag的字符串。其中只有一个符合Flag格式,因此使用如下指令即可解出题目(来自队伍“4047”):
zbarimg -q * | grep Spirit
此外,编写Python脚本或使用在线工具也可以解出此题。
One more thing…
- 本题共有33个队伍解出
- 本题出题人已经毕业,非常感谢他对校赛的帮助
- 为了保证此题不被暴力解出,平台增加了Flag提交的次数限制
一只小小小小鸟
题目
小k同学最近开始拓宽业务做游戏了,它陆陆续续的边学边做了不少游戏。但是其中有一个作品却让它又爱又恨,因为就连小k同学自己打了一下午也没打过去!你能帮帮小k同学吗?
题解
本题为脑洞题,旨在考查参赛者对黑盒漏洞测试的思维过程。游戏为Flappy Bird的 抄袭 复刻,唯一的操作形式就是点按,唯一可控的角色属性就是高度。题目要求参赛者达到150分,因此直接游玩就是一种解法,但显然由于游戏难度较大加之本题为赛题,基本可以排除这个解法。
因此,本题需要找到一个可以完成得分过程,但同时难度又小的解法。对于游戏过程,只有“角色高度”这一属性可被控制,因此可以对边界情况进行测试。若角色高度过低,游戏会直接失败;但若角色高度过高,游戏并不会直接停止。而且通过掉落时间可以推断,游戏角色即便超出了画面,其高度仍然会增加。因此尝试一直增加游戏高度,于是发现游戏角色可以飞过柱子但同时完成得分。
我认为一个比较合理的解法是使用鼠标连点工具(如按键精灵)或者鼠标宏完成题目,达到150分大约需要3分钟。
One more thing…
- 本题共有12个队伍解出
- 本题目的名字取自歌曲《我是一只小小鸟》的歌词:“我是一只小小小小鸟,想要飞呀飞却飞也飞不高”
- 本题出题时花了大力气防范非预期解。实际上在比赛过程中,每一个措施竟然都成功防住了非预期解,这令出题人倍感欣慰。
- 对逆向的防范:本题文件增加了VMP壳,脱去难度大。并且游戏本身使用了GMS2的YYC编译方式,即使脱壳成功,逆向难度也非同寻常。
- 对CheatEngine等内存修改工具的防范:其实VMP壳本身就有内存加密功能,但是出于出题人的恶趣味所以特地没有开启
- 游戏界面中显示的所有分数都来自假变量,修改只会影响界面元素的显示。真的分数变量计算方式如下: r_0 + r_1 \cdot {\rm score},其中 r_0、 r_1 是每局开始产生的随机数。
- 在整个游戏过程中Flag的加密密钥都会发生改变,因此就算成功修改判断变量通过150的分数校验,也只会得到乱码Flag。
- 比赛前日出题人决定降低一点柱子高度,但是没想到忘记修改碰撞检测的下界,所以正式比赛时一开始放出的程序无法正常游玩(钻不过空挡),但却可以正常解题。而且还有队伍利用这个版本程序解出题目,等于变相暗示了解法。这也是本次比赛中Misc方向题目唯一一次出锅。
捌拾捌万行
题目
小k同学整理硬盘的时候找到了当初刚学Python时写下的代码,打开之后,他虽然看不懂,但他大受震撼。
题解
本题主要考察参赛者的脚本编写能力、正则表达式能力。本题Python脚本大致的过程就是如下循环的展开:
for i in range(len(flag)): # 遍历每一个输入的字符 for val in range(1, 2048): # 如果输入字符的 ASCII 码等于 Flag 的对应位置,就判断下一位 if val == ord(true_flag[i]): break # 如果不正确,就换一个数字判断 if ord(flag[i]) == val: print("Wrong flag!") exit(0)
因此,本题实际上要求出的是每一位中,1~2047内缺少的那个数字。因此编写脚本时就需要对问题进行一些转换,相对来说没有那么简单。解题脚本修改自队伍“mercer”:
with open('./88w.py', 'r') as f: lines = f.readlines() flag = '' count = 1 k = 0 for i in range(8, len(lines), 3): if f"flag[{k}]" in lines[i]: if f"== {count}:" in lines[i]: count += 1 else: flag += chr(count) count = 1 k += 1 print(flag)
One more thing…
- 本题共有19个队伍解出
- 本题NETA自网络热门事件“C语言作业编写了88万行代码”,虽然题目最后只有27万行代码
- 队伍“Sprite”的解题方式很有趣,首先取出每一位的所有数字,之后利用异或性质:将所有数字异或,然后异或1~2047
- 真的有队伍是纯手工解出的,而且还是线下参赛的队伍,出题人目击了整个过程……出题人向该队伍致敬
赛博空调
题目
小k同学来赛博空间生活已经三年多了。虽然赛博空间里大部分时间气温都很宜居,但是一年里总有几天是特别热、冷的。所以小k同学一直想买一台空调,但是空调显然太贵了。不过有天小k同学突然来了灵感:既然一个人买不起,那做一个“共享空调”大家一起刷卡取用不就可以了嘛!于是小k同学立马着手开发“空调租用系统”。当然,既然是赛博空间的卡,自然要有个赛博样,于是小k同学采用了“近场通信”技术来发送卡片,只要使用者将卡片文件装到手机等等设备里就可以用了。可是好景不长,没等运营几天,小k同学就要被薅到破产了……
题解
本题涉及的内容是NDEF消息格式,也就是NFC数据交换格式,这一点题目中也有提示“近场通信”。但是完成此题实际上完全不需要了解任何NDEF协议相关的细节,因为本题实际上考察的是二进制数据的表示形式、二进制文件的修改。
观察开户后得到的卡片数据,并对比消费1元后的卡片数据:
可以发现只有最后2字节发生改变。多次尝试可以发现,倒数第2字节的内容和卡片余额相同。因此可以尝试更改00 00 00 05
为FF FF FF FF
,但是提交时却提示“异或校验失败!卡片数据已损毁,或者,该不会是你偷偷改了吧→_→”。搜索“异或校验”可以找到其大致原理,因此可以推测最后一字节就是校验位。计算可得,倒数9~倒数1字节就是异或校验的源数据,如:00 99 31 B8 00 00 00 05 => 15
。因此重新计算修改后的文件,得到校验位10
,就可以通过校验并购买FLAG了。
还有一个提示是,请求卡片状态的/status
接口实际上还会返回卡号字段,其实就是数据的前四字节00 99 31 B8
,这也可以是用来推测数据长度的一个依据。
关于卡片本身的NDEF数据,实际上包含了两条记录。第一条是设备信息记录:
- 厂商:
Spirit
- 型号:
KCVC-114
- 名称:
K's Cyber air conditioner VIP Card
- UUID:
86dec93c-3fd8-4bb9-aec2-940949c0b86b
- 版本:
KCVC-114 v5.14
另一条就是卡片的卡号、余额、校验数据,被记录在了一条类型为urn:nfc:ext:jluctf.tech:x-aircond
的记录中。感兴趣的同学可以自行研究一下它的数据格式。
One more thing…
- 本题共有11个队伍解出
- 由于部分工具默认以小端按字/半字解释数据(如
hexdump
指令),加上卡片NDEF数据实际上不是按字对齐的,因此显示的数据将很难让人看出规律。如00 98 ab ff 00 00 00 05 c9
会显示成ab98 00ff 0000 c905
,很难看出数据的构造方式,因此只能通过找比特位变化规律解题。队伍“kkk”“CTF-masters”都是在这种情况下观察规律解出的,非常硬核。 - 本题的空调进口自YunYouJun/air-conditioner,非常感谢。
- 本题题目灵感来自于校内某机器。但请注意:禁止在真实机器上通过篡改数据谋取利益,否则你将可能面临包括“破坏计算机系统罪”在内的多项指控。学习网络安全请务必遵守法律、保持道德操守。
交白卷
题目
期末周到了,小k同学想起学长曾经说过这学期有门课交白卷就满绩点了。不过小k同学才不相信,学长肯定在吹牛嘛!不过也差不多到期末考的时间了,得赶紧登录进去准备一下。
- 第一题、第二题为随机生成的Whitespace指令选择题
- 第三题为程序阅读题,给出了随机生成的输入,要求给出输出
- 第四题:请编程实现如下程序:程序读入一个整数 n(1≤n≤100),之后打印 n 行。对第 i 行(从 1 开始),若 i 整除 3 则输出“Spirit”,若整除 2 则输出“CTF”,若同时整除则输出“SpiritCTF”,其他情况则打印整数 i。注意,评测过程中运行指令不可超过 20,000 条。
- 第五题:请编程实现如下程序:该程序无输入,且运行后打印其自身。注意,评测过程中运行指令不可超过 500,000 条。
题解
本题考察的是深奥的编程语言(Esolang)中的Whitespace语言,是Misc类题目中出镜率较高的一种Esolang。
前两题考察的是指令的意义,主要希望做题者能善用浏览器开发者工具、翻阅文档。
第三题可以使用在线的Whitespace运行工具模拟运行解决,比如第四题使用的工具。
第四题是编程题,需要实现一个类FizzBuzz程序。虽然也可以按照文档编写,但是难度会很大。因此需要找到Whitespace的Assembler或转换工具,我使用的是Whitelips IDE,它提供了一个支持宏的汇编语言与完善的IO标准库。解题代码如下:
include "lib/std.wsa" push 0 readi push 0 retrieve # i = 0 push 0 loop: # i = i + 1 push 1 add # Print call output # Judge dup push 0 retrieve sub jz .end jmp loop .end: end output: dup push 6 mod jz .mod6 dup push 2 mod jz .mod2 dup push 3 mod jz .mod3 dup printi nl ret .mod2: prints "CTF\n" ret .mod3: prints "Spirit\n" ret .mod6: prints "SpiritCTF\n" ret
第五题提到的程序实际上有个专有名词:自产生程序(Quine)。任何图灵完备的语言都可以写出自产生程序。当然本题并不要求做题者真的编写,因此主要考察搜索引擎的使用能力。可以在Github上找到Whitespace的quine.ws。
One more thing…
- 本题共有1个队伍解出
- 队伍“tepo”使用了jgkaplan/whitespaceTranspiler完成第四题
- 本题目选择其他编译器的骚话提示语大全:
- C:C 语言编译器正在维修中,请选择别的语言…
- C++:C++ 语言编译器正在编译上一个同学的模板元程序,应该还要编译 10 小时,请选择别的语言…
- Java:Java 语言评测姬的指令拼成“jvav”了,正在抢修,请选择别的语言…
- Python:评测姬怕蛇,请选择别的语言…
- JavaScript:评测姬还在想为什么“[]+{}”是“[object Object]”但“{}+[]”是0,请选择别的语言…
- Rust:Rust 语言编译器长 rust 了,正在除锈,请选择别的语言…
- Go:Go 语言评测机还在 GC,等等再说吧,请选择别的语言…
沙箱逃逸
题目
你和你的队友耗费千辛万苦终于进入了小k同学电脑里的“沙箱”,这时摆在你们面前的就是装着Flag的麻袋。但是突然间警报声大作,原来这一切都是小k同学的陷阱!你们赶紧抓起麻袋就跑,虽然边跑麻袋里的Flag边往外掉,但是也管不了那么多了,一定要成功地带着Flag逃出去!
提示:Flag格式为 Spirit{uuid}
题解
本题考察的是伪随机数种子爆破。游戏加载时,将会从当前时间中产生随机数种子rand.seed(int(time.time() * 100))
,单位是10ms,因此有爆破的可能。但是相比于普通的伪随机数种子爆破题目,本题有如下障碍:
- 每一步能得到的随机数信息十分有限,只有
rand.randint(18, 21)
。因此爆破时多解的概率极高。 - 不同程序分支要求的随机数不同。如果答案错误,程序还会通过
rand.choice([...])
随机选择一句骚话提示,这也会影响随机数的状态。
因此解题脚本必须多次验证爆破得到的解,并且还得处理不同分支情况下的随机数,编程难度较大。大致流程如下:
- 产生状态:经测试,前后10s的状态都可以同时爆破
- 根据输出进行筛选,之后生成一个猜测
- 根据猜测结果增加筛选的条件限制,重复第2步
最终解题脚本为70行。大多数情况下,3~4轮就可以爆破出随机数种子了。由于本题没有人做出,所以我就不放解题脚本了。尝试做过本题的同学可以联系我获取。
One more thing…
- 本题最终0解,但其实出题人预期中是希望有1解的。
- 设计Flag碎片的目的是为了避免欧皇真的能试出来。
- 考虑到代码可能需要调试比较久,因此本题在初始放题时就已经放出了。
- 虽然本题和沙箱逃逸并没有什么关系,但如果解出此题的话,将解锁题目《真·沙箱逃逸》。可惜此次校赛没能放出此题,也许这题明年有机会能和大家见面?
海报
题目
小k同学正在设计一个海报。为了让海报看起来更有科技感,它随手找了一些代码作为海报的设计元素。果然这样设计之后科技感就高了很多,于是小k同学心满意足的把海报发了出去。没想到几天之后,小k同学刚出的绝密超强题目就被解出来了,这是怎么回事呢?
题解
本题难度较大,无论是形式上还是难度上都类似真实比赛中的杂项题。首先是海报中明确提到的代码,不难发现这就是一段相反的Base64,直接解码就可以得到前半部分Flag。
然后就是后半段Flag了。根据题目的提示,可以发现海报背景的十六进制串很可能隐藏了信息。OCR出开头一部分HEX,转文本后会发现类似Base64的字符串,再次转码后可以得到Python脚本的开头。因此显然是需要提取完整的背景内容,但背景却被大片遮挡了。
此处考查的是Adobe Photoshop导出PNG格式图片的元数据。在导出PNG格式图片时,PS会在PNG开头增加一个iTXt
块,内容是XML格式的元数据,可以在这里查到其详细信息。其中,photoshop:TextLayers
标签会完整记录所有文本图层的内容,由此就可以得到完整的背景文本了。但是该标签的内容实际上是按照PS中的图层顺序排列的,和海报中背景的文字顺序并不相同,需要手动按照图片内容重新排序。整理后解码就可以得到如下内容:
from gmpy2 import * n = 0xb299498e2cafc1efae26d4238fae641c65572b49f17fe69f0672f5cb68d6f7581882333e157cfd72abece88abb90176143f5af74e223f78e5b0b6f12be26ce3cfcb839af318f995f703e033f722185f6e8ee189021d7b05693641152a32b8534b8affcaf0024f16f703b83db5541f7c6179c80d9df410f27e9cd52041ed8295d705e72ed4186c14a0f6c59c2f61af9fc32d05481f333ebc8d11ca29cb36e5f63fe3603fd86d40f43b46bc2bcfd81dadad8efc2c8013cd9ff4f36f62cdb13b8c3142b6888ac72305a4640dac0f6b28ea300dc0f5ecef2ec5a22b8787c536708f7d19a5f0ec7dd74c2922a324317084f326095f79ed482398867af813688ace829 e1 = 65537 e2 = 17 s = gcdext(e1, e2) s1, s2 = s[1], -s[2] c1 = 0x62657f566e5cad94aab0ea29aedbce169e19b55a9a6da8fe31b24b28c48e269379d4be0164cf4984bd889a741396f363ec17b033ace90315d73237dadd8b928c8ad6e77eb656f4fea08287af594a9a5b81cd2d08a62251f2f8d170ffe0fd76129819da40ae2bf9b9c5d58664f24cebf6a96bade76eda82ed5c9700ac841bac75d4b1b4e6336b8cdcf69e8ad9dfe3bcb673382215892590afd6434fd7b4ad1209490ea51ebcc34a7164d897cdb406ee376d788232e47ac2602279af5456c73a58e5d876e30230851cf8310744acf7a873c5a96d2a2f9068a2f14c3f72ddd4867f5fde39665967cd00f2fe244b1a809e14c6119bcec06ac53c05fa897a123f5b6b c2 = 0xa41616c9172bc9ec110c0a95ea60e799eee21d9a237530104a076ea33c95033a703732165253b3e95e045d25caca9d3363810775091a577d05c1e9f784c235bdb5a34d87caef80774302356a6b5977ea743ab0b58e7a7c92171620a8e77690b52b93ef85d378ea79072fa49cd353cfd1a56dde7834c91fe716dd1fc7de2e4510c99f42657d9381d1c8c3e2ade4fabd689cfb937834443336f576901ac902b3dabb4c5ec654b74b88ceaf86d0aa61c86309513ef19d514bcdac866c058e465facbc285c1e33a276a3f8234300c080db641d22f82f510aad605727e309bc41dc86a1d96c285c406d126b261efb461569d6b4aea505d273c9f4478a4a03b4d92d76 c2 = invert(c2, n) m = (pow(c1, s1, n)
不难发现,这是一段RSA共模攻击的解题脚本片段。因此补齐脚本后就可以解出剩余部分的FLAG了。
One more thing…
- 本题和SpiritCTF 2020的某一题冥冥之中有所对应,是哪一题呢?
- 使用Base64的原因:为了保证只有还原顺序才可以解码得到文本。原因请自行思考~
[Reverse] 计图(懂得都懂)
题目
小k同学的游戏大获成功,一举取得了AppStore下载榜榜首……虽然小k同学是这么幻想的,但实际上他只是借鉴了另一个游戏罢了,眼下还是要打好计算机图形学基本功。然而马上就要到作业截止时间了,小k同学的程序却怎么也调不对……
题解
本题希望强调Patch对于解Reverse题目的重要性。不难发现,题目中FLAG部分因为窗口太小而无法看到。丢进IDA发现程序符号信息都在,大体是一个调用OpenGL进行绘制的简单程序。
由于模型正在旋转,因此想到可以更改旋转的方向来看到Flag。于是定位到draw
函数中的glRotate
函数,掉转x、y两轴。
此时应该可以看到部分FLAG了,但是可能还会有点重叠。因此可以修改上下两个glTranslate
的x轴数值,让模型稍微远离一点屏幕,然后就可以看到FLAG了。
值得一提的是,本题在赛后被一位21级同学解出。它的解题方法是修改ui_loop
函数中的SDL_SetVideoMode
、reshape
函数的参数来调整窗口大小。但由于同时画面也会随着窗口大小而调整,因此还需要修改reshape
函数中glFrustum
的近面位置near
。由此得到的Flag要比上述方法更加清楚。
One more thing…
- 本题本来是Misc,但它确实应该是Reverse,不是吗?
- 本题给非预期解增加了若干障碍,包括但不限于花命令干扰模型加载代码、反调试。
- 本题使用的渲染器为出题人移植的RepicoGL软件渲染器(详见:在ESP32上移植OpenGL实现(一)),所以这题原定计划是一道ICS题目。
- 本题NETA自梗”想要5000兆円“,也就是一行红字、一行白字。大和赤骥.jpg
后记
本次校赛是我个人比较满意的一次。每个方向的题都有被解出,并且榜前几名的队伍解出的题目也不尽相同。Misc部分的解题数量与难度梯度基本符合我个人的预期。由于这大概率是我本科期间最后一次能参与筹备的校赛了,因此对我个人而言也算是一个满意的句号。特别感谢NSA的各位对本次校赛的辛勤付出,也感谢各位选手的支持!
评论