12 KiB
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
Я обнаружил следующие векторы атак:
- AddrMan Logic: Логика вытеснения адресов (
is_terrible) проверяет только адреса из прошлого, но не из будущего. - Hardcoded Identity: Ключи для Mainnet и Testnet идентичны, что позволяет Replay-атаки.
- Signing Oracle: Подпись
AuthChallengeне использует Domain Separation (префикс), в отличие от остальных подписей в системе.
4. Найденные уязвимости
[HIGH] AddrMan Time-Travel Poisoning (Вечное удержание слотов)
Файл: net/addrman.rs:137-150 и net/types.rs
Уязвимый код:
net/addrman.rs:
// 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.
Вектор атаки:
- Атакующий генерирует 65,536 адресов (контролируемых им или мусорных).
- Атакующий отправляет их жертве в сообщении
Addr, устанавливаяtimestamp=u64::MAX(или далекое будущее, например, 3000 год). AddrManжертвы помещает их вnew_table.- Функция
is_terrible()проверяет только старые адреса (timestamp < now - 30 days). Адрес из 3000 года считается "свежим" и "хорошим". - Когда честный пир присылает валидный адрес, он попадает в коллизию бакета.
- Код видит, что существующий адрес (из 3000 года) "не ужасен" (
!is_terrible()), и отвергает новый честный адрес. - Жертва оказывается в изоляции от новых пиров, таблица адресов забита вечным мусором.
expire()их также не удалит.
Импакт: Denial of Service (Eclipse) для новых узлов. Невозможность узнать о новых честных пирах.
Сложность: Низкая. Требуется только возможность отправлять Addr сообщения.
PoC сценарий:
// Отправляем 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
Уязвимый код:
/// Mainnet hardcoded nodes
pub static MAINNET_HARDCODED: LazyLock<Vec<HardcodedNode>> = LazyLock::new(|| {
vec![
HardcodedNode {
// ...
pubkey: TIMEWEB_MOSCOW_PUBKEY, // <--- ТОТ ЖЕ КЛЮЧ
// ...
},
]
});
/// Testnet hardcoded nodes
pub static TESTNET_HARDCODED: LazyLock<Vec<HardcodedNode>> = LazyLock::new(|| {
vec![
HardcodedNode {
// ...
pubkey: TIMEWEB_MOSCOW_PUBKEY, // <--- ТОТ ЖЕ КЛЮЧ
// ...
},
]
});
Вектор атаки:
- Атакующий поднимает ноду в Testnet.
- Атакующий инициирует соединение с жертвой в Mainnet (или перехватывает её, если может).
- Жертва отправляет
AuthChallenge(C). - Атакующий пересылает
Cнастоящему узлуtimeweb-moscow-testnetв сети Testnet (порт 19334). - Тестнет-узел подписывает
Cсвоим ключом (который идентичен мейннет-ключу). - Атакующий возвращает подпись жертве в Mainnet.
- Жертва проверяет подпись ключом
TIMEWEB_MOSCOW_PUBKEY— она валидна. - Жертва считает, что говорит с доверенным узлом Mainnet.
Импакт: Impersonation доверенного узла. Возможность скормить жертве ложную цепь (если удастся изолировать её от других). Нарушение изоляции сетей. Сложность: Средняя (требуется доступ к Testnet и Mainnet одновременно).
[MEDIUM] Generic Signing Oracle (No Domain Separation)
Файл: net/protocol.rs:980
Уязвимый код:
// 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. Атаки, которые НЕ работают
- Sybil Flooding (Consensus): Adaptive Cooldown эффективно предотвращает мгновенное получение веса новыми Sybil-узлами. Атака растягивается на месяцы, что делает её экономически нецелесообразной.
- Memory Exhaustion (Inventory): Использование
BoundedInvSetиLruHashSetс жесткими лимитами (100k элементов) и FIFO-вытеснением защищает память узла даже при флуде уникальными хешами. - Slowloris:
HANDSHAKE_TIMEOUT_SECS = 60иEncryptedStreamтаймауты корректно закрывают зависшие соединения.
6. Рекомендации
-
Fix AddrMan: В
net/addrman.rsдобавить проверкуtimestampпри добавлении адреса.// Reject future timestamps (> 10 min from now) if addr.timestamp > crate::types::now() + 600 { return false; }И обновить
is_terrible, чтобы считать адреса из будущего "ужасными". -
Fix Keys: Сгенерировать разные ключи для Mainnet и Testnet. Никогда не переиспользовать ключи между сетями.
-
Fix Auth Domain Separation: Добавить префикс при подписи челленджа.
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 вызывается без префикса. |
Заявление о безопасности ложно. Код уязвим.