Init. Commit
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<h1 align="center">NullRoute KEM</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>
|
||||
|
||||
В раздатке есть `capsule.json` с отклонённой KEM-капсулой, клиент совместимости `kem_client.py` и описание формата. Клиент умеет расшифровывать заметки для невалидных капсул. Проверяем, как он вычисляет общий секрет.
|
||||
|
||||
## Решение
|
||||
|
||||
В `decapsulate()` есть две ветки. Валидная капсула должна иметь длину 52 байта, префикс `NRK1` и правильный HMAC-тег. Тогда секрет вычисляется как `sha256(b"valid-shared:" + ct)`. Любая ошибка ведёт к одной константе:
|
||||
|
||||
```python
|
||||
from hashlib import sha256
|
||||
|
||||
shared_secret = sha256(
|
||||
b"NullRoute/KEM-reject/fallback-shared-secret/v1"
|
||||
).digest()
|
||||
```
|
||||
|
||||
Капсула из раздатки отклонена, поэтому попадает именно сюда. Общий секрет известен из исходника, а все остальные параметры лежат в JSON:
|
||||
|
||||
| Поле | Роль |
|
||||
|---|---|
|
||||
| `salt_hex` | Соль для вывода ключа |
|
||||
| `nonce_hex` | Nonce потока и проверки тега |
|
||||
| `aad` | Дополнительные аутентифицируемые данные |
|
||||
| `ciphertext_hex`, `tag_hex` | Зашифрованная заметка и её тег |
|
||||
|
||||
Повторяем `decrypt_capsule()`. Поля с суффиксом `_hex` переводим в байты, `aad` кодируем так же, как клиент. Через HKDF на HMAC-SHA256 выводим из `shared_secret` и соли `salt_hex` 32-байтный ключ с `info = b"NullRoute reject lane note key"`. Этим ключом считаем HMAC-SHA256 и сверяем его первые 16 байт с тегом заметки. В HMAC включаем следующие данные.
|
||||
|
||||
```text
|
||||
b"NRK-AEAD-v1" + aad + nonce + ciphertext
|
||||
```
|
||||
|
||||
Если тег сошёлся, генерируем поток блоками по 32 байта:
|
||||
|
||||
```text
|
||||
block = hmac_sha256(
|
||||
key, b"nullroute-stream" + nonce + counter.to_bytes(4, "big")
|
||||
)
|
||||
```
|
||||
|
||||
Повторяем начальное значение и порядок увеличения счётчика из `decrypt_capsule()`, склеиваем блоки и обрезаем поток до длины шифротекста. XOR возвращает операторскую заметку с флагом. Чтобы сохранить порядок вывода ключа, проверки тега и генерации потока, используем `decrypt_capsule()` из выданного клиента. Загружаем модуль и передаём ему разобранный JSON капсулы.
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
import importlib.util
|
||||
import json
|
||||
|
||||
spec = importlib.util.spec_from_file_location("kem_client", "public/kem_client.py")
|
||||
client = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(client)
|
||||
capsule = json.loads(Path("public/capsule.json").read_text())
|
||||
print(client.decrypt_capsule(capsule).decode())
|
||||
```
|
||||
|
||||
[Солвер](solve/solve.py).
|
||||
|
||||
## Флаг
|
||||
|
||||
`caplag{kem_reject_lane_used_the_same_secret}`
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
import importlib.util
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load_client():
|
||||
path = ROOT / "public" / "kem_client.py"
|
||||
spec = importlib.util.spec_from_file_location("nullroute_client", path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def main() -> None:
|
||||
client = load_client()
|
||||
capsule = json.loads((ROOT / "public" / "capsule.json").read_text())
|
||||
plaintext = client.decrypt_capsule(capsule).decode()
|
||||
match = re.search(r"caplag\{[^}]+\}", plaintext)
|
||||
if not match:
|
||||
raise SystemExit("flag not found")
|
||||
print(match.group(0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user