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

28 KiB
Raw Blame 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:127save() сохраняет только адреса в файл
    • addrman.rs:107load() загружает адреса при старте
    • Проблема: При рестарте все соединения теряются, узел начинает с нуля
  2. ConnectionManager не сохраняет активные соединения:

    • connection.rs:189-225ConnectionManager не имеет механизма сохранения активных соединений
    • connection.rs:228-238 — сохраняются только баны, но не соединения
    • Проблема: При рестарте узел теряет все outbound соединения
  3. Protocol не имеет anchor механизма:

    • protocol.rs:410-413select() выбирает адреса случайно из AddrMan
    • protocol.rs:395-512connection_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

// Атакующий отправляет 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

// Атакующий устанавливает соединения и завершает handshake
for addr in attacker_addresses {
    connect(victim, addr);
    send_version();
    receive_verack();
    // mark_good() вызывается автоматически
}

Шаг 3: Ожидание рестарта

// После рестарта жертвы:
// 1. AddrMan загружается из addresses.dat
// 2. Все соединения потеряны
// 3. connection_loop() начинает выбирать адреса заново
// 4. Высокая вероятность выбора адресов атакующего

Слабое место: Netgroup diversity не защищает при первом выборе

Код: connection.rs:253-258

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-248select() должен учитывать текущие соединения
    • Избегать выбора адресов из подсетей, где уже есть соединения

2. Memory Exhaustion — FLOW CONTROL ОБХОДИМ

Файлы: protocol.rs, sync.rs

Статус: 🔴 КРИТИЧЕСКАЯ УЯЗВИМОСТЬ

Описание проблемы

Flow control проверяется ПОСЛЕ чтения сообщения, что позволяет атакующему исчерпать память жертвы до применения ограничений.

Анализ кода

Код: protocol.rs:645-671

// 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

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: Подготовка атаки

// Атакующий устанавливает 117 inbound соединений
for i in 0..117 {
    let stream = connect(victim);
    spawn_connection(stream);
}

Шаг 2: Отправка больших сообщений

// Каждое соединение отправляет сообщение с 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

// Отправка 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

// 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-324add() должен проверять общий размер перед добавлением
    • Ограничить не только количество, но и общий размер orphans

TIER 1: ВЫСОКИЕ УЯЗВИМОСТИ

3. Connection Slot Exhaustion — EVICTION НЕ ЗАЩИЩАЕТ DIVERSITY

Файлы: connection.rs, eviction.rs

Статус: 🟠 ВЫСОКАЯ УЯЗВИМОСТЬ

Описание проблемы

Eviction логика не гарантирует разнообразие netgroup при выборе кандидата на удаление. Атакующий может занять все 117 inbound слотов, используя адреса из разных /16 подсетей.

Анализ кода

Код: eviction.rs:113-126

// 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: Подготовка адресов

// Атакующий готовит 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: Установка соединений

// Устанавливает соединения с 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

// Атакующий поддерживает метрики для защиты:
// - Низкая latency (< 50ms)
// - Недавние транзакции
// - Недавние slices
// - Долгосрочное соединение (поддерживает > 1 час)

Слабое место: Rate limit на новые соединения отсутствует

Код: protocol.rs:316-334

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

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

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

// Атакующий отправляет множество SliceHeaders
let headers = generate_fake_headers(2000);
for _ in 0..100 {
    send_slice_headers(victim, headers.clone());
    // Жертва валидирует каждый header → CPU exhaustion
}

Шаг 2: GetSlices amplification

// Атакующий запрашивает множество 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

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 (предположительно)

pub struct Peer {
    // ...
    pub rate_limits: PeerRateLimits,  // ← PER-PEER RATE LIMITS
}

Проблема: Каждый peer имеет свой собственный набор rate limiters. Атакующий может установить множество соединений и использовать полный лимит на каждом.

Код: rate_limit.rs:194-214

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: Установка множественных соединений

// Атакующий устанавливает 117 соединений
let mut connections = Vec::new();
for i in 0..117 {
    let conn = connect(victim, attacker_ip);
    connections.push(conn);
}

Шаг 2: Использование полного лимита на каждом

// Каждое соединение использует полный 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

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 позволяет обход через множественные соединения

Рекомендуется немедленно исправить критические уязвимости и приоритизировать исправление высоких уязвимостей.