El equipo Skyfall de CertiK descubrió recientemente múltiples vulnerabilidades en los nodos RPC basados en Rust en varias cadenas de bloques, incluidas Aptos, StarCoin y Sui. Dado que los nodos RPC son componentes de infraestructura críticos que conectan dApps y la cadena de bloques subyacente, su robustez es fundamental para un funcionamiento sin problemas. Los diseñadores de blockchain conocen la importancia de los servicios RPC estables, por lo que adoptan lenguajes seguros para la memoria como Rust para evitar vulnerabilidades comunes que pueden destruir los nodos RPC.
La adopción de un lenguaje seguro para la memoria, como Rust, ayuda a los nodos RPC a evitar muchos ataques basados en vulnerabilidades de corrupción de la memoria. Sin embargo, a través de una auditoría reciente, descubrimos que incluso las implementaciones de Rust seguras para la memoria, si no se diseñan y examinan cuidadosamente, pueden ser vulnerables a ciertas amenazas de seguridad que pueden interrumpir la disponibilidad de los servicios RPC.
En este artículo, presentaremos nuestro descubrimiento de una serie de vulnerabilidades a través de casos prácticos.
Rol de nodo RPC de cadena de bloques
El servicio de llamada a procedimiento remoto (RPC) de la cadena de bloques es el componente central de la infraestructura de la capa 1 de la cadena de bloques. Proporciona a los usuarios una interfaz de API importante y actúa como una puerta de entrada a la red de cadena de bloques de back-end. Sin embargo, el servicio RPC de cadena de bloques es diferente del servicio RPC tradicional en que facilita la interacción del usuario sin autenticación. La disponibilidad continua del servicio es crítica y cualquier interrupción en el servicio puede afectar gravemente la disponibilidad de la cadena de bloques subyacente.
Perspectiva de auditoría: servidor RPC tradicional VS servidor RPC blockchain
La auditoría de los servidores RPC tradicionales se centra principalmente en la verificación de entrada, autorización/autenticación, falsificación de solicitudes entre sitios/falsificación de solicitudes del lado del servidor (CSRF/SSRF), vulnerabilidades de inyección (como inyección SQL, inyección de comandos) y fuga de información.
Sin embargo, la situación es diferente para los servidores RPC de cadena de bloques. Siempre que la transacción esté firmada, no es necesario autenticar al cliente solicitante en la capa RPC. Como front-end de blockchain, uno de los principales objetivos del servicio RPC es garantizar su disponibilidad. Si falla, los usuarios no pueden interactuar con la cadena de bloques, lo que les impide consultar datos en la cadena, enviar transacciones o emitir funciones de contrato.
Por lo tanto, el aspecto más vulnerable de un servidor RPC de cadena de bloques es la "disponibilidad". Si el servidor se cae, los usuarios pierden la capacidad de interactuar con la cadena de bloques. Lo que es más grave es que algunos ataques se extenderán por la cadena, afectarán a una gran cantidad de nodos e incluso provocarán la parálisis de toda la red.
Por qué la nueva cadena de bloques utilizará RPC seguro para la memoria
Algunas cadenas de bloques de capa 1 conocidas, como Aptos y Sui, utilizan el lenguaje de programación seguro para la memoria Rust para implementar sus servicios RPC. Gracias a su sólida seguridad y estrictas comprobaciones en tiempo de compilación, Rust hace que los programas sean prácticamente inmunes a las vulnerabilidades de corrupción de la memoria, como los desbordamientos de pila y las vulnerabilidades de desreferencia de puntero nulo y de referencia después de liberar.
Para asegurar aún más el código base, los desarrolladores siguen estrictamente las mejores prácticas, como no introducir código no seguro. Use #![forbid(unsafe_code)] en el código fuente para asegurarse de que el código no seguro se bloquee y filtre.
Ejemplos de desarrolladores de blockchain que implementan prácticas de programación Rust
Para evitar el desbordamiento de enteros, los desarrolladores suelen utilizar funciones como comprobado_sumar, comprobado_sub, saturar_sumar, saturar_sub, etc. en lugar de simples sumas y restas (+, -). Mitigue el agotamiento de los recursos estableciendo tiempos de espera adecuados, límites de tamaño de solicitud y límites de elementos de solicitud.
Amenazas RPC de seguridad de la memoria en la cadena de bloques de capa 1
Si bien no son vulnerables a la inseguridad de la memoria en el sentido tradicional, los nodos RPC están expuestos a entradas fácilmente manipulables por parte de los atacantes. En una implementación de RPC segura para la memoria, hay varias situaciones que pueden conducir a una denegación de servicio. Por ejemplo, la amplificación de la memoria puede agotar la memoria del servicio, mientras que los problemas de lógica pueden generar bucles infinitos. Además, las condiciones de carrera pueden representar una amenaza por la cual las operaciones simultáneas pueden tener una secuencia inesperada de eventos, dejando el sistema en un estado indefinido. Además, las dependencias administradas incorrectamente y las bibliotecas de terceros pueden introducir vulnerabilidades desconocidas en el sistema.
En esta publicación, nuestro objetivo es llamar la atención sobre formas más inmediatas en que se pueden activar las protecciones de tiempo de ejecución de Rust, lo que hace que los servicios se cancelen solos.
Explícito Rust Panic: una forma de terminar los servicios RPC directamente
Los desarrolladores pueden introducir un código de pánico explícito, de forma intencionada o no. Estos códigos se utilizan principalmente para manejar condiciones inesperadas o excepcionales. Algunos ejemplos comunes incluyen:
afirmar! (): use esta macro cuando se deba cumplir una condición. Si la condición afirmada falla, el programa entrará en pánico, lo que indica que hay un error grave en el código.
panic!(): Esta función se llama cuando el programa encuentra un error del cual no puede recuperarse y no puede continuar.
inalcanzable! (): use esta macro cuando no se deba ejecutar una pieza de código. Si se invoca esta macro, indica un error lógico grave.
no implementado!() y todo!(): estas macros son marcadores de posición para la funcionalidad no implementada. Si se alcanza este valor, el programa fallará.
unwrap(): este método se utiliza para los tipos de opción o resultado. Cuando se encuentra una variable Err o ninguna, el programa fallará.
Vulnerabilidad 1: ¡Active la aserción en Move Verifier!
La cadena de bloques de Aptos utiliza el verificador de código de bytes Move para realizar un análisis de seguridad de referencia a través de una interpretación abstracta del código de bytes. La función ute() es parte de la implementación del rasgo TransferFunctions y simula la ejecución de instrucciones de bytecode en bloques básicos.
La tarea de la función ute_inner() es interpretar la instrucción de código de bytes actual y actualizar el estado en consecuencia. Si hemos ejecutado hasta la última instrucción en el bloque básico, como lo indica index == last_index, la función llamará a assert!(self.stack.is_empty()) para asegurar que la pila esté vacía. La intención detrás de este comportamiento es garantizar que todas las operaciones estén equilibradas, lo que también significa que cada pulsación tiene un pop correspondiente.
En el flujo normal de ejecución, la pila siempre está equilibrada durante la interpretación abstracta. Esto está garantizado por Stack Balance Checker, que verifica el bytecode antes de interpretarlo. Sin embargo, una vez que ampliamos nuestra perspectiva al ámbito de los intérpretes abstractos, vemos que la suposición del equilibrio de la pila no siempre es válida.
Parche para la vulnerabilidad de analize_function en AbstractInterpreter
En esencia, un intérprete abstracto emula el código de bytes en el nivel de bloque básico. En su implementación original, encontrar un error durante ute_block haría que el proceso de análisis registrara el error y continuara la ejecución al siguiente bloque en el gráfico de flujo de control. Esto puede crear una situación en la que un error en un bloque de ejecución puede hacer que la pila se desequilibre. Si la ejecución continúa en este caso, se realizará una comprobación de afirmación si la pila no está vacía, lo que provocará un pánico.
Esto da a los atacantes la oportunidad de explotar. Un atacante puede desencadenar un error al diseñar un código de bytes específico en ute_block(), y luego ute() puede ejecutar una aserción si la pila no está vacía, lo que hace que falle la verificación de aserción. Esto generará más pánico y terminará el servicio RPC, lo que afectará su disponibilidad.
Para evitar esto, la solución implementada garantiza que todo el proceso de análisis se detenga cuando la función ute_block encuentra un error por primera vez, lo que evita el riesgo de fallas posteriores que pueden ocurrir al continuar el análisis debido al desequilibrio de la pila debido a errores. Esta modificación elimina las condiciones que podrían causar pánico y ayuda a mejorar la solidez y seguridad del intérprete abstracto.
** Vulnerabilidad 2: ¡Desencadena el pánico en StarCoin! **
La cadena de bloques de Starcoin tiene su propia bifurcación de la implementación de Move. En este repositorio de Move, hay un pánico en el constructor del tipo Struct. Si la StructDefinition proporcionada tiene información de campo nativo, ¡el pánico se activará explícitamente! .
Pánicos explícitos para estructuras inicializadas en rutinas de normalización
Este riesgo potencial existe en el proceso de redistribución de módulos. Si el módulo publicado ya existe en el almacén de datos, se requiere la normalización del módulo tanto para el módulo existente como para el módulo de entrada controlado por el atacante. Durante este proceso, la función "normalizado::Módulo::nuevo" construye la estructura del módulo a partir de los módulos de entrada controlados por el atacante, provocando un "pánico".
Requisitos previos para la rutina de normalización
Este pánico se puede desencadenar al enviar una carga útil especialmente diseñada por el cliente. Por lo tanto, los actores maliciosos pueden interrumpir la disponibilidad de los servicios RPC.
Parche de pánico de inicialización de estructura
El parche de Starcoin introduce un nuevo comportamiento para manejar el caso nativo. Ahora, en lugar de entrar en pánico, devuelve un ec vacío. Esto reduce la posibilidad de que los usuarios envíen datos causando pánico.
Pánico de óxido implícito: una forma fácil de pasar por alto para terminar los servicios RPC
Los pánicos explícitos son fácilmente identificables en el código fuente, mientras que es más probable que los desarrolladores ignoren los pánicos implícitos. Los pánicos implícitos generalmente ocurren cuando se usan API proporcionadas por bibliotecas estándar o de terceros. Los desarrolladores deben leer y comprender la documentación de la API a fondo, o sus programas Rust pueden detenerse inesperadamente.
Pánico implícito en BtreeMap
Tomemos como ejemplo BTreeMap de Rust STD. BTreeMap es una estructura de datos de uso común que organiza pares clave-valor en un árbol binario ordenado. BTreeMap proporciona dos métodos para recuperar valores por clave: get(&self, key: &Q) e index(&self, key: &Q).
El método get(&self, clave: &Q) recupera el valor usando la clave y devuelve una opción. La opción puede ser Some(&V), si la clave existe, devolver la referencia del valor, si la clave no se encuentra en el BTreeMap, devolver Ninguno.
Por otro lado, index(&self, key: &Q) devuelve directamente una referencia al valor correspondiente a la clave. Sin embargo, tiene un gran riesgo: desencadenará un pánico implícito si la clave no existe en el BTreeMap. Si no se maneja correctamente, el programa puede fallar inesperadamente, lo que lo convierte en una vulnerabilidad potencial.
De hecho, el método index(&self, key: &Q) es la implementación subyacente del rasgo std::ops::Index. Este rasgo es una operación de índice en un contexto inmutable (es decir, contenedor [index] ) proporciona azúcar sintáctico conveniente. Los desarrolladores pueden usar directamente btree_map [key] , llame al método index(&self, key: &Q). Sin embargo, pueden ignorar el hecho de que este uso puede causar pánico si no se encuentra la clave, lo que representa una amenaza implícita para la estabilidad del programa.
Vulnerabilidad 3: desencadenar un pánico implícito en Sui RPC
La rutina de lanzamiento del módulo Sui permite a los usuarios enviar cargas útiles del módulo a través de RPC. El controlador RPC utiliza la función SuiCommand::Publish para desensamblar directamente el módulo recibido antes de enviar la solicitud a la red de verificación de back-end para la verificación del código de bytes.
Durante este desensamblado, la sección code_unit del módulo enviado se usa para construir un VMControlFlowGraph. El proceso de construcción consiste en crear bloques básicos, que se almacenan en un BTreeMap llamado "'blocks'". El proceso incluye la creación y manipulación del Mapa, donde se activa un pánico implícito bajo ciertas condiciones.
Aquí hay un código simplificado:
Pánico implícito al crear VMControlFlowGraph
En ese código, se crea un nuevo VMControlFlowGraph recorriendo el código y creando un nuevo bloque básico para cada unidad de código. Los bloques básicos se almacenan en un bloque con nombre BTreeMap.
El mapa de bloques se indexa mediante block[&block] en un bucle que itera sobre la pila, que se ha inicializado con ENTRY_BLOCK_ID. La suposición aquí es que hay al menos una ENTRADA_BLOQUE_ID en el mapa de bloques.
Sin embargo, esta suposición no siempre se cumple. Por ejemplo, si el código confirmado está vacío, el "mapa de bloques" seguirá estando vacío después del proceso de "crear bloque básico". Cuando el código más tarde intenta atravesar el mapa de bloques usando for succ in &blocks[&block].successors , se puede generar un pánico implícito si no se encuentra la clave. Esto se debe a que la expresión blocks[&block] es esencialmente una llamada al método index() que, como se mencionó anteriormente, entrará en pánico si la clave no existe en el BTreeMap.
Un atacante con acceso remoto podría explotar la vulnerabilidad en esta función al enviar una carga útil de módulo con formato incorrecto con un campo code_unit vacío. Esta simple solicitud RPC bloquea todo el proceso JSON-RPC. Si un atacante continúa enviando tales cargas malformadas con un esfuerzo mínimo, se producirá una interrupción sostenida del servicio. En una red blockchain, esto significa que es posible que la red no pueda confirmar nuevas transacciones, lo que resulta en una situación de denegación de servicio (DoS). La funcionalidad de la red y la confianza del usuario en el sistema se verán gravemente afectadas.
Solución de Sui: eliminar el desmontaje de la rutina de problemas de RPC
Vale la pena señalar que CodeUnitVerifier en Move Bytecode Verifier es responsable de garantizar que la sección code_unit nunca esté vacía. Sin embargo, el orden de las operaciones expone a los controladores de RPC a posibles vulnerabilidades. Esto se debe a que el proceso de validación tiene lugar en el nodo Validator, que es una etapa posterior a que el RPC procese los módulos de entrada.
En respuesta a este problema, Sui resolvió la vulnerabilidad eliminando la función de desensamblado en la rutina RPC de lanzamiento del módulo. Esta es una forma efectiva de evitar que los servicios RPC procesen códigos de bytes no validados y potencialmente peligrosos.
Además, vale la pena señalar que otros métodos RPC relacionados con la búsqueda de objetos también contienen capacidades de desensamblaje, pero no son vulnerables al uso de celdas de código vacías. Esto se debe a que siempre consultan y desensamblan los módulos publicados existentes. Los módulos publicados deben haber sido verificados, por lo que la suposición de celdas de código no vacías siempre se mantiene cuando se crea un VMControlFlowGraph.
Sugerencias para desarrolladores
Después de comprender las amenazas a la estabilidad de los servicios RPC en las cadenas de bloques por pánicos explícitos e implícitos, los desarrolladores deben dominar las estrategias para prevenir o mitigar estos riesgos. Estas estrategias pueden reducir la posibilidad de cortes de servicio no planificados y aumentar la resistencia del sistema. Por lo tanto, el equipo de expertos de CertiK presenta las siguientes sugerencias y las enumera como las mejores prácticas para la programación de Rust.
Abstracción de Rust Panic: siempre que sea posible, considere usar la función catch_unwind de Rust para detectar pánicos y convertirlos en mensajes de error. Esto evita que todo el programa se bloquee y permite que los desarrolladores manejen los errores de manera controlada.
Use las API con precaución: los pánicos implícitos generalmente ocurren debido al uso indebido de las API proporcionadas por bibliotecas estándar o de terceros. Por lo tanto, es crucial comprender completamente la API y aprender a manejar los posibles errores de manera adecuada. Los desarrolladores siempre deben asumir que las API pueden fallar y prepararse para tales situaciones.
Manejo adecuado de errores: use los tipos de resultado y opción para el manejo de errores en lugar de recurrir al pánico. El primero proporciona una forma más controlada de manejar errores y casos especiales.
Agregue documentación y comentarios: asegúrese de que su código esté bien documentado y agregue comentarios a las secciones críticas (incluidas aquellas en las que pueden ocurrir situaciones de pánico). Esto ayudará a otros desarrolladores a comprender los riesgos potenciales y tratarlos de manera efectiva.
Resumir
Los nodos RPC basados en Rust juegan un papel importante en los sistemas de cadena de bloques como Aptos, StarCoin y Sui. Dado que se utilizan para conectar DApps y la cadena de bloques subyacente, su confiabilidad es fundamental para el buen funcionamiento del sistema de cadena de bloques. Aunque estos sistemas usan el lenguaje seguro para la memoria Rust, aún existe el riesgo de un diseño deficiente. El equipo de investigación de CertiK exploró estos riesgos con ejemplos del mundo real que demuestran la necesidad de un diseño cuidadoso y cuidadoso en la programación segura para la memoria.
Ver originales
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
Bloqueo de RPC: análisis de un nuevo tipo de vulnerabilidad en nodos RPC de cadena de bloques con memoria segura
El equipo Skyfall de CertiK descubrió recientemente múltiples vulnerabilidades en los nodos RPC basados en Rust en varias cadenas de bloques, incluidas Aptos, StarCoin y Sui. Dado que los nodos RPC son componentes de infraestructura críticos que conectan dApps y la cadena de bloques subyacente, su robustez es fundamental para un funcionamiento sin problemas. Los diseñadores de blockchain conocen la importancia de los servicios RPC estables, por lo que adoptan lenguajes seguros para la memoria como Rust para evitar vulnerabilidades comunes que pueden destruir los nodos RPC.
La adopción de un lenguaje seguro para la memoria, como Rust, ayuda a los nodos RPC a evitar muchos ataques basados en vulnerabilidades de corrupción de la memoria. Sin embargo, a través de una auditoría reciente, descubrimos que incluso las implementaciones de Rust seguras para la memoria, si no se diseñan y examinan cuidadosamente, pueden ser vulnerables a ciertas amenazas de seguridad que pueden interrumpir la disponibilidad de los servicios RPC.
En este artículo, presentaremos nuestro descubrimiento de una serie de vulnerabilidades a través de casos prácticos.
Rol de nodo RPC de cadena de bloques
El servicio de llamada a procedimiento remoto (RPC) de la cadena de bloques es el componente central de la infraestructura de la capa 1 de la cadena de bloques. Proporciona a los usuarios una interfaz de API importante y actúa como una puerta de entrada a la red de cadena de bloques de back-end. Sin embargo, el servicio RPC de cadena de bloques es diferente del servicio RPC tradicional en que facilita la interacción del usuario sin autenticación. La disponibilidad continua del servicio es crítica y cualquier interrupción en el servicio puede afectar gravemente la disponibilidad de la cadena de bloques subyacente.
Perspectiva de auditoría: servidor RPC tradicional VS servidor RPC blockchain
La auditoría de los servidores RPC tradicionales se centra principalmente en la verificación de entrada, autorización/autenticación, falsificación de solicitudes entre sitios/falsificación de solicitudes del lado del servidor (CSRF/SSRF), vulnerabilidades de inyección (como inyección SQL, inyección de comandos) y fuga de información.
Sin embargo, la situación es diferente para los servidores RPC de cadena de bloques. Siempre que la transacción esté firmada, no es necesario autenticar al cliente solicitante en la capa RPC. Como front-end de blockchain, uno de los principales objetivos del servicio RPC es garantizar su disponibilidad. Si falla, los usuarios no pueden interactuar con la cadena de bloques, lo que les impide consultar datos en la cadena, enviar transacciones o emitir funciones de contrato.
Por lo tanto, el aspecto más vulnerable de un servidor RPC de cadena de bloques es la "disponibilidad". Si el servidor se cae, los usuarios pierden la capacidad de interactuar con la cadena de bloques. Lo que es más grave es que algunos ataques se extenderán por la cadena, afectarán a una gran cantidad de nodos e incluso provocarán la parálisis de toda la red.
Por qué la nueva cadena de bloques utilizará RPC seguro para la memoria
Algunas cadenas de bloques de capa 1 conocidas, como Aptos y Sui, utilizan el lenguaje de programación seguro para la memoria Rust para implementar sus servicios RPC. Gracias a su sólida seguridad y estrictas comprobaciones en tiempo de compilación, Rust hace que los programas sean prácticamente inmunes a las vulnerabilidades de corrupción de la memoria, como los desbordamientos de pila y las vulnerabilidades de desreferencia de puntero nulo y de referencia después de liberar.
Para asegurar aún más el código base, los desarrolladores siguen estrictamente las mejores prácticas, como no introducir código no seguro. Use #![forbid(unsafe_code)] en el código fuente para asegurarse de que el código no seguro se bloquee y filtre.
Ejemplos de desarrolladores de blockchain que implementan prácticas de programación Rust
Para evitar el desbordamiento de enteros, los desarrolladores suelen utilizar funciones como comprobado_sumar, comprobado_sub, saturar_sumar, saturar_sub, etc. en lugar de simples sumas y restas (+, -). Mitigue el agotamiento de los recursos estableciendo tiempos de espera adecuados, límites de tamaño de solicitud y límites de elementos de solicitud.
Amenazas RPC de seguridad de la memoria en la cadena de bloques de capa 1
Si bien no son vulnerables a la inseguridad de la memoria en el sentido tradicional, los nodos RPC están expuestos a entradas fácilmente manipulables por parte de los atacantes. En una implementación de RPC segura para la memoria, hay varias situaciones que pueden conducir a una denegación de servicio. Por ejemplo, la amplificación de la memoria puede agotar la memoria del servicio, mientras que los problemas de lógica pueden generar bucles infinitos. Además, las condiciones de carrera pueden representar una amenaza por la cual las operaciones simultáneas pueden tener una secuencia inesperada de eventos, dejando el sistema en un estado indefinido. Además, las dependencias administradas incorrectamente y las bibliotecas de terceros pueden introducir vulnerabilidades desconocidas en el sistema.
En esta publicación, nuestro objetivo es llamar la atención sobre formas más inmediatas en que se pueden activar las protecciones de tiempo de ejecución de Rust, lo que hace que los servicios se cancelen solos.
Explícito Rust Panic: una forma de terminar los servicios RPC directamente
Los desarrolladores pueden introducir un código de pánico explícito, de forma intencionada o no. Estos códigos se utilizan principalmente para manejar condiciones inesperadas o excepcionales. Algunos ejemplos comunes incluyen:
afirmar! (): use esta macro cuando se deba cumplir una condición. Si la condición afirmada falla, el programa entrará en pánico, lo que indica que hay un error grave en el código.
panic!(): Esta función se llama cuando el programa encuentra un error del cual no puede recuperarse y no puede continuar.
inalcanzable! (): use esta macro cuando no se deba ejecutar una pieza de código. Si se invoca esta macro, indica un error lógico grave.
no implementado!() y todo!(): estas macros son marcadores de posición para la funcionalidad no implementada. Si se alcanza este valor, el programa fallará.
unwrap(): este método se utiliza para los tipos de opción o resultado. Cuando se encuentra una variable Err o ninguna, el programa fallará.
Vulnerabilidad 1: ¡Active la aserción en Move Verifier!
La cadena de bloques de Aptos utiliza el verificador de código de bytes Move para realizar un análisis de seguridad de referencia a través de una interpretación abstracta del código de bytes. La función ute() es parte de la implementación del rasgo TransferFunctions y simula la ejecución de instrucciones de bytecode en bloques básicos.
La tarea de la función ute_inner() es interpretar la instrucción de código de bytes actual y actualizar el estado en consecuencia. Si hemos ejecutado hasta la última instrucción en el bloque básico, como lo indica index == last_index, la función llamará a assert!(self.stack.is_empty()) para asegurar que la pila esté vacía. La intención detrás de este comportamiento es garantizar que todas las operaciones estén equilibradas, lo que también significa que cada pulsación tiene un pop correspondiente.
En el flujo normal de ejecución, la pila siempre está equilibrada durante la interpretación abstracta. Esto está garantizado por Stack Balance Checker, que verifica el bytecode antes de interpretarlo. Sin embargo, una vez que ampliamos nuestra perspectiva al ámbito de los intérpretes abstractos, vemos que la suposición del equilibrio de la pila no siempre es válida.
Parche para la vulnerabilidad de analize_function en AbstractInterpreter
En esencia, un intérprete abstracto emula el código de bytes en el nivel de bloque básico. En su implementación original, encontrar un error durante ute_block haría que el proceso de análisis registrara el error y continuara la ejecución al siguiente bloque en el gráfico de flujo de control. Esto puede crear una situación en la que un error en un bloque de ejecución puede hacer que la pila se desequilibre. Si la ejecución continúa en este caso, se realizará una comprobación de afirmación si la pila no está vacía, lo que provocará un pánico.
Esto da a los atacantes la oportunidad de explotar. Un atacante puede desencadenar un error al diseñar un código de bytes específico en ute_block(), y luego ute() puede ejecutar una aserción si la pila no está vacía, lo que hace que falle la verificación de aserción. Esto generará más pánico y terminará el servicio RPC, lo que afectará su disponibilidad.
Para evitar esto, la solución implementada garantiza que todo el proceso de análisis se detenga cuando la función ute_block encuentra un error por primera vez, lo que evita el riesgo de fallas posteriores que pueden ocurrir al continuar el análisis debido al desequilibrio de la pila debido a errores. Esta modificación elimina las condiciones que podrían causar pánico y ayuda a mejorar la solidez y seguridad del intérprete abstracto.
** Vulnerabilidad 2: ¡Desencadena el pánico en StarCoin! **
La cadena de bloques de Starcoin tiene su propia bifurcación de la implementación de Move. En este repositorio de Move, hay un pánico en el constructor del tipo Struct. Si la StructDefinition proporcionada tiene información de campo nativo, ¡el pánico se activará explícitamente! .
Pánicos explícitos para estructuras inicializadas en rutinas de normalización
Este riesgo potencial existe en el proceso de redistribución de módulos. Si el módulo publicado ya existe en el almacén de datos, se requiere la normalización del módulo tanto para el módulo existente como para el módulo de entrada controlado por el atacante. Durante este proceso, la función "normalizado::Módulo::nuevo" construye la estructura del módulo a partir de los módulos de entrada controlados por el atacante, provocando un "pánico".
Requisitos previos para la rutina de normalización
Este pánico se puede desencadenar al enviar una carga útil especialmente diseñada por el cliente. Por lo tanto, los actores maliciosos pueden interrumpir la disponibilidad de los servicios RPC.
Parche de pánico de inicialización de estructura
El parche de Starcoin introduce un nuevo comportamiento para manejar el caso nativo. Ahora, en lugar de entrar en pánico, devuelve un ec vacío. Esto reduce la posibilidad de que los usuarios envíen datos causando pánico.
Pánico de óxido implícito: una forma fácil de pasar por alto para terminar los servicios RPC
Los pánicos explícitos son fácilmente identificables en el código fuente, mientras que es más probable que los desarrolladores ignoren los pánicos implícitos. Los pánicos implícitos generalmente ocurren cuando se usan API proporcionadas por bibliotecas estándar o de terceros. Los desarrolladores deben leer y comprender la documentación de la API a fondo, o sus programas Rust pueden detenerse inesperadamente.
Pánico implícito en BtreeMap
Tomemos como ejemplo BTreeMap de Rust STD. BTreeMap es una estructura de datos de uso común que organiza pares clave-valor en un árbol binario ordenado. BTreeMap proporciona dos métodos para recuperar valores por clave: get(&self, key: &Q) e index(&self, key: &Q).
El método get(&self, clave: &Q) recupera el valor usando la clave y devuelve una opción. La opción puede ser Some(&V), si la clave existe, devolver la referencia del valor, si la clave no se encuentra en el BTreeMap, devolver Ninguno.
Por otro lado, index(&self, key: &Q) devuelve directamente una referencia al valor correspondiente a la clave. Sin embargo, tiene un gran riesgo: desencadenará un pánico implícito si la clave no existe en el BTreeMap. Si no se maneja correctamente, el programa puede fallar inesperadamente, lo que lo convierte en una vulnerabilidad potencial.
De hecho, el método index(&self, key: &Q) es la implementación subyacente del rasgo std::ops::Index. Este rasgo es una operación de índice en un contexto inmutable (es decir, contenedor [index] ) proporciona azúcar sintáctico conveniente. Los desarrolladores pueden usar directamente btree_map [key] , llame al método index(&self, key: &Q). Sin embargo, pueden ignorar el hecho de que este uso puede causar pánico si no se encuentra la clave, lo que representa una amenaza implícita para la estabilidad del programa.
Vulnerabilidad 3: desencadenar un pánico implícito en Sui RPC
La rutina de lanzamiento del módulo Sui permite a los usuarios enviar cargas útiles del módulo a través de RPC. El controlador RPC utiliza la función SuiCommand::Publish para desensamblar directamente el módulo recibido antes de enviar la solicitud a la red de verificación de back-end para la verificación del código de bytes.
Durante este desensamblado, la sección code_unit del módulo enviado se usa para construir un VMControlFlowGraph. El proceso de construcción consiste en crear bloques básicos, que se almacenan en un BTreeMap llamado "'blocks'". El proceso incluye la creación y manipulación del Mapa, donde se activa un pánico implícito bajo ciertas condiciones.
Aquí hay un código simplificado:
Pánico implícito al crear VMControlFlowGraph
En ese código, se crea un nuevo VMControlFlowGraph recorriendo el código y creando un nuevo bloque básico para cada unidad de código. Los bloques básicos se almacenan en un bloque con nombre BTreeMap.
El mapa de bloques se indexa mediante block[&block] en un bucle que itera sobre la pila, que se ha inicializado con ENTRY_BLOCK_ID. La suposición aquí es que hay al menos una ENTRADA_BLOQUE_ID en el mapa de bloques.
Sin embargo, esta suposición no siempre se cumple. Por ejemplo, si el código confirmado está vacío, el "mapa de bloques" seguirá estando vacío después del proceso de "crear bloque básico". Cuando el código más tarde intenta atravesar el mapa de bloques usando for succ in &blocks[&block].successors , se puede generar un pánico implícito si no se encuentra la clave. Esto se debe a que la expresión blocks[&block] es esencialmente una llamada al método index() que, como se mencionó anteriormente, entrará en pánico si la clave no existe en el BTreeMap.
Un atacante con acceso remoto podría explotar la vulnerabilidad en esta función al enviar una carga útil de módulo con formato incorrecto con un campo code_unit vacío. Esta simple solicitud RPC bloquea todo el proceso JSON-RPC. Si un atacante continúa enviando tales cargas malformadas con un esfuerzo mínimo, se producirá una interrupción sostenida del servicio. En una red blockchain, esto significa que es posible que la red no pueda confirmar nuevas transacciones, lo que resulta en una situación de denegación de servicio (DoS). La funcionalidad de la red y la confianza del usuario en el sistema se verán gravemente afectadas.
Solución de Sui: eliminar el desmontaje de la rutina de problemas de RPC
Vale la pena señalar que CodeUnitVerifier en Move Bytecode Verifier es responsable de garantizar que la sección code_unit nunca esté vacía. Sin embargo, el orden de las operaciones expone a los controladores de RPC a posibles vulnerabilidades. Esto se debe a que el proceso de validación tiene lugar en el nodo Validator, que es una etapa posterior a que el RPC procese los módulos de entrada.
En respuesta a este problema, Sui resolvió la vulnerabilidad eliminando la función de desensamblado en la rutina RPC de lanzamiento del módulo. Esta es una forma efectiva de evitar que los servicios RPC procesen códigos de bytes no validados y potencialmente peligrosos.
Además, vale la pena señalar que otros métodos RPC relacionados con la búsqueda de objetos también contienen capacidades de desensamblaje, pero no son vulnerables al uso de celdas de código vacías. Esto se debe a que siempre consultan y desensamblan los módulos publicados existentes. Los módulos publicados deben haber sido verificados, por lo que la suposición de celdas de código no vacías siempre se mantiene cuando se crea un VMControlFlowGraph.
Sugerencias para desarrolladores
Después de comprender las amenazas a la estabilidad de los servicios RPC en las cadenas de bloques por pánicos explícitos e implícitos, los desarrolladores deben dominar las estrategias para prevenir o mitigar estos riesgos. Estas estrategias pueden reducir la posibilidad de cortes de servicio no planificados y aumentar la resistencia del sistema. Por lo tanto, el equipo de expertos de CertiK presenta las siguientes sugerencias y las enumera como las mejores prácticas para la programación de Rust.
Abstracción de Rust Panic: siempre que sea posible, considere usar la función catch_unwind de Rust para detectar pánicos y convertirlos en mensajes de error. Esto evita que todo el programa se bloquee y permite que los desarrolladores manejen los errores de manera controlada.
Use las API con precaución: los pánicos implícitos generalmente ocurren debido al uso indebido de las API proporcionadas por bibliotecas estándar o de terceros. Por lo tanto, es crucial comprender completamente la API y aprender a manejar los posibles errores de manera adecuada. Los desarrolladores siempre deben asumir que las API pueden fallar y prepararse para tales situaciones.
Manejo adecuado de errores: use los tipos de resultado y opción para el manejo de errores en lugar de recurrir al pánico. El primero proporciona una forma más controlada de manejar errores y casos especiales.
Agregue documentación y comentarios: asegúrese de que su código esté bien documentado y agregue comentarios a las secciones críticas (incluidas aquellas en las que pueden ocurrir situaciones de pánico). Esto ayudará a otros desarrolladores a comprender los riesgos potenciales y tratarlos de manera efectiva.
Resumir
Los nodos RPC basados en Rust juegan un papel importante en los sistemas de cadena de bloques como Aptos, StarCoin y Sui. Dado que se utilizan para conectar DApps y la cadena de bloques subyacente, su confiabilidad es fundamental para el buen funcionamiento del sistema de cadena de bloques. Aunque estos sistemas usan el lenguaje seguro para la memoria Rust, aún existe el riesgo de un diseño deficiente. El equipo de investigación de CertiK exploró estos riesgos con ejemplos del mundo real que demuestran la necesidad de un diseño cuidadoso y cuidadoso en la programación segura para la memoria.