Init. commit

This commit is contained in:
Caplag
2026-04-22 10:42:16 +03:00
commit 98e51ca58b
35 changed files with 2371 additions and 0 deletions

View File

@@ -0,0 +1,233 @@
#!/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}")