这是我第一次参加 Hackergame,之前也没有接触过 ctf 之类的东西,全靠自己积累的计算机常识在玩。
一开始只是想着随便玩玩,后面发现好像好多题都很有意思,而且开头排名还不错,于是变成了激情的前100守卫战,最后也是成功上榜啦!

整个比赛下来感觉 Hackergame 真好玩,明年还玩 (●ˇ∀ˇ●)。
接下来的顺序按照的是我的做题顺序喵,与难度无关。
签到
因为完全没打过 Hackergame,在打之前偷看了往年的题目和 writeups ,大致对题目有一点了解,也清楚了签到题的做法,一般都是改 url,今年也是这样的。
但是因为一开始不熟悉,还是先F12看了很久的代码,才发现点了提交后url会变成这样。
1 | http://202.38.93.141:12024/?pass=false |
把 false 改成 true 就好啦!
打不开的盒
下载题目后是一个 .stl 文字,直接谷歌搜这个文件发现是一个3D模型的文件,然后就搜在线打开这种类型文件的网站。
这个 flag 是在里面被镂空的部分,而这个网站好像没法进行编辑,最后通过一些刁钻的角度还是看清楚了 flag。

这个0一开始不确定是啥,所以O和0都尝试了一下。
1 | flag{Dr4W_Us!nG_fR3E_C4D!!w0W} |
猫咪问答(Hackergame 十周年纪念版)
猫咪问答好像是 Hackergame 的保留节目。
1. 在 Hackergame 2015 比赛开始前一天晚上开展的赛前讲座是在哪个教室举行的?(30 分)
在科大的 wiki 上可以翻到每年 Hackergame的一些活动记录,找到 2015 那一届的活动记录,找到答案是 3A204。
2. 众所周知,Hackergame 共约 25 道题目。近五年(不含今年)举办的 Hackergame 中,题目数量最接近这个数字的那一届比赛里有多少人注册参加?(30 分)
还是在 wiki 里可以找到一些活动结束的文稿,而且比赛网站的首页其实也放了近几年的圆满结束新闻稿,一个个找可以找到 2019 的新闻稿中有 2682 这个具体的注册人数。
3. Hackergame 2018 让哪个热门检索词成为了科大图书馆当月热搜第一?(20 分)
直接到github上找到花絮环节,第一个就是 “程序员的自我修养”已经成为了图书馆本月热搜词的第一名 。
4. 在今年的 USENIX Security 学术会议上中国科学技术大学发表了一篇关于电子邮件伪造攻击的论文,在论文中作者提出了 6 种攻击方法,并在多少个电子邮件服务提供商及客户端的组合上进行了实验?
直接在谷歌学术上搜关键词 USENIX Security 2024 ustc mail,就找到了论文,但是就是不知道怎么算出答案,16*20 = 320 不对,最后一怒之下和它爆了,写了 python 直接爆破喵!数字肯定不会很大,直接一个个试。最后答案是 336。
5. 10 月 18 日 Greg Kroah-Hartman 向 Linux 邮件列表提交的一个 patch 把大量开发者从 MAINTAINERS 文件中移除。这个 patch 被合并进 Linux mainline 的 commit id 是多少?(5 分)
这个我熟,比赛前刚刷到这个事情,有点抽象,难以评价喵。直接到github上找到linux,issue里就有一个控诉的,可以直接看到答案是 6e90b6 。
6. 大语言模型会把输入分解为一个一个的 token 后继续计算,请问这个网页的 HTML 源代码会被 Meta 的 Llama 3 70B 模型的 tokenizer 分解为多少个 token?(5 分)
看到这个问题发现第4问的python没有白写,这个token感觉应该也不会很大,所以直接从0开始一个个试。最后得到答案是 1833 。
1 | import requests |
旅行照片 4.0
这个开盒题好像也是 Hackergame 的保留节目了,好好玩!
直接在百度地图搜了科里科气科创驿站,然后搜到了一个好像是公司。然后又搜了中科大的各个校区,发现有一个中科大的高新校区好像是最近的,但是答案是一个字,所以又认真看了一下,发现科里科气科创驿站还有一个科大站,然后就可以得到答案是 东校区西门。
先是谷歌搜了关键词,发现找不到,然后感觉这种音乐会什么的B站肯定有视频吧,于是到B站搜索,找到了主播的直播间以及某个节目的简介,都发现了答案是 20240519。


感觉图上没有什么信息,直到看到了垃圾桶上有字,依稀可以看到 “六安xx”,我感觉像是六安国科,搜了一下发现没有六安国科这种东西,所以把国科删了,又加了跑道这个关键词,搜 “六安 公园 跑道”,就找到答案啦,是 中央公园。
这个景观太有特色了,直接谷歌搜图就出答案了,是 坛子岭。
四编组动车是关键词,而且周边还有医院,碰巧舍友之前在北京实习经常坐这个车,于是获得了场外援助喵,答案是 积水潭医院。
答案是 CRH6F-A。
每日论文太多了!
直接在 Chrome 里打开 pdf 搜索 flag,直接 Ctrl+F 搜索到一条记录,但是就是看不到,怀疑是什么白色字体,于是下载下来上传到了在线的 pdf 编辑器上,再搜索flag可以发现有一个地方高亮了,复制出来的文本是 flag here,所以直接用编辑器把周围的图片删干净,就找到了一张写着 flag 的图片。

1 | flag{h4PpY_hAck1ng_3veRyd4y} |
喜欢做签到的 CTFer 你们好呀
这题一开始跳过了,因为没有找到这个战队的招新主页QAQ,谷歌一直搜不到,后面发现原来比赛网站的主页里,承办单位的超链接指向的就是这个招新主页。
习惯性的 ls -a 一下,就看到了有一个 .flag 文件,直接 cat 就得到了。
之后各种尝试 cd ,发现权限不够,然后就加了 sudo ,于是触发了奶龙彩蛋QAQ。
之后看到刚进入的适合其实提示了 help 可以看可用的命令,一个个试试就发现 env 命令里就是另一个 flag 了。
PaoluGPT
这题解题思路应该还是比较明显的,肯定是要写一个 python 脚本遍历所有的 <li> ,然后访问所有的链接找 flag 了。
第二个 flag 还是有点懵的,然后认真看了一下提供的服务端代码,发现 /view 这个接口有一个用户输入的 conversation_id 来进行数据库查询,然后看到了 /list 里筛选了 shown = true。
计算机专业学了数据库喵!所以直接就想到了 sql 注入,但是完全没有进行过 sql 注入,也不知道怎么注入。
一直各种 1 or 1=1 好像都没有成功。
最后认真找了一些博客看了一下,才理解要怎么构造,首先看原本的 sql 语句。
1 | select title, contents from messages where id = '{conversation_id}' |
这个语句是有一对单引号的!所以要先用一个单引号和第一个单引号进行配对,这样才能用 or。
所以构造出来是
1 | 'or shown=false -- |
其中 -- 是 sql 里的注释,用来把之前剩下的单引号注释掉。
1 | import requests |
比大小王
这个一看就是要写一个脚本来计算,但是我对这种需要实时交互的脚本不是很了解QAQ。
对 web 的了解只达到了能看懂简单的 html、css 和 js,最后尝试在 F12 的控制台里写 js 代码看看。
直接看代码可以发现它是在前端先做一次校验,然后保存下用户按的按钮序列,最后还要把按钮序列回传后端的,所以直接改前端代码是不可行的。
发现使用 chooseAnswer 函数就可以模拟按下按钮,而比较的两个数通过 state.value1/2 也可以拿到,于是写出了第一版代码。
1 | setInterval(function () { |
直接扔到控制台里执行,发现我的速度怎么这么慢QAQ,对手已经结束了我才几十题,然后才发现校验是做了限速的。
之后又各种折腾,还是无法战胜强大的对手。偶然间直接输出了state.values,发现它原来是100道题都提前发过来了。那就好办了,根本不需要模拟按下按钮,只要往inputs里塞我按下的是啥就行了,反正是给后端的信息。
于是写出了第二版代码。
1 | for (let i=0;i<100;i++){ |
需要注意的是代码执行的时机,需要等到游戏开始才能执行,否则values的数据还没有得到。
零知识数独
这个只做了第一个 flag,正好我是数独热爱者喵,无聊的时候经常玩数独,就是网站的输入设计太难受了。
做完4个难度的数独就可以拿到 flag 了。
Node.js is Web Scale
先看代码,可以看到一个很可疑的 /execute,它居然存在一个命令执行,尝试了一下 execute?cmd=test,
也确实可以看到 hello world。
所以想到了只要能往cmd里塞一条 cat /flag 的指令就好啦!而唯一能进行修改的只有 /set 接口。
然后就想不到其他的东西了QAQ于是问了强大的GPT,并尝试谷歌 “js 攻击”,最后强大的GPT给了我 __proto__。
对 js 一知半解的我完全不知道是什么,于是在学了博客后学习到了污染攻击。
就如博客里所说,在赋值的时候如果key是 __proto__, 就可以尝试污染攻击。
我们只要让 current.__proto__.get 为 cat /flag,此时由于 current 和 cmds 都是最基础的一个 object,通过修改 current 的 __proto__,就可以修改到 cmds 的。
于是我们让 cmds 对象拥有了一个 get 的键,其值为 cat /flag,然后只要执行 execute?cmd=get 就可以拿到 flag 啦!
由于是常见的攻击方式,所以谷歌和 GPT 让我找到了这个方法,还是学习到了不少东西喵!
惜字如金 3.0
这道题非常有意思!中间很长一段时间都在研究 C 怎么做,每次都觉得很有戏,但都没有成功QAQ。
A 可以直接人工补充,缺少的都是很常见的单词,由于每行都用空格补充到了80个字符,所以很容易看出来每一行缺了多少个字符,然后再到这行认真看每一个单词就很好补充了。
B 开始进入了这道题的核心做法,除了第7行外,其他行和A是一样的,重点就在这个第7行。
可以看到 poly 应该是一堆 B,但是可能为大写 B,也可能为小写 b,我们无法确定。
简单看一下代码发现 poly 其实就是 crc 里的多项式,B 是 1,b 是 0,在运算后就是flip。
之后根据输入进行了 crc 运算,又经过一个 hash 最后得到了一个 hash 值,这个 hash 值会作为这一行的文件名创建一个 txt 文件,文件的内容是这一行代码。
把第7行没改的代码传上去,可以发现是会返回错误信息的,而且还会贴心的告诉我们 hash 值,这个 hash 值是正确代码下根据我们的输入生成的。
于是我想到了能否构造一个输入,让这个 hash 直接就能显示出 flip,这好像有点困难,所以我开始先忽略 hash,只关注 crc,有没有什么办法构造一个输入,让 digest 直接就是 flip?
这下打 ACM 的舒适区来了喵,很容易想到这样的输入。
1 | line = b'\xff' * 6 + b'\x00' * 73 + b'\x80' |
因为一开始 digest 是全 1 串,所以先构造 48 个 1,每次 digest 的末位都会变成 0,这样就不会 xor 上 flip。在经过 48 次(6 bytes)后,digest就变成了 0。所以只要接下来的所有输入都是 0,最终的 digest 也会变成 0。只要 digest 是 0,就自由多啦!我们只要最后在输入一个 1,digest 从 0 xor 上一个 flip,就变成了 flip!
最后 digest 的 return 只是进行了一个 0 变 1, 1 变 0,无伤大雅。
但这样的输入后,我们得到的是 hash,而不是 digest,所以接下来我们需要关注到 hash 了。这是一个很常见的 hash 方法,我们的问题就是把 hash 还原成 digest。
令 $digest = x$ 那么有 $u_2 \times x^2 + u_1 \times x + u_0 \equiv hash (\mod 2^{48})$
容易发现这是我们算法常见的数学题喵,二次剩余!
对于模数是 $p^k$ 的情况,可以直接通过这样一个方法获得 $x$ 。
考虑 $x$ 在 p 进制下的最低位,这里也就是 2 进制啦。对同余式两边都进行 mod 2 的操作,同余式依然成立,通过这样我们就可以判断x是否是合法的,然后不断的在之前合法的基础上增加一位,印象里 2 进制应该是只有两个合法解的。直接用 c++ 写一个 dfs 就好啦。在得到答案后要注意 01 的翻转。
1 |
|
C 用同样的方法可以解决第 7 行,因为 u0,u1,u2 都是已知的,只是大小写不确定,然后我就陷入如何求解第 20 行的怪圈了。
一开始是读代码,发现了当 hash 值相同时,会打开对应的文件进行匹配,还会匹配出不相同的地方。
所以为了打开第 20 行,必须知道 20 行的 hash,但是如果我知道了 hash,其实也就知道了这行QAQ。
一陷入舒适区就一直在数学上想了,这就是打ACM打的了QAQ。后面看了官方的 writeups,才发现应该想办法去构造一个 hash 为 “answer_c” 的行,来直接获得 flag,有时间再来补题吧。
优雅的不等式
这就是纯粹的数学题了QAQ根据代码里对于 $\pi > 2$ 的证明,可以发现它是在第一象限画了一个四分之一圆,然后用一条直线去拟合四分之一圆,这样的拟合精度肯定太低了。
所以到了第二问,考虑把 $\sqrt{1-x^2}$ 泰勒展开,展开之后可以得到 $1-x^2$ 就证明了第二问。
到了第三问,我原来的思路是接着展开,但是由于 $\sqrt{1-x^2}$ 的定义域是(0,1),展开的拟合速度非常慢,在 1 附近是很慢的,而题目是有长度限制的,所以只能放弃。
那么说明不能用 $\sqrt{1-x^2}$ 拟合,于是我又换了 $\frac{1}{1+x^2}$,通过arctan来凑出 $\pi$,但是这个泰勒展开也失败了QAQ
然后我记起来之前在知乎上经常看到一些神仙构造,《注意到》。于是我在知乎上搜索了”如何证明 $\pi$ 大于” 这个关键词,搜到了如何证明这个数小于pi),并找到了作者的一篇如何构造的文章,【科普】如何优雅地“注意到”关于e、π的不等式。
按照文章里的方法构造 $\int_{0}^{1}\frac{x^n(1-x)^n(a+bx+cx^2)}{1+x^2}dx$,之后积分然后就是解方程,写了一份python代码来帮助解答。
积分式中的 n 需要枚举以找到一个能够满足大于0的二次函数,为了偷懒直接让 n=80 了,它能够通过题目。
1 | import re |
不宽的宽字符
看到熟悉的 c++,就先下载下来了,题目已经给了提示,在 (char *) 强转的地方有问题,所以就想着打印出来看看,结果发现怎么只会输出第一个字符。
然后就了解到了 wchar 占两个字节,对于简单的 ASCII 字符,在 wchar 中占两个字节,一个是本体一个是 0,于是使用 (char *) 强转就会导致出现一个 0 表示字符串结尾,所以就只剩一个字符了。
那就可以完全不理会代码加的 you_cant_get_the_flag 了,它没有什么用。
于是很自然的会想到应该构造一系列的 wchar,使得它刚好是 Z:\theflag,这里还是有点绕的,因为我发现我输入的中文其实是 3 个字节,它到底怎么变成 2 字节的 wchar 呢?
在疯狂的 printf 和测试后,我发现其实是这样的,首先从终端会输入一些字符,这些字符应该按照 utf-8 编码,这个编码是一个汉字 3 字节的,在经过 MultiByteToWideChar() 后,它将我们输入的 utf-8 编码的字符变成了 wchar,而这个 wchar 的保存格式其实是 Unicode,因为所有的 Unicode 字符都是 2 字节的。
搞清楚了整条变化链,就很好构造了!其实就是找到 Z:\theflag 每两个字符拼到一起后对应的 Unicode 字符,这个可以通过在线的 Unicode 转换得到,然后我们复制到终端,终端默认应该是 utf-8 编码的,所以我们的 Unicode 字符会自己按照 utf-8 编码输入,我们不需要做额外的转换。注意字符串的后面应该加上一个 0 结尾的字符,这样才能截断。那么这串字符就是 㩚瑜敨汦条ÿ,最末尾的字符其实只要能让结尾是0就行!
强大的正则表达式
第一问 16 的倍数很容易构造,因为 16 的倍数是有循环的,因为 16 * 625 = 10000,所以其实只需要考虑末尾的 4 位,于是暴力找到长度 4 位的 16 的倍数就行啦!构造的时候可以偷懒不考虑长度小于 4 位的数,因为测试到这些数的概率太小了,不匹配也没关系。
第二问 13 的倍数就不那么显然了,因为没有办法找到循环节。但是作为大四的兔子,我学过编译原理啦!很容易构造一个 DFA,一共 13 个状态表示当前数 $\mod 13$ 后的值,转移就更简单了,枚举每一个新增在后面的数 j,对于状态 i 向 $(i * 10 + j) \mod 13$ 连一条边就好啦!
之后需要考虑的就是把上面的 DFA 转为正则表达式,在学编译原理的时候正好研究过这件事情,只要考虑每次删掉一个点,然后考虑删掉这个点对其他点的影响就行了。会发现其实就是这个点的所有入边点和出边点之间两两连线,线上的转移正则就是 左边 + (自己)* + 右边。
这个题一开始卡了很久,因为我以为题目给的是 10 进制啊QAQ。生成出来的正则长达几千万位,我一直在想要怎么优化这个正则,对于加括号扣扣嗖嗖,还总是出错!最后才发现原来给的是一个 2 进制的数,这样边数大大减少了,生成的正则表达式长度也短了很多。注意转移也变成了 $(i * 2 + j) \mod 13$
由于初始态和最终态是同一个状态,也就是状态 0,所以最后的正则就是 0 到 0 的闭包。
第三问有了第二问的基础就很简单了,关键其实就是构造出 DFA,完全不理解 libscrc.gsm3(x) 是啥,但是可以在 python 里直接利用算出转移表,然后就和第二问差不多啦!但是第三问的初始态和最终态是不同的两个状态,所以最后好像只能删到两个点,之后就要基于 0->0,7->7,0->7,7->0 这四个正则拼成一个新的正则。很容易发现就是起点可以一直走自环,然后到终点,然后在终点自环,之后还可以选择回来起点,起点自环到终点,终点自环,这样无限反复。具体可以看代码最后的注释。
1 |
|
先不说关于我从零开始独自在异世界转生成某大厂家的 LLM 龙猫女仆这件事可不可能这么离谱,发现 Hackergame 内容审查委员会忘记审查题目标题了ごめんね,以及「这么长都快赶上轻小说了真的不会影响用户体验吗🤣」
一个神奇的 AI 题,只想到了把 after 扔给 GPT,GPT 给的原文其实已经七七八八了,但是还是有不少错误,于是我把 GPT 给我的原文重新加密,然后和真正的 after 做比较,先把一定不正确的单词找到。然后再让 GPT 多恢复几次,基于多个版本重点检查这些单词,最后复原了很多,但是 GPT 死活不知道 xxtxxxxx 是啥,我感觉 GPT 其他的单词都复原的很好,所以就想着试试暴力枚举这个单词,然后check sha256,最后 GPT 确实也只有这个单词错了,是 gathered。
第二问不会喵QAQ,虽然知道大模型是基于之前的单词一个个生成下一个单词的,但是也仅仅是这样了,对 AI 了解的还是太少了。
关灯
ACM 题,好耶!
一开始不知道开灯游戏是啥,看着代码好像很复杂的样子就没看题了,后面发现过的人好多,才发现原来开灯游戏是点一个格子,其周围格子也变化。
似乎是常见题了,ACM 里应该是有 2D 的原题,改成 3D 其实是一样的,也是构造出矩阵然后高斯消元。假设我们对格子 i 的操作是 $x_i$,点了是 1,没点是 0。然后我们列出最终状态的方程组,其实就是把 + 改成 xor 的方程组,还是可以直接高斯消元,这样前 3 问都可以解决。
对于第四问,我是注意到矩阵是很稀疏的,而且这个方程组的左边是固定的,右边才是变化的,所以有想过是不是可以预处理出一些答案,但是这题已经是很后面才开始做的了,所以没什么时间QAQ。前面被 powerfulshell 和惜字如金C 卡了好久,结果都没做出来。
1 |
|
禁止内卷
靠这题成功苟住了前100,这题其实很早之前就有思路,肯定是在文件名上搞鬼,之前实习的时候公司让我改的第一个 bug 就是对外部的文件名加一个校验,所以我对这个漏洞比较清楚。
但是我的思路被卡住了,因为我一直想着修改 answer.json,但是 answer.json 一旦被修改,原来的内容也拿不到了,后面又尝试了 result.json,发现改了完全没用,因为它会被重新写入修改的。
然后又开始向 index.html 思考,一直在想怎么才能让 index.html 调用 python 里的 get_answer 函数,结果 index.html 好像一直改不成功。
本来是想着放弃了的,一遍看排行榜不断下降的排名,一遍读题,然后突然发现了一个关键的信息!--reload!对 flask 不熟的我立马谷歌了,然后把代码在本地部署了一下,才发现原来是直接修改 python 代码可以实时的加载。于是想到了要改 python 文件,但是 python 文件的名字好像不清楚,一开始试了 main.py,然后看到了很多 flask 部署的教程都是 app.py,就又试了一下 app.py,然后就成功了! 把 app.py 的 get 接口返回的 result 改成了返回 answer,然后发现 answer 小于 0 会被修改成0,又把这行删了,由于太激动了直接把 answer 的前几项一个个抄下来然后转成 flag。其实可以直接在 app.py 里解析完然后输出QAQ。

