Files
Serdtse-Sysoly/finals/scalaralias-crypto-easy/WRITEUP.md
T
2026-09-17 00:50:07 +03:00

83 lines
4.3 KiB
Markdown
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.
<h1 align="center">ScalarAlias</h1>
<p align="center">
<img src="https://img.shields.io/badge/category-Crypto-blueviolet" alt="Crypto"/>
<img src="https://img.shields.io/badge/difficulty-easy-brightgreen" alt="easy"/>
</p>
Реестр KiteLedger выдаёт аудиторскую квитанцию с подписью и запечатанный экспорт. Подпись проверяется по числовым значениям, а ключ экспорта привязан к байтам JSON. Если найти две разные записи одной валидной подписи, можно получить нужный вариант квитанции.
## Решение
В раздатке есть `receipt.json`, `sealed_export.json`, `verify_receipt.py` и `NOTES.md`. Смотрим проверку Schnorr-подобной подписи:
```text
left = ((s % Q) * BASE) % Q
right = (R + e * public_key) % Q
```
Здесь `Q = 170141183460469231731687303715884105727`, `BASE = 9`, а `e` вычисляется через SHA-256. Скаляр `s` приводят по модулю `Q`, но диапазон `0 <= s < Q` перед этим не проверяют.
Получается, `(R, s)` и `(R, s + Q)` проходят одну и ту же проверку, поскольку остаток одинаковый. При этом значение `s` в сериализованном JSON различается. Это и есть **malleability**, возможность изменить подпись, сохранив её валидность.
Пробуем ближайший эквивалентный вариант `s + Q`. Загружаем квитанцию и функции из `public/verify_receipt.py`, затем меняем скаляр, сохраняя его строковое представление.
```python
from pathlib import Path
from hashlib import sha256
import importlib.util
import json
spec = importlib.util.spec_from_file_location("receipt_verifier", "public/verify_receipt.py")
verifier = importlib.util.module_from_spec(spec)
spec.loader.exec_module(verifier)
Q = verifier.Q
receipt = json.loads(Path("public/receipt.json").read_text())
changed = json.loads(json.dumps(receipt))
changed["signature"]["s"] = str(int(changed["signature"]["s"]) + Q)
```
Проверяем изменённую квитанцию через `verify_receipt()`, затем получаем ключ:
```python
assert verifier.verify_receipt(changed)
key = sha256(
b"KiteLedger sealed export binding\0" + verifier.canonical_json(changed)
).digest()
```
`canonical_json` сортирует ключи и использует компактные разделители. Повторяем именно эту сериализацию, иначе получим другой ключ. Из `sealed_export.json` переводим `nonce_hex`, `ciphertext_hex` и `tag_hex` в байты. Первые 16 байт `sha256(key + b"export-tag" + nonce + ciphertext).digest()` сравниваем с тегом экспорта. Проверяем тег до расшифровки.
```python
sealed = json.loads(Path("public/sealed_export.json").read_text())
nonce = bytes.fromhex(sealed["nonce_hex"])
ciphertext = bytes.fromhex(sealed["ciphertext_hex"])
tag = bytes.fromhex(sealed["tag_hex"])
expected = sha256(key + b"export-tag" + nonce + ciphertext).digest()[:16]
assert expected == tag
```
Для варианта `s + Q` тег сходится, подтверждая выбор квитанции.
После проверки снимаем XOR. Блоки потока считаются как `sha256(key + b"scalaralias-stream" + nonce + counter4be).digest()`, где `counter4be` — четыре байта счётчика в big-endian. Счётчик начинается с нуля.
```python
stream = bytearray()
counter = 0
while len(stream) < len(ciphertext):
stream.extend(sha256(
key + b"scalaralias-stream" + nonce + counter.to_bytes(4, "big")
).digest())
counter += 1
plain = bytes(a ^ b for a, b in zip(ciphertext, stream))
print(plain.decode())
```
Полученный текст содержит флаг.
[Солвер](solve/solve.py).
## Флаг
`caplag{noncanonical_scalar_unsealed_the_export}`