675 lines
28 KiB
Markdown
675 lines
28 KiB
Markdown
|
|
# Анализ уязвимостей сетевого слоя 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 позволяет обход через множественные соединения
|
|||
|
|
|
|||
|
|
Рекомендуется немедленно исправить критические уязвимости и приоритизировать исправление высоких уязвимостей.
|
|||
|
|
|