这次比赛是跟我们a1natas打的,最后也是取得了16名的好成绩,也是第一次在xctf这种大型比赛中拿到一血🩸🥰,也多亏了队友们,成功ak了misc。

但还是不得不说,这个misc的**Why not read it out?**真恶心吧

量子双生影

还是蛮简单的,考了一个ntfs,image combiner,二维码。

先用7z打开rar,会发现另一张图片,解压一下,得到总共两张图片

两张图片stegsolve,image combiner一下,得到这个

1752482169114

用支付宝扫一下就出了(扫不出的话调整下视角)

PaperBack

网上搜索一下就可以发现是利用程序将电脑内容打印到纸上的,我们现在有了这个纸上的内容,需要返回到电脑的内容

在github里搜关键词paperbake可以搜到https://github.com/timwaters/paperback,然后找到官网https://www.ollydbg.de/Paperbak/

下载后将图片拖进去就可以得到一个flag.ws

打开发现是空格tab隐写

1752482402311

手敲一下忽略掉最前面的00000得到

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
01100
0110011
1001000
1000011
1010100
1000110
1111011
1110111
1100101
1101100
1100011
1101111
1101101
1100101
1011111
1110100
1101111
1011111
1101100
0110011
1101000
1100011
1110100
1100110
0110010
0110000
0110010
0110101
1111101
00

转换ascii得到flag:L3HCTF{welcome_to_l3hctf2025}

LearnRag

RAG泄露,附件给了嵌入向量

1752482652777

原本想相似性查询去爆的,但是都是些无意义的字符

转变思路为向量逆向,用工具vec2text

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


def solve_with_vec2text():
"""使用vec2text库逆向RAG embedding"""
print("🚀 使用vec2text库解决RAG embedding逆向问题")
print("="*60)

# 步骤1: 安装vec2text库
print("📦 步骤1: 确保vec2text库已安装")
try:
import vec2text
print("✅ vec2text库已安装")
except ImportError:
print("❌ vec2text库未安装,请运行: pip install vec2text")
print("🔧 安装命令:")
print(" pip install vec2text")
return

# 步骤2: 加载RAG数据(解决pickle问题)
print("\n📂 步骤2: 加载RAG数据")

# 创建虚拟RagData类
class RagData:
def __init__(self):
pass

# 注册到全局命名空间
import __main__
__main__.RagData = RagData
globals()['RagData'] = RagData

try:
with open('rag_data.pkl', 'rb') as f:
rag_data = pickle.load(f)

print("✅ RAG数据加载成功")

if hasattr(rag_data, 'embedding_model'):
print(f" 模型: {rag_data.embedding_model}")

if hasattr(rag_data, 'embeddings'):
embeddings = np.array(rag_data.embeddings)
print(f" 向量数量: {len(embeddings)}")
print(
f" 向量维度: {embeddings.shape[1] if len(embeddings.shape) > 1 else '?'}")
else:
print("❌ 未找到embeddings数据")
return

except Exception as e:
print(f"❌ 加载RAG数据失败: {e}")
return

# 步骤3: 加载GTR corrector模型
print("\n🤖 步骤3: 加载vec2text的GTR corrector模型")

try:
# 根据vec2text文档,加载预训练的GTR corrector
corrector = vec2text.load_pretrained_corrector("gtr-base")
print("✅ GTR corrector模型加载成功")
except Exception as e:
print(f"❌ 加载corrector失败: {e}")
print("🔧 尝试其他加载方式...")

try:
# 尝试手动加载模型
inversion_model = vec2text.models.InversionModel.from_pretrained(
"jxm/gtr__nq__32")
corrector_model = vec2text.models.CorrectorEncoderModel.from_pretrained(
"jxm/gtr__nq__32__correct")
corrector = vec2text.load_corrector(
inversion_model, corrector_model)
print("✅ 手动加载corrector成功")
except Exception as e2:
print(f"❌ 手动加载也失败: {e2}")
print("📋 可能需要先下载模型或使用其他方法")
return

# 步骤4: 转换embedding格式
print("\n🔄 步骤4: 转换embedding格式")

# 将numpy数组转换为torch tensor
embeddings_tensor = torch.tensor(embeddings, dtype=torch.float32)

# 如果有GPU,移动到GPU
if torch.cuda.is_available():
embeddings_tensor = embeddings_tensor.cuda()
print("✅ 使用GPU进行逆向")
else:
print("⚠️ 使用CPU进行逆向(可能较慢)")

print(f" Tensor形状: {embeddings_tensor.shape}")

# 步骤5: 执行逆向重建
print("\n🎯 步骤5: 执行embedding逆向重建")

reconstructed_texts = []

print("🔍 开始逐个重建文本...")

for i, embedding in enumerate(embeddings_tensor):
try:
print(f" 处理向量 #{i+1}/{len(embeddings_tensor)}")

# 为单个embedding添加batch维度
single_embedding = embedding.unsqueeze(0)

# 使用vec2text进行逆向重建
# 使用更多步数和更大的beam width获得更好结果
reconstructed = vec2text.invert_embeddings(
embeddings=single_embedding,
corrector=corrector,
num_steps=20, # 增加步数提高质量
sequence_beam_width=4 # 使用beam search
)

reconstructed_text = reconstructed[0] if reconstructed else ""
reconstructed_texts.append(reconstructed_text)

print(f" 重建文本: '{reconstructed_text}'")

# 检查是否包含flag
if "L3HCTF" in reconstructed_text or "flag" in reconstructed_text.lower():
print(f" 🎉 发现疑似flag: {reconstructed_text}")

except Exception as e:
print(f" ❌ 向量#{i+1}逆向失败: {e}")
reconstructed_texts.append("")

# 步骤6: 分析结果
print(f"\n📊 步骤6: 分析重建结果")

print("🔍 所有重建的文本:")
for i, text in enumerate(reconstructed_texts):
if text.strip():
print(f" 向量#{i+1}: {text}")

# 检查各种flag模式
text_lower = text.lower()
if any(keyword in text_lower for keyword in ['l3hctf', 'flag', 'ctf']):
print(f" 🎯 可能的flag内容!")

# 寻找最可能的flag
potential_flags = []
for text in reconstructed_texts:
if text and ("L3HCTF{" in text or "flag{" in text or "ctf{" in text):
potential_flags.append(text)

if potential_flags:
print(f"\n🏆 发现潜在flag:")
for flag in potential_flags:
print(f" 🎉 {flag}")
else:
print(f"\n🤔 未发现明显的flag格式,但重建的文本可能包含线索")
print(f"📋 重建文本汇总:")
for i, text in enumerate(reconstructed_texts):
if text.strip():
print(f" {i+1}. {text}")

return reconstructed_texts


def alternative_vec2text_approach():
"""备用的vec2text方法"""
print("\n🔄 备用方法: 简化的vec2text逆向")
print("="*50)

try:
import vec2text

# 如果主要方法失败,尝试更简单的方法
print("🔧 尝试简化的逆向方法...")

# 加载数据
class RagData:
pass

import __main__
__main__.RagData = RagData

with open('rag_data.pkl', 'rb') as f:
rag_data = pickle.load(f)

embeddings = torch.tensor(rag_data.embeddings, dtype=torch.float32)

# 尝试不同的corrector
try:
corrector = vec2text.load_pretrained_corrector(
"text-embedding-ada-002")
print("✅ 使用OpenAI embedding corrector")
except:
print("❌ 无法加载预训练模型")
return

# 简单逆向,每个向量单独处理
for i, emb in enumerate(embeddings):
try:
result = vec2text.invert_embeddings(
embeddings=emb.unsqueeze(0),
corrector=corrector,
num_steps=5
)
print(f"向量#{i+1}: {result[0] if result else 'N/A'}")
except Exception as e:
print(f"向量#{i+1}失败: {e}")

except Exception as e:
print(f"备用方法也失败: {e}")


if __name__ == "__main__":
# 先尝试主要方法
texts = solve_with_vec2text()

# 如果主要方法失败,尝试备用方法
if not texts or not any(texts):
alternative_vec2text_approach()
1752482709557

Please Sign In

通过代码可知系统使用ShuffleNet V2模型提取人脸特征向量,通过比较用户上传图像与预存特征向量的差异(MSE < 5e-6)进行身份验证。附件中已有特征向量文件embedding.json,而模型特征向量可被逆向工程生成匹配图像。有两个方向,一个是用特征反演生成图像;一个是上传特征向量注入的假图等伪造手法。

但是我目前的伪造手法都无法通过系统的要求,可能有大佬可以。以下是我的特征反演代码:

(精简版)注意最后要使用png格式保存,因为jpg是有损压缩。

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
import torch
import torch.optim as optim
from torchvision import transforms
from torchvision.models import shufflenet_v2_x1_0
from PIL import Image
import json

# 1. 加载模型和目标特征向量
model = shufflenet_v2_x1_0(pretrained=True)
model.fc = torch.nn.Identity() # 只提取特征
model.eval()

with open("embedding.json", "r") as f:
target_embedding = torch.tensor(json.load(f)).unsqueeze(0)

# 2. 初始化可优化图像
input_img = torch.nn.Parameter(
torch.rand(1, 3, 224, 224) * 0.5 + 0.25, # 中灰色范围初始化
requires_grad=True
)

# 3. 设置优化器
optimizer = optim.Adam([input_img], lr=0.05)

# 4. 优化循环
for i in range(1500):
optimizer.zero_grad()

# 生成特征向量
generated_embedding = model(input_img)

# 计算损失(均方误差)
loss = torch.mean((target_embedding - generated_embedding) ** 2)

# 反向传播
loss.backward()
optimizer.step()

# 约束像素值在[0,1]范围
input_img.data.clamp_(0, 1)

# 每100次迭代打印进度
if i % 100 == 0:
print(f"Iteration {i}, Loss: {loss.item():.8f}")

# 提前终止条件
if loss.item() < 1e-6:
print(f"Success at iteration {i}!")
break

# 5. 保存生成的图像(关键:使用PNG无损格式)
output_img = transforms.ToPILImage()(input_img.squeeze(0).detach())
output_img.save("solution.png", format="PNG")
print("Successfully generated solution.png")

最后cmd上传图像:(curl -X POST http://1.95.34.119:50001/signin/ -F “file=@solution.png”)

1752482774430

Why not read it out?

史题当然是要压轴出场了。出的很好下次别出了

010打开文件发现是jpg,末尾有一串倒转过来的base64,改后缀得到一张谜语文字。

1752482863733

看到

1752482872620

猜测是游戏里的文字,根据题目里给出的“小狐狸”进行搜索,找到游戏TUNIC

1752482939843

然后根据一堆解说发现,这纸上面的文字不太一样,猜测是魔改过的。在IGN review里搜索tunic查看评论,发现最开始两段的内容和纸上前两段对应,于是开始逐个翻译。

第一条是flag内容,后面4条是规则

最后翻译如下:

1.the content of flag is come on little brave fox

2.o→0,L→1

3.A→@

4.e大写

5.下划线连接每个词

1752482992713

还有

1752482994549

最后得到flag:L3HCTF{c0mE_0n_1itt1E_br@vE_f0x}

这真恶心吧,,这个文字是音标+重叠+游戏魔改,得自己建立符号对应的表,还得音标转字母。。

彩蛋(其实是乱七八糟的笔记..):

232130d1b6bf795c0492dc46cd7bdba0

还有

7f9eda09102a546ba89ca5fe7be9e45d

额为了这道题甚至动上了笔。。。