# Анализ уязвимостей сетевого слоя 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(reader: &mut R) -> Result { // 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 { // ... 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 позволяет обход через множественные соединения Рекомендуется немедленно исправить критические уязвимости и приоритизировать исправление высоких уязвимостей.