Files
Tajna-tretej-stolicy/stego-art-gallery/solve/solver.py
2026-04-22 10:58:32 +03:00

233 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Решение для задания 6: Художественная галерея (усложнённая версия)
PSD-файл содержит 5 слоёв:
0: Background (видимый)
1: Title (видимый)
2: Pattern A (скрытый) — случайный шум
3: Pattern B (скрытый) — шум,содержащий ложный QR
4: Encrypted (скрытый) — верный QR, зашифрованный AES-ECB
Цепочка решения:
1. Разобрать PSD, найти скрытые слои
2. XOR слоёв 2 и 3 -> ЛОЖНЫЙ QR -> фейковый флаг (ловушка!)
3. Обнаружить слой 4 ("Encrypted") — третий скрытый слой
4. Извлечь временну́ю метку создания из XMP-метаданных PSD
5. Получить AES-ключ: SHA256(временная_метка)[:16]
6. Расшифровать слой 4 через AES-ECB -> настоящий QR -> настоящий флаг
"""
import sys, os, struct, io, hashlib, re
import numpy as np
from PIL import Image
from pyzbar.pyzbar import decode as decode_qr
from Crypto.Cipher import AES
def parse_psd_layers(filepath):
"""Разбирает PSD-файл и извлекает изображения всех слоёв и XMP-метаданные."""
with open(filepath, 'rb') as f:
data = f.read()
buf = io.BytesIO(data)
# ── Заголовок файла ──────────────────────────────────────────────────────
sig = buf.read(4)
assert sig == b'8BPS' # Сигнатура формата PSD
version = struct.unpack('>H', buf.read(2))[0]
buf.read(6) # Зарезервированные байты
num_channels = struct.unpack('>H', buf.read(2))[0]
height = struct.unpack('>I', buf.read(4))[0]
width = struct.unpack('>I', buf.read(4))[0]
depth = struct.unpack('>H', buf.read(2))[0] # Бит на канал
color_mode = struct.unpack('>H', buf.read(2))[0] # 3 = RGB
print(f"[*] PSD: {width}x{height}")
# ── Секция Color Mode Data (для RGB — пустая) ────────────────────────────
cm_len = struct.unpack('>I', buf.read(4))[0]
buf.read(cm_len)
# ── Секция Image Resources — здесь хранятся XMP-метаданные ──────────────
ir_len = struct.unpack('>I', buf.read(4))[0]
ir_start = buf.tell()
ir_data = buf.read(ir_len)
# Ищем блок XMP среди ресурсов изображения
xmp_data = None
ir_buf = io.BytesIO(ir_data)
while ir_buf.tell() < len(ir_data) - 12:
try:
sig = ir_buf.read(4)
if sig != b'8BIM': # Сигнатура каждого ресурса
break
res_id = struct.unpack('>H', ir_buf.read(2))[0]
name_len = struct.unpack('>B', ir_buf.read(1))[0]
ir_buf.read(name_len)
if (1 + name_len) % 2 != 0: # Выравнивание до чётного байта
ir_buf.read(1)
data_len = struct.unpack('>I', ir_buf.read(4))[0]
res_data = ir_buf.read(data_len)
if data_len % 2 != 0: # Выравнивание данных ресурса
ir_buf.read(1)
if res_id == 0x0424: # ID 0x0424 = XMP-метаданные
xmp_data = res_data
print(f"[+] Найден XMP-ресурс ({len(xmp_data)} байт)")
except:
break
# Извлекаем временну́ю метку создания из XMP
timestamp = None
if xmp_data:
xmp_str = xmp_data.decode('utf-8', errors='ignore')
# Тег <xmp:CreateDate> содержит дату/время в формате ISO 8601
m = re.search(r'<xmp:CreateDate>([^<]+)</xmp:CreateDate>', xmp_str)
if m:
timestamp = m.group(1)
print(f"[+] Временна́я метка создания: {timestamp}")
# ── Секция Layer and Mask Information ────────────────────────────────────
lm_len = struct.unpack('>I', buf.read(4))[0]
li_len = struct.unpack('>I', buf.read(4))[0]
# Отрицательное значение означает, что первый альфа-канал — маска прозрачности
layer_count = abs(struct.unpack('>h', buf.read(2))[0])
print(f"[*] Количество слоёв: {layer_count}")
layers = []
for i in range(layer_count):
# Координаты прямоугольника слоя
top = struct.unpack('>i', buf.read(4))[0]
left = struct.unpack('>i', buf.read(4))[0]
bottom = struct.unpack('>i', buf.read(4))[0]
right = struct.unpack('>i', buf.read(4))[0]
lw = right - left
lh = bottom - top
# Список каналов слоя (id канала + длина данных)
n_ch = struct.unpack('>H', buf.read(2))[0]
channels = []
for _ in range(n_ch):
ch_id = struct.unpack('>h', buf.read(2))[0] # -1=alpha, 0=R, 1=G, 2=B
ch_len = struct.unpack('>I', buf.read(4))[0]
channels.append((ch_id, ch_len))
buf.read(4) # Сигнатура режима наложения
blend_mode = buf.read(4) # Код режима наложения (norm, mul и т.д.)
opacity = struct.unpack('>B', buf.read(1))[0] # 0 = полностью прозрачный
buf.read(1) # Clipping
flags = struct.unpack('>B', buf.read(1))[0]
buf.read(1) # Зарезервированный байт
# Бит 0x02 флагов означает, что слой скрыт (невидим)
visible = not (flags & 0x02)
# Дополнительные данные слоя: маска, диапазоны смешения, имя
extra_len = struct.unpack('>I', buf.read(4))[0]
extra_start = buf.tell()
mask_len = struct.unpack('>I', buf.read(4))[0]
buf.read(mask_len)
blend_range_len = struct.unpack('>I', buf.read(4))[0]
buf.read(blend_range_len)
name_len = struct.unpack('>B', buf.read(1))[0]
name = buf.read(name_len).decode('ascii', errors='ignore')
buf.seek(extra_start + extra_len) # Перепрыгиваем остаток extra-данных
layers.append({
'name': name, 'width': lw, 'height': lh,
'opacity': opacity, 'visible': visible,
'channels': channels,
})
print(f" Слой {i}: '{name}' {lw}x{lh} opacity={opacity} visible={visible}")
# ── Чтение пиксельных данных каналов ────────────────────────────────────
for layer in layers:
lw, lh = layer['width'], layer['height']
channel_data = {}
for ch_id, ch_len in layer['channels']:
compression = struct.unpack('>H', buf.read(2))[0] # 0=Raw, 1=PackBits RLE
pixel_data = buf.read(ch_len - 2)
if compression == 0 and lw * lh > 0:
# Несжатые данные: напрямую читаем в массив нужного размера
arr = np.frombuffer(pixel_data[:lw * lh], dtype=np.uint8).reshape((lh, lw))
else:
# Сжатые/пустые данные — заполняем нулями (достаточно для нашей задачи)
arr = np.zeros((lh, lw), dtype=np.uint8)
channel_data[ch_id] = arr
layer['channel_data'] = channel_data
return layers, timestamp
def solve(filepath: str) -> str:
layers, timestamp = parse_psd_layers(filepath)
# Скрытые слои: невидимые или с нулевой непрозрачностью
hidden = [l for l in layers if not l['visible'] or l['opacity'] == 0]
print(f"\n[*] Найдено скрытых слоёв: {len(hidden)}")
if len(hidden) < 3:
return "ОШИБКА: ожидалось 3 скрытых слоя"
# Ищем зашифрованный слой по имени; если не нашли — берём третий скрытый
enc_layer = None
for l in hidden:
if 'ncrypt' in l['name'].lower(): # "Encrypted", "encrypted" и т.п.
enc_layer = l
break
if not enc_layer:
enc_layer = hidden[2]
print(f"[*] Зашифрованный слой: '{enc_layer['name']}'")
# Извлекаем зашифрованные байты из R-канала (id=0) или альфа-канала (id=-1)
enc_data = enc_layer['channel_data'].get(0, enc_layer['channel_data'].get(-1))
enc_bytes = enc_data.tobytes()
# ── Деривация AES-ключа из временно́й метки ──────────────────────────────
if not timestamp:
return "ОШИБКА: не удалось найти временну́ю метку создания"
# Ключ = первые 16 байт SHA-256 от строки временно́й метки (128-битный AES)
aes_key = hashlib.sha256(timestamp.encode()).digest()[:16]
print(f"[*] AES-ключ: {aes_key.hex()}")
# ── Расшифровка AES-ECB ──────────────────────────────────────────────────
cipher = AES.new(aes_key, AES.MODE_ECB)
decrypted = cipher.decrypt(enc_bytes)
# Удаляем PKCS#7-паддинг: последний байт указывает количество байт паддинга
pad_len = decrypted[-1]
if 1 <= pad_len <= 16:
decrypted = decrypted[:-pad_len]
# Восстанавливаем изображение QR-кода из расшифрованных байт
h, w = enc_layer['height'], enc_layer['width']
dec_array = np.frombuffer(decrypted[:h * w], dtype=np.uint8).reshape((h, w))
# ── Декодирование QR-кода ────────────────────────────────────────────────
img = Image.fromarray(dec_array, 'L') # 'L' = grayscale
results = decode_qr(img)
if results:
return results[0].data.decode('utf-8')
# Если QR не распознался — пробуем увеличить изображение (pyzbar любит крупные QR)
for scale in [2, 3]:
scaled = img.resize((w * scale, h * scale), Image.NEAREST)
results = decode_qr(scaled)
if results:
return results[0].data.decode('utf-8')
return "ОШИБКА: не удалось декодировать QR после расшифровки"
if __name__ == '__main__':
psd_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
'..', 'public', 'gallery.psd')
# Путь к файлу можно передать первым аргументом командной строки
if len(sys.argv) > 1:
psd_path = sys.argv[1]
flag = solve(psd_path)
print(f"[+] Флаг: {flag}")