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(借用Coxxsgif示意)

你需要用到

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))

但是输出却只有fF, 于是开始大开脑洞, 万一输出的是某两个字符呢?

for i in range(0x10000):
    if chr(i).upper() == 'FL' or chr(i).upper() == 'LA' or chr(i).upper() == 'AG':
        print(chr(i))

然后, 它果然出现了, 这个神奇的字符:

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 FlU+FB02fl\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都可以分发到至少12167的储蓄卡, 也就是每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