Hackergame 2020 (第七届中科大信安赛) Writeup 0x00
Hackergame 2020 write up 0x00
一场新手友好且各种玩梗的CTF, 也算我是第一次正式地玩一场Hackergame, 之前了解到科大CTF还是从Coxxs的博客中得知的, 这次也很不意外的在榜上看到了他(。・ω・。)
很巧合的是, 今年恰逢我的考试(放假)周, 时间很充裕, 做题期间也学很多好玩的新东西(奇怪的知识又增加啦!)
后期基本上每道题要花费12h+, 也是看到群友不断做出来才有了继续冲的动力, 不然我可能就止步3k分了(o゜▽゜)o☆
对了, 说到群友
mcfx tql orz
这里是主办方的官方write up
Let’s go!
2048
路漫漫其修远兮,FLXG 永不放弃!
要实现 FLXG,你需要过人的智慧,顽强的意志,和命运的眷属。只有在 2048 的世界里证明自己拥有这些宝贵的品质,实现「大成功」,你才有资格扛起 FLXG 的大旗。
打开网站映入眼帘的便是一个前端精美(夸!!!φ(゜▽゜*)♪)的2048小游戏.
但作为一个最多只凑出来过1024的2048新手, 第一反应当然是打开F12去看看有没有可以操作的地方(正经人谁真的玩啊)
然后发现其实游戏的进度信息, 输赢信息甚至是分数全部都存在本地qwq
那其实可操作的余地就真的蛮大了, 你可以改动won
字段直接获胜, 或者把cells
里面的值改成想要的布局, 轻轻松松获胜(虽然这并不是预期解)
几步之后便可以拿到flag
flxg{8G6so5g-FLXG-29b79008fc}
PS: 还是想说出题人这个前端写的很棒啊qwq
超简单的世界模拟器
你知道生命游戏(Conway’s Game of Life)吗?
你的任务是在生命游戏的世界中,复现出蝴蝶扇动翅膀,引起大洋彼岸风暴的效应。
通过改变左上角 15x15 的区域,在游戏演化 200 代之后,如果被特殊标注的正方形内的细胞被“清除”,你将会得到对应的 flag:
“清除”任意一个正方形,你将会得到第一个 flag。同时“清除”两个正方形,你将会得到第二个 flag。
注: 你的输入是 15 行文本,每行由 15 个 0 或者 1 组成,代表该区域的内容。
生命游戏一直是我觉得很有趣的小玩具之一, 简单的规则, 简单的布局, 在特定的游戏进化条件下, 千变万化, 甚至有人用它实现了的生命游戏本身, 用边长2048的正方形区块模拟了一个格子的"生命活动"(虽然一个周期有35328个回合就是了)
想要了解的话可以推荐一个视频:【混乱博物馆】生命游戏:另一种计算机
回到这道题, 第一个flag
需要我们毁灭掉两个区块之一, 简单搜索后可以很轻易地发现Spaceship
图样, 只需let it go便可以摧毁掉右侧的区块拿到第一个flag
(借用Coxxs的gif
示意)
你需要用到
00110
11011
11110
01100
拿到第一个flag
flag{D0_Y0U_l1k3_g4me_0f_l1fe?_a3750a8c5d}
对于第二问, 刚开始的想法是用已知的图块去拼接或者干脆暴力尝试, 由于没有很好的思路于是就暂时跳过了一段时间, 而后意识到了蝴蝶效应, 嗯, 爆炸就是艺术.
只需要一个
010
011
110
拿到第二个flag
flag{1s_th3_e55ence_0f_0ur_un1ver5e_ju5t_c0mputat1on?_1248956586}
自复读的复读机
能够复读其他程序输出的程序只是普通的复读机。
顶尖的复读机还应该能复读出自己的源代码。
什么是国际复读机啊(战术后仰)
你现在需要编写两个只有一行 Python 代码的顶尖复读机:
其中一个要输出代码本身的逆序(即所有字符从后向前依次输出)
另一个是输出代码本身的 sha256 哈希值,十六进制小写
满足两个条件分别对应了两个 flag。快来开始你的复读吧~
打开终端看看他想让我做什么:
Your one line python code to exec(): print(1+2)
Your code is:
'print(1+2)'
Output of your code is:
'3\n'
Checking reversed(code) == output
Failed!
Checking sha256(code) == output
Failed!
原来如此, 是想让一个一行的python代码运行之后产生和代码本身倒序完全一样的输出啊……等等, 什么?
一波搜索之后找到了一个词语Quine
A quine is a computer program which takes no input and produces a copy of its own source code as its only output.
在Wikipedia
的上面我找到了一串如下的代码:
exec(s:='print("exec(s:=%r)"%s)')
了解了一下, 其中:
:= 是海象运算符(像不像一只小海象qwq)
%r 用rper()方法处理对象
%s 用str()方法处理对象
于是兴致勃勃地改编了起来:
exec(s:='print(("exec(s:=%r)"%s)[::-1])')
这不就输出自身的逆序了嘛, 拿去网站实验, 结果发现多了个\n
…╰(艹皿艹 )
exec(s:='print(("exec(s:=%r)"%s)[::-1], end="")')
于是拿到flag1
flag{Yes!_Y0U_h4v3_a_r3v3rs3d_Qu1ne_0b2133a489}
同样的, 只要将("exec(s:=%r)"%s)
作为一个整体, 就可以随意地改变输出的形式了
exec(s:='import hashlib\nprint(hashlib.sha256(("exec(s:=%r)"%s).encode("utf-8")).hexdigest(), end="")')
于是拿到flag2
flag{W0W_Y0Ur_c0de_0utputs_1ts_0wn_sha256_c1e8076e2d}
233 同学的字符串工具
233 同学最近刚刚学会了 Python 的字符串操作,于是写了两个小程序运行在自己的服务器上。这个工具提供两个功能:
字符串大写工具
UTF-7 到 UTF-8 转换工具
打开源码可以看到两段程序分别使用正则判断用户输入不是flag
的任意大小写组合, 正则本身应该没什么太大问题, 但是要求经过处理之后的文本变为flag
方可拿到最终的flag
.
看第一个, 只是调用了upper()
而已, 似乎并没有什么特殊的东西.
但是既然能出来FLAG
而又不是f
或者F
的字符有什么呢…?
说着拿起python
就开始一波穷举:
for i in range(0x10000):
if chr(i).upper() == 'F':
print(chr(i))
但是输出却只有f
和F
, 于是开始大开脑洞, 万一输出的是某两个字符呢?
for i in range(0x10000):
if chr(i).upper() == 'FL' or chr(i).upper() == 'LA' or chr(i).upper() == 'AG':
print(chr(i))
然后, 它果然出现了, 这个神奇的字符: fl
PS: 这里其实可以写成
for i in range(0x10000):
if chr(i).upper() in 'FLAG':
print(chr(i) + ' : ' + chr(i).upper())
真是 amazing Unicode 啊
fl 拉丁文小型连字 Fl
Unicode名称 Unicode编号 HTML代码 CSS代码 Latin Small Ligature Fl U+FB02 fl
\FB02
于是我们就有思路了:flag
flag{badunic0debadbad_c5701643d2}
hia hia hia怎么就bad bad bad了ヽ(゜▽゜ )-C<(/;◇;)/~>
继续前进, 关于UTF-7
我们搜一下:
有些字元本身可以直接以单一的 ASCII 字元来呈现。
第一个群组被称作“direct characters”,其中包含了 62 个数字与英文字母,以及包含了九个符号字元:’ ( ) , - . / : ?
…
其他的字元则必须被编码成 UTF-16 然后转换为修改的Base64
。这些区块的开头会以 + 符号来标示,结尾则以任何不在Base64
里定义的字元来标示。
那其实思路就比较明显了, 将一部分的字母转换为UTF-16
之后再Base64
编码, 放进去就好。
同时还找到了一个网站可以帮助我们完成这一操作: xssor
将 flag
丢进去得到 f+AGwAYQBn-
flag{please_visit_www.utf8everywhere.org_fb5ac8efab}
这是上面的网址: utf8everywhere
Unicode 真是个伟大的发明啊 ( •̀ ω •́ )✧
狗狗银行
你能在狗狗银行成功薅到羊毛吗?
我喜欢叫它狗子银行(DogeBank)
最开始, 我以为这是一道关于整型溢出的题目。
于是疯狂从信用卡贷款。
拥有了这辈子都不可能有的财富的同时,欠下了这辈子都还不清的钱。
以及,鬼才知道的净资产。。。(╬▔皿▔)╯
(说起来川普还没有解雇让他的选举网站布满NaN
的员工吗)
之后静下心来边玩边想, 发现了一个问题:
每天的利息是经过四舍五入计算的, 这可能真的是薅羊毛的关键!!
信用卡每日利息0.5%
最低10
元, 但是我在每天利息是10
元的时候却能够最多借出2099
元, 10/2099 = 0.4764%
也就是说我的实际的利率是0.4764%
储蓄卡每日利息0.3%
, 但是会四舍五入, 数学直觉告诉我, 分的卡越多, 每张卡金额越少的时候其实我的实际利率会提高!
举例说明:
为了每天拿到5
元, 我需要至少1501
元(此时的收入实际上是4.503
元, 被四舍五入为5
元), 实际利率是0.333%
为了每天拿到2
元, 我需要至少501
元(此时的收入实际上是1.503
元, 被四舍五入为2
元), 实际利率是0.399%
为了每天拿到1
元, 我需要至少167
元(此时的收入实际上是0.503
元, 被四舍五入为1
元), 实际利率是0.599%
所以, 一张只存了167
元的储蓄卡能拿到接近于0.6%
的利率, 高于信用卡的0.5%
, 也就是说, 我们赚了.
简单算一下, 每借出2099
都可以分发到至少12
张167
的储蓄卡, 也就是每13
张卡片就可以获得2
元每天的收入.但这离我们达成两千元的目标还是有点远了, 所以这势必是要写脚本了.
于是看一看相关的网页API, 开始着手写脚本
pay_id = [] #存储需要还款的卡号
get_id = [] #存储可以拿利息的卡号
order = 1 #卡片的 id
bank = Bank()
bank.reset()
for _ in range(20): #打算来 20 组卡片
bank.create('credit') #先创建用于分发的信用卡
order += 1 #卡片 id 自增
pay_id.append(order) #信用卡是需要还款的
bank.transfer(order, 1, 2099) #从信用卡转给主账户 2099
for i in range(12):
bank.create('debit') #创建 12 张储蓄卡
order += 1 #卡片 id 自增
bank.transfer(1, order, 167) #从主账户转给每张卡 167
if i < 10:
get_id.append(order) #每次还款 10 张卡就够 10 元了
print('card done.')
days = 0 #记录天数并输出
while True:
flag = bank.user()['flag'] #尝试获取 flag
if flag != None:
print(flag) #有 flag 则过关了
bank.eat(1) #用主卡吃饭
num = 0
for pay in pay_id:
for _ in range(10):
bank.transfer(get_id[num], pay, 1) #用储蓄卡给对应信用卡还 10 元
num += 1
print('day ' + str(days)) #输出下信息让我知道程序还活着
days += 1
output:
card done.
day 0
......
day 25
flag{W0W.So.R1ch.Much.Smart.52f2d579}
所以我们就拿到了flag
!!!
flag{W0W.So.R1ch.Much.Smart.52f2d579}
>>> 0x00 END