#!/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') # Тег содержит дату/время в формате ISO 8601 m = re.search(r'([^<]+)', 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}")