USTC Hackergame 2023 解題總結 (write-up)

前言

2018 年第一次參加中國科技大學 (USTC) 的 hackergame,了解到 Catch The Flag (CTF) 這項有趣的活動。當時對此模式完全不熟悉,能做出來的題也非常有限,大約三分之一左右。其餘題目後來參看有人寫的總結 (write-up) 才恍然大悟,覺得此類活動雖無力精通,但其覆蓋電子信息技術領域的方方面面,對於個人技術知識增長與更新甚有裨益。其後由於精力有限,並未在業餘對 CTF 進行訓練。過去數年亦是在 hackergame 結束後才看到/想起此事,頗感可惜。月餘之前在藍底白鳥的社交平台上看到有人提到今年的賽期,於是加入了備忘錄,終於有機會參與活動。這次大概做出來一半左右的題目,正好是自然順序 (前一半) 。

題目詳情與正式題解參見 GitHub hackergame 2023 Write-ups

吐槽一句,USTC hackergame 的運營方一如既往偏愛 Python,所有題目後端基本均用 Python 寫成。為此解題過程中不得不去翻其基本語法,十分不爽。

題目

Hackergame 启动

題目連結

打開題目網頁發現需要對著網頁錄製一些中二台詞,直接提交是零分。觀察了一下發現提交後網頁地址變為 https://cnhktrz3k5nc.hack-challenge.lug.ustc.edu.cn:13202/?similarity=,判明服務端接收 GET 請求。猜測 similarity (相似度) 是按照百分制計算,於是直接將 URL 末尾改為 similarity=100 並回車,獲得 flag。

貓咪小測

題目連結

及格喵

四個問題都可從在線信息檢索獲取:

想要借阅世界图书出版公司出版的《A Classical Introduction To Modern Number Theory 2nd ed.》,应当前往中国科学技术大学西区图书馆的哪一层?

中國科技大學圖書館網站 檢索「A Classical Introduction To Modern Number Theory 2nd ed.」,發現沒有精確匹配。去掉版本信息再次檢索,找到答案為西區圖書館 12 層館藏。

今年 arXiv 网站的天体物理版块上有人发表了一篇关于「可观测宇宙中的鸡的密度上限」的论文,请问论文中作者计算出的鸡密度函数的上限为 10 的多少次方每立方秒差距?

谷歌檢索關鍵詞「arxiv observable chicken density」,結果網頁中搜索「upper bound」,找到答案為 23。

为了支持 TCP BBR 拥塞控制算法,在编译 Linux 内核时应该配置好哪一条内核选项?

谷歌檢索「TCP BRR compilation linux CONFIG_」,檢索結果 GitHub 頁面 中看到相關參數有兩個,一個是 CONFIG_TCP_CONG_BBR,另一個是 CONFIG_DEFAULT_BBR。試著用第一個進行提交,答案被接收。

🥒🥒🥒:「我……从没觉得写类型标注有意思过」。在一篇论文中,作者给出了能够让 Python 的类型检查器 MyPY mypy 陷入死循环的代码,并证明 Python 的类型检查和停机问题一样困难。请问这篇论文发表在今年的哪个学术会议上?

谷歌搜索「mypy infinite loop conference」,找到 相關論文 Python Type Hints Are Turing Complete (pdf),內容提到其發表於 37th European Conference on Object-Oriented Programming (ECOOP 2023).,遂找到答案。

滿分喵

這一問一開始沒做出來,研究瀏覽器 Network tab 也看不出網絡請求端倪。後來返回此題嘗試重新提交,發現自動獲得了第二個 flag。推測題目是考察網絡請求的 idempotency (幂等性,指多次目的相同的網絡請求應該導致一樣的結果,服務端已存有過往處理結果,不再額外消耗資源進行計算)。

更深更暗

題目連結

題目打開以後好長一個網頁,懶得往下拖了,直接看 HTML 源代碼,發現底部就有 flag。

旅行照片

題目連結

1、 你还记得与学长见面这天是哪一天吗?(格式:yyyy-mm-dd)
2、在学校该展厅展示的所有同种金色奖牌的得主中,出生最晚者获奖时所在的研究所缩写是什么?

這道題也是一開始卡住了。找到日記具體日期是解題關鍵,很快就看到照片中學長參與會議掛牌上有 STATPHYS28 並找到其正式網站,得知開會日期是 August 7th-11th, 2023。

第一張圖查詢獎牌上的拉丁文,得知是諾貝爾獎牌,得主 M. KOSHIBA,獲獎年份 MMII 為羅馬數字 2002。查詢得知此獎牌屬於當年獲得諾貝爾物理學獎的 小柴昌俊。看日記上下文推測此獎牌展示地點為日本,小柴獎牌應該展示在東京大學。東京大學所有諾獎得主收錄於此維基條目 List of University of Tokyo people - Nobel prize laureates,逐一查詢,最年輕的獲獎者為 1959 年出生的 梶田隆章,得獎時所在研究機構為東京大學的 Institute for Cosmic Ray Research,縮寫 ICRR。

然而用四個日期 + ICRR 輸入以後提交卻都不對,我開始懷疑自己查詢是否有誤,或者日記中學長尚在會議準備期,會議還未開始,亦或者學長是參加其他活動,只不過還戴著參與 STATPHYS28 時的掛繩。

反覆核對以後覺得第二題答案應該是 ICRR 無誤,於是用 TypeScript 寫了窮舉,遍歷 2023 年所有日期然而還是不對。隨即恍然發現此處被坑,JS 的日期類 Date 取得的年份為數字年,月內日期也為對應數字,不過月份為零開頭的索引😓(八月對應 7),轉換為 YYYY-MM-DD 時月份需要 + 1。後來回想起以前也吐槽過這個來著,看來沒長記性。

代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import axios from 'axios';
import { Buffer } from 'buffer';

const prefillZero = (n: number): string => ('00' + n).slice(-2);

for (let d = new Date(2023, 0, 1); d <= new Date(2023, 11, 30); d.setDate(d.getDate() + 1)) {
const answer1 = `2023-${prefillZero(d.getMonth() + 1)}-${prefillZero(d.getDate())}`;
const payload = `Answer1=${answer1}&Answer2=ICRR`;
console.log(payload);
await axios
.post(
'http://202.38.93.111:12345',
`${Buffer.from(payload).toString('base64')}.txt`,
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Cookie:
'session=<瀏覽器 Cookie 獲取>', // 測試提交 POST 請求時發現 Cookie 只要有 session id 就行,目測 session id 是 JWT
},
},
)
.then((res) => console.log(res.data))
.catch((err: Error) => {
console.log(err.message);
});
}

通過以上代碼發現在日期為 8 月 10 日時可獲得 flag,看來可能是我一開始日期輸入錯了。

3、帐篷中活动招募志愿者时用于收集报名信息的在线问卷的编号(以字母 S 开头后接数字)是多少?
4、学长购买自己的博物馆门票时,花费了多少日元?

噴泉拖入 Google Images 搜索得知是上野公園,附近博物館有 東京國立博物館國立科學博物館
8 月 11 日上野公園舉行的活動是 全国梅酒まつり,其網頁底部招募志愿者(ボランティアSTAFF募集)頁面內問卷連結為 https://ws.formzu.net/dist/S495584522/,故第三問答案為 S495584522

看照片方位查詢地圖推測作者去的是科學博物館,個人票價為 630 日圓,高中生免費。既然學長都參與學會了(出題人也為大學生),學長應該是大學生而不是高中生,答案應該是 630。不過發現填 0 才能提交成功,可能是出題失誤。

5、学长当天晚上需要在哪栋标志性建筑物的附近集合呢?(请用简体中文回答,四个汉字)
6、进站时,你在 JR 上野站中央检票口外看到「ボタン&カフリンクス」活动正在销售动物周边商品,该活动张贴的粉色背景海报上是什么动物(记作 A,两个汉字)? 在出站处附近建筑的屋顶广告牌上,每小时都会顽皮出现的那只 3D 动物是什么品种?(记作 B,三个汉字)?(格式:A-B)

反覆看 STATPHYS28 網站,社交活動欄目 提到星期三於星期四有夜遊東京的活動,會參觀兩座橋。參與者需要在 the venue, University of Tokyo 集合。進一步研究網站發現 venue 地點在安田講堂附近,是為第五問答案。

第六題,檢索「ボタン&カフリンクス 上野駅改札口」出來的圖片結果裡有一張海報,背景是醒目的粉色,前景是熊貓。繼續搜索車站附近的 3D 廣告牌,得知自 2022 年起,廣告牌上定期展出秋田犬。於是第六問答案為 熊猫-秋田犬

賽博井字棋

題目連結

觀察網頁源代碼,向服務器提交落子位置即可。嘗試發現可以往已經落子的位置提交落子請求,覆蓋電腦放置的棋子,連成三子一線即可獲得 flag。

奶奶的睡前 flag 故事

題目連結

乍一看此題題目以為是要利用「奶奶睡前故事」技巧進行 AI 引導(prompt engineering)。

仔細端詳以後發現「親兒子」三字被加粗,題目中也提到截圖裁剪以後未進行元數據(metadata)消除。「親兒子」是暱稱,通常指代谷歌自家的 Pixel 手機。聯想到今年有安全研究發現 Pixel 手機的裁剪圖有元數據殘餘,可以借其恢復原始全圖,遂找到演示網站 acropalypse.app 選擇不同機型逐一嘗試。在嘗試到 Pixel 5 還是 5a 的時候,恢復得出的截圖中出現了 flag 信息。

組委會模擬器

題目連結

此題介面會密集出現 1000 條信息,參賽選手需要在符合要求的信息出來後三秒內將其點擊撤回。很明顯拼手速是不夠的,觀察網絡請求發現 1000 條信息是最開始就預載好的,按時出現。撤回的時候會發送指向 /api/deleteMessage 的撤回請求,最後用 /api/getflag 取得 flag (順便吐槽一下,兩個路徑的命名沒有遵循相同的大小寫規則)。

本地嘗試幾次以後發現一次性全部撤回和循環等待撤回均會造成「時空跳躍」或者「撤回太晚」的問題。最後乾脆用 setTimeout + Promise 預先準備好所有的請求,然後用 Promise.all 解決了問題。

代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import axios from 'axios';

interface Response {
server_starttime: string;
messages: { delay: number; text: string }[];
}

enum Action {
getMessages = 'getMessages',
deleteMessage = 'deleteMessage',
getFlag = 'getflag',
}

const getUrl = (action: Action): string => `http://202.38.93.111:10021/api/${action}`;

const cookie =
'session=<session id>';
const postOptions = {
method: 'POST',
headers: { cookie },
};

const response = (await axios.post(getUrl(Action.getMessages), undefined, postOptions)).data as Response;

const messages = response.messages.map((e, i) => ({ ...e, id: i })).filter(({ text }) => /hack\[[a-z]+\]/.test(text));

const allPromises = [];

for (const { delay, id } of messages) {
allPromises.push(new Promise(() => setTimeout(async () => {
const res = await axios.post(getUrl(Action.deleteMessage), { id }, postOptions);
console.log(`deleted ${id}-th message: ${JSON.stringify(res.data)}`);
}, delay * 1000)));
}

await Promise.all(allPromises);

const flag = (await axios.post(getUrl(Action.getFlag), undefined, postOptions)).data;

console.log(flag);

題目連結

題目中說

这听起来像是一种通过无线信道传输图片的方式,如果精通此道,或许就可以接收来自国际空间站(ISS)的图片了。

並且提到了 ISS,於是搜索了一下 ISS 信號轉圖片的工具,最開始出現的是一個基於 Windows 的工具,看起來還要設置參數,很難用。又找了一下發現有人寫了基於 Python 的命令行工具 SSTV Decoder。試著用了一下,結果安裝失敗,看來是  M2 芯片的問題,於是找了台 linux 服務器,在上面解碼成功。

代碼:

1
2
3
4
5
6
7
8
git clone https://github.com/colaclanth/sstv.git

cd sstv && python -m venv env
source env/bin/activate

python setup.py install

sstv -d path/to/insect.wav -o result.png

JSON ⊂ YAML?

題目連結

題目考察 JSON 和 YAML 並不完全兼容這件事 (順便吐槽萬惡的 YAML,工作中讀/寫 CloudFormation template 的時候經常看著 JSON 和 YAML 語法混用抓耳撓腮)。

JSON ⊄ YAML 1.1

此問利用 JSON 和 YAML 對科學計數法的處理不同可解。

1
2
3
Input your JSON: {"a":1e2}
As JSON: {'a': 100.0}
As YAML 1.1: {'a': '1e2'}

JSON ⊄ YAML 1.2

YAML 1.2 對 JSON 的處理還是小心了很多,觀察題目代碼發現需要觸發 Exception,幾番嘗試以後發現利用 JSON 允許同名 key 但是 YAML 不允許這一點可以解題。

1
2
3
4
Input your JSON: {"foo":true,"foo":false}
As JSON: {'foo': False}
As YAML 1.1: {'foo': False}
No flag1

延伸閱讀

Git? Git!

題目連結

此題考察 git 提交到本地的文件會被追蹤。進入 .git/objects 文件夾,逐一對 object 文件(名字是二級目錄名與文件名連接其來的字符串)進行 git cat-file -p $object-full-name | grep flag 即可。

後續得知此題應該有更好的解法,git 有內置指令 git reflog 可用於顯示過往文件內容。參見 git-reflog - Manage reflog information

HTTP 集邮册

題目連結

此題需要發送原始 HTTP 請求來獲取不同狀態碼。我取得了 5 種 + 無狀態碼的回覆。

五種狀態碼

  • 200 OK: 送分題,默認 GET 請求即可取得

    1
    2
    GET / HTTP/1.1\r\n
    Host: example.com\r\n\r\n
  • 400 BAD REQUEST: 通過設定不合規範的 Host 取得。

    1
    2
    GET / HTTP/1.1\r\n
    Host: example.com/test\r\n\r\n
  • 404 NOT FOUND: nginx 對於不存在的路徑默認 fallback 到 404 頁面,並返回 404 狀態碼

    1
    2
    GET /404 HTTP/1.1\r\n
    Host: example.com\r\n\r\n
  • 405 NOT ALLOWED: 使用不同的 HTTP 方法發送請求,服務器返回 405 說不支持該請求方法。

    1
    2
    POST / HTTP/1.1\r\n
    Host: example.com\r\n\r\n
  • 505 HTTP Version Not Supported: 要求使用 HTTP/2 進行通信,服務器返回 505 說不支持該協議版本。

    1
    2
    GET / HTTP/2.0\r\n
    Host: example.com/test\r\n\r\n

無狀態碼

解決此題屬於誤打誤撞,把請求中的協議部分去掉以後,服務器按照 1991 年的 HTTP 協議進行了答覆,不提供狀態碼。

1
2
GET / \r\n
Host: example.com\r\n\r\n

Docker for Everyone

題目連結

此題需要在 Docker 容器中獲取設置在 /flag 位置的 flag。觀察發現 flag 實際位於 /dev/shm/flag,同時登入用戶在 docker 用戶組,并且可以啟動一个 alpine linux 鏡像。docker 用戶組有root訪問,於是將 flag 文件掛載至 alpine linux 鏡像內,在容器中讀取 flag。

代码:

1
2
3
docker run -it --rm -v /dev/shm:/dev/shm alpine
### inside alpine linux
cat /dev/shm/flag

惜字如金 2.0

題目連結

此題有點麻煩,提供的代碼被作為文本文件進行了「惜字如金」式去重:

  • 第一原则(又称 creat 原则):如单词最后一个字母为「e」或「E」,且该字母的上一个字母为辅音字母,则该字母予以删除。
  • 第二原则(又称 referer 原则):如单词中存在一串全部由完全相同(忽略大小写)的辅音字母组成的子串,则该子串仅保留第一个字母。

拿到代碼以後發現跑不通,发现 __name__ 被上述規則改成了 __nam__,加上 e 以後此行通過。

1
if __nam__ == '__main__':

讀完程序內容得知,flag 解密過程是從下列字符串陣列中按照不同的 offset 來讀字符,不過顯然提供的字符串也被經過了「惜字如金」處理。

1
2
3
4
5
cod_dict += ['nymeh1niwemflcir}echaet']
cod_dict += ['a3g7}kidgojernoetlsup?h']
cod_dict += ['ulw!f5soadrhwnrsnstnoeq']
cod_dict += ['ct{l-findiehaai{oveatas']
cod_dict += ['ty9kxborszstguyd?!blm-p']

根據 flag 是由 flag{ 五個字符開頭以及 } 字符結尾,對上述五個字符串進行加 e 或者增位調試数次後,得到了 flag。

🪐 高频率星球

題目連結

此題提供經過 asciinema 錄製的一個 shell session。安裝 asciinema 後使用 asciinema cat 可以還原出原始 session。不過該 session 應該包括了一些指示顏色之類的控制字符,需要進行過濾處理。首先用 sed 處理掉大部分控制字符,然後再對所得文件進行微調,最終運行輸出中顯示出的 flag.js 代碼,得到答案

代码如下:

1
2
3
asciinema cat asciinema_restore.rec |sed 's/\x1B\[[0-9;]*[JKmsu]//g' > output.txt
### 人工刪除殘餘控制字符後
node flag.js

🪐 流式星球

題目連結

此題給出一個使用 opencv 錄製的視頻,提供的文件好像有些損壞。題目說是直接把原始流數據寫了下來,不過沒寫完。

懶得研究了,直接 ffmpeg 暴力嘗試。沒得出精確的分辨率和幀率等參數,不過嘗試不同分辨率後生成的亂碼視頻裡勉強能看到 flag,所以算是勝利✌️。

1
yes | ffmpeg -f rawvideo -video_size 1280x720 -i video.bin video.mp4

🪐 低带宽星球

題目連結

此題明顯考察 PNG 格式圖片的壓縮。閱讀評分代碼,要求壓縮後的圖片與壓縮前的每個像素都一致。

第一問要求將給定圖片壓縮到 2 KB 以內,隨便找一個在線 PNG compression 工具就能壓縮到 1.6KB (我用的 compresspng)。

第二問要求將同一張圖壓縮到 50 Bytes 以下,未能成功解出。思路來說,大致明白需要詳細研究 PNG 的參數,選擇最合適的 filter。觀察了一下圖片,只有三塊不同顏色的矩形,可以使用 Palette 塊存顏色,色彩深度只需要 2 bit 用來存 3 種顏色的索引。filter 應該需要用某一個差分算法,壓縮算法應該也可以優化。

總結

解體過程中不時感到妙趣橫生,相較幾年前的第一次嘗試,此次完成題目大約一半左右,雖然在 binary 和 AI 分類都只拿了 0 分,還是比較有成就感的。期待此活動能長久地舉辦下去,以後也會嘗試參與。

不過工作日廢寢忘食,導致白天工作精神抖擻筋疲力竭😵,並非益事,日後還需多多注意💦。