54 lines
3.3 KiB
Markdown
54 lines
3.3 KiB
Markdown
<h1 align="center">Художественная галерея</h1>
|
||
|
||
<p align="center">
|
||
<img src="https://img.shields.io/badge/category-Stego-blueviolet" alt="Stego"/>
|
||
<img src="https://img.shields.io/badge/points-979-critical" alt="979 pts"/>
|
||
</p>
|
||
|
||
Изучаем выданный `gallery.psd` — PSD-файл с пятью слоями:
|
||
|
||
| # | Имя | Состояние |
|
||
|---|---|---|
|
||
| 0 | `Background` | видимый |
|
||
| 1 | `Title` | видимый |
|
||
| 2 | `Pattern A` | скрытый |
|
||
| 3 | `Pattern B` | скрытый |
|
||
| 4 | `Encrypted` | скрытый |
|
||
|
||
Два из трёх скрытых слоёв — ложный след, а настоящий QR-код лежит в третьем и дополнительно зашифрован AES'ом.
|
||
|
||
## Решение
|
||
|
||
PSD разбираем руками по [официальной спецификации Adobe](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/). Структура файла такая:
|
||
|
||
```mermaid
|
||
block-beta
|
||
columns 1
|
||
H["8BPS header"]
|
||
CM["Color Mode Data"]
|
||
IR["Image Resources<br/>XMP ID 0x0424 ← ключ к расшифровке"]
|
||
LMI["Layer and Mask Info<br/>слои, флаги, каналы"]
|
||
ID["Image Data"]
|
||
|
||
classDef base fill:#f3f4f6,stroke:#6b7280,color:#111827
|
||
classDef hit fill:#fef3c7,stroke:#f59e0b,color:#78350f
|
||
class H,CM,LMI,ID base
|
||
class IR hit
|
||
```
|
||
|
||
В секции `Image Resources` ищем XMP-метаданные (блок с ID `0x0424`) — там и запрятан ключ ко всей задаче. Внутри XMP смотрим на тег `<xmp:CreateDate>`: ISO 8601 timestamp создания файла. В секции `Layer and Mask Information` для каждого слоя лежит имя, флаги видимости (бит `0x02` в flags = «скрытый»), `opacity` и сырые данные каналов. Пиксельные данные хранятся несжатыми — можно напрямую залить в `numpy`-массив `h × w`.
|
||
|
||
Первое, что теперь приходит в голову при виде двух скрытых «паттернов» — это XOR'нуть их между собой. Берём `Pattern A` и `Pattern B`, XOR — получается картинка с вполне читаемым QR-кодом. Сканируем, внутри флаг, таск решён… *кроме того, что флаг внутри фейковый*. Настоящий QR живёт в третьем скрытом слое — `Encrypted`, и это шифротекст исходного QR в режиме AES-ECB.
|
||
|
||
Ключ для AES собирается из той самой timestamp-метки:
|
||
|
||
```python
|
||
aes_key = sha256(timestamp.encode()).digest()[:16]
|
||
```
|
||
|
||
Расшифровываем R-канал в `AES.MODE_ECB`, снимаем PKCS#7-паддинг по последнему байту, интерпретируем результат как `h × w` grayscale-картинку. Внутри — настоящий QR, внутри QR — флаг. Если `pyzbar` с первого раза не узнал код, помогает `NEAREST`-апскейл в 2–3 раза.
|
||
|
||
Готовый солвер — [`solve/solver.py`](solve/solver.py).
|
||
|
||
## Флаг
|
||
`caplag{l4y3rs_0f_d3c3pt10n_unv31l3d}` |