Protecci贸n sin miedo. Seguridad de roscas en 贸xido

Esta es la segunda parte de la serie de art铆culos Fearless Protection. En el primero hablamos de seguridad de memoria

Las aplicaciones modernas son multiproceso: en lugar de ejecutar tareas secuencialmente, el programa utiliza subprocesos para realizar simult谩neamente varias tareas. Todos observamos trabajo simult谩neo y concurrencia todos los d铆as:

  • Varios sitios web son atendidos por varios usuarios al mismo tiempo.
  • La interfaz de usuario realiza un trabajo en segundo plano que no molesta al usuario (imagine que cada vez que escribe un car谩cter, la aplicaci贸n se congela para verificar la ortograf铆a).
  • Una computadora puede ejecutar m煤ltiples aplicaciones al mismo tiempo.

Las transmisiones paralelas aceleran el trabajo, pero presentan un conjunto de problemas de sincronizaci贸n, a saber, puntos muertos y condiciones de carrera. Desde el punto de vista de la seguridad, 驴por qu茅 nos importa la seguridad de los hilos? Porque la seguridad de la memoria y los subprocesos tiene el mismo problema principal: el uso inapropiado de los recursos. Los ataques aqu铆 tienen los mismos efectos que los ataques de memoria, incluida la escalada de privilegios, la ejecuci贸n de c贸digo arbitrario (ACE) y eludir las comprobaciones de seguridad.

Los errores de concurrencia, como los errores de implementaci贸n, est谩n estrechamente relacionados con la correcci贸n del programa. Si bien las vulnerabilidades de la memoria son casi siempre peligrosas, los errores de implementaci贸n / l贸gica no siempre indican un problema de seguridad si no ocurren en la parte del c贸digo relacionada con el cumplimiento de los contratos de seguridad (por ejemplo, permiso para eludir las verificaciones de seguridad). Pero los errores de concurrencia tienen una peculiaridad. Si los problemas de seguridad debidos a errores l贸gicos a menudo aparecen junto al c贸digo correspondiente, los errores de concurrencia a menudo ocurren en otras funciones, y no en la que se cometi贸 directamente , lo que dificulta su seguimiento y eliminaci贸n. Otra dificultad es una cierta superposici贸n entre el procesamiento inadecuado de la memoria y los errores de concurrencia, que vemos en las carreras de datos.

Los lenguajes de programaci贸n han desarrollado diversas estrategias de concurrencia para ayudar a los desarrolladores a gestionar el rendimiento y los problemas de seguridad de las aplicaciones de subprocesos m煤ltiples.

Problemas de concurrencia


En general, se acepta que la programaci贸n paralela es m谩s dif铆cil de lo habitual: nuestro cerebro est谩 mejor adaptado al razonamiento secuencial. El c贸digo paralelo puede tener interacciones inesperadas y no deseadas entre subprocesos, incluidos puntos muertos, contenciones y carreras de datos.

Un punto muerto se produce cuando varios subprocesos esperan que otros realicen ciertas acciones para continuar trabajando. Aunque este comportamiento no deseado puede causar un ataque de denegaci贸n de servicio, no causar谩 vulnerabilidades como ACE.

Una condici贸n de carrera es una situaci贸n en la que el tiempo o el orden de las tareas pueden afectar la correcci贸n de un programa. La carrera de datos ocurre cuando varias secuencias intentan acceder simult谩neamente a la misma ubicaci贸n de memoria con al menos un intento de escritura. Sucede que una condici贸n de carrera y una carrera de datos se producen independientemente una de la otra. Pero las carreras de datos son siempre peligrosas .

Posibles consecuencias de errores de concurrencia


  1. Punto muerto
  2. P茅rdida de informaci贸n: otro hilo sobrescribe la informaci贸n
  3. P茅rdida de integridad: la informaci贸n de varias corrientes est谩 entrelazada
  4. P茅rdida de viabilidad: problemas de rendimiento debido al acceso desigual a los recursos compartidos.

El tipo de ataque de concurrencia m谩s famoso se llama TOCTOU (tiempo de verificaci贸n hasta tiempo de uso): de hecho, el estado de una carrera es entre las condiciones de verificaci贸n (por ejemplo, credenciales de seguridad) y el uso de los resultados. Un ataque TOCTOU resulta en una p茅rdida de integridad.

Los bloqueos mutuos y la p茅rdida de capacidad de supervivencia se consideran problemas de rendimiento, no problemas de seguridad, mientras que la p茅rdida de informaci贸n y la integridad probablemente est茅n relacionadas con la seguridad. Un art铆culo de Red Balloon Security analiza algunas de las posibles haza帽as. Un ejemplo es una corrupci贸n de puntero seguida de escalada de privilegios o ejecuci贸n remota de c贸digo. En el exploit, una funci贸n que carga la biblioteca compartida ELF (formato ejecutable y enlazable) inicia correctamente un sem谩foro solo en la primera llamada, y luego limita incorrectamente el n煤mero de subprocesos, lo que provoca da帽os en la memoria del n煤cleo. Este ataque es un ejemplo de p茅rdida de informaci贸n.

La parte m谩s dif铆cil de la programaci贸n concurrente es probar y depurar, porque los errores de concurrencia son dif铆ciles de reproducir. Tiempo de eventos, decisiones del sistema operativo, tr谩fico de red y otros factores ... todo esto cambia el comportamiento del programa en cada inicio.


A veces es realmente m谩s f谩cil eliminar todo el programa que buscar un error. Heisenbugs

El comportamiento no solo cambia cada vez que se inicia, sino que incluso insertar declaraciones de salida o depuraci贸n puede cambiar el comportamiento, lo que resulta en "errores de Heisenberg" (errores no deterministas, dif铆ciles de reproducir, t铆picos de la programaci贸n paralela) que surgen y desaparecen misteriosamente.

La programaci贸n paralela es dif铆cil. Es dif铆cil predecir c贸mo interactuar谩 el c贸digo paralelo con otro c贸digo paralelo. Cuando aparecen errores, son dif铆ciles de encontrar y corregir. En lugar de confiar en los probadores, veamos formas de desarrollar programas y el uso de lenguajes que faciliten la escritura de c贸digo paralelo.

Primero, formulamos el concepto de "seguridad del hilo":

"Un tipo de datos o m茅todo est谩tico se considera seguro para subprocesos si se comporta correctamente cuando se llama desde varios subprocesos, independientemente de c贸mo se ejecutan estos subprocesos, y no requiere coordinaci贸n adicional del c贸digo de llamada". MIT

C贸mo funcionan los lenguajes de programaci贸n con paralelismo


En idiomas sin seguridad de subprocesos est谩ticos, los programadores tienen que monitorear constantemente la memoria que se comparte con otro subproceso y que puede cambiar en cualquier momento. En la programaci贸n secuencial, se nos ense帽a a evitar las variables globales si otra parte del c贸digo las cambia silenciosamente. Es imposible exigir a los programadores que garanticen un cambio seguro en los datos compartidos, as铆 como la gesti贸n manual de la memoria.


"隆Vigilancia constante!"

Por lo general, los lenguajes de programaci贸n se limitan a dos enfoques:

  1. Limitaci贸n de la mutabilidad o restricci贸n del acceso compartido.
  2. Seguridad manual del hilo (p. Ej., Cerraduras, sem谩foros)

Los idiomas con restricci贸n de subprocesos ponen un l铆mite de 1 subproceso para variables mutables o requieren que todas las variables comunes sean inmutables. Ambos enfoques abordan el problema b谩sico de la carrera de datos (datos compartidos incorrectamente modificables), pero las restricciones son demasiado severas. Para resolver el problema, los lenguajes hicieron primitivas de sincronizaci贸n de bajo nivel, como mutexes. Se pueden usar para construir estructuras de datos seguras para subprocesos.

Python y bloqueo global por int茅rprete


La implementaci贸n de referencia en Python y Cpython tiene un tipo de mutex llamado Global Interpreter Lock (GIL), que bloquea todos los otros hilos cuando un hilo accede a un objeto. Python multiproceso es conocido por su ineficiencia debido a la latencia GIL. Por lo tanto, la mayor铆a de los programas concurrentes de Python funcionan en varios procesos para que cada uno tenga su propio GIL.

Java y excepciones de tiempo de ejecuci贸n


Java admite programaci贸n concurrente a trav茅s de un modelo de memoria compartida. Cada hilo tiene su propia ruta de ejecuci贸n, pero puede acceder a cualquier objeto en el programa: el programador debe sincronizar el acceso entre los hilos usando las primitivas Java incorporadas.

Aunque Java tiene bloques de construcci贸n para crear programas seguros para subprocesos , el compilador no garantiza la seguridad de subprocesos (a diferencia de la seguridad de la memoria). Si se produce un acceso no sincronizado a la memoria (es decir, la carrera de datos), Java lanzar谩 una excepci贸n en tiempo de ejecuci贸n, pero los programadores deben usar correctamente las primitivas de concurrencia.

C ++ y el cerebro del programador


Si bien Python evita las condiciones de carrera con GIL y Java lanza excepciones en tiempo de ejecuci贸n, C ++ espera que el programador sincronice manualmente el acceso a la memoria. Antes de C ++ 11, la biblioteca est谩ndar no inclu铆a primitivas de concurrencia .

La mayor铆a de los idiomas proporcionan herramientas para escribir c贸digo seguro para subprocesos, y existen m茅todos especiales para detectar datos de carrera y estado de carrera; pero no ofrece ninguna garant铆a de seguridad de subprocesos y no protege contra la carrera de datos.

驴C贸mo resolver el problema del 贸xido?


Rust adopta un enfoque multifac茅tico para eliminar las condiciones de carrera, utilizando reglas de propiedad y tipos seguros para proteger completamente contra las condiciones de carrera en tiempo de compilaci贸n.

En el primer art铆culo, presentamos el concepto de propiedad, este es uno de los conceptos b谩sicos de Rust. Cada variable tiene un propietario 煤nico, y la propiedad puede ser transferida o prestada. Si otro hilo quiere cambiar el recurso, entonces transferimos la propiedad moviendo la variable a un nuevo hilo.

Mover arroja una excepci贸n: varios subprocesos pueden escribir en la misma memoria, pero nunca al mismo tiempo. Dado que el propietario siempre est谩 solo, 驴qu茅 sucede si otro hilo toma prestada una variable?

En Rust, tienes un pr茅stamo mutable o varios inmutables. No es posible introducir simult谩neamente pr茅stamos mutables e inmutables (o varios pr茅stamos mutables). En la seguridad de la memoria, es importante que los recursos se liberen correctamente, y en la seguridad de los subprocesos es importante que solo un subproceso tenga derecho a cambiar una variable en un momento dado. Adem谩s, en tal situaci贸n, ning煤n otro flujo se referir谩 a pr茅stamos obsoletos: es posible registrarlos o compartirlos, pero no ambos.

El concepto de propiedad est谩 dise帽ado para abordar las vulnerabilidades de la memoria. Result贸 que tambi茅n evita la carrera de datos.

Aunque muchos idiomas tienen m茅todos de seguridad de la memoria (como el conteo de enlaces y la recolecci贸n de basura), por lo general dependen de la sincronizaci贸n manual o las prohibiciones del uso compartido simult谩neo para evitar la carrera de datos. El enfoque de Rust aborda ambos tipos de seguridad, tratando de resolver el problema principal de determinar el uso aceptable de los recursos y garantizar esta validez en el momento de la compilaci贸n.



Pero espera! Eso no es todo!


Las reglas de propiedad evitan que m煤ltiples hilos escriban datos en la misma ubicaci贸n de memoria y proh铆ben el intercambio simult谩neo de datos entre hilos y mutabilidad, pero esto no necesariamente proporciona estructuras de datos seguras para hilos. Cada estructura de datos en Rust es segura para subprocesos o no. Esto se pasa al compilador utilizando un sistema de tipos.

"Un programa bien escrito no puede cometer un error". - Robin Milner, 1978

En lenguajes de programaci贸n, los sistemas de tipos describen un comportamiento aceptable. En otras palabras, un programa bien escrito est谩 bien definido. Mientras nuestros tipos sean lo suficientemente expresivos como para capturar el significado deseado, un programa bien tipado se comportar谩 seg煤n lo previsto.

Rust es un lenguaje de tipo seguro, aqu铆 el compilador verifica la consistencia de todos los tipos. Por ejemplo, el siguiente c贸digo no se compila:

let mut x = "I am a string"; x = 6; 

  error[E0308]: mismatched types --> src/main.rs:6:5 | 6 | x = 6; // | ^ expected &str, found integral variable | = note: expected type `&str` found type `{integer}` 

Todas las variables en Rust son de tipo a menudo impl铆cito. Tambi茅n podemos definir nuevos tipos y describir las capacidades de cada tipo utilizando el sistema de rasgos . Los rasgos proporcionan una abstracci贸n de la interfaz. Dos rasgos incorporados importantes son Send y Sync , que el compilador proporciona de manera predeterminada para cada tipo:

  • Send indica que la estructura se puede transferir de forma segura entre subprocesos (requerido para transferir la propiedad)
  • Sync indica que los hilos pueden usar la estructura de forma segura.

El siguiente ejemplo es una versi贸n simplificada del c贸digo de la biblioteca est谩ndar que genera hilos:

  fn spawn<Closure: Fn() + Send>(closure: Closure){ ... } let x = std::rc::Rc::new(6); spawn(|| { x; }); 

La funci贸n spawn toma un solo argumento, closure y requiere un tipo para este 煤ltimo que implemente los rasgos Send y Fn . Al intentar crear una secuencia y pasar el valor de closure con la variable x compilador arroja un error:

  error [E0277]: `std :: rc :: Rc <i32>` no se puede enviar entre hilos de forma segura
      -> src / main.rs: 8: 1
       El |
     8 |  engendrar (mover || {x;});
       El |  ^^^^^ `std :: rc :: Rc <i32>` no se puede enviar entre hilos de forma segura
       El |
       = ayuda: dentro de `[cierre@src/main.rs: 8: 7: 8:21 x: std :: rc :: Rc <i32>]`, el rasgo `std :: marker :: Send` no est谩 implementado para `std :: rc :: Rc <i32>`
       = nota: requerido porque aparece dentro del tipo `[cierre@src/main.rs: 8: 7: 8:21 x: std :: rc :: Rc <i32>]`
     nota: requerido por `spawn` 

Los rasgos de Send y Sync permiten que el sistema de tipo Rust entienda qu茅 datos se pueden compartir. Al incluir esta informaci贸n en el sistema de tipos, la seguridad de los hilos se convierte en parte de la seguridad de tipos. En lugar de documentaci贸n, la seguridad del subproceso se implementa mediante la ley del compilador .

Los programadores ven claramente objetos comunes entre subprocesos, y el compilador garantiza la fiabilidad de esta instalaci贸n.



Aunque las herramientas de programaci贸n paralela est谩n disponibles en muchos idiomas, evitar las condiciones de carrera no es f谩cil. Si requiere que los programadores alternen complejas instrucciones e interact煤en entre hilos, entonces los errores son inevitables. Aunque las infracciones de seguridad de la memoria y los hilos conducen a consecuencias similares, las protecciones de memoria tradicionales, como el conteo de enlaces y la recolecci贸n de basura, no impiden las condiciones de la carrera. Adem谩s de la garant铆a est谩tica de seguridad de la memoria, el modelo de propiedad Rust tambi茅n evita cambios de datos inseguros y el intercambio incorrecto de objetos entre subprocesos, mientras que el sistema de tipos proporciona seguridad de subprocesos en tiempo de compilaci贸n.

Source: https://habr.com/ru/post/441370/


All Articles