河南省第七届金盾信安杯部分wp(原赛+重赛)

本文最后更新于 2026年3月29日 晚上

前言

嗯….

这一篇wp本来应该及时发出的,但那时还没有blog😓,然后因为一些众所周知的原因,导致这次比赛在我心中的地位骤降,故没有搭理;后来想想还是补上吧,怎么也是我参加的第一个省B类竞赛,遂借着期末周忙里偷闲做一下。

不过还是忍不住想吐槽一句:

“致敬传奇比赛金盾杯”

观前声明:博主能力有限,只做了misc部分。

原赛

llmlog

下方可以下载题目附件哦⬇️

📥 题目附件下载
文件名:llmlog.zip
大小:51 KB
点我下载

很简单又很恶心的一道题。说简单是因为这题就是按图索骥,答案连个字都不用改就明明白白写在那里;说难是因为干扰项太多,比较考验阅读理解能力。由于结果需要转换成MD5,稍有不慎,便谬之千里。

话不多说,上截图!

如图所示,真的就是阅读理解

于是我们带着五个问题开始审查llm日志,分别在如下位置找到答案:

攻击者第一次冒充系统用户询问的时间
攻击者冒充后台管理用户询问的问题
攻击者得知完整手机号的时间
攻击者使用“特殊身份”询问得到回答的时间
邮箱被查询到的次数,复制到VS Code中ctrl+F查询或者直接在记事本里手动计数,反正也不多

需要注意的小细节挺多,比如“系统用户”真的就是系统用户,不是“system”;攻击者冒充后台管理用户询问的问题一定要加“我是后台管理用户”这半句(当时就因为这个卡了好久);遍历日志内容可以发现,攻击者先后获取了手机号的前七位和后四位,所以获取后四位时就已经得知完整密码了。而不是等到后来使用“金盾杯后台管理用户”这一“特殊身份”询问后才获知;输入法问题,等等。这道misc考验了审查大量数据的能力,也考验了我不断试错的耐心。

拿到五个问题的答案后就可以去赛博厨子那里获取flag啦🎉

赞美伟大的赛博厨子!

flag{1fcfbcd14f58c6b7add09ab13258ef14}

乱七八糟的意味

下方可以下载题目附件哦⬇️

📥 题目附件下载
文件名:附件.zip
大小:10726 KB
点我下载

看到题目名称就有预感,出题人多少肯定带点..不过个人很喜欢这道题,比赛期间给了我很大的满足感。

拿到附件先解压,直接开幕雷击:

何意味?

字都没显示完整,朋友你疑似有点过于明显了😓

扔进010看一眼果然有CRC校验错误

正合我意

当时被llmlog磨灭了耐心,于是随波逐流一下直接生成修复宽高后的图片

无异味--随波逐流

下面露出来这一行,老实说我从来没见过,但第一眼就知道肯定是某种加密,第二眼发现好像都是wasd这四个字母,电脑玩家的DNA瞬间动了——这东西怕不是真和上下左右有关。

于是随手找来纸笔,随便取一个点为起点,按照w上s下a左d右,在纸上画线。

画出第一个数字的时候我就明白怎么回事了。就像这样👇

当时随手画的,不好看但能证明解题过程百分百纯原创

于是得到压缩包密码为972561,成功解压。

解压完成后得到两个文件:一个不知道是啥的高雅人士请打开,先扔一边;一张png图片,叫孩子们还认得出我吗

化成灰都认识

简单看一下结构,没啥问题。用Stegsolve进行位平面切片分析,在图像 RGB 通道的 LSB中发现了提示信息

红绿蓝三个通道都勾选0即可出现信息

这句英文(arnoldarnoldarnold,all parameters are set to 1)中,arnold提示我们是阿诺德猫脸变换,重复三次的意思就是要进行三次变换。很明显,这张图是被事先处理过的,只要按照提示给图片还原过来应该就能看到信息。在阿诺德变换的算法中,涉及到a和b两个参数。提示中说all parameters are set to 1,意思是所有参数都设置为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
import numpy as np
from PIL import Image
import os

def recover_arnold(image_path, iterations=3):
# 1. 加载图像
if not os.path.exists(image_path):
print(f"找不到文件: {image_path}")
return

img = Image.open(image_path).convert('RGB')
img_array = np.array(img)

# 获取图像尺寸 (假设是正方形 N x N)
height, width, channels = img_array.shape
if height != width:
print(f"警告:图像不是正方形 ({width}x{height}),变换可能不符合标准阿诺德映射。")

N = height
current_array = img_array.copy()

# 创建保存结果的目录
output_dir = os.path.join(os.path.dirname(image_path), "recovery_results")
if not os.path.exists(output_dir):
os.makedirs(output_dir)

print(f"开始尝试逆变换,结果将保存在: {output_dir}")

# 2. 迭代逆变换
# 逆矩阵为 [[2, -1], [-1, 1]]
for i in range(1, iterations + 1):
new_array = np.zeros_like(current_array)
for x in range(N):
for y in range(N):
# Arnold 逆变换公式
nx = (2 * x - 1 * y) % N
ny = (-1 * x + 1 * y) % N
new_array[nx, ny] = current_array[x, y]

current_array = new_array.copy()

# 保存每一轮的结果
res_img = Image.fromarray(current_array)
save_path = os.path.join(output_dir, f"recovered_step_{i}.png")
res_img.save(save_path)
print(f"第 {i} 次逆变换结果已保存。")

if __name__ == "__main__":
file_path = r"你的图片的实际路径"
# 默认尝试 3 次(对应提示词 arnold 出现了 3 次)
recover_arnold(file_path, iterations=3)

有个小细节要注意,我们要对图片做逆变换来还原,因为原图实际上是被正向变换处理过的。不知道正逆也没关系,那就都试试,看哪个还原对了就是那个[doge],顺带一提,由于该变换具有周期性,正向一定次数也可以正确还原哦❤️

有关阿诺德变换的具体原理,感兴趣的话可以自行搜索学习,这里就不作解释啦~

运行脚本后成功得到了清晰的图片,仔细观察可以发现图中的密码👁️

注意大小写

密码为VCp@ssw0rd114514!@# 这个密码有什么用呢?唉🤓,注意到文件夹名称为do you know VeraCrypt,这不就是提示吗。

于是我们安装一个VeraCrypt,了解到它是一款开源磁盘加密软件,这下就好办了。刚刚的高雅人士请打开显然就是用这东西加密的。

打开VeraCrypt,点击选择文件,找到高雅人士请打开,打开,然后随便选个盘符,点击加载,就会弹出输入密码的弹窗。输入VCp@ssw0rd114514!@#并点击确定,之后你就能在此电脑中找到你刚才选的盘了。

VeraCrypt似乎无法被截屏,所以就直接拍屏了

凑合着看吧喵喵喵

打开这个盘更是两眼一黑

还有暗广

检查一番发现只有67号图片的大小是106KB,比其他的大了1KB,所以我们着重分析这张图片。放进010一看,原来末尾附加了一张png。

找到你啦

用binwalk或者foremost分离出这张png

你的身份是...

这不是残缺的二维码,而是残缺的Data Matrix码。由于左侧L形部分完好,随便找个在线网站就能扫出来

拿下!

flag{Y0u_@r3_gOOOOOOd_4t_m15c}

重赛

签到

如图所示

没啥说的,看见Zmxh一眼base64,赛博厨子直接带走

flag{fb243025-d204-4eda-b3dc-50fefa1089fd}

英勇投弹手

下方可以下载题目附件哦⬇️

📥 题目附件下载
文件名:英勇投弹手.zip
大小:14 KB
点我下载

所有文件全加密,密码半天没找出来,以为漏了提示,结果就是直接爆破😓

那么我将拿出对密码爆破神器-ARCHPR 4.54

加载附件里的压缩包后直接弹出提示,要用担保WinZip恢复,5分钟左右就能爆出密码

想出这密码的家里请谁都没用了

成功解锁后我们挨个检查这些文件,发现它们似乎是一个网页小游戏飞机大战的组成部分。png图片很干净,css正常,html网页除了开始不了游戏以外也没啥问题(不影响做题)。着重检查js源码,感觉这个core.js相当奇怪。继续审查,作为游戏框架的game.js引入了player.js,bullet.js等其他几个js源码,唯独没提到core.js。这就说明core.js有问题。

通过这一段可知core.js不属于游戏本身

观察core.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
(function (_0x308471, _0x5643fe) {
const _0x5a606a = _0x2460, _0x33a4a5 = _0x308471();
while (!![]) {
try {
const _0x2afb4f = parseInt(_0x5a606a(0x1b3)) / (-0x19c * -0xd + -0xe77 + -0x674) * (-parseInt(_0x5a606a(0x1b7)) / (0xb5 * 0x5 + 0x133f + -0x2 * 0xb63)) + -parseInt(_0x5a606a(0x1ba)) / (-0x2644 + -0x68b + 0x2cd2) + parseInt(_0x5a606a(0x1bf)) / (0x66 + 0x46d + -0x4cf) + parseInt(_0x5a606a(0x1b5)) / (-0x32e + 0x2683 + -0x2350) + parseInt(_0x5a606a(0x1b4)) / (0x513 * 0x1 + 0x1cce + -0x1b * 0x141) + parseInt(_0x5a606a(0x1bb)) / (0x1710 + 0x638 + -0x1 * 0x1d41) + -parseInt(_0x5a606a(0x1bd)) / (0x19 * 0x105 + -0xe * 0x193 + -0x36b);
if (_0x2afb4f === _0x5643fe)
break;
else
_0x33a4a5['push'](_0x33a4a5['shift']());
} catch (_0x5220d0) {
_0x33a4a5['push'](_0x33a4a5['shift']());
}
}
}(_0x2f3d, -0x9dc5 + 0xf * 0x18fc + 0x49c84));
let k = 0x166c + -0x7 * -0x4cf + -0x1 * 0x357b;
function _0x2460(_0x111017, _0x5efafa) {
const _0x2f1f50 = _0x2f3d();
return _0x2460 = function (_0x2f7d39, _0x466e46) {
_0x2f7d39 = _0x2f7d39 - (0x256e + -0x1a4d + -0x96e * 0x1);
let _0x2b3801 = _0x2f1f50[_0x2f7d39];
return _0x2b3801;
}, _0x2460(_0x111017, _0x5efafa);
}
function core() {
const _0x47b80f = _0x2460, _0x3074a9 = {
'gFKKr': function (_0xe55803, _0xa329bf) {
return _0xe55803 === _0xa329bf;
},
'HPqhV': _0x47b80f(0x1b6) + _0x47b80f(0x1b8) + _0x47b80f(0x1be) + 'a3'
};
if (_0x3074a9[_0x47b80f(0x1b9)](k, -0x295 * 0xf + -0x1 * 0x134b + -0x1 * -0x41ef))
return _0x3074a9[_0x47b80f(0x1bc)];
}
function _0x2f3d() {
const _0x239d05 = [
'3088265aLnAAu',
'873bf00c57',
'25372BhvWzP',
'7c3f7bbf99',
'gFKKr',
'821466gZMlek',
'2569812BGsTZj',
'HPqhV',
'4040648FCWnre',
'76849b468b',
'1006744yccwII',
'25COywAc',
'1304166xohEjZ'
];
_0x2f3d = function () {
return _0x239d05;
};
return _0x2f3d();
}

这是一个Obfuscator.io 混淆脚本,目的就是让代码变得极度难以阅读,但功能不变。前面那堆看不懂的东西可以不管,真正有用的部分从这里开始:

1
let k = 0x166c + -0x7 * -0x4cf + -0x1 * 0x357b;

这里定义了一个变量k,计算它的初始值为 5740 + 8617 - 13691 = 666。

1
2
3
4
5
6
7
8
9
10
11
function core() {
const _0x47b80f = _0x2460, _0x3074a9 = {
'gFKKr': function (_0xe55803, _0xa329bf) {
return _0xe55803 === _0xa329bf;
},
'HPqhV': _0x47b80f(0x1b6) + _0x47b80f(0x1b8) + _0x47b80f(0x1be) + 'a3'
};
// 这里判断 k 是否等于某个值
if (_0x3074a9[_0x47b80f(0x1b9)](k, -0x295 * 0xf + -0x1 * 0x134b + -0x1 * -0x41ef))
return _0x3074a9[_0x47b80f(0x1bc)];
}

这里的gfKKr是用来执行===(严格相等运算符)比较的,目的是确保k的绝对精确。

HPqhV则是由三个字符串加上a3拼接而成的,这很可能就是flag。

大致意思就是,这个core函数会检查变量 k 是否等于特定的数值,如果匹配,则返回一串拼接好的加密字符串或 Key。

计算一下这个值:-9915 - 4939 + 16879 = 2025,我们需要让k的值达到2025才能触发这个函数,这样网页会返回HPqhV的值。但分析所有的js代码可知,我们在飞机大战的游戏中没有能够改变k值的用户行为。难道没有办法了吗?

其实不然。既然游戏内部毫无办法,我们可以在游戏外进行降维打击。

打开html网页,按F12打开控制台,直接输入以下内容:

1
2
k = 2025; // 强制让 k 达到触发条件
console.log(core()); // 打印结果

直接就吐出了flag。没错,就这么简单。

用点web知识

flag{873bf00c577c3f7bbf9976849b468ba3}

data

下方可以下载题目附件哦⬇️

📥 题目附件下载
文件名:data.zip
大小:717 KB
点我下载

解压题目附件发现一个没有扩展名的data,不知道是什么,先扔进010看一眼

验明真身

文件头是51 46 49 FB,加上dirty bit、corrupt bit等可读文本,可以确定这是一个 QCOW2 (QEMU Copy-On-Write) 磁盘镜像文件。

这时候就要用到R-STUDIO,一款功能极其强大的数据恢复和磁盘策略工具。注意是R-STUDIO,不是RStudio,这是两个东西!

为了防止你踩坑,这里提供正确的下载方式⬇️

点我打开下载界面

然后你会发现正版下载要50刀…富哥富姐们可以直接购买,像我一样喜欢白嫖的那就仁者见仁智者见智啦,这里不多赘述哦

双击启动R-STUDIO,点击打开镜像,在弹出的对话框中,定位到data的存储位置。记得右下角筛选要选全部文件(.),不然不显示。点击打开后,就可以看到左侧设备/磁盘一栏多出了镜像文件。选中所有镜像文件点击扫描,出现了Recognized 分区和原始文件分区。在Recognized 分区中找到了7-zip归档,对对应的原始文件进行恢复。

对镜像文件扫描后得到了两个分区

恢复7z文件

两个7z文件内容是一样的,选一个恢复就行,记得指定恢复路径。恢复后解压7z,发现all.zip,里面有64个png。简单查看发现似乎是64个二维码碎片,我们需要用脚本将其拼接。但仔细观察,二维码的定位角出现了12次,说明实际上是4个二维码,每个被切成了16份。一般来说,这种碎片都是脚本处理后的结果,碎片的产生具有时间顺序。所以我们优先按照时间顺序恢复,如果时间相同(相差极短),就按照文件名排序,从左到右,从上到下依次恢复成4张4×4的图片。

脚本如下:

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
import os
from PIL import Image

def solve_quad_puzzles():
# 设为当前目录
source_dir = '.'

# 1. 识别所有图片碎片(排除掉脚本本身和可能生成的结果图)
valid_extensions = ('.png', '.jpg', '.jpeg', '.bmp')
files = []
for f in os.listdir(source_dir):
if f.lower().endswith(valid_extensions) and not f.startswith('result_'):
path = os.path.join(source_dir, f)
# 获取高精度修改时间
mtime = os.path.getmtime(path)
files.append({
'name': f,
'path': path,
'mtime': mtime
})

# 2. 排序:时间为主,名称为辅
files.sort(key=lambda x: (x['mtime'], x['name']))

if len(files) != 64:
print(f"警告:检测到 {len(files)} 个碎片。当前逻辑按 16 碎片/图处理。")

# 3. 准备参数
rows, cols = 4, 4 # 每张二维码是 4x4 拼接
img_per_qr = rows * cols

# 获取碎片尺寸
sample_img = Image.open(files[0]['path'])
w, h = sample_img.size

# 4. 分组拼接(每 16 个碎片一张图)
for i in range(0, len(files), img_per_qr):
qr_index = i // img_per_qr + 1
batch = files[i : i + img_per_qr]

# 创建 4x4 的画布
canvas = Image.new('RGB', (w * cols, h * rows))

for index, file_info in enumerate(batch):
img = Image.open(file_info['path'])
r = index // cols
c = index % cols
canvas.paste(img, (c * w, r * h))

output_name = f'result_{qr_index}.png'
canvas.save(output_name)
print(f"已生成第 {qr_index} 张二维码: {output_name} (包含 {len(batch)} 个碎片)")

if __name__ == "__main__":
solve_quad_puzzles()

扔进all文件夹里运行一下得到四张图片,发现只有第四张二维码是完整的。

result_4.png是我们需要的

扫描得到以下内容:

1
K5LGIS2TGBIXSVKEJJKVCVJRJBKGW4CZKNDFUSKUIVTTAUSWIJFFC2SWKZLFKWTBK5LGIWSVGFUFKVTKJF4VO3DIIZLFMULZKIYGG6KRKVCXSVLKLJDFKVSGIRLDCVSKKZDHAUCTKZSEUV2GJZMU4222IJLEMZCTKMYFUS2XKVDFOVCVOBFFE2S2JBKWYVSTKBKDAOKQKQYDS===

接下来就是赛博厨子魅力时刻。经过不断尝试,解密顺序为base32–> base64 –> rot13 –> base32 –> base64

DDDDDecode

flag{4a154507-ba7d-3f79-8739-91533b2bafc7}

未解之谜

题目名称:流量包中的秘密(这下真成秘密了)

附件放在下面了,哪位misc大手子看到了有兴趣可以解一下👇

📥 题目附件下载
文件名:flag.zip
大小:20566 KB
点我下载


河南省第七届金盾信安杯部分wp(原赛+重赛)
https://koyanrush.github.io/2026/01/11/Jindun2025/
作者
Koyanrush
发布于
2026年1月11日
许可协议