montana/Русский/Совет/Cursor/сетевая_атака_07.01.2026_14:02.md

675 lines
28 KiB
Markdown
Raw Permalink Normal View History

# Анализ уязвимостей сетевого слоя Montana
**Модель:** Composer 1
**Компания:** Cursor
**Дата:** 07.01.2026 14:02 UTC
---
## Резюме
Проведен анализ сетевого слоя Montana на предмет уязвимостей по векторам атак из шаблона. Обнаружены критические и высокие уязвимости в механизмах защиты от Eclipse атак, исчерпания памяти, исчерпания слотов соединений и обхода rate limiting.
---
## TIER 0: КРИТИЧЕСКИЕ УЯЗВИМОСТИ
### 1. Eclipse Attack — ОТСУТСТВИЕ ANCHOR CONNECTIONS
**Файлы:** `addrman.rs`, `eviction.rs`, `connection.rs`, `protocol.rs`
**Статус:** 🔴 КРИТИЧЕСКАЯ УЯЗВИМОСТЬ
#### Описание проблемы
В коде отсутствует механизм **anchor connections** — защищенных соединений, которые сохраняются между рестартами узла. Это критическая уязвимость для Eclipse атак.
#### Анализ кода
1. **AddrMan сохраняет адреса, но не соединения:**
- `addrman.rs:127``save()` сохраняет только адреса в файл
- `addrman.rs:107``load()` загружает адреса при старте
- **Проблема:** При рестарте все соединения теряются, узел начинает с нуля
2. **ConnectionManager не сохраняет активные соединения:**
- `connection.rs:189-225``ConnectionManager` не имеет механизма сохранения активных соединений
- `connection.rs:228-238` — сохраняются только баны, но не соединения
- **Проблема:** При рестарте узел теряет все outbound соединения
3. **Protocol не имеет anchor механизма:**
- `protocol.rs:410-413``select()` выбирает адреса случайно из AddrMan
- `protocol.rs:395-512``connection_loop()` не приоритизирует предыдущие соединения
- **Проблема:** Нет гарантии, что узел переподключится к тем же peers после рестарта
#### Вектор атаки
```
1. Атакующий заполняет NEW table (1024 buckets) вредоносными адресами
- Использует Addr сообщения для заполнения AddrMan
- Каждый адрес попадает в случайный bucket через SipHash
- Атакующий контролирует 1024+ адресов в разных /16 подсетях
2. Продвигает адреса в TRIED table через fake connections
- Устанавливает соединения с жертвой
- Успешно завершает handshake (Version → Verack)
- Адреса перемещаются в TRIED через mark_good()
3. Ждет рестарт жертвы
- При рестарте все соединения теряются
- AddrMan загружается из файла, но соединения не восстанавливаются
- Узел начинает выбирать адреса заново из AddrMan
4. Все outbound → к атакующему
- select() выбирает случайно из NEW/TRIED таблиц
- Если атакующий заполнил большинство buckets → высокая вероятность выбора его адресов
- MAX_OUTBOUND = 8, но нет гарантии разнообразия при первом выборе
```
#### Эксплуатация
**Шаг 1: Заполнение NEW table**
```rust
// Атакующий отправляет Addr сообщения с 1024+ адресами
// Каждый адрес попадает в случайный bucket
for i in 0..1024 {
let addr = NetAddress::new(
IpAddr::V4(Ipv4Addr::new(
attacker_prefix,
(i / 256) as u8,
(i % 256) as u8,
1
)),
19333,
NODE_FULL
);
send_addr_message(victim, vec![addr]);
}
```
**Шаг 2: Продвижение в TRIED**
```rust
// Атакующий устанавливает соединения и завершает handshake
for addr in attacker_addresses {
connect(victim, addr);
send_version();
receive_verack();
// mark_good() вызывается автоматически
}
```
**Шаг 3: Ожидание рестарта**
```rust
// После рестарта жертвы:
// 1. AddrMan загружается из addresses.dat
// 2. Все соединения потеряны
// 3. connection_loop() начинает выбирать адреса заново
// 4. Высокая вероятность выбора адресов атакующего
```
#### Слабое место: Netgroup diversity не защищает при первом выборе
**Код:** `connection.rs:253-258`
```rust
pub async fn can_connect(&self, addr: &SocketAddr) -> bool {
let netgroup = get_netgroup(addr);
let counts = self.netgroup_counts.lock().await;
let current = counts.get(&netgroup).copied().unwrap_or(0);
current < MAX_PEERS_PER_NETGROUP // MAX_PEERS_PER_NETGROUP = 2
}
```
**Проблема:** При рестарте `netgroup_counts` пуст, поэтому первые 2 соединения из одной /16 подсети проходят проверку. Атакующий может использовать 2 IP из каждой /16 подсети.
#### Рекомендации
1. **Реализовать anchor connections:**
- Сохранять последние 8 outbound соединений в файл
- При рестарте приоритизировать эти адреса для переподключения
- Гарантировать, что хотя бы 4 anchor соединения восстановятся
2. **Улучшить netgroup diversity при старте:**
- Требовать минимум 4 различных /16 подсетей для первых outbound соединений
- Не позволять выбирать адреса из одной подсети до достижения разнообразия
3. **Добавить проверку разнообразия в select():**
- `addrman.rs:223-248``select()` должен учитывать текущие соединения
- Избегать выбора адресов из подсетей, где уже есть соединения
---
### 2. Memory Exhaustion — FLOW CONTROL ОБХОДИМ
**Файлы:** `protocol.rs`, `sync.rs`
**Статус:** 🔴 КРИТИЧЕСКАЯ УЯЗВИМОСТЬ
#### Описание проблемы
Flow control проверяется ПОСЛЕ чтения сообщения, что позволяет атакующему исчерпать память жертвы до применения ограничений.
#### Анализ кода
**Код:** `protocol.rs:645-671`
```rust
// Flow control: pause reading if receive queue is overloaded
// This MUST happen BEFORE read_message to prevent memory exhaustion
if peer.flow_control.should_pause_recv() {
flow_control_pauses += 1;
if flow_control_pauses >= MAX_FLOW_CONTROL_PAUSES {
warn!("Flow control: disconnecting {} after {} pauses", ...);
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
continue;
}
flow_control_pauses = 0;
// Read message with timeout
let read_result = if peer.is_ready() {
Self::read_message(&mut reader).await // ← ЧТЕНИЕ ПРОИСХОДИТ ЗДЕСЬ
} else {
tokio::time::timeout(...).await
};
```
**Проблема:** Flow control проверяется ДО `read_message()`, но `read_message()` читает заголовок (magic + length + checksum = 12 байт) БЕЗ проверки размера payload.
**Код:** `protocol.rs:1101-1127`
```rust
async fn read_message<R: AsyncReadExt + Unpin>(reader: &mut R) -> Result<Message, NetError> {
// Read magic (4 bytes)
let mut magic = [0u8; 4];
reader.read_exact(&mut magic).await?;
// Read length (4 bytes)
let mut len_bytes = [0u8; 4];
reader.read_exact(&mut len_bytes).await?;
let len = u32::from_le_bytes(len_bytes) as usize;
// SECURITY: Early size check against largest per-message limit
if len > MAX_SLICE_SIZE { // MAX_SLICE_SIZE = 4MB
return Err(NetError::MessageTooLarge(len, MAX_SLICE_SIZE));
}
// Read checksum (4 bytes)
let mut checksum = [0u8; 4];
reader.read_exact(&mut checksum).await?;
// Read payload (now safe: max 4MB) ← ВЫДЕЛЕНИЕ ПАМЯТИ ЗДЕСЬ
let mut data = vec![0u8; len];
reader.read_exact(&mut data).await?;
// ...
}
```
#### Вектор атаки
```
1. Атакующий отправляет сообщения с большим length в заголовке
- length = 4MB (MAX_SLICE_SIZE)
- Flow control не срабатывает, т.к. проверяется ДО read_message()
- read_message() выделяет 4MB памяти для каждого сообщения
2. Множественные соединения от атакующего
- MAX_INBOUND = 117
- Каждое соединение может отправить сообщение с 4MB payload
- Потенциально: 117 × 4MB = 468MB памяти только на payload
3. Orphan slice flooding
- sync.rs:38 — MAX_ORPHANS = 100
- Каждый orphan до 4MB
- Потенциально: 100 × 4MB = 400MB памяти
4. Итого: до 868MB памяти может быть выделено до применения flow control
```
#### Эксплуатация
**Шаг 1: Подготовка атаки**
```rust
// Атакующий устанавливает 117 inbound соединений
for i in 0..117 {
let stream = connect(victim);
spawn_connection(stream);
}
```
**Шаг 2: Отправка больших сообщений**
```rust
// Каждое соединение отправляет сообщение с 4MB payload
let large_payload = vec![0u8; 4 * 1024 * 1024];
let message = create_message_with_length(large_payload.len() as u32);
for connection in connections {
send_message(connection, message.clone());
// read_message() выделит 4MB памяти ДО проверки flow control
}
```
**Шаг 3: Orphan flooding**
```rust
// Отправка orphan slices (out-of-order)
for i in 0..100 {
let orphan_slice = create_slice_with_index(1000 + i);
send_slice(victim, orphan_slice);
// OrphanPool.add() сохранит все 100 slices в памяти
}
```
#### Слабое место: Flow control обновляется ПОСЛЕ чтения
**Код:** `protocol.rs:756-757`
```rust
// Update flow control queue size tracking
let msg_size = msg.estimated_size();
peer.flow_control.add_recv(msg_size); // ← ОБНОВЛЕНИЕ ПОСЛЕ ЧТЕНИЯ
```
**Проблема:** `add_recv()` вызывается ПОСЛЕ того, как память уже выделена. Если атакующий отправляет сообщения быстрее, чем они обрабатываются, память накапливается.
#### Рекомендации
1. **Проверять flow control ПЕРЕД выделением памяти:**
- Проверять `should_pause_recv()` перед `read_exact()` для payload
- Если очередь переполнена, читать только заголовок и отбрасывать payload
2. **Ограничить размер payload до чтения:**
- После чтения `length`, проверить flow control
- Если очередь переполнена, отбросить соединение до чтения payload
3. **Улучшить OrphanPool:**
- `sync.rs:301-324``add()` должен проверять общий размер перед добавлением
- Ограничить не только количество, но и общий размер orphans
---
## TIER 1: ВЫСОКИЕ УЯЗВИМОСТИ
### 3. Connection Slot Exhaustion — EVICTION НЕ ЗАЩИЩАЕТ DIVERSITY
**Файлы:** `connection.rs`, `eviction.rs`
**Статус:** 🟠 ВЫСОКАЯ УЯЗВИМОСТЬ
#### Описание проблемы
Eviction логика не гарантирует разнообразие netgroup при выборе кандидата на удаление. Атакующий может занять все 117 inbound слотов, используя адреса из разных /16 подсетей.
#### Анализ кода
**Код:** `eviction.rs:113-126`
```rust
// Find netgroup with most candidates (highest risk)
let netgroup_counts = count_by_netgroup(&candidates);
let worst_netgroup = netgroup_counts
.iter()
.max_by_key(|(_, count)| *count)
.map(|(netgroup, _)| *netgroup)?;
// Evict youngest peer from worst netgroup
candidates
.iter()
.filter(|c| c.netgroup == worst_netgroup)
.max_by_key(|c| c.connected_at) // ← МОЛОДЕЙШИЙ, НО НЕ ГАРАНТИРУЕТ DIVERSITY
.map(|c| c.addr)
```
**Проблема:** Eviction выбирает из netgroup с наибольшим количеством кандидатов, но не гарантирует, что после eviction останется разнообразие. Если атакующий использует 2 IP из каждой /16 подсети, eviction не поможет.
#### Вектор атаки
```
1. Атакующий занимает все 117 inbound slots
- Использует 2 IP из каждой /16 подсети (MAX_PEERS_PER_NETGROUP = 2)
- Нужно: 117 / 2 = 59 различных /16 подсетей
- Атакующий контролирует 59+ /16 подсетей (возможно через BGP hijacking)
2. Gaming eviction через fake metrics
- Устанавливает низкую latency (protect_by_ping защищает 8)
- Отправляет транзакции (protect_by_recent_tx защищает 4)
- Отправляет slices (protect_by_recent_slice защищает 4)
- Долгосрочные соединения (protect_by_longevity защищает 8)
- Итого защищено: 28 слотов
3. Остается 117 - 28 = 89 слотов для атакующего
- Атакующий может занять все 89 слотов
- Честные peers не могут подключиться
```
#### Эксплуатация
**Шаг 1: Подготовка адресов**
```rust
// Атакующий готовит 59+ /16 подсетей с 2 IP каждая
let mut addresses = Vec::new();
for subnet in 0..59 {
let base = subnet * 256;
addresses.push((base, 1));
addresses.push((base, 2));
}
```
**Шаг 2: Установка соединений**
```rust
// Устанавливает соединения с fake metrics
for addr in addresses {
let connection = connect(victim, addr);
// Fake низкая latency
respond_to_ping_with_low_latency(connection);
// Отправляет транзакции для защиты
send_transaction(connection);
// Отправляет slices для защиты
send_slice(connection);
}
```
**Шаг 3: Защита от eviction**
```rust
// Атакующий поддерживает метрики для защиты:
// - Низкая latency (< 50ms)
// - Недавние транзакции
// - Недавние slices
// - Долгосрочное соединение (поддерживает > 1 час)
```
#### Слабое место: Rate limit на новые соединения отсутствует
**Код:** `protocol.rs:316-334`
```rust
match listener.accept().await {
Ok((stream, addr)) => {
// Check if banned
if connections.is_banned(&addr).await {
continue;
}
// Check connection limits
if !connections.can_accept_inbound() {
continue;
}
// Check netgroup diversity
if !connections.can_connect(&addr).await {
continue;
}
// ← НЕТ RATE LIMIT НА НОВЫЕ СОЕДИНЕНИЯ
```
**Проблема:** Нет ограничения на скорость установки новых соединений. Атакующий может быстро установить все 117 соединений.
#### Рекомендации
1. **Добавить rate limit на новые соединения:**
- Ограничить количество новых соединений в секунду (например, 10/сек)
- Использовать token bucket для контроля скорости
2. **Улучшить eviction для защиты diversity:**
- Приоритизировать eviction peers из netgroups с избытком соединений
- Гарантировать минимум 1 peer из каждой netgroup после eviction
3. **Ужесточить проверку netgroup diversity:**
- Требовать минимум 25 различных /16 подсетей для inbound соединений
- Отклонять новые соединения, если разнообразие недостаточно
---
### 4. Sync DoS — ОТСУТСТВИЕ RATE LIMITING ДЛЯ HEADERS И GETSLICES
**Файлы:** `sync.rs`, `bootstrap.rs`
**Статус:** 🟠 ВЫСОКАЯ УЯЗВИМОСТЬ
#### Описание проблемы
Отсутствует rate limiting для `SliceHeaders` и `GetSlices` сообщений, что позволяет атакующему исчерпать CPU и bandwidth жертвы.
#### Анализ кода
**Код:** `protocol.rs:1035-1038`
```rust
Message::GetSlices { start, count } => {
let count = count.min(500); // Limit
let _ = event_tx.send(NetEvent::NeedSlices(peer.addr, start, count)).await;
}
```
**Проблема:** Нет rate limiting для `GetSlices`. Атакующий может отправлять множество запросов, заставляя жертву обрабатывать их.
**Код:** `sync.rs:129-167`
```rust
pub fn get_downloads(&mut self, peer: SocketAddr, max: usize) -> Vec<u64> {
// ...
while downloads.len() < available {
if let Some(idx) = self.download_queue.pop_front() {
// ...
downloads.push(idx);
}
}
downloads
}
```
**Проблема:** Нет ограничения на частоту запросов `GetSlices`. Атакующий может запрашивать slices быстрее, чем они обрабатываются.
#### Вектор атаки
**Headers flooding:**
```
1. Атакующий отправляет множество SliceHeaders сообщений
- Каждое сообщение содержит до 2000 headers (из Bitcoin Core)
- Нет rate limiting → атакующий может отправлять неограниченно
- Жертва должна валидировать каждый header (CPU exhaustion)
2. CPU exhaustion через валидацию headers
- Каждый header проверяется на валидность
- Проверка prev_hash, timestamp, signature
- Множество headers → высокая CPU нагрузка
```
**GetSlices amplification:**
```
1. Атакующий запрашивает множество slices через GetSlices
- count = 500 (максимум)
- Каждый slice до 4MB
- Потенциально: 500 × 4MB = 2GB данных на запрос
2. Bandwidth exhaustion
- Жертва должна отправить 2GB данных на каждый запрос
- Множество запросов → исчерпание bandwidth
```
#### Эксплуатация
**Шаг 1: Headers flooding**
```rust
// Атакующий отправляет множество SliceHeaders
let headers = generate_fake_headers(2000);
for _ in 0..100 {
send_slice_headers(victim, headers.clone());
// Жертва валидирует каждый header → CPU exhaustion
}
```
**Шаг 2: GetSlices amplification**
```rust
// Атакующий запрашивает множество slices
for i in 0..1000 {
send_get_slices(victim, start: i * 500, count: 500);
// Жертва отправляет 2GB данных на каждый запрос → bandwidth exhaustion
}
```
#### Слабое место: Отсутствие rate limiting для sync сообщений
**Код:** `protocol.rs:730-1064`
```rust
match msg {
// ...
Message::SliceHeaders(headers) => {
// ← НЕТ RATE LIMITING
}
Message::GetSlices { start, count } => {
// ← НЕТ RATE LIMITING
}
}
```
**Проблема:** `SliceHeaders` и `GetSlices` не имеют rate limiting, в отличие от `Addr`, `Inv`, `GetData`.
#### Рекомендации
1. **Добавить rate limiting для SliceHeaders:**
- Ограничить количество headers в секунду (например, 1000/сек)
- Использовать token bucket для контроля
2. **Добавить rate limiting для GetSlices:**
- Ограничить количество запросов в секунду (например, 1/сек)
- Ограничить общий размер запрашиваемых данных
3. **Улучшить валидацию headers:**
- Проверять headers пакетами, а не по одному
- Использовать параллельную валидацию для ускорения
---
### 5. Rate Limit Bypass — PER-CONNECTION LIMITING
**Файл:** `rate_limit.rs`
**Статус:** 🟠 ВЫСОКАЯ УЯЗВИМОСТЬ
#### Описание проблемы
Rate limiting применяется per-connection, а не per-IP. Атакующий может обойти ограничения, используя множественные соединения с одного IP.
#### Анализ кода
**Код:** `peer.rs` (предположительно)
```rust
pub struct Peer {
// ...
pub rate_limits: PeerRateLimits, // ← PER-PEER RATE LIMITS
}
```
**Проблема:** Каждый peer имеет свой собственный набор rate limiters. Атакующий может установить множество соединений и использовать полный лимит на каждом.
**Код:** `rate_limit.rs:194-214`
```rust
pub struct PeerRateLimits {
pub addr: AddrRateLimiter,
pub inv: InvRateLimiter,
pub getdata: GetDataRateLimiter,
}
```
**Проблема:** Нет глобального per-IP rate limiting. Атакующий может установить 117 соединений и использовать 117× лимит.
#### Вектор атаки
```
1. Атакующий устанавливает множество соединений
- MAX_INBOUND = 117
- Каждое соединение имеет свой rate limiter
- Атакующий может использовать полный лимит на каждом
2. Обход rate limits
- Addr: 1000 burst × 117 = 117,000 адресов
- Inv: 5000 burst × 117 = 585,000 items
- GetData: 1000 burst × 117 = 117,000 items
3. Исчерпание ресурсов
- Addr flooding → заполнение AddrMan
- Inv flooding → исчерпание памяти
- GetData flooding → исчерпание bandwidth
```
#### Эксплуатация
**Шаг 1: Установка множественных соединений**
```rust
// Атакующий устанавливает 117 соединений
let mut connections = Vec::new();
for i in 0..117 {
let conn = connect(victim, attacker_ip);
connections.push(conn);
}
```
**Шаг 2: Использование полного лимита на каждом**
```rust
// Каждое соединение использует полный burst лимит
for conn in connections {
// Addr: 1000 адресов
send_addr(conn, generate_addresses(1000));
// Inv: 5000 items
send_inv(conn, generate_inv_items(5000));
// GetData: 1000 items
send_getdata(conn, generate_getdata_items(1000));
}
```
#### Слабое место: Отсутствие per-IP агрегации
**Код:** `connection.rs:189-225`
```rust
pub struct ConnectionManager {
// ...
// ← НЕТ TRACKING RATE LIMITS PER IP
}
```
**Проблема:** `ConnectionManager` не отслеживает rate limits per-IP. Каждое соединение имеет независимые лимиты.
#### Рекомендации
1. **Добавить per-IP rate limiting:**
- Отслеживать rate limits по IP адресу, а не по соединению
- Агрегировать лимиты для всех соединений с одного IP
2. **Ограничить количество соединений per-IP:**
- Разрешить максимум 2 соединения с одного IP
- Это предотвратит обход rate limits через множественные соединения
3. **Улучшить token bucket:**
- Использовать shared token bucket для всех соединений с одного IP
- Гарантировать, что общий лимит не превышается
---
## Чеклист проверок
- [ ] Eclipse: netgroup diversity работает — ⚠️ ЧАСТИЧНО (не защищает при первом выборе)
- [ ] Eclipse: anchor connections есть — ❌ ОТСУТСТВУЕТ
- [ ] Memory: flow control до allocation — ⚠️ ЧАСТИЧНО (проверяется до read_message, но память выделяется внутри)
- [ ] Memory: orphan pool bounded — ✅ ЕСТЬ (MAX_ORPHANS = 100)
- [ ] Memory: все collections bounded — ✅ ЕСТЬ (проверено)
- [ ] Slots: eviction защищает diversity — ⚠️ ЧАСТИЧНО (не гарантирует разнообразие после eviction)
- [ ] Sync: headers rate limited — ❌ ОТСУТСТВУЕТ
- [ ] Sync: GetSlices rate limited — ❌ ОТСУТСТВУЕТ
- [ ] Rate: все messages covered — ❌ НЕТ (SliceHeaders, GetSlices не покрыты)
- [ ] Rate: per-IP limiting — ❌ ОТСУТСТВУЕТ (только per-connection)
---
## Заключение
Обнаружены критические и высокие уязвимости в сетевом слое Montana:
1. **Критические:**
- Отсутствие anchor connections делает узел уязвимым к Eclipse атакам при рестарте
- Flow control не предотвращает выделение памяти до проверки, что позволяет исчерпать память
2. **Высокие:**
- Eviction не гарантирует разнообразие netgroup
- Отсутствие rate limiting для sync сообщений позволяет DoS атаки
- Per-connection rate limiting позволяет обход через множественные соединения
Рекомендуется немедленно исправить критические уязвимости и приоритизировать исправление высоких уязвимостей.