Resumen
Un nodo de Monero mantiene un número máximo de conexiones salientes hacia otros pares (12 por defecto). El fallo estaba en cómo calculaba cuántas tenía abiertas: además de las conexiones salientes completas (con el handshake terminado), contaba también las conexiones temporales que abre solo para enviar una petición Ping a quien le ha mandado un Handshake.
El nodo usa dos cifras distintas:
get_outgoing_connections_count(), que calcula el número de conexiones salientes en tiempo real.m_current_number_of_out_peers, una variable que el hilomPeersLoggerThreadactualiza una vez por segundo y que incluye esas conexiones temporales de Ping.
La función try_to_connect_and_handshake_with_new_peer() decide con la segunda cifra. Si la ve por encima del máximo, cierra al azar una conexión saliente existente, aunque sea legítima. Este es el fragmento que cita el investigador, con la línea de traza que él añadió para observarlo:
else if (zone.m_current_number_of_out_peers > zone.m_config.m_net_config.max_out_connection_count)
{
MGINFO("in try_to_connect_and_handshake_with_new_peer, current out count " << zone.m_current_number_of_out_peers << " is more than 12, del_out_connections"); //add this code
zone.m_net_server.get_config_object().del_out_connections(1);
--(zone.m_current_number_of_out_peers); // atomic variable, update time = 1s
return false;
}
Como cada Handshake entrante obliga al nodo a abrir una conexión saliente hacia la IP de origen para enviarle un Ping, un atacante con muchas IP puede inflar m_current_number_of_out_peers muy por encima de 12. El investigador llama a esta técnica Handshake Flood. Afecta a la v0.18.4.0 y anteriores.
Pasos de reproducción
El ataque explota el desfase entre las dos cifras y se hace por rondas:
- El nodo víctima cierra una de sus conexiones salientes por motivos normales: la rutina
update_sync_search, que se ejecuta cada 101 segundos, o porque un par no responde a tiempo a una petición Timed Sync. Ahora tiene menos de 12. - El atacante lanza el Handshake Flood: envía peticiones Handshake al nodo desde muchas IP distintas, acepta las conexiones que el nodo abre hacia ellas y responde a sus Ping. Cuando
mPeersLoggerThreadhace su actualización de cada segundo, guarda enm_current_number_of_out_peersun valor enorme (en las pruebas, 362). - El atacante para. Durante esa pausa,
connections_maker(), que se ejecuta cada segundo, consulta el recuento en tiempo real, ve menos de 12 conexiones salientes e intenta abrir otras nuevas. - Al intentarlo,
try_to_connect_and_handshake_with_new_peer()consulta el valor inflado, cree que el nodo se ha pasado del límite y cierra una conexión saliente legítima. Comoconnections_maker()la llama varias veces seguidas, cae una conexión tras otra. - Se repite el ciclo.
La pausa del paso 3 es imprescindible: si la inundación no se detiene, el recuento en tiempo real nunca baja del límite y el nodo no intenta abrir conexiones nuevas.
En la prueba del investigador, el nodo víctima ejecutaba la v0.18.4.0 en una máquina de 4 núcleos y 13 GB de RAM, y el ataque se lanzaba desde 1.000 IP. Tras cada ráfaga de Handshakes, el programa atacante esperaba 5 segundos y volvía a empezar. Tras unos tres minutos de ataque (de 12:05 a 12:08 del 9 de junio de 2025), el nodo se había quedado sin ninguna conexión saliente.
Para verlo, el investigador añadió trazas al código del nodo. Este fragmento de su registro muestra el momento clave:
2025-06-09 12:06:00.585 I in mPeersLoggerThread, Update m_current_number_of_out_peers: 362
2025-06-09 12:06:01.464 I in get_outgoing_connections_count(), out conn count is : 11
2025-06-09 12:06:01.471 I in try_to_connect_and_handshake_with_new_peer, current out count 362 is more than 12, del_out_connections
2025-06-09 12:06:01.477 I in try_to_connect_and_handshake_with_new_peer, current out count 361 is more than 12, del_out_connections
2025-06-09 12:06:01.484 I in try_to_connect_and_handshake_with_new_peer, current out count 360 is more than 12, del_out_connections
2025-06-09 12:06:01.485 I in get_outgoing_connections_count(), out conn count is : 8
2025-06-09 12:06:01.485 I in try_to_connect_and_handshake_with_new_peer, current out count 359 is more than 12, del_out_connections
2025-06-09 12:06:01.486 I in try_to_connect_and_handshake_with_new_peer, current out count 358 is more than 12, del_out_connections
2025-06-09 12:06:01.486 I in try_to_connect_and_handshake_with_new_peer, current out count 357 is more than 12, del_out_connections
2025-06-09 12:06:01.487 I in get_outgoing_connections_count(), out conn count is : 5
En un segundo, con el recuento real en 11, el nodo baja a 5 conexiones salientes porque sigue creyendo que tiene más de 350.
Impacto
Un atacante con un número grande de IP (en la prueba, 1.000) puede hacer que un nodo corte muchas de sus conexiones salientes, o todas. Combinado con la técnica para ocupar la lista de pares descrita en el artículo "Eclipse attack on Monero's peer to peer network" (NDSS 2025), puede conseguir que las conexiones salientes que el nodo abre después vayan a nodos suyos. Según el investigador:
- En un nodo con límite de conexiones entrantes, eso basta para un ataque eclipse: el nodo queda aislado del resto de la red y solo ve lo que el atacante le deja ver.
- En un nodo sin ese límite, el atacante puede deducir qué transacciones se originaron en ese nodo cuando está en la fase fluff, lo que compromete la privacidad de quien las envía.
HackerOne clasificó la debilidad como violación de privacidad.
Remediación
El investigador propuso contar como salientes solo las conexiones con el handshake completo y dejar fuera las temporales que se abren para enviar el Ping. El reporte figura como resuelto, pero no detalla el cambio aplicado ni la versión que lo incluye.
Qué aprender de este caso
- Si un límite se aplica con una cifra cacheada (aquí,
m_current_number_of_out_peers, refrescada cada segundo) y otra parte del código decide con el valor en tiempo real, busca el modo de que las dos discrepen: ese hueco puede permitir a un atacante provocar decisiones contradictorias. - En software P2P, revisa qué conexiones puede forzar un tercero que se cuenten para un límite. Las conexiones auxiliares, como las que se abren solo para enviar un Ping, no deberían ocupar el mismo cupo que los pares con el handshake completo.
- Cuando se supera un límite de conexiones y la respuesta es cerrar una al azar, el atacante que sepa provocar ese exceso decide cuándo se cae un par legítimo. Es mejor contar solo las conexiones completas y no expulsar pares buenos por exceso de las temporales.