28 KiB
Анализ уязвимостей сетевого слоя 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 атак.
Анализ кода
-
AddrMan сохраняет адреса, но не соединения:
addrman.rs:127—save()сохраняет только адреса в файлaddrman.rs:107—load()загружает адреса при старте- Проблема: При рестарте все соединения теряются, узел начинает с нуля
-
ConnectionManager не сохраняет активные соединения:
connection.rs:189-225—ConnectionManagerне имеет механизма сохранения активных соединенийconnection.rs:228-238— сохраняются только баны, но не соединения- Проблема: При рестарте узел теряет все outbound соединения
-
Protocol не имеет anchor механизма:
protocol.rs:410-413—select()выбирает адреса случайно из AddrManprotocol.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
// Атакующий отправляет 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 подсети.
Рекомендации
-
Реализовать anchor connections:
- Сохранять последние 8 outbound соединений в файл
- При рестарте приоритизировать эти адреса для переподключения
- Гарантировать, что хотя бы 4 anchor соединения восстановятся
-
Улучшить netgroup diversity при старте:
- Требовать минимум 4 различных /16 подсетей для первых outbound соединений
- Не позволять выбирать адреса из одной подсети до достижения разнообразия
-
Добавить проверку разнообразия в select():
addrman.rs:223-248—select()должен учитывать текущие соединения- Избегать выбора адресов из подсетей, где уже есть соединения
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() вызывается ПОСЛЕ того, как память уже выделена. Если атакующий отправляет сообщения быстрее, чем они обрабатываются, память накапливается.
Рекомендации
-
Проверять flow control ПЕРЕД выделением памяти:
- Проверять
should_pause_recv()передread_exact()для payload - Если очередь переполнена, читать только заголовок и отбрасывать payload
- Проверять
-
Ограничить размер payload до чтения:
- После чтения
length, проверить flow control - Если очередь переполнена, отбросить соединение до чтения payload
- После чтения
-
Улучшить 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
// 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 соединений.
Рекомендации
-
Добавить rate limit на новые соединения:
- Ограничить количество новых соединений в секунду (например, 10/сек)
- Использовать token bucket для контроля скорости
-
Улучшить eviction для защиты diversity:
- Приоритизировать eviction peers из netgroups с избытком соединений
- Гарантировать минимум 1 peer из каждой netgroup после eviction
-
Ужесточить проверку 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.
Рекомендации
-
Добавить rate limiting для SliceHeaders:
- Ограничить количество headers в секунду (например, 1000/сек)
- Использовать token bucket для контроля
-
Добавить rate limiting для GetSlices:
- Ограничить количество запросов в секунду (например, 1/сек)
- Ограничить общий размер запрашиваемых данных
-
Улучшить валидацию 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. Каждое соединение имеет независимые лимиты.
Рекомендации
-
Добавить per-IP rate limiting:
- Отслеживать rate limits по IP адресу, а не по соединению
- Агрегировать лимиты для всех соединений с одного IP
-
Ограничить количество соединений per-IP:
- Разрешить максимум 2 соединения с одного IP
- Это предотвратит обход rate limits через множественные соединения
-
Улучшить 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:
-
Критические:
- Отсутствие anchor connections делает узел уязвимым к Eclipse атакам при рестарте
- Flow control не предотвращает выделение памяти до проверки, что позволяет исчерпать память
-
Высокие:
- Eviction не гарантирует разнообразие netgroup
- Отсутствие rate limiting для sync сообщений позволяет DoS атаки
- Per-connection rate limiting позволяет обход через множественные соединения
Рекомендуется немедленно исправить критические уязвимости и приоритизировать исправление высоких уязвимостей.