题目

Loader ida分析

发现是打包的python源码,通过python3、ffmpeg等进行处理

逻辑如下:

1
2
3
4
1. 检查 video.mp4 文件是否存在
2. 解码嵌入的 Base64 数据 → 生成 stager.pyc
3. 设置执行权限 (chmod 0x1ED = 0755)
4. 通过 python3 stager.pyc 执行 Python 脚本

提取中间的base64解码得到pyc文件

1
2
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

通过uncompyle6反编译得到源码:

uncompyle6 /Users/y5neko/draft/ctf222/stager.pyc

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
# uncompyle6 version 3.9.3
# Python bytecode version base 3.7.0 (3394)
# Decompiled from: Python 3.13.9 | packaged by Anaconda, Inc. | (main, Oct 21 2025, 19:11:29) [Clang 20.1.8 ]
# Embedded file name: Payload_To_PixelCode_video.py
# Compiled at: 2026-01-04 12:02:18
# Size of source mod 2**32: 2394 bytes
from PIL import Image
import math, os, sys, numpy as np, imageio
from tqdm import tqdm

def file_to_video(input_file, width=640, height=480, pixel_size=8, fps=10, output_file='video.mp4'):
if not os.path.isfile(input_file):
return
file_size = os.path.getsize(input_file)
binary_string = ""
with open(input_file, "rb") as f:
for chunk in tqdm(iterable=(iter((lambda: f.read(1024)), b'')),
total=(math.ceil(file_size / 1024)), unit="KB", desc="读取文件"):
binary_string += "".join(((f"{byte:08b}") for byte in chunk))

xor_key = "10101010"
xor_binary_string = ""
for i in range(0, len(binary_string), 8):
chunk = binary_string[i[:i + 8]]
if len(chunk) == 8:
chunk_int = int(chunk, 2)
key_int = int(xor_key, 2)
xor_result = chunk_int ^ key_int
xor_binary_string += f"{xor_result:08b}"
else:
xor_binary_string += chunk

binary_string = xor_binary_string
pixels_per_image = width // pixel_size * (height // pixel_size)
num_images = math.ceil(len(binary_string) / pixels_per_image)
frames = []
for i in tqdm((range(num_images)), desc="生成视频帧"):
start = i * pixels_per_image
bits = binary_string[start[:start + pixels_per_image]]
if len(bits) < pixels_per_image:
bits = bits + "0" * (pixels_per_image - len(bits))
img = Image.new("RGB", (width, height), color="white")
for r in range(height // pixel_size):
row_start = r * (width // pixel_size)
row_end = (r + 1) * (width // pixel_size)
row = bits[row_start[:row_end]]
for c, bit in enumerate(row):
color = (0, 0, 0) if bit == "1" else (255, 255, 255)
x1, y1 = c * pixel_size, r * pixel_size
img.paste(color, (x1, y1, x1 + pixel_size, y1 + pixel_size))

frames.append(np.array(img))

with imageio.get_writer(output_file, fps=fps, codec="libx264") as ffmpeg:
for frame in tqdm(frames, desc="写入视频帧"):
writer.append_data(frame)


if __name__ == "__main__":
input_path = "payload"
if os.path.exists(input_path):
file_to_video(input_path)
else:
sys.exit(1)

分析源码,逻辑如下:

1
2
3
4
5
6
1. 读取文件 → 转为二进制字符串 (每个字节 8 位)
2. XOR 加密 → 使用固定密钥 0xAA (二进制 10101010)
3. 像素映射 →
- 1 → 黑色像素 (0,0,0)
- 0 → 白色像素 (255,255,255)
4. 生成视频 → 640x480 分辨率,8x8 像素块,10fps

直接编写脚本提取所有视频帧:

编写脚本解密:

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
#!/usr/bin/env python3
"""
视频隐写解码器
从 video.mp4 中提取隐藏的 payload

编码原理:
1. 文件按字节转为二进制字符串
2. 每 8 位用 XOR 密钥 0xAA 加密
3. 每位映射为 8x8 像素块(黑=1, 白=0)
4. 生成 640x480 视频帧
"""

from PIL import Image
import os

# 参数 (与编码器一致)
WIDTH, HEIGHT = 640, 480
PIXEL_SIZE = 8
XOR_KEY = 0xAA # 二进制 10101010

# 计算每帧的像素块数
blocks_per_row = WIDTH // PIXEL_SIZE # 80
blocks_per_col = HEIGHT // PIXEL_SIZE # 60
bits_per_frame = blocks_per_row * blocks_per_col # 4800 bits = 600 bytes

def extract_from_frames(frames_dir, output_file):
"""从帧图片中提取隐藏数据"""

binary_string = ""
frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')])

for frame_file in frame_files:
img = Image.open(os.path.join(frames_dir, frame_file))
pixels = img.load()

for row in range(blocks_per_col):
for col in range(blocks_per_row):
# 取每个块的左上角像素
x = col * PIXEL_SIZE
y = row * PIXEL_SIZE
r, g, b = pixels[x, y][:3]

# 判断黑白 (黑=1, 白=0)
if r < 128: # 黑色
binary_string += "1"
else: # 白色
binary_string += "0"

print(f"提取的二进制位数: {len(binary_string)}")

# XOR 解密
decrypted_binary = ""
for i in range(0, len(binary_string), 8):
chunk = binary_string[i:i+8]
if len(chunk) == 8:
byte_val = int(chunk, 2)
decrypted_byte = byte_val ^ XOR_KEY
decrypted_binary += f"{decrypted_byte:08b}"
else:
decrypted_binary += chunk

# 转换为字节
byte_data = bytearray()
for i in range(0, len(decrypted_binary), 8):
chunk = decrypted_binary[i:i+8]
if len(chunk) == 8:
byte_data.append(int(chunk, 2))

# 保存文件
with open(output_file, 'wb') as f:
f.write(byte_data)

print(f"解码完成! 输出: {output_file} ({len(byte_data)} bytes)")
return byte_data


def extract_from_video(video_path, output_file):
"""直接从视频文件提取隐藏数据"""
import imageio

frames_dir = '/tmp/video_frames'
os.makedirs(frames_dir, exist_ok=True)

# 提取帧
reader = imageio.get_reader(video_path, format='FFMPEG')
for i, frame in enumerate(reader):
imageio.imwrite(f'{frames_dir}/frame_{i:03d}.png', frame)
reader.close()

# 从帧提取数据
result = extract_from_frames(frames_dir, output_file)

# 清理临时文件
import shutil
shutil.rmtree(frames_dir)

return result


if __name__ == "__main__":
import sys

if len(sys.argv) < 3:
print("用法:")
print(f" {sys.argv[0]} <frames_dir> <output_file> # 从帧图片提取")
print(f" {sys.argv[0]} --video <video.mp4> <output_file> # 从视频提取")
print()
print("示例:")
print(f" {sys.argv[0]} ./frames extracted_payload")
print(f" {sys.argv[0]} --video video.mp4 extracted_payload")
sys.exit(1)

if sys.argv[1] == "--video":
extract_from_video(sys.argv[2], sys.argv[3])
else:
extract_from_frames(sys.argv[1], sys.argv[2])

得到Linux可执行文件

ida反编译

执行后输出了两句话

UTF8解码后得到提示:

1
2
提示:每个 MD5 哈希对应一个 ASCII 字符
按顺序组合这些字符即可得到 flag

随后查找字符串发现了md5列表,编写脚本解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
#!/usr/bin/env python3
"""
MD5 哈希解码器
通过预计算的 MD5 查找表解码 flag

原理:
- 每个 MD5 哈希对应一个 ASCII 字符
- 按顺序组合这些字符即可得到 flag
"""

import hashlib

def md5_char(c):
"""计算单个字符的 MD5"""
return hashlib.md5(c.encode()).hexdigest()


def build_lookup_table():
"""构建 MD5 -> 字符 的查找表"""
char_md5 = {}

# 可打印 ASCII 字符
for i in range(32, 127):
c = chr(i)
char_md5[md5_char(c)] = c

# 数字 0-9
for i in range(10):
c = str(i)
char_md5[md5_char(c)] = c

# 小写字母
for c in 'abcdefghijklmnopqrstuvwxyz':
char_md5[md5_char(c)] = c

# 大写字母
for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
char_md5[md5_char(c)] = c

# 常见符号
for c in '{}_!@#$%^&*()-+=[]|;:,.<>?/~`':
char_md5[md5_char(c)] = c

return char_md5


def decode_md5_list(md5_list, lookup_table=None):
"""解码 MD5 列表"""
if lookup_table is None:
lookup_table = build_lookup_table()

result = []
unknown = []

for md5 in md5_list:
md5 = md5.lower().strip()
if md5 in lookup_table:
result.append(lookup_table[md5])
else:
result.append('?')
unknown.append(md5)

return ''.join(result), unknown


# 从 extracted_payload 中提取的 MD5 列表
MD5_LIST_FROM_PAYLOAD = [
"8277e0910d750195b448797616e091ad",
"0cc175b9c0f1b6a831c399e269772661",
"4b43b0aee35624cd95b910189b3dc231",
"e358efa489f58062f10dd7316b65649e",
"f95b70fdc3088560732a5ac135644506",
"c81e728d9d4c2f636f067f89cc14862c",
"92eb5ffee6ae2fec3ad71c777531578f",
"c4ca4238a0b923820dcc509a6f75849b",
"8fa14cdd754f91cc6554c9e71929cce7",
"c9f0f895fb98ab9159f51fd0297e236d",
"336d5ebc5436534e61d16e63ddfca327",
"eccbc87e4b5ce2fe28308fd9f2a7baf3",
"cfcd208495d565ef66e7dff9f98764da",
"a87ff679a2f3e71d9181a67b7542122c",
"e4da3b7fbbce2345d7772b0674a318d5",
"e1671797c52e15f763380b45e841ec32",
"8f14e45fceea167a5a36dedd4bea2543",
"1679091c5a880faf6fb5e6087eb1b2dc",
"4a8a08f09d37b73795649038408b5f33",
"cbb184dd8e05c9709e5dcaedaa0495cf",
]


def main():
import sys

# 检查是否有命令行参数(自定义 MD5 列表)
if len(sys.argv) > 1:
# 从文件读取 MD5 列表
if sys.argv[1].endswith('.txt'):
with open(sys.argv[1], 'r') as f:
md5_list = [line.strip() for line in f if line.strip()]
else:
# 从命令行参数读取
md5_list = sys.argv[1:]
else:
# 使用内置列表
md5_list = MD5_LIST_FROM_PAYLOAD
print("使用内置 MD5 列表 (从 extracted_payload 提取)")
print()

# 构建查找表
lookup_table = build_lookup_table()

# 解码
print("=" * 50)
print("MD5 解码结果:")
print("=" * 50)

for md5 in md5_list:
md5 = md5.lower().strip()
if md5 in lookup_table:
char = lookup_table[md5]
print(f"{md5} -> '{char}'")
else:
print(f"{md5} -> UNKNOWN")

# 输出 flag
flag, unknown = decode_md5_list(md5_list, lookup_table)

print()
print("=" * 50)
print(f"Flag: {flag}")
print("=" * 50)

if unknown:
print(f"\n未知 MD5 ({len(unknown)} 个):")
for u in unknown:
print(f" {u}")


if __name__ == "__main__":
main()

FLAG

得到flag:dart{2b1f8-3045e76c}