Reports
BAJA[OTROS]#3709703

Tor: celdas INTRODUCE2 con MAC inválido hacen crecer sin límite la caché anti-replay de un servicio onion

Un servicio onion de Tor guardaba en su caché anti-replay, que nunca caduca, el contenido cifrado de celdas INTRODUCE2 antes de comprobar su MAC. Cualquier cliente anónimo podía llenarla con basura y agotar poco a poco la memoria del servicio.

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 error de orden en las comprobaciones de los servicios onion v3 de Tor permitía a cualquier cliente, sin autenticarse, hacer crecer indefinidamente el consumo de memoria del servicio. Es un problema de consumo de recursos sin control (CWE-400) que solo afecta a la disponibilidad.

Para contactar con un servicio onion, el cliente envía una celda INTRODUCE1 a uno de sus puntos de introducción, que la reenvía al servicio como INTRODUCE2. Esa celda lleva una sección cifrada protegida por un MAC. El servicio mantiene, por cada punto de introducción, una caché anti-replay para rechazar celdas repetidas. El fallo estaba en cómo se combinaban tres detalles:

  1. Se guardaba antes de validar. En hs_cell_parse_introduce2() (src/feature/hs/hs_cell.c:1015-1034), la sección cifrada que controla el atacante entraba en la caché anti-replay antes de comprobar las claves y el MAC:
if (replaycache_add_test_and_elapsed(data->replay_cache, encrypted_section,
                                     encrypted_section_len, &elapsed)) {
  ...
  goto done;
}

...
intro_keys = get_introduce2_keys_and_verify_mac(data, encrypted_section,
                                                encrypted_section_len);
if (!intro_keys) {
  ...
  goto done;
}
  1. La caché nunca caduca. Se creaba sin horizonte temporal en src/feature/hs/hs_service.c:539:
ip->replay_cache = replaycache_new(0, 0);

Según src/feature/hs_common/replaycache.c:122-127, cada entrada nueva reserva un time_t e inserta un digest. Con horizon == 0, replaycache_scrub_if_needed_internal() sale sin limpiar nada.

  1. Las celdas inválidas no cuentan. El contador ip->introduce2_count, que sirve para rotar el punto de introducción cuando recibe muchas celdas, solo se incrementa cuando hs_cell_parse_introduce2() tiene éxito (src/feature/hs/hs_circuit.c:1342-1369). Las celdas con MAC erróneo se descartan antes, así que nunca provocan esa rotación.

El resultado: cada celda con MAC inválido pero contenido distinto dejaba una entrada permanente en memoria y no activaba ningún mecanismo de limpieza.

Por qué un atacante externo podía llegar a este código

  • El cliente obtiene el descriptor público del servicio onion y envía celdas INTRODUCE1 al punto de introducción anunciado.
  • El punto de introducción solo analiza la parte no cifrada, localiza el circuito del servicio por su clave de autenticación y reenvía el mismo contenido como RELAY_COMMAND_INTRODUCE2 (src/feature/hs/hs_intropoint.c:653-710).
  • Cada circuito de cliente solo admite una INTRODUCE1, pero el atacante puede abrir muchos circuitos de introducción. Las defensas que podrían frenarlo vienen desactivadas por defecto: HS_CONFIG_V3_DOS_DEFENSE_DEFAULT 0 (src/feature/hs/hs_config.h:20) y HS_DOS_INTRODUCE_ENABLED_DEFAULT 0 (src/feature/hs/hs_dos.c:47).
  • Basta con alterar el último byte del MAC: el formato de la celda sigue siendo válido para el analizador (trunnel), pero la verificación del MAC falla siempre.

Pasos de reproducción

El investigador lo demostró con una prueba de regresión añadida al propio árbol de Tor, en src/test/test_hs_service.c, con el nombre hs_service/intro2_invalid_mac_replay_cache_growth. La prueba:

  1. Construye una carga INTRODUCE1 legítima con la función auxiliar existente hs_circ_send_introduce1().
  2. Modifica únicamente el último byte del MAC, de modo que el formato de INTRODUCE1/INTRODUCE2 sigue siendo correcto.
  3. Entrega ocho cargas distintas con MAC inválido a través de hs_circ_handle_introduce2().
  4. Comprueba que las ocho se rechazan.
  5. Comprueba que ip->introduce2_count sigue a cero.
  6. Comprueba que digest256map_size(ip->replay_cache->digests_seen) pasa de 0 a 8.

Para ejecutarla, se aplica el parche del PoC, se compila y se lanza la prueba:

git apply h1-submissions/tor-hs-introduce2-invalid-mac-replaycache-dos-poc.patch
make -j2 src/test/test
./src/test/test hs_service/intro2_invalid_mac_replay_cache_growth

Salida obtenida (la prueba pasa, es decir, el crecimiento de la caché se confirma):

hs_service/intro2_invalid_mac_replay_cache_growth: [forking] OK
1 tests ok.  (0 skipped)

También se volvió a ejecutar la prueba existente del camino normal, que sigue funcionando:

hs_service/intro2_handling: [forking] OK
1 tests ok.  (0 skipped)

Impacto

El atacante decide cuántas entradas permanentes se añaden a la caché de cada punto de introducción del servicio. Esas entradas no caducan por tiempo y, como las celdas inválidas no suman al umbral de rotación (entre 16.384 y 32.768 INTRODUCE2 válidas), el punto de introducción solo rota al agotar su vida útil, de 18 a 24 horas. Es una ventana larga para acumular memoria.

Cada sección cifrada única genera una entrada en un digest256map (clave de 32 bytes, puntero, enlaces de la tabla hash y metadatos del asignador) más un time_t reservado aparte. El investigador estima entre 64 y 96 bytes por celda en un sistema de 64 bits, sin contar el redimensionado de la tabla. Por cada punto de introducción atacado:

| Circuitos de introducción por minuto | Crecimiento aproximado | |---|---| | 100 | 0,4 – 0,6 MB/hora | | 500 | 1,9 – 2,9 MB/hora | | 1000 | 3,8 – 5,8 MB/hora |

A lo largo de las 18-24 horas de vida del punto de introducción, un ataque sostenido puede sumar desde decenas hasta unos pocos centenares de MB de memoria residente en el proceso del servicio, y más si se usan varios clientes en paralelo o se abren circuitos más deprisa. En un servicio con poca memoria disponible, esto puede acabar en falta de memoria (OOM).

El impacto se limita a la disponibilidad: el investigador no alega ejecución de código, fuga de información ni alteración de datos. Propuso esta puntuación:

CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Argumentó que podría subirse a A:H (CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H) si se considera que un crecimiento de memoria sostenido y sin autenticación hasta el OOM es un impacto alto, sobre todo en servicios que funcionan cerca de su límite de memoria. En HackerOne el reporte figura con severidad baja y con recompensa de importe no público.

Remediación

El reporte figura como resuelto, pero no detalla el parche que aplicó Tor. La corrección que propuso el investigador es:

  • No insertar la sección cifrada de INTRODUCE2 en la caché anti-replay permanente del punto de introducción hasta después de verificar el MAC.
  • Si se quiere seguir descartando pronto celdas inválidas repetidas para ahorrar trabajo criptográfico, usar una caché negativa separada, con tamaño máximo y caducidad corta.
  • Como mínimo, garantizar que las secciones cifradas con MAC inválido no puedan crear entradas permanentes ni esquivar la rotación y el control de recursos del punto de introducción.

Cronología: reporte enviado el 2 de mayo de 2026 y divulgado el 10 de septiembre de 2026.