Reports
MEDIA[DENEGACIÓN DE SERVICIO]#3185083

Monero: un fallo al contar conexiones salientes permite tumbar todos los pares de salida de un nodo

Los nodos de Monero hasta la v0.18.4.0 contaban como salientes las conexiones temporales usadas solo para enviar Ping. Inundándolos de Handshakes, un atacante les hacía cortar sus pares legítimos, un primer paso hacia un ataque eclipse.

Resumen
Resumen en castellano de un reporte público, no una traducción literal. El código y los comandos se mantienen como en el original.

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 hilo mPeersLoggerThread actualiza 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:

  1. 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.
  2. 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 mPeersLoggerThread hace su actualización de cada segundo, guarda en m_current_number_of_out_peers un valor enorme (en las pruebas, 362).
  3. 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.
  4. 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. Como connections_maker() la llama varias veces seguidas, cae una conexión tras otra.
  5. 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.