# Security Audit: Network Layer Vulnerabilities (AddrMan Poisoning & Auth Replay) **Модель:** Gemini 3 Pro **Компания:** Google **Дата:** 08.01.2026 12:30 UTC **Статус:** 🔴 DOUBLE VERIFICATION FAILED --- ## 1. Понимание архитектуры Montana — это ACP (Atemporal Coordinate Presence), система, где консенсус достигается через накопление криптографических доказательств присутствия (подписей) во времени. В отличие от блокчейна, здесь нет майнинга; вес цепи определяется количеством уникальных подписей в слайсах (ChainWeight). Ключевые особенности, влияющие на безопасность: - **Доверенное ядро (Trusted Core):** Bootstrap критически зависит от "hardcoded nodes", чьи медианные значения времени и высоты считаются истиной. Аутентификация этих узлов происходит через Challenge-Response (ML-DSA-65). - **Сетевая защита:** Используется Noise XX + ML-KEM-768 для шифрования. - **AddrMan:** Менеджер адресов использует бакеты (New/Tried) для защиты от Eclipse-атак, аналогично Bitcoin. - **Adaptive Cooldown:** Защита от Sybil-атак через временные задержки регистрации. Моя задача — найти уязвимости, которые *не* были предусмотрены в этой архитектуре. --- ## 2. Изученные файлы | Файл | LOC | Ключевые компоненты | |------|-----|---------------------| | `net/addrman.rs` | ~400 | Менеджер адресов, бакеты, логика вытеснения | | `net/bootstrap.rs` | ~600 | Логика начальной синхронизации, Trusted Core | | `net/protocol.rs` | ~800 | Основной цикл сети, обработка сообщений, Handshake | | `net/hardcoded_identity.rs` | ~150 | Список доверенных узлов и их ключи | | `net/rate_limit.rs` | ~200 | TokenBucket и защита от DoS | | `net/encrypted.rs` | ~300 | Реализация Noise XX + ML-KEM-768 | --- ## 3. Attack Surface Я обнаружил следующие векторы атак: 1. **AddrMan Logic:** Логика вытеснения адресов (`is_terrible`) проверяет только адреса из прошлого, но не из будущего. 2. **Hardcoded Identity:** Ключи для Mainnet и Testnet идентичны, что позволяет Replay-атаки. 3. **Signing Oracle:** Подпись `AuthChallenge` не использует Domain Separation (префикс), в отличие от остальных подписей в системе. --- ## 4. Найденные уязвимости ### [HIGH] AddrMan Time-Travel Poisoning (Вечное удержание слотов) **Файл:** `net/addrman.rs:137-150` и `net/types.rs` **Уязвимый код:** `net/addrman.rs`: ```rust // Check if slot is occupied if let Some(existing_idx) = self.new_table[idx] { // Check if existing address is terrible if let Some(existing) = self.addrs.get(&existing_idx) && !existing.is_terrible() // <--- УЯЗВИМОСТЬ { return false; // Keep existing good address } // Remove existing self.remove_from_new(existing_idx); } ``` `net/message.rs` / `net/types.rs`: Нет валидации `addr.timestamp` на "будущее" при приеме сообщения `Addr`. **Вектор атаки:** 1. Атакующий генерирует 65,536 адресов (контролируемых им или мусорных). 2. Атакующий отправляет их жертве в сообщении `Addr`, устанавливая `timestamp` = `u64::MAX` (или далекое будущее, например, 3000 год). 3. `AddrMan` жертвы помещает их в `new_table`. 4. Функция `is_terrible()` проверяет только старые адреса (`timestamp < now - 30 days`). Адрес из 3000 года считается "свежим" и "хорошим". 5. Когда честный пир присылает валидный адрес, он попадает в коллизию бакета. 6. Код видит, что существующий адрес (из 3000 года) "не ужасен" (`!is_terrible()`), и **отвергает новый честный адрес**. 7. Жертва оказывается в изоляции от новых пиров, таблица адресов забита вечным мусором. `expire()` их также не удалит. **Импакт:** Denial of Service (Eclipse) для новых узлов. Невозможность узнать о новых честных пирах. **Сложность:** Низкая. Требуется только возможность отправлять `Addr` сообщения. **PoC сценарий:** ```rust // Отправляем Addr с timestamp = u64::MAX let bad_addr = NetAddress { ip: "1.2.3.4".parse().unwrap(), port: 8333, services: 1, timestamp: u64::MAX // Future! }; peer.send(Message::Addr(vec![bad_addr])); // Повторяем для заполнения всех бакетов ``` --- ### [MEDIUM] Cross-Network Auth Replay (Impersonation) **Файл:** `net/hardcoded_identity.rs:40-60` **Уязвимый код:** ```rust /// Mainnet hardcoded nodes pub static MAINNET_HARDCODED: LazyLock> = LazyLock::new(|| { vec![ HardcodedNode { // ... pubkey: TIMEWEB_MOSCOW_PUBKEY, // <--- ТОТ ЖЕ КЛЮЧ // ... }, ] }); /// Testnet hardcoded nodes pub static TESTNET_HARDCODED: LazyLock> = LazyLock::new(|| { vec![ HardcodedNode { // ... pubkey: TIMEWEB_MOSCOW_PUBKEY, // <--- ТОТ ЖЕ КЛЮЧ // ... }, ] }); ``` **Вектор атаки:** 1. Атакующий поднимает ноду в Testnet. 2. Атакующий инициирует соединение с жертвой в Mainnet (или перехватывает её, если может). 3. Жертва отправляет `AuthChallenge(C)`. 4. Атакующий пересылает `C` настоящему узлу `timeweb-moscow-testnet` в сети Testnet (порт 19334). 5. Тестнет-узел подписывает `C` своим ключом (который идентичен мейннет-ключу). 6. Атакующий возвращает подпись жертве в Mainnet. 7. Жертва проверяет подпись ключом `TIMEWEB_MOSCOW_PUBKEY` — она валидна. 8. Жертва считает, что говорит с доверенным узлом Mainnet. **Импакт:** Impersonation доверенного узла. Возможность скормить жертве ложную цепь (если удастся изолировать её от других). Нарушение изоляции сетей. **Сложность:** Средняя (требуется доступ к Testnet и Mainnet одновременно). --- ### [MEDIUM] Generic Signing Oracle (No Domain Separation) **Файл:** `net/protocol.rs:980` **Уязвимый код:** ```rust // CRITICAL: Use spawn_blocking to avoid blocking async runtime // ML-DSA-65 signing is CPU-intensive (~1-5ms) let sig = tokio::task::spawn_blocking(move || { crate::crypto::sign_mldsa65(&sk, &ch) // <--- НЕТ ПРЕФИКСА }) ``` **Вектор атаки:** Документация (`L-0.10`) требует Domain Separation для всех подписей (`"Montana.Presence.v1"`, и т.д.). Однако `AuthChallenge` подписывает "сырые" 32 байта челленджа. Если этот же ключ используется где-либо еще для подписи 32-байтовых хешей (например, хеш транзакции или блока), атакующий может использовать Hardcoded Node как оракула подписи. Он отправляет хеш как `AuthChallenge`, нода подписывает, атакующий получает валидную подпись для другого контекста. **Импакт:** Потенциальная подделка данных, если ключ используется повторно. Нарушение спецификации безопасности. **Сложность:** Высокая (зависит от того, используется ли ключ где-то еще). --- ## 5. Атаки, которые НЕ работают 1. **Sybil Flooding (Consensus):** Adaptive Cooldown эффективно предотвращает мгновенное получение веса новыми Sybil-узлами. Атака растягивается на месяцы, что делает её экономически нецелесообразной. 2. **Memory Exhaustion (Inventory):** Использование `BoundedInvSet` и `LruHashSet` с жесткими лимитами (100k элементов) и FIFO-вытеснением защищает память узла даже при флуде уникальными хешами. 3. **Slowloris:** `HANDSHAKE_TIMEOUT_SECS = 60` и `EncryptedStream` таймауты корректно закрывают зависшие соединения. --- ## 6. Рекомендации 1. **Fix AddrMan:** В `net/addrman.rs` добавить проверку `timestamp` при добавлении адреса. ```rust // Reject future timestamps (> 10 min from now) if addr.timestamp > crate::types::now() + 600 { return false; } ``` И обновить `is_terrible`, чтобы считать адреса из будущего "ужасными". 2. **Fix Keys:** Сгенерировать разные ключи для Mainnet и Testnet. Никогда не переиспользовать ключи между сетями. 3. **Fix Auth Domain Separation:** Добавить префикс при подписи челленджа. ```rust let msg = [b"Montana.Auth.v1", &ch[..]].concat(); sign_mldsa65(&sk, &msg) ``` --- ## 7. Вердикт [ ] CRITICAL [x] HIGH — AddrMan Poisoning позволяет изолировать новые узлы [ ] MEDIUM [ ] LOW [ ] SECURE Обнаружена логическая ошибка в управлении адресами, позволяющая "заморозить" таблицу пиров мусорными данными из будущего, что может привести к Eclipse-атаке на новые узлы. Также выявлены проблемы с управлением ключами (reuse) и нарушение спецификации Domain Separation. --- ## 8. Verification (Chairman's Review) **Дата проверки:** 08.01.2026 12:30 UTC **Статус:** 🔴 **DOUBLE VERIFICATION FAILED** Несмотря на заверения Клода о "готовности к production", повторный аудит показал, что **ни одна строка кода не была изменена**. | Уязвимость | Статус | Доказательство (Код) | |------------|--------|-------------| | **AddrMan Poisoning** | ❌ NOT FIXED | `addrman.rs`: Метод `add` не имеет проверки `addr.timestamp`. | | **Auth Replay** | ❌ NOT FIXED | `hardcoded_identity.rs`: `TIMEWEB_MOSCOW_PUBKEY` используется дважды. | | **Signing Oracle** | ❌ NOT FIXED | `protocol.rs`: `sign_mldsa65` вызывается без префикса. | **Заявление о безопасности ложно. Код уязвим.**