217 lines
12 KiB
Markdown
217 lines
12 KiB
Markdown
|
|
# 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<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, // <--- ТОТ ЖЕ КЛЮЧ
|
|||
|
|
// ...
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Вектор атаки:**
|
|||
|
|
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` вызывается без префикса. |
|
|||
|
|
|
|||
|
|
**Заявление о безопасности ложно. Код уязвим.**
|