Reports
MEDIA[DENEGACIÓN DE SERVICIO]#3241102

Monero: un block_blob demasiado corto en la llamada RPC calc_pow tumba el nodo monerod

La llamada RPC calc_pow de monerod aceptaba un block_blob de menos de 43 bytes, llegaba al hash Cryptonight variante 1 y el proceso terminaba con _exit(1). Lo encontró el fuzzing de Ada Logics para MAGIC Grants.

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

El método calc_pow del servidor RPC de monerod (manejador cryptonote::core_rpc_server::on_calcpow en src/rpc/core_rpc_server.cpp) calcula la prueba de trabajo de un bloque que le pasa el cliente en el parámetro block_blob. El manejador comprobaba que el blob se pudiera interpretar, pero no su longitud mínima. Con major_version 7 el cálculo usa Cryptonight variante 1, que necesita al menos 43 bytes de datos; si recibe menos, el código de src/crypto/slow-hash.c no devuelve un error, sino que termina el proceso con _exit(1).

El resultado es que una sola petición con datos controlados por el cliente basta para cerrar el nodo entero. El fallo lo encontró Ada Logics con un arnés de fuzzing de los endpoints RPC de Monero, trabajo contratado por el MAGIC Monero Fund (comité de la organización benéfica MAGIC Grants), que lo remitió al equipo de Monero. MAGIC Grants renunció expresamente a la recompensa económica.

Cadena de llamadas hasta la terminación, según la traza del fuzzer:

cryptonote::core_rpc_server::on_calcpow      src/rpc/core_rpc_server.cpp:2098
cryptonote::get_block_longhash               src/cryptonote_core/cryptonote_tx_utils.cpp:702
cn_slow_hash                                 src/crypto/hash.h:74
cn_slow_hash                                 src/crypto/slow-hash.c:911

La salida problemática está en esta línea del código de entonces: https://github.com/monero-project/monero/blob/17f6fb871c09507cd13c23fdecd9cbcca3f01326/src/crypto/slow-hash.c#L139

Pasos de reproducción

  1. Arranca monerod en una máquina local (RPC en el puerto por defecto, 18081).
  2. Envía esta llamada JSON-RPC, con un block_blob de solo 3 bytes (010101) y major_version 7:
$ curl http://127.0.0.1:18081/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"calc_pow","params":{"major_version": 7, "height": 5, "block_blob": "010101", "seed_hash": "1111111111111111111111111111111111111111111111111111111111111111"}' -H 'Content-Type: application/json'
  1. monerod se cierra mostrando el mensaje:
Cryptonight variant 1 needs at least 43 bytes of data

Impacto

Quien pueda llegar a la interfaz RPC del nodo puede provocar su caída con una única petición: el proceso monerod termina y deja de dar servicio hasta que se vuelva a arrancar. El reporte lo describe como una posible denegación de servicio por cierre de monerod.

Remediación

El reporte figura como resuelto en HackerOne, pero no detalla el parche definitivo. Los autores proponen añadir la comprobación más arriba en la pila de llamadas, antes de llegar al hash. Como referencia, este es el cambio que usan ellos para que el fuzzer no se detenga en este fallo (los números de línea pueden variar): rechazar en on_calcpow cualquier blob de menos de 43 bytes con el mismo error que un blob inválido.

--- a/src/rpc/core_rpc_server.cpp
+++ b/src/rpc/core_rpc_server.cpp
@@ -2074,6 +2075,12 @@ namespace cryptonote
       error_resp.message = "Wrong block blob";
       return false;
     }
+    // std::cout << "Blob data: " << blockblob.size() << " bytes" << std::endl;
+    if (blockblob.size() < 43) {
+      error_resp.code = CORE_RPC_ERROR_CODE_WRONG_BLOCKBLOB;
+      error_resp.message = "Wrong block blob";
+      return false;
+    }
     if(!m_core.check_incoming_block_size(blockblob))
     {
       error_resp.code = CORE_RPC_ERROR_CODE_WRONG_BLOCKBLOB_SIZE;

Qué aprender de este caso

  • Busca exit(), _exit(), abort() o assert dentro de funciones criptográficas o de bajo nivel y sigue hacia atrás quién las llama: si algún endpoint RPC o API les hace llegar datos del cliente, cada una es un botón de apagado remoto.
  • Los métodos RPC que reciben un blob en hexadecimal y lo pasan tal cual a una función de hash o de parseo son un buen objetivo de fuzzing: prueba tamaños mínimos (1-3 bytes), vacíos y justo por debajo del límite que exige cada variante del algoritmo.
  • Cuando una función interna tiene un requisito de tamaño (aquí, 43 bytes para Cryptonight variante 1), compruébalo en el manejador que recibe la petición y devuelve allí un error al cliente, en vez de confiar en que la capa baja lo gestione sin matar el proceso.
  • Cuidado con los parámetros que eligen la rama de código (major_version decidía la variante del hash): cada valor posible puede tener sus propias precondiciones, y hay que validar la entrada para todas ellas.