669 lines
27 KiB
Markdown
669 lines
27 KiB
Markdown
|
|
# Детальный анализ Eclipse Attack на сеть Montana
|
|||
|
|
|
|||
|
|
**Модель:** Composer 1
|
|||
|
|
**Компания:** Cursor
|
|||
|
|
**Дата:** 07.01.2026 14:15 UTC
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Резюме
|
|||
|
|
|
|||
|
|
Обнаружена **критическая уязвимость** к Eclipse атаке в сетевом слое Montana. Основная проблема: отсутствие механизма anchor connections между рестартами узла, что позволяет атакующему изолировать жертву от честной сети после перезапуска.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Что такое Eclipse Attack?
|
|||
|
|
|
|||
|
|
Eclipse атака — это изоляция узла от честной сети путем заполнения всех его соединений вредоносными узлами атакующего. После рестарта жертва подключается только к узлам атакующего и не может получить доступ к честной сети.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Анализ защиты от Eclipse в Montana
|
|||
|
|
|
|||
|
|
### 1. Netgroup Diversity — ЧАСТИЧНО РАБОТАЕТ
|
|||
|
|
|
|||
|
|
**Код:** `connection.rs:264-270`
|
|||
|
|
```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` пуст, проверка неэффективна
|
|||
|
|
|
|||
|
|
**Анализ:**
|
|||
|
|
- `MAX_PEERS_PER_NETGROUP = 2` — максимум 2 peers из одной /16 подсети
|
|||
|
|
- Для `MAX_OUTBOUND = 8` нужно минимум 4 различных /16 подсети
|
|||
|
|
- При рестарте `netgroup_counts` сбрасывается в пустое состояние
|
|||
|
|
- Первые 2 соединения из одной подсети проходят проверку
|
|||
|
|
- Атакующий может использовать 2 IP из каждой /16 подсети
|
|||
|
|
|
|||
|
|
**Слабое место:**
|
|||
|
|
```rust
|
|||
|
|
// При рестарте:
|
|||
|
|
netgroup_counts = HashMap::new() // ← ПУСТОЙ
|
|||
|
|
// Первое соединение: current = 0 < 2 ✅ ПРОХОДИТ
|
|||
|
|
// Второе соединение: current = 1 < 2 ✅ ПРОХОДИТ
|
|||
|
|
// Третье соединение: current = 2 < 2 ❌ ОТКЛОНЯЕТСЯ
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Anchor Connections — ОТСУТСТВУЕТ
|
|||
|
|
|
|||
|
|
**Код:** `protocol.rs:167-204`, `connection.rs:188-229`
|
|||
|
|
|
|||
|
|
**Статус:** ❌ **КРИТИЧЕСКАЯ УЯЗВИМОСТЬ**
|
|||
|
|
|
|||
|
|
**Анализ:**
|
|||
|
|
|
|||
|
|
1. **AddrMan сохраняет только адреса:**
|
|||
|
|
```rust
|
|||
|
|
// protocol.rs:171-179
|
|||
|
|
let addr_path = config.data_dir.join("addresses.dat");
|
|||
|
|
let addresses = if addr_path.exists() {
|
|||
|
|
AddrMan::load(&addr_path).unwrap_or_else(|e| {
|
|||
|
|
warn!("Failed to load addresses: {}", e);
|
|||
|
|
AddrMan::new()
|
|||
|
|
})
|
|||
|
|
} else {
|
|||
|
|
AddrMan::new()
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
- Сохраняются адреса из NEW и TRIED таблиц
|
|||
|
|
- **НЕ сохраняются активные соединения**
|
|||
|
|
- При рестарте все соединения теряются
|
|||
|
|
|
|||
|
|
2. **ConnectionManager не сохраняет соединения:**
|
|||
|
|
```rust
|
|||
|
|
// connection.rs:188-229
|
|||
|
|
pub struct ConnectionManager {
|
|||
|
|
outbound_count: AtomicUsize,
|
|||
|
|
inbound_count: AtomicUsize,
|
|||
|
|
netgroup_counts: Mutex<HashMap<u32, usize>>,
|
|||
|
|
// ← НЕТ МЕХАНИЗМА СОХРАНЕНИЯ СОЕДИНЕНИЙ
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- Сохраняются только баны (`ban_list`)
|
|||
|
|
- Активные соединения не сохраняются
|
|||
|
|
- При рестарте `netgroup_counts` пуст
|
|||
|
|
|
|||
|
|
3. **Protocol не приоритизирует предыдущие соединения:**
|
|||
|
|
```rust
|
|||
|
|
// protocol.rs:494-502
|
|||
|
|
let net_addr = {
|
|||
|
|
let mut addrman = addresses.write().await;
|
|||
|
|
match addrman.select() {
|
|||
|
|
Some(addr) => addr, // ← СЛУЧАЙНЫЙ ВЫБОР
|
|||
|
|
None => continue,
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
- `select()` выбирает случайно из AddrMan
|
|||
|
|
- Нет приоритизации предыдущих outbound соединений
|
|||
|
|
- Нет гарантии восстановления соединений после рестарта
|
|||
|
|
|
|||
|
|
**Вектор атаки:**
|
|||
|
|
```
|
|||
|
|
1. Атакующий заполняет AddrMan своими адресами
|
|||
|
|
2. Жертва перезапускается
|
|||
|
|
3. Все соединения теряются
|
|||
|
|
4. select() выбирает случайно из AddrMan
|
|||
|
|
5. Высокая вероятность выбора адресов атакующего
|
|||
|
|
6. Все outbound → к атакующему
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. Bucket Collision Handling — РАБОТАЕТ, НО НЕДОСТАТОЧНО
|
|||
|
|
|
|||
|
|
**Код:** `addrman.rs:154-163`, `201-205`
|
|||
|
|
|
|||
|
|
**Статус:** ✅ Работает, но не защищает от Eclipse
|
|||
|
|
|
|||
|
|
**Анализ:**
|
|||
|
|
|
|||
|
|
1. **NEW table collision handling:**
|
|||
|
|
```rust
|
|||
|
|
// addrman.rs:154-163
|
|||
|
|
if let Some(existing_idx) = self.new_table[idx] {
|
|||
|
|
if let Some(existing) = self.addrs.get(&existing_idx)
|
|||
|
|
&& !existing.is_terrible()
|
|||
|
|
{
|
|||
|
|
return false; // Keep existing good address
|
|||
|
|
}
|
|||
|
|
self.remove_from_new(existing_idx);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- Если слот занят хорошим адресом, новый адрес отклоняется
|
|||
|
|
- Если слот занят плохим адресом, он заменяется
|
|||
|
|
- **Проблема:** Атакующий может заполнить большинство buckets хорошими адресами
|
|||
|
|
|
|||
|
|
2. **TRIED table collision handling:**
|
|||
|
|
```rust
|
|||
|
|
// addrman.rs:201-205
|
|||
|
|
if let Some(existing_idx) = self.tried_table[idx] {
|
|||
|
|
self.move_to_new(existing_idx); // Move existing back to new
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- При коллизии существующий адрес перемещается обратно в NEW
|
|||
|
|
- **Проблема:** Если атакующий заполнил TRIED, честные адреса выталкиваются
|
|||
|
|
|
|||
|
|
3. **Bucket distribution:**
|
|||
|
|
```rust
|
|||
|
|
// addrman.rs:441-450
|
|||
|
|
fn get_new_bucket(&self, addr: &SocketAddr, source: Option<&SocketAddr>) -> usize {
|
|||
|
|
let mut hasher = SipHasher24::new_with_key(&self.key[..16].try_into().unwrap());
|
|||
|
|
hasher.write(&get_netgroup_bytes(addr));
|
|||
|
|
if let Some(src) = source {
|
|||
|
|
hasher.write(&get_netgroup_bytes(src));
|
|||
|
|
}
|
|||
|
|
(hasher.finish() as usize) % NEW_BUCKET_COUNT // 1024 buckets
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- Random key предотвращает предсказание buckets
|
|||
|
|
- Но атакующий может заполнить большинство buckets через множество адресов
|
|||
|
|
- NEW table: 1024 buckets × 64 слота = 65,536 адресов
|
|||
|
|
- Если атакующий заполняет 80% buckets → высокая вероятность выбора его адресов
|
|||
|
|
|
|||
|
|
**Слабое место:**
|
|||
|
|
- Коллизии защищают от замены хороших адресов плохими
|
|||
|
|
- Но не защищают от заполнения большинства buckets хорошими адресами атакующего
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. Select Logic — СЛУЧАЙНЫЙ ВЫБОР ИЗ ЗАПОЛНЕННОГО ADDRMAN
|
|||
|
|
|
|||
|
|
**Код:** `addrman.rs:223-248`, `511-545`, `547-569`
|
|||
|
|
|
|||
|
|
**Статус:** ⚠️ Работает корректно, но уязвим к Eclipse
|
|||
|
|
|
|||
|
|
**Анализ:**
|
|||
|
|
|
|||
|
|
1. **50/50 выбор между NEW и TRIED:**
|
|||
|
|
```rust
|
|||
|
|
// addrman.rs:233-247
|
|||
|
|
fn select_inner(&mut self, new_only: bool) -> Option<NetAddress> {
|
|||
|
|
let mut rng = ChaCha20Rng::from_entropy();
|
|||
|
|
let use_new = new_only || rng.gen_bool(0.5); // 50% chance
|
|||
|
|
|
|||
|
|
if use_new && self.new_count > 0 {
|
|||
|
|
self.select_from_new(&mut rng)
|
|||
|
|
} else if self.tried_count > 0 {
|
|||
|
|
self.select_from_tried(&mut rng)
|
|||
|
|
} else if self.new_count > 0 {
|
|||
|
|
self.select_from_new(&mut rng)
|
|||
|
|
} else {
|
|||
|
|
None
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- Случайный выбор между NEW и TRIED таблицами
|
|||
|
|
- **Проблема:** Если обе таблицы заполнены адресами атакующего, выбор будет из них
|
|||
|
|
|
|||
|
|
2. **Случайный выбор из buckets:**
|
|||
|
|
```rust
|
|||
|
|
// addrman.rs:519-544
|
|||
|
|
for _ in 0..1000 {
|
|||
|
|
let bucket = rng.gen_range(0..NEW_BUCKET_COUNT); // Random bucket
|
|||
|
|
let pos = rng.gen_range(0..BUCKET_SIZE); // Random position
|
|||
|
|
let idx = bucket * BUCKET_SIZE + pos;
|
|||
|
|
|
|||
|
|
if let Some(addr_idx) = self.new_table[idx]
|
|||
|
|
&& let Some(info) = self.addrs.get(&addr_idx)
|
|||
|
|
{
|
|||
|
|
// Skip if connected, terrible, or too old
|
|||
|
|
if self.connected.contains(&socket_addr) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
if info.is_terrible() || info.addr.timestamp < horizon {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
return Some(info.addr.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
- Случайный выбор bucket и позиции
|
|||
|
|
- До 1000 попыток найти валидный адрес
|
|||
|
|
- **Проблема:** Если большинство buckets заполнены адресами атакующего, высока вероятность выбора его адресов
|
|||
|
|
|
|||
|
|
**Вектор атаки:**
|
|||
|
|
```
|
|||
|
|
1. Атакующий заполняет 80% buckets в NEW table
|
|||
|
|
2. Атакующий заполняет 80% buckets в TRIED table
|
|||
|
|
3. При select():
|
|||
|
|
- 50% вероятность выбрать из NEW → 80% вероятность выбрать адрес атакующего
|
|||
|
|
- 50% вероятность выбрать из TRIED → 80% вероятность выбрать адрес атакующего
|
|||
|
|
4. Общая вероятность выбора адреса атакующего: ~80%
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 5. Feeler Connections — СЛИШКОМ МЕДЛЕННО
|
|||
|
|
|
|||
|
|
**Код:** `feeler.rs:15-122`
|
|||
|
|
|
|||
|
|
**Статус:** ✅ Работает, но недостаточно для защиты
|
|||
|
|
|
|||
|
|
**Анализ:**
|
|||
|
|
|
|||
|
|
1. **Feeler interval:**
|
|||
|
|
```rust
|
|||
|
|
// feeler.rs:15-16
|
|||
|
|
pub const FEELER_INTERVAL: Duration = Duration::from_secs(120); // 2 минуты
|
|||
|
|
pub const MAX_CONCURRENT_FEELERS: usize = 1; // Только 1 feeler одновременно
|
|||
|
|
```
|
|||
|
|
- Один feeler каждые 2 минуты
|
|||
|
|
- **Проблема:** Слишком медленно для защиты от Eclipse
|
|||
|
|
- Для проверки 1000 адресов нужно: 1000 × 2 минуты = 2000 минут = 33 часа
|
|||
|
|
|
|||
|
|
2. **Feeler валидирует только NEW table:**
|
|||
|
|
```rust
|
|||
|
|
// feeler.rs:110-114
|
|||
|
|
let addr = {
|
|||
|
|
let mut am = addrman.lock().await;
|
|||
|
|
am.select_with_option(true).map(|a| a.socket_addr()) // new_only = true
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
- Проверяет только адреса из NEW table
|
|||
|
|
- Успешные feeler перемещают адреса в TRIED
|
|||
|
|
- **Проблема:** Не проверяет адреса в TRIED table
|
|||
|
|
|
|||
|
|
**Слабое место:**
|
|||
|
|
- Feeler слишком медленный для защиты от Eclipse
|
|||
|
|
- Не проверяет адреса в TRIED table
|
|||
|
|
- Атакующий может заполнить TRIED через fake connections быстрее, чем feeler проверит их
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Детальный вектор Eclipse атаки
|
|||
|
|
|
|||
|
|
### Фаза 1: Заполнение NEW table
|
|||
|
|
|
|||
|
|
**Цель:** Заполнить большинство buckets в NEW table вредоносными адресами
|
|||
|
|
|
|||
|
|
**Метод:**
|
|||
|
|
```rust
|
|||
|
|
// Атакующий отправляет Addr сообщения с множеством адресов
|
|||
|
|
// Каждый адрес попадает в случайный bucket через SipHash
|
|||
|
|
for i in 0..50000 {
|
|||
|
|
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]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NEW table: 1024 buckets × 64 слота = 65,536 адресов
|
|||
|
|
// Атакующий заполняет 50,000 адресов → ~76% buckets заполнены
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Результат:**
|
|||
|
|
- Большинство buckets в NEW table содержат адреса атакующего
|
|||
|
|
- Честные адреса занимают только ~24% buckets
|
|||
|
|
- При `select_from_new()` высока вероятность выбора адреса атакующего
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Фаза 2: Продвижение в TRIED table
|
|||
|
|
|
|||
|
|
**Цель:** Переместить адреса из NEW в TRIED через fake connections
|
|||
|
|
|
|||
|
|
**Метод:**
|
|||
|
|
```rust
|
|||
|
|
// Атакующий устанавливает соединения и завершает handshake
|
|||
|
|
for addr in attacker_addresses_from_new_table {
|
|||
|
|
let stream = connect(victim, addr);
|
|||
|
|
|
|||
|
|
// Завершаем handshake
|
|||
|
|
send_version(stream);
|
|||
|
|
receive_verack(stream);
|
|||
|
|
|
|||
|
|
// mark_good() вызывается автоматически
|
|||
|
|
// Адрес перемещается из NEW в TRIED
|
|||
|
|
close_connection(stream);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// После множества успешных соединений:
|
|||
|
|
// - Адреса атакующего перемещаются в TRIED
|
|||
|
|
// - Честные адреса остаются в NEW или выталкиваются
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Результат:**
|
|||
|
|
- Большинство адресов в TRIED table принадлежат атакующему
|
|||
|
|
- Честные адреса выталкиваются из TRIED при коллизиях
|
|||
|
|
- При `select_from_tried()` высока вероятность выбора адреса атакующего
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Фаза 3: Ожидание рестарта
|
|||
|
|
|
|||
|
|
**Цель:** Дождаться рестарта жертвы для активации атаки
|
|||
|
|
|
|||
|
|
**Метод:**
|
|||
|
|
```rust
|
|||
|
|
// Атакующий мониторит жертву
|
|||
|
|
while victim_is_online {
|
|||
|
|
// Поддерживаем соединения
|
|||
|
|
// Отправляем валидные сообщения
|
|||
|
|
// Избегаем подозрительного поведения
|
|||
|
|
wait_for_restart();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// При рестарте:
|
|||
|
|
// 1. Все соединения теряются
|
|||
|
|
// 2. AddrMan загружается из addresses.dat
|
|||
|
|
// 3. netgroup_counts сбрасывается
|
|||
|
|
// 4. connection_loop() начинает выбирать адреса заново
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Результат:**
|
|||
|
|
- При рестарте все соединения теряются
|
|||
|
|
- AddrMan загружается с адресами атакующего
|
|||
|
|
- Нет anchor connections для восстановления честных соединений
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Фаза 4: Захват outbound соединений
|
|||
|
|
|
|||
|
|
**Цель:** Заставить жертву подключиться только к узлам атакующего
|
|||
|
|
|
|||
|
|
**Метод:**
|
|||
|
|
```rust
|
|||
|
|
// После рестарта жертвы:
|
|||
|
|
// connection_loop() начинает выбирать адреса из AddrMan
|
|||
|
|
|
|||
|
|
for i in 0..MAX_OUTBOUND { // MAX_OUTBOUND = 8
|
|||
|
|
let addr = addrman.select(); // Случайный выбор
|
|||
|
|
|
|||
|
|
// Вероятность выбора адреса атакующего: ~80%
|
|||
|
|
if addr.is_attacker() {
|
|||
|
|
accept_connection(victim, addr);
|
|||
|
|
// Жертва подключается к атакующему
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Результат:
|
|||
|
|
// - Все 8 outbound соединений → к атакующему
|
|||
|
|
// - Жертва изолирована от честной сети
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Результат:**
|
|||
|
|
- Все outbound соединения направлены к атакующему
|
|||
|
|
- Жертва не может получить доступ к честной сети
|
|||
|
|
- Eclipse атака успешна
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Эксплуатация Eclipse атаки
|
|||
|
|
|
|||
|
|
### Шаг 1: Подготовка адресов
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// Атакующий готовит множество адресов из разных /16 подсетей
|
|||
|
|
// Для обхода MAX_PEERS_PER_NETGROUP = 2 использует 2 IP из каждой подсети
|
|||
|
|
|
|||
|
|
let mut attacker_addresses = Vec::new();
|
|||
|
|
for subnet in 0..1000 { // 1000 различных /16 подсетей
|
|||
|
|
let base = subnet * 256;
|
|||
|
|
attacker_addresses.push((base, 1)); // Первый IP
|
|||
|
|
attacker_addresses.push((base, 2)); // Второй IP
|
|||
|
|
}
|
|||
|
|
// Итого: 2000 адресов из 1000 подсетей
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Шаг 2: Заполнение AddrMan
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// Отправка Addr сообщений с адресами атакующего
|
|||
|
|
// Используем rate limiting: 1000 burst, 0.1/sec sustained
|
|||
|
|
|
|||
|
|
let mut sent = 0;
|
|||
|
|
for batch in attacker_addresses.chunks(1000) {
|
|||
|
|
send_addr_message(victim, batch);
|
|||
|
|
sent += batch.len();
|
|||
|
|
|
|||
|
|
// Ждем refill token bucket (0.1/sec)
|
|||
|
|
if sent >= 1000 {
|
|||
|
|
wait_for_refill();
|
|||
|
|
sent = 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Результат: AddrMan заполнен адресами атакующего
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Шаг 3: Продвижение в TRIED
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// Установка fake connections для перемещения адресов в TRIED
|
|||
|
|
// Используем MAX_CONNECTIONS_PER_IP = 2 для обхода ограничений
|
|||
|
|
|
|||
|
|
for subnet in 0..1000 {
|
|||
|
|
let addr1 = (subnet * 256, 1);
|
|||
|
|
let addr2 = (subnet * 256, 2);
|
|||
|
|
|
|||
|
|
// Первое соединение
|
|||
|
|
connect_and_handshake(victim, addr1);
|
|||
|
|
// mark_good() → перемещение в TRIED
|
|||
|
|
|
|||
|
|
// Второе соединение
|
|||
|
|
connect_and_handshake(victim, addr2);
|
|||
|
|
// mark_good() → перемещение в TRIED
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Результат: Большинство адресов в TRIED принадлежат атакующему
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Шаг 4: Ожидание рестарта
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// Атакующий поддерживает соединения и ждет рестарта
|
|||
|
|
while victim_is_online {
|
|||
|
|
// Отправляем валидные сообщения
|
|||
|
|
send_ping();
|
|||
|
|
send_slice();
|
|||
|
|
send_transaction();
|
|||
|
|
|
|||
|
|
// Избегаем подозрительного поведения
|
|||
|
|
avoid_ban();
|
|||
|
|
|
|||
|
|
// Мониторим рестарт
|
|||
|
|
if victim_restarted() {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Шаг 5: Захват после рестарта
|
|||
|
|
|
|||
|
|
```rust
|
|||
|
|
// После рестарта жертвы:
|
|||
|
|
// 1. Все соединения потеряны
|
|||
|
|
// 2. AddrMan загружен с адресами атакующего
|
|||
|
|
// 3. connection_loop() начинает выбирать адреса
|
|||
|
|
|
|||
|
|
// Жертва пытается установить 8 outbound соединений
|
|||
|
|
for i in 0..8 {
|
|||
|
|
let addr = addrman.select(); // ~80% вероятность выбрать адрес атакующего
|
|||
|
|
|
|||
|
|
if addr.is_attacker() {
|
|||
|
|
accept_connection(victim, addr);
|
|||
|
|
// Жертва подключается к атакующему
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Результат: Все outbound → к атакующему
|
|||
|
|
// Eclipse атака успешна!
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Критические слабые места
|
|||
|
|
|
|||
|
|
### 1. Отсутствие Anchor Connections
|
|||
|
|
|
|||
|
|
**Проблема:**
|
|||
|
|
- При рестарте все соединения теряются
|
|||
|
|
- Нет механизма восстановления предыдущих outbound соединений
|
|||
|
|
- Узел начинает выбирать адреса заново из AddrMan
|
|||
|
|
|
|||
|
|
**Влияние:**
|
|||
|
|
- Высокая уязвимость к Eclipse атаке после рестарта
|
|||
|
|
- Атакующий может заполнить AddrMan и захватить все соединения
|
|||
|
|
|
|||
|
|
**Рекомендация:**
|
|||
|
|
- Реализовать anchor connections механизм
|
|||
|
|
- Сохранять последние 8 outbound соединений в файл
|
|||
|
|
- При рестарте приоритизировать эти адреса для переподключения
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Netgroup Diversity не защищает при первом выборе
|
|||
|
|
|
|||
|
|
**Проблема:**
|
|||
|
|
- При рестарте `netgroup_counts` пуст
|
|||
|
|
- Первые 2 соединения из одной подсети проходят проверку
|
|||
|
|
- Атакующий может использовать 2 IP из каждой подсети
|
|||
|
|
|
|||
|
|
**Влияние:**
|
|||
|
|
- Атакующий может обойти netgroup diversity при первом выборе
|
|||
|
|
- Достаточно 4 различных /16 подсетей для 8 outbound соединений
|
|||
|
|
|
|||
|
|
**Рекомендация:**
|
|||
|
|
- Требовать минимум 4 различных /16 подсетей для первых outbound соединений
|
|||
|
|
- Не позволять выбирать адреса из одной подсети до достижения разнообразия
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. Select Logic уязвим к заполнению AddrMan
|
|||
|
|
|
|||
|
|
**Проблема:**
|
|||
|
|
- Случайный выбор из заполненного AddrMan
|
|||
|
|
- Если большинство адресов принадлежат атакующему, высока вероятность выбора его адресов
|
|||
|
|
- Нет приоритизации честных адресов
|
|||
|
|
|
|||
|
|
**Влияние:**
|
|||
|
|
- При заполнении 80% AddrMan адресами атакующего, вероятность выбора его адресов ~80%
|
|||
|
|
- Eclipse атака становится вероятной
|
|||
|
|
|
|||
|
|
**Рекомендация:**
|
|||
|
|
- Добавить приоритизацию адресов по источнику (DNS seeds, hardcoded)
|
|||
|
|
- Ограничить количество адресов от одного источника
|
|||
|
|
- Использовать разнообразие источников при выборе
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. Feeler Connections слишком медленные
|
|||
|
|
|
|||
|
|
**Проблема:**
|
|||
|
|
- Один feeler каждые 2 минуты
|
|||
|
|
- Для проверки 1000 адресов нужно 33 часа
|
|||
|
|
- Не проверяет адреса в TRIED table
|
|||
|
|
|
|||
|
|
**Влияние:**
|
|||
|
|
- Атакующий может заполнить AddrMan быстрее, чем feeler проверит адреса
|
|||
|
|
- Недостаточно для защиты от Eclipse атаки
|
|||
|
|
|
|||
|
|
**Рекомендация:**
|
|||
|
|
- Увеличить количество concurrent feelers
|
|||
|
|
- Уменьшить feeler interval
|
|||
|
|
- Добавить проверку адресов в TRIED table
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Защита от Eclipse атаки
|
|||
|
|
|
|||
|
|
### Текущие механизмы защиты:
|
|||
|
|
|
|||
|
|
1. ✅ **Netgroup diversity** — работает для активных соединений
|
|||
|
|
2. ✅ **Bucket collision handling** — защищает от замены хороших адресов
|
|||
|
|
3. ✅ **Feeler connections** — валидирует адреса, но слишком медленно
|
|||
|
|
4. ✅ **Per-IP limits** — ограничивает количество соединений с одного IP
|
|||
|
|
5. ✅ **Rate limiting** — ограничивает скорость заполнения AddrMan
|
|||
|
|
|
|||
|
|
### Отсутствующие механизмы защиты:
|
|||
|
|
|
|||
|
|
1. ❌ **Anchor connections** — критическая уязвимость
|
|||
|
|
2. ❌ **Приоритизация источников** — нет приоритизации DNS seeds/hardcoded
|
|||
|
|
3. ❌ **Разнообразие источников** — нет требования разнообразия источников
|
|||
|
|
4. ❌ **Быстрая валидация** — feeler слишком медленный
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Выводы
|
|||
|
|
|
|||
|
|
### Критическая уязвимость: Eclipse Attack возможна
|
|||
|
|
|
|||
|
|
**Основные причины:**
|
|||
|
|
1. Отсутствие anchor connections между рестартами
|
|||
|
|
2. Netgroup diversity не защищает при первом выборе после рестарта
|
|||
|
|
3. Select logic уязвим к заполнению AddrMan адресами атакующего
|
|||
|
|
4. Feeler connections слишком медленные для защиты
|
|||
|
|
|
|||
|
|
**Вероятность успешной атаки:**
|
|||
|
|
- При заполнении 80% AddrMan адресами атакующего: ~80%
|
|||
|
|
- При заполнении 90% AddrMan адресами атакующего: ~90%
|
|||
|
|
|
|||
|
|
**Сложность атаки:**
|
|||
|
|
- Средняя: требует контроля множества IP адресов и подсетей
|
|||
|
|
- Время подготовки: несколько дней для заполнения AddrMan
|
|||
|
|
- Триггер: рестарт жертвы (может быть спровоцирован)
|
|||
|
|
|
|||
|
|
**Рекомендации:**
|
|||
|
|
1. **Немедленно:** Реализовать anchor connections механизм
|
|||
|
|
2. **Высокий приоритет:** Улучшить netgroup diversity при первом выборе
|
|||
|
|
3. **Средний приоритет:** Добавить приоритизацию источников адресов
|
|||
|
|
4. **Низкий приоритет:** Ускорить feeler connections
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Дополнительные векторы атаки
|
|||
|
|
|
|||
|
|
### Вектор 1: BGP Hijacking
|
|||
|
|
|
|||
|
|
**Описание:**
|
|||
|
|
- Атакующий использует BGP hijacking для контроля множества /16 подсетей
|
|||
|
|
- Заполняет AddrMan адресами из контролируемых подсетей
|
|||
|
|
- После рестарта жертва подключается к контролируемым подсетям
|
|||
|
|
|
|||
|
|
**Сложность:** Высокая (требует BGP access)
|
|||
|
|
**Вероятность:** Средняя (при наличии BGP access)
|
|||
|
|
|
|||
|
|
### Вектор 2: Sybil Attack + Eclipse
|
|||
|
|
|
|||
|
|
**Описание:**
|
|||
|
|
- Атакующий создает множество Sybil узлов
|
|||
|
|
- Заполняет AddrMan адресами Sybil узлов
|
|||
|
|
- После рестарта жертва подключается только к Sybil узлам
|
|||
|
|
|
|||
|
|
**Сложность:** Средняя (требует множество IP адресов)
|
|||
|
|
**Вероятность:** Высокая (при наличии множества IP)
|
|||
|
|
|
|||
|
|
### Вектор 3: DNS Seed Poisoning
|
|||
|
|
|
|||
|
|
**Описание:**
|
|||
|
|
- Атакующий компрометирует DNS seeds
|
|||
|
|
- DNS seeds возвращают адреса атакующего
|
|||
|
|
- Жертва загружает адреса атакующего при bootstrap
|
|||
|
|
|
|||
|
|
**Сложность:** Высокая (требует компрометации DNS)
|
|||
|
|
**Вероятность:** Низкая (DNS seeds защищены)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Заключение
|
|||
|
|
|
|||
|
|
Сеть Montana **уязвима к Eclipse атаке** из-за отсутствия механизма anchor connections и недостаточной защиты при первом выборе адресов после рестарта. Рекомендуется немедленно реализовать anchor connections и улучшить механизмы защиты от Eclipse атаки.
|
|||
|
|
|