第九届“强网杯”行业领域专项赛车联网安全赛-初赛(CTF)WP

第九届“强网杯”行业领域专项赛车联网安全赛-初赛(CTF)WP

第九届“强网杯”行业领域专项赛车联网安全赛是由中央网信办、河南省人民政府指导,河南省委网信办、河南省教育厅、郑州市人民政府、信息工程大学、郑州大学、中国网络空间安全协会等单位联合主办的车联网安全专项赛事。车联网安全赛道线上CTF初赛旨在通过CTF解题,全面检验选手在智能网联汽车安全领域的实践能力。

强网-stop

通过分析给出的代码,可以得到攻击本质是降低 stop sign 的 logits,那我们做防御策略就是增加 stop sign 的 logits(偏置+权重),即使被攻击降低,增加后的 logits 仍然足够高

策略1:增加偏置(Bias)

在分类层的输出层,直接增加 stop sign 类别的偏置:

1
bias[stop_sign_id] += 2.5

原理:

  • 分类输出:output = conv(features) + bias
  • 增加偏置 → 直接增加 logits → 提高 softmax 后的概率

优点:

  • 简单直接,效果明显
  • 不改变特征提取过程,对 other 类别影响小

策略2:增加权重(Weight)

同时增加 stop sign 对应通道的权重:

1
weight[stop_sign_id, :, :, :] += 0.1 * weight[stop_sign_id, :, :, :]

原理:

  • 增强 stop sign 特征在分类时的响应强度
  • 使模型对 stop sign 相关特征更敏感

优点:

  • 与偏置结合,双重保障
  • 权重调整幅度小(10%),避免过度微调

微调策略:

  • 只修改分类输出层,不影响特征提取

  • 同时修改 3 个检测头,覆盖多尺度

  • 调整幅度适中,既防御攻击又尽可能保持对其他类别的识别

所用脚本如下:

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
import torch
from ultralytics import YOLO
import torch.nn as nn
import re

def modify_model_for_defense(model_path, output_path):
"""
微调模型以防御对抗样本攻击
策略:修改Detect层的分类卷积层,增加 stop sign 类别的偏置
"""
# 加载原始模型
model = YOLO(model_path)
yolo_model = model.model

# 找到 stop sign 的类别ID
stop_sign_id = None
for k, v in model.names.items():
if "stop" in v.lower():
stop_sign_id = k
break

if stop_sign_id is None:
print("Error: Could not find stop sign class")
return False

print(f"Stop sign class ID: {stop_sign_id}")
print(f"Total classes: {len(model.names)}")

# 找到Detect层
detect_layer = None
detect_layer_name = None
for name, module in yolo_model.named_modules():
if isinstance(module, nn.Module) and 'Detect' in type(module).__name__:
detect_layer = module
detect_layer_name = name
print(f"Found Detect layer: {name}, type: {type(module)}")
break

if detect_layer is None:
# 尝试直接访问最后一个模块
detect_layer = yolo_model.model[-1]
detect_layer_name = "model[-1]"
print(f"Using model[-1] as Detect layer, type: {type(detect_layer)}")

print(f"Detect layer attributes: {dir(detect_layer)}")
if hasattr(detect_layer, 'cv3'):
print(f"cv3 type: {type(detect_layer.cv3)}")
else:
print("cv3 not found in detect_layer")

modified = False

# 修改cv3(分类层)
if hasattr(detect_layer, 'cv3'):
cv3_modules = detect_layer.cv3
print(f"Found cv3: {type(cv3_modules)}, length: {len(cv3_modules)}")

# cv3是一个ModuleList,包含多个Sequential(对应多个检测头/尺度)
for i, seq in enumerate(cv3_modules):
if isinstance(seq, nn.Sequential):
# 找到最后一个Conv2d层(分类输出层)
last_conv = None
for j, layer in enumerate(seq):
if isinstance(layer, nn.Conv2d):
last_conv = layer
out_channels = layer.out_channels
print(f"cv3[{i}][{j}]: Conv2d, out_channels={out_channels}")

# 修改最后一个Conv2d层
if last_conv is not None:
out_channels = last_conv.out_channels

# 在YOLOv8中,cv3输出80个通道,对应80个类别
if out_channels == len(model.names):
print(f"Modifying classification layer cv3[{i}] (detection head {i})")
# 增加stop_sign对应通道的权重
weight = last_conv.weight.data

# 增加stop_sign类别的权重(使该类别更容易被激活)
weight[stop_sign_id, :, :, :] += 0.1 * weight[stop_sign_id, :, :, :]

# 增加偏置(直接增加logits)
if last_conv.bias is not None:
last_conv.bias.data[stop_sign_id] += 2.5
else:
# 如果没有bias,创建一个
bias = torch.zeros(out_channels, device=weight.device)
bias[stop_sign_id] = 2.5
last_conv.bias = nn.Parameter(bias)
last_conv.bias.requires_grad = False

modified = True
print(f"Modified cv3[{i}]: increased weight and bias for stop_sign (class {stop_sign_id})")

# 如果输出通道更多,可能是包含了box+obj+cls
elif out_channels >= len(model.names):
# 假设类别在最后nc个通道中
cls_start = out_channels - len(model.names)
stop_channel = cls_start + stop_sign_id

if stop_channel < out_channels:
print(f"Modifying classification layer cv3[{i}], channel {stop_channel} (class {stop_sign_id})")

weight = last_conv.weight.data
# 增加stop_sign通道的权重
weight[stop_channel, :, :, :] += 0.08 * weight[stop_channel, :, :, :]

# 增加偏置
if last_conv.bias is not None:
last_conv.bias.data[stop_channel] += 2.5
else:
bias = torch.zeros(out_channels, device=weight.device)
bias[stop_channel] = 2.5
last_conv.bias = nn.Parameter(bias)
last_conv.bias.requires_grad = False

modified = True

# 如果还没修改成功,尝试更通用的方法:查找所有输出80通道的Conv2d
if not modified:
print("Trying alternative approach: searching for classification Conv2d layers")
# 只修改每个检测头的最后一层(.2),避免过度微调
for name, module in yolo_model.named_modules():
if isinstance(module, nn.Conv2d):
out_channels = module.out_channels
# 查找cv3相关的分类层,输出80个通道,并且是最后一层(以.2结尾,而不是.2.conv)
# 匹配 pattern: model.22.cv3.X.2 (X是0,1,2,表示检测头编号)
if out_channels == len(model.names) and 'cv3' in name:
# 检查是否是最后一层:name应该以 .X.2 结尾(X是数字)
if re.search(r'\.\d+\.2$', name):
print(f"Found classification output layer: {name}, out_channels={out_channels}")

stop_channel = stop_sign_id
print(f"Modifying {name}, channel {stop_channel} (stop_sign)")

weight = module.weight.data
# 增加权重(幅度适中,既能防御攻击又不过度微调)
weight[stop_channel, :, :, :] += 0.1 * weight[stop_channel, :, :, :]

if module.bias is not None:
module.bias.data[stop_channel] += 2.5
else:
bias = torch.zeros(out_channels, device=weight.device)
bias[stop_channel] = 2.5
module.bias = nn.Parameter(bias)
module.bias.requires_grad = False

modified = True

if not modified:
print("Error: Could not find classification layer to modify")
return False

# 保存修改后的模型
# 使用torch.save保存模型权重,然后重新加载
try:
# 方法1: 直接保存YOLO模型
model.save(output_path)
print(f"Model saved to {output_path}")
return True
except Exception as e:
print(f"Error saving model: {e}")
# 方法2: 手动保存权重
try:
torch.save(yolo_model.state_dict(), output_path.replace('.pt', '_weights.pt'))
# 然后需要重新构建模型,但YOLO的保存格式特殊
print("Warning: Using alternative save method")
return False
except:
return False

if __name__ == "__main__":
# 微调模型
success = modify_model_for_defense("best.pt", "defended.pt")
if success:
print("Model fine-tuning completed successfully!")
print("Please upload 'defended.pt' to test.")
else:
print("Model fine-tuning failed!")

运行后将生成的pt文件上传后得到flag

强网-SCAAES

题干如下:

1
安全研究人员在进行某品牌汽车硬件安全测试时,发现其中某核心零部件内用于实现AES密码算法的芯片存在侧信道信息泄露。研究人员对该芯片执行密码运算过程中的功耗信息进行了采集,并确认可以利用该数据集进行侧信道攻击取得算法密钥,请尝试利用技术手段复现研究人员获取到的密钥信息,该密钥即为题解。

题目给了一个trs文件,我们可以用python的trsfile 库直接读取

大概思路如下:

  1. 泄露模型(Leakage Model):采用汉明重量(Hamming Weight, HW)。
    • 针对 AES 第 1 轮 SBox 的中间值建立选择函数:
      [ v_i(k) = S\big(p_i \oplus k\big) \quad\Rightarrow\quad L_i(k) = HW\big(v_i(k)\big) ]
      其中 (p_i) 为第 (i) 条轨迹的第 (b) 个明文字节,(k) 为猜测的密钥字节。
  2. 统计量:对每个时间采样点 t,计算 L(k) 与功耗 T[:, t] 的皮尔逊相关系数,取全时域的最大绝对相关值作为该 k 的评分:
    [ \rho(k) = \max_t \big|\mathrm{corr}(L(k), T[:, t])\big| ]
  3. 取最优猜测:对每个字节 b,取使得 (\rho(k)) 最大的 k* 作为该字节的估计。16 个字节拼接即为 128-bit 密钥。
  4. 大样本收敛:当轨迹数充足且对齐良好时,正确密钥对应的相关峰显著高于其他假设。

首先进行数据读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# pip install trsfile
import trsfile, numpy as np

with trsfile.open('AES128.trs', 'r') as trs:
n_traces = len(trs)
n_samples = trs.number_of_samples
traces = np.empty((n_traces, n_samples), dtype=np.float32)
pts = np.empty((n_traces, 16), dtype=np.uint8) # 明文(若文件中提供)

for i, tr in enumerate(trs):
traces[i] = tr.samples
# 许多数据集把明文放在 trace.data 或 title/metadata 中
# 下面视具体字段而定(示例:Inspector 的 'plaintext' 键)
pts[i] = bytes(tr.data['plaintext'])

然后进行侧信道攻击,下面给出精炼版 CPA 代码,默认使用 SBox 输出的 HW 泄露模型。

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
import numpy as np
from tqdm import trange

# AES S-Box
SBOX = np.array([
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
], dtype=np.uint8)

HW = np.array([bin(i).count("1") for i in range(256)], dtype=np.uint8)

def cpa_byte(traces: np.ndarray, pts: np.ndarray, byte_idx: int) -> int:
# 返回给定字节索引的密钥估计(0..255)
x = pts[:, byte_idx] # 明文字节 (N,)
N, S = traces.shape
# 标准化功耗矩阵(零均值,单位方差),便于快速相关计算
T = (traces - traces.mean(axis=0)) / traces.std(axis=0, ddof=1)
best_k, best_r = 0, -1.0

for k in range(256):
z = HW[SBOX[np.bitwise_xor(x, k)]] # (N,)
z = (z - z.mean()) / z.std(ddof=1) # 标准化
# 与所有时间点同时计算相关:等价于 (z^T T) / (N-1)
r = np.abs(np.dot(z, T) / (len(z) - 1)) # (S,)
rmax = r.max()
if rmax > best_r:
best_r, best_k = rmax, k
return best_k

def recover_key(traces: np.ndarray, pts: np.ndarray) -> bytes:
key_bytes = [cpa_byte(traces, pts, b) for b in range(16)]
return bytes(key_bytes)

注:上面将相关的所有时间点向量化一次算完,速度远快于逐点循环。若存在对齐抖动,可在 CPA 前做滑动窗口/降采样/高通滤波/动态时间规整等预处理。

在本题数据集上,使用上述流程复现得到的 16 字节密钥为:

1
CC AE EE D6 67 CD 6C 04 C7 AB CC C2 6D C7 93 F1

拼接为 32 位十六进制(大写):

1
CCAEEED667CD6C04C7ABCCC26DC793F1

按题目格式输出即:flag{CCAEEED667CD6C04C7ABCCC26DC793F1}

强网-cancanneedflag

题目提供了以下附件:

  • canlog.txt - CAN总线日志文件(100306行)
  • signal_map/signal_map.csv - 信号映射配置文件

signal_map.csv 结构:

该文件定义了信号到CAN消息的映射关系,包含以下字段:

  • signal: 信号名称(如speed, rpm, steer_angle, sig_1到sig_72等)
  • can_id: CAN消息ID(16进制)
  • byte: 数据在CAN消息中的字节位置
  • len: 数据长度(字节数)
  • type: 数据类型(uint8, uint16, int16等)

共有75个信号定义,其中sig_1到sig_72这72个信号很可能用于隐藏flag数据。

canlog.txt 格式:

CAN日志格式为:

1
(时间戳) can0 CAN_ID#数据(16进制)

示例:

1
2
(1759295819.807137) can0 10c#8e2ef9c16b60c369
(1759295819.807166) can0 10d#d27fbd12e35d2984

步骤1:读取signal_map.csv,建立信号映射

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

signal_map = {}
with open('signal_map/signal_map.csv', 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
signal = row['signal']
can_id = int(row['can_id'], 16) # 16进制转10进制
byte_pos = int(row['byte'])
length = int(row['len'])
sig_type = row['type']
signal_map[signal] = {
'can_id': can_id,
'byte_pos': byte_pos,
'length': length,
'type': sig_type
}

步骤2:解析canlog.txt,提取CAN消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re
from collections import defaultdict

can_data = defaultdict(list)

with open('canlog.txt', 'r', encoding='utf-8') as f:
for line in f:
match = re.match(r'\(([\d.]+)\)\s+can0\s+([0-9a-fA-F]+)#([0-9a-fA-F]+)', line)
if match:
timestamp = float(match.group(1))
can_id = int(match.group(2), 16)
data_hex = match.group(3)
data_bytes = bytes.fromhex(data_hex)
can_data[can_id].append((timestamp, data_bytes))

步骤3:按照signal_map提取每个信号的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
signals_data = defaultdict(list)

for signal, config in signal_map.items():
can_id = config['can_id']
byte_pos = config['byte_pos']
length = config['length']
sig_type = config['type']

if can_id in can_data:
for timestamp, data_bytes in can_data[can_id]:
if byte_pos + length <= len(data_bytes):
value_bytes = data_bytes[byte_pos:byte_pos+length]
if sig_type == 'uint8':
value = int.from_bytes(value_bytes, byteorder='little', signed=False)
elif sig_type == 'int16':
value = int.from_bytes(value_bytes, byteorder='little', signed=True)
elif sig_type == 'uint16':
value = int.from_bytes(value_bytes, byteorder='little', signed=False)
else:
value = int.from_bytes(value_bytes, byteorder='little', signed=False)

signals_data[signal].append((timestamp, value))

步骤4:按信号顺序提取最后一个值并转换为ASCII

1
2
3
4
5
6
7
8
9
10
11
sig_names_ordered = [f'sig_{i}' for i in range(1, 73)]
flag_chars = []

for sig_name in sig_names_ordered:
if sig_name in signals_data and len(signals_data[sig_name]) > 0:
# 取最后一个值
val = signals_data[sig_name][-1][1]
flag_chars.append(val)

# 转换为ASCII字符串
flag_str = ''.join([chr(v) if 32 <= v <= 126 else '.' for v in flag_chars])

步骤5:提取flag

1
2
3
4
5
6
7
import re

flag_pattern = r'flag\{[^}]+\}'
match = re.search(flag_pattern, flag_str, re.IGNORECASE)
if match:
flag = match.group()
print(f"Found flag: {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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import csv
import re
from collections import defaultdict

# 读取signal_map
signal_map = {}
with open('signal_map/signal_map.csv', 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
signal = row['signal']
can_id = int(row['can_id'], 16)
byte_pos = int(row['byte'])
length = int(row['len'])
sig_type = row['type']
signal_map[signal] = {
'can_id': can_id,
'byte_pos': byte_pos,
'length': length,
'type': sig_type
}

# 解析canlog.txt
can_data = defaultdict(list)

with open('canlog.txt', 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
match = re.match(r'\(([\d.]+)\)\s+can0\s+([0-9a-fA-F]+)#([0-9a-fA-F]+)', line)
if match:
timestamp = float(match.group(1))
can_id = int(match.group(2), 16)
data_hex = match.group(3)
data_bytes = bytes.fromhex(data_hex)
can_data[can_id].append((timestamp, data_bytes))

# 提取每个信号的数值
signals_data = defaultdict(list)

for signal, config in signal_map.items():
can_id = config['can_id']
byte_pos = config['byte_pos']
length = config['length']
sig_type = config['type']

if can_id in can_data:
for timestamp, data_bytes in can_data[can_id]:
if byte_pos + length <= len(data_bytes):
value_bytes = data_bytes[byte_pos:byte_pos+length]
if sig_type == 'uint8':
value = int.from_bytes(value_bytes, byteorder='little', signed=False)
elif sig_type == 'int16':
value = int.from_bytes(value_bytes, byteorder='little', signed=True)
elif sig_type == 'uint16':
value = int.from_bytes(value_bytes, byteorder='little', signed=False)
else:
value = int.from_bytes(value_bytes, byteorder='little', signed=False)

signals_data[signal].append((timestamp, value))

# 排序
for signal in signals_data:
signals_data[signal].sort(key=lambda x: x[0])

# 按信号顺序提取最后一个值
sig_names_ordered = [f'sig_{i}' for i in range(1, 73)]
flag_chars = []

for sig_name in sig_names_ordered:
if sig_name in signals_data and len(signals_data[sig_name]) > 0:
val = signals_data[sig_name][-1][1]
flag_chars.append(val)

# 转换为ASCII字符串
flag_str = ''.join([chr(v) if 32 <= v <= 126 else '.' for v in flag_chars])

print(f"Extracted string: {flag_str}")

# 查找flag模式
flag_pattern = r'flag\{[^}]+\}'
match = re.search(flag_pattern, flag_str, re.IGNORECASE)
if match:
flag = match.group()
print(f"\nFound flag: {flag}")
else:
# 手动查找flag开始和结束位置
flag_start = flag_str.lower().find('flag{')
if flag_start != -1:
flag_end = flag_str.find('}', flag_start)
if flag_end != -1:
flag = flag_str[flag_start:flag_end+1]
print(f"Found flag: {flag}")

成功拿到flag:flag{V3h1cle_N3tw0rk1ng_53cu71ty}

强网-applet

解压文件发现是一个小程序的包,写一个代码来解析一下这个小程序

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
#!/usr/bin/env python3
import struct
import os
def unpack_wxapkg(wxapkg_file, output_dir):
"""解包微信小程序 wxapkg 文件"""
with open(wxapkg_file, 'rb') as f:
# 读取头部信息
first_mark = struct.unpack('B', f.read(1))[0]
f.read(4) # 跳过Info1
f.read(4) # 跳过Info2
# 读取数据区偏移量 (大端序,用'>I'表示)
data_section_offset = struct.unpack('>I', f.read(4))[0]
f.read(1) # 跳过保留字节
# 读取文件数量
file_count = struct.unpack('>I', f.read(4))[0]
# 读取文件列表
file_list = []
for i in range(file_count):
# 文件名长度
name_len = struct.unpack('>I', f.read(4))[0]
# 文件名 (UTF-8编码)
name = f.read(name_len).decode('utf-8')
# 文件偏移和大小
offset = struct.unpack('>I', f.read(4))[0]
size = struct.unpack('>I', f.read(4))[0]
file_list.append({
'name': name,
'offset': offset,
'size': size
})
# 创建输出目录
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# 解包每个文件
for file_info in file_list:
name = file_info['name'].lstrip('/')
file_path = os.path.join(output_dir, name)
file_dir = os.path.dirname(file_path)
# 创建文件所在目录
if file_dir and not os.path.exists(file_dir):
os.makedirs(file_dir)
# 读取并写入文件数据
f.seek(file_info['offset'])
file_data = f.read(file_info['size'])
with open(file_path, 'wb') as out_f:
out_f.write(file_data)
print(f"Extracted: {file_info['name']}")
x=r"F:\Desktop\比赛练习题目\Reverse\强网杯车联网\题目\__APP__.wxapkg"
y=r"F:\Desktop\比赛练习题目\Reverse\强网杯车联网\题目\out"
unpack_wxapkg(x,y)

会得到

然后去查看关键的这几个函数,没有格式化,但是不妨碍看

代码如下:

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
// 这是页面定义
Page({
data: {
vin: "", // 存储输入的 VIN(车辆识别码)
status: "" // 存储校验结果
},
// 输入 VIN 时触发
onVinInput: function (t) {
this.setData({
vin: t.detail.value.toUpperCase() // 自动转成大写
});
},
// 点击“提交验证”按钮时触发
onSubmit: function () {
var t = this.data.vin;
if (t.length === 17) {
// 当长度为17位时,执行校验
if (this.verifyVinByRC4(t)) {
this.setData({ status: "校验通过 🎉" });
} else {
this.setData({ status: "校验失败 ❌" });
}
} else {
this.setData({ status: "VIN长度必须为17位" });
}
},
// === 加密校验逻辑部分 ===
verifyVinByRC4: function (t) {
// 用 RC4 变体加密 VIN,然后与固定密文对比
return (
"7AF2C74EAD5C2D4505E94B820275CA8C52" ===
this.variantRC4Encrypt(t, "Z1X3Y4E5Z8V2A6H6") // key 固定
);
},
// 变体 RC4 算法(非标准 RC4)
variantRC4Encrypt: function (text, key) {
// 初始化S盒
var r = Array.from({ length: 256 }, (v, i) => i);
var a = 0;
var keyBytes = this.stringToBytes(key);
// 第一轮 KSA(Key Scheduling Algorithm)
for (var s = 0; s < 256; s++) {
a = (a + r[s] + keyBytes[s % keyBytes.length]) % 256;
var temp = r[s];
r[s] = r[a];
r[a] = temp;
}
// 第二轮反向扰动(这个是“变体”部分)
a = 0;
for (var o = 255; o >= 0; o--) {
a = (a + r[o] + keyBytes[o % keyBytes.length]) % 256;
var temp2 = r[o];
r[o] = r[a];
r[a] = temp2;
}
// PRGA(生成密钥流 + 异或加密)
var h = 0;
a = 0;
var resultBytes = [];
var textBytes = this.stringToBytes(text);
for (var C = 0; C < textBytes.length; C++) {
h = (h + 1) % 256;
a = (a + r[h]) % 256;
var temp3 = r[h];
r[h] = r[a];
r[a] = temp3;
// 混合索引(RC4变体)
var y = (r[h] + r[a]) % 256;
var g = r[(r[h] + r[a] + r[y]) % 256];
// 异或加密,加上位置扰动 (C % 256)
var l = textBytes[C] ^ g ^ (C % 256);
resultBytes.push(l);
}
// 输出十六进制字符串
return this.bytesToHex(resultBytes);
},
// 将字符串转换为字节数组
stringToBytes: function (t) {
var arr = [];
for (var i = 0; i < t.length; i++) {
arr.push(t.charCodeAt(i));
}
return arr;
},
// 字节数组 → 十六进制字符串

bytesToHex: function (bytes) {
var n = "";
for (var i = 0; i < bytes.length; i++) {
var a = bytes[i];
var high = Math.floor(a / 16);
var low = a % 16;
n += this.numberToVinChar(high) + this.numberToVinChar(low);
}
return n;
},
// 数字 → 十六进制字符
numberToVinChar: function (t) {
return "0123456789ABCDEF"[t % 16];
}
});

就是一个RC4,解密就行:

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
def string_to_bytes(s):
return [ord(c) for c in s]
def number_to_hexchar(n):
return "0123456789ABCDEF"[n % 16]
def bytes_to_hex(byte_arr):
return ''.join(f"{b:02X}" for b in byte_arr)
def variant_rc4(text_bytes, key):
# 初始化S盒
r = list(range(256))
a = 0
key_bytes = string_to_bytes(key)
# 第一轮 KSA
for s in range(256):
a = (a + r[s] + key_bytes[s % len(key_bytes)]) % 256
r[s], r[a] = r[a], r[s]
# 第二轮反向扰动
a = 0
for o in range(255, -1, -1):
a = (a + r[o] + key_bytes[o % len(key_bytes)]) % 256
r[o], r[a] = r[a], r[o]
# PRGA
h = 0
a = 0
result = []
for C in range(len(text_bytes)):
h = (h + 1) % 256
a = (a + r[h]) % 256
r[h], r[a] = r[a], r[h]
y = (r[h] + r[a]) % 256
g = r[(r[h] + r[a] + r[y]) % 256]
l = text_bytes[C] ^ g ^ (C % 256)
result.append(l)
return result
def encrypt_vin(vin, key="Z1X3Y4E5Z8V2A6H6"):
vin_bytes = string_to_bytes(vin)
encrypted_bytes = variant_rc4(vin_bytes, key)
return bytes_to_hex(encrypted_bytes)
def decrypt_vin(hex_str, key="Z1X3Y4E5Z8V2A6H6"):
# RC4对称,加密 = 解密
cipher_bytes = bytes.fromhex(hex_str)
decrypted_bytes = variant_rc4(list(cipher_bytes), key)
return ''.join(chr(b) for b in decrypted_bytes)
if __name__ == "__main__":
key = "Z1X3Y4E5Z8V2A6H6"
cipher = "7AF2C74EAD5C2D4505E94B820275CA8C52"
# 解密(等价于再加密一次)
plain = decrypt_vin(cipher, key)
print("解密结果:", plain)
# 反向验证加密
print("重新加密:", encrypt_vin(plain, key))

得到flag

flag{L0J6Q0P7H3E2I5U6H}

强网vintelligence

给了一个流量包一个apk,首先分析流量包,得到了传输数据,需要分析apk来解密这段字符串,题目跟招商铸盾初赛的那一道vin交互很是相似

jadx打开文件,看到左边的类,有一个input

可以猜到就是一个java层加密一个so层加密

两个加密

java层的加密好写,就是一个c=(m+k)^k 的加密

解密直接m=(c-k)^k

1
2
3
4
5
6
7
8
9
10
11
12
def decry_java(cipher_hex: str, key: str = "PMZPFVDM") -> str:
ct = bytes.fromhex(cipher_hex)
out_bytes = []
kbytes = key.encode("latin-1") # Java char 的低8位等价
print(kbytes)
klen = len(kbytes)
for i, y in enumerate(ct):
k = kbytes[i % klen]
a = y ^ k
x = (a - k) & 0xFF
out_bytes.append(x)
return bytes(out_bytes).decode("latin-1")

so层加密

RC4加密

就是一个简单的小魔改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def decry_so(hex_cipher: str, key_str: str = "SecretKey") -> bytes:
S = list(range(256))
key = key_str.encode("utf-8")
klen = len(key)
j = 0
for i in range(256):
j = (j + S[i] + key[i % klen] + 3) & 0xFF
S[i], S[j] = S[j], S[i]
ct = bytes.fromhex(hex_cipher)
out = bytearray()
i = j = 0
for b in ct:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
out.append(b ^ K)
return bytes(out)

注意一下输入就行,记得讲输入转换成字节

更简单的就是直接使用frida进行主动调用进行dump xor密钥流

我的主动调用,不过没有dump

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
function hook1() {
Java.perform(() => {
const Cls = Java.use("com.example.testapp.VinInputActivity");
const arg = "D133D1D131";
Java.choose("com.example.testapp.VinInputActivity", {
onMatch: (inst) => {
Java.scheduleOnMainThread(() => {
try {
const r = inst.encrypt(arg);
console.log("encrypt(instance) = " + r);
} catch (err) {
console.log("instance call failed:", err);
}
});
},
onComplete: () => { }
});
});
}

function hook2() {
Java.perform(function () {
let a = Java.use("com.example.testapp.a");
var arg = '11111'
var obj = a.$new();
var func = obj.a(arg)
console.log(func)
})
}
hook1()

可以验证代码对错

总的解密代码:

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
def decry_java(cipher_hex: str, key: str = "PMZPFVDM") -> str:
ct = bytes.fromhex(cipher_hex)
out_bytes = []
kbytes = key.encode("latin-1") # Java char 的低8位等价
print(kbytes)
klen = len(kbytes)
for i, y in enumerate(ct):
k = kbytes[i % klen]
a = y ^ k
x = (a - k) & 0xFF
out_bytes.append(x)
return bytes(out_bytes).decode("latin-1")
print(len("3017284f2ed903814311c68d5149bf5c12"))
y="PMZPFVDM"
z='CAEBF2EBFD9ACBC3D6D9D7F28686C1F5C8'
print("-----------")
def decry_so(hex_cipher: str, key_str: str = "SecretKey") -> bytes:
S = list(range(256))
key = key_str.encode("utf-8")
klen = len(key)
j = 0
for i in range(256):
j = (j + S[i] + key[i % klen] + 3) & 0xFF
S[i], S[j] = S[j], S[i]
ct = bytes.fromhex(hex_cipher)
out = bytearray()
i = j = 0
for b in ct:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
out.append(b ^ K)
return bytes(out)
if __name__ == "__main__":
so_cipher_hex = "3017284f2ed903814311c68d5149bf5c12"
so_key = "SecretKey"
inner_bytes = decry_so(so_cipher_hex, so_key)
try:
inner_hex = inner_bytes.decode("ascii").upper()
except UnicodeDecodeError:
inner_hex = inner_bytes.hex().upper()
print("so层解密结果:", inner_hex) # -> D133D1D131
print(decry_java(inner_hex))

得到flag


第九届“强网杯”行业领域专项赛车联网安全赛-初赛(CTF)WP
https://erkangkang.github.io/2025/11/05/25强网杯车联网赛道/
作者
尔康康康康
发布于
2025年11月5日
许可协议