El a帽o pasado, Mozilla lanz贸
Quantum CSS para Firefox, la culminaci贸n de ocho a帽os de desarrollo de Rust, un lenguaje de programaci贸n de sistemas amigable con la memoria. Tom贸 m谩s de un a帽o reescribir el componente principal del navegador en Rust.
Hasta ahora, todos los principales motores de navegador est谩n escritos en C ++, principalmente por razones de eficiencia. Pero el gran rendimiento conlleva una gran responsabilidad: los programadores de C ++ deben administrar manualmente la memoria, lo que abre el cuadro de vulnerabilidad de Pandora. Rust no solo corrige dichos errores, sino que sus m茅todos tambi茅n evitan
las carreras de datos , lo que permite a los programadores implementar de manera m谩s eficiente el c贸digo paralelo.
驴Qu茅 es la seguridad de la memoria?
Cuando hablamos de crear aplicaciones seguras, a menudo mencionamos la seguridad de la memoria. Extraoficialmente, queremos decir que en ning煤n estado el programa puede acceder a memoria no v谩lida. Causas de violaciones de seguridad:
- guardar el puntero despu茅s de liberar memoria (use-after-free);
- desreferenciar un puntero nulo;
- uso de memoria no inicializada;
- intento del programa para liberar la misma celda dos veces (doblemente libre);
- desbordamiento de b煤fer.
Para una definici贸n m谩s formal, vea Michael Hicks
'What is Memory Security' , as铆 como un
art铆culo cient铆fico sobre este tema.
Dichas violaciones pueden provocar un bloqueo inesperado o un cambio en el comportamiento esperado del programa. Consecuencias potenciales: fuga de informaci贸n, ejecuci贸n de c贸digo arbitrario y ejecuci贸n remota de c贸digo.
Gesti贸n de la memoria
La gesti贸n de la memoria es cr铆tica para el rendimiento y la seguridad de la aplicaci贸n. En esta secci贸n, consideramos el modelo b谩sico de memoria. Uno de los conceptos clave son los
punteros . Estas son variables en las que se almacenan las direcciones de memoria. Si vamos a esta direcci贸n, veremos algunos datos all铆. Por lo tanto, decimos que el puntero es una referencia a estos datos (o los se帽ala). As铆 como la direcci贸n de la casa le dice a las personas d贸nde encontrarlo, la direcci贸n de la memoria muestra al programa d贸nde encontrar los datos.
Todo en el programa se encuentra en direcciones de memoria espec铆ficas, incluidas las instrucciones de c贸digo. El uso incorrecto de los punteros puede generar serias vulnerabilidades, incluida la filtraci贸n de informaci贸n y la ejecuci贸n de c贸digo arbitrario.
Asignaci贸n / Liberaci贸n
Cuando creamos una variable, el programa debe asignar suficiente espacio en la memoria para almacenar los datos de esta variable. Como cada proceso tiene una cantidad limitada de memoria, por supuesto, necesita una forma de
liberar recursos. Cuando se libera la memoria, est谩 disponible para almacenar nuevos datos, pero los datos antiguos permanecen all铆 hasta que se sobrescribe la celda.
Tampones
Un b煤fer es un 谩rea de memoria contigua en la que se almacenan varias instancias del mismo tipo de datos. Por ejemplo, la frase "Mi gato es Batman" se almacenar谩 en un b煤fer de 16 bytes. Las memorias intermedias est谩n determinadas por la direcci贸n de inicio y la longitud. Para no da帽ar los datos en la memoria vecina, es importante asegurarse de que no leemos ni escribimos fuera del b煤fer.
Flujo de control
Los programas consisten en rutinas que se ejecutan en un orden espec铆fico. Al final de la subrutina, la computadora va al puntero almacenado a la siguiente parte del c贸digo (llamada
direcci贸n de retorno ). Cuando va a la direcci贸n del remitente, ocurre una de tres cosas:
- El proceso contin煤a normalmente (la direcci贸n del remitente no cambia).
- El proceso se bloquea (la direcci贸n ha sido cambiada y apunta a memoria no ejecutable).
- El proceso contin煤a, pero no como se esperaba (la direcci贸n de retorno ha cambiado y el flujo de control ha cambiado).
C贸mo los idiomas proporcionan seguridad de memoria
Todos los lenguajes de programaci贸n pertenecen a diferentes partes del
espectro . Por un lado del espectro hay lenguajes como C / C ++. Son efectivos, pero requieren administraci贸n manual de memoria. Por otro lado, los idiomas interpretados con administraci贸n autom谩tica de memoria (por ejemplo, conteo de referencias y recolecci贸n de basura (GC)), pero dan resultado con el rendimiento. Incluso los idiomas con recolecci贸n de basura bien optimizada no se pueden comparar en
rendimiento con los idiomas sin GC.
Gesti贸n manual de memoria
Algunos lenguajes (por ejemplo, C) requieren que los programadores administren manualmente la memoria: cu谩ndo y cu谩nta memoria asignar, cu谩ndo liberarla. Esto le da al programador un control completo sobre c贸mo el programa usa los recursos, proporcionando un c贸digo r谩pido y eficiente. Pero este enfoque es propenso a errores, especialmente en bases de c贸digo complejas.
Errores que son f谩ciles de cometer:
- olvide que los recursos son gratuitos y trate de usarlos;
- no asigne suficiente espacio para el almacenamiento de datos;
- leer memoria fuera del b煤fer.
Instrucciones de seguridad adecuadas para quienes manejan la memoria manualmentePunteros inteligentes
Los punteros inteligentes proporcionan informaci贸n adicional para evitar la gesti贸n incorrecta de la memoria. Se utilizan para la gesti贸n autom谩tica de la memoria y la verificaci贸n de bordes. A diferencia de un puntero normal, un puntero inteligente puede autodestruirse y no esperar谩 a que el programador lo elimine manualmente.
Hay varias opciones para tal construcci贸n, que envuelve el puntero original en varias abstracciones 煤tiles. Algunos punteros inteligentes
cuentan referencias a cada objeto, mientras que otros implementan una pol铆tica de alcance para limitar la vida 煤til del puntero a ciertas condiciones.
Al contar enlaces, los recursos se liberan cuando se elimina la 煤ltima referencia al objeto. Las implementaciones b谩sicas de conteo de referencias adolecen de bajo rendimiento, mayor consumo de memoria y son dif铆ciles de usar en entornos de subprocesos m煤ltiples. Si los objetos se refieren entre s铆 (enlaces circulares), el recuento de referencia para cada objeto nunca llegar谩 a cero, por lo que se requieren m茅todos m谩s complejos.
Recolecci贸n de basura
Algunos lenguajes (por ejemplo, Java, Go, Python) implementan la
recolecci贸n de basura . Una parte del tiempo de ejecuci贸n llamada recolector de basura (GC) monitorea las variables e identifica recursos inaccesibles en el gr谩fico de enlaces entre objetos. Tan pronto como el objeto no est茅 disponible, el GC libera memoria base para su futura reutilizaci贸n. Cualquier asignaci贸n y liberaci贸n de memoria ocurre sin un comando expl铆cito del programador.
Aunque el GC garantiza que la memoria siempre se use correctamente, no libera la memoria de la manera m谩s eficiente; a veces, el 煤ltimo uso de un objeto ocurre mucho antes de que el recolector de basura libere la memoria. Los costos de rendimiento son prohibitivos para aplicaciones de misi贸n cr铆tica: a veces es necesario usar 5 veces m谩s memoria para evitar la degradaci贸n del rendimiento.
Posesi贸n
Rust utiliza la propiedad para garantizar un alto rendimiento y seguridad de la memoria. M谩s formalmente, este es un ejemplo de
mecanograf铆a de
afinidad . Todo el c贸digo Rust sigue ciertas reglas que permiten al compilador administrar la memoria sin perder tiempo de ejecuci贸n:
- Cada valor tiene una variable llamada propietario.
- Solo un propietario puede ser a la vez.
- Cuando el propietario se mueve fuera del alcance, el valor se elimina.
Los valores pueden
transferirse o tomarse
prestados de una variable a otra. Estas reglas se aplican a una parte del compilador llamada verificador de pr茅stamos.
Cuando una variable queda fuera de alcance, Rust libera esta memoria. En el siguiente ejemplo, las variables
s1
y
s2
van m谩s all谩 del alcance, ambas intentan liberar la misma memoria, lo que conduce a un error de doble liberaci贸n. Para evitar esto, al transferir un valor de una variable, el propietario anterior se vuelve inv谩lido. Si el programador intenta utilizar una variable no v谩lida, el compilador rechazar谩 el c贸digo. Esto se puede evitar creando una copia profunda de los datos o utilizando enlaces.
Ejemplo 1 : Transferencia de propiedad
let s1 = String::from("hello"); let s2 = s1;
Otro conjunto de reglas de verificaci贸n de pr茅stamos se relaciona con la vida 煤til de las variables. Rust proh铆be el uso de variables no inicializadas y punteros colgantes a objetos inexistentes. Si compila el c贸digo del ejemplo a continuaci贸n,
r
se referir谩 a una memoria que se libera cuando
x
sale del alcance: se produce un puntero colgante. El compilador monitorea todas las 谩reas y verifica la validez de todas las transferencias, a veces requiere que el programador indique expl铆citamente la vida 煤til de la variable.
Ejemplo 2 : puntero colgante
let r; { let x = 5; r = &x; } println!("r: {}", r);
El modelo de propiedad proporciona una base s贸lida para el acceso correcto a la memoria, evitando comportamientos indefinidos.
Vulnerabilidades de memoria
Las principales consecuencias de la memoria vulnerable:
- Bloqueo : acceder a memoria no v谩lida puede provocar la finalizaci贸n inesperada de la aplicaci贸n.
- Fuga de informaci贸n : provisi贸n involuntaria de datos privados, incluida informaci贸n confidencial, como contrase帽as.
- Ejecuci贸n de c贸digo arbitrario (ACE) : permite a un atacante ejecutar comandos arbitrarios en la m谩quina de destino. Si esto sucede a trav茅s de la red, lo llamamos Ejecuci贸n remota de c贸digo (RCE).
Otro problema es
una p茅rdida de memoria cuando la memoria asignada no se libera despu茅s de que finaliza el programa. Por lo tanto, puede usar toda la memoria disponible: las solicitudes de recursos se bloquean, lo que provocar谩 una denegaci贸n de servicio. Este es un problema de memoria que no se puede resolver a nivel de PL.
En el mejor de los casos, con un error de memoria, la aplicaci贸n se bloquear谩. En el peor de los casos, un atacante obtiene el control de un programa a trav茅s de una vulnerabilidad (que podr铆a conducir a m谩s ataques).
Abusos de la memoria liberada (uso libre posterior, doble libre)
Esta subclase de vulnerabilidades se produce cuando se libera un recurso, pero a煤n se conserva un enlace a su direcci贸n. Este es un
poderoso m茅todo de hackers que puede conducir a un acceso fuera de rango, fuga de informaci贸n, ejecuci贸n de c贸digo y mucho m谩s.
Los idiomas con recolecci贸n de basura y conteo de referencias evitan el uso de punteros inv谩lidos, destruyendo solo objetos inaccesibles (que pueden conducir a la degradaci贸n del rendimiento), y los lenguajes controlados manualmente son susceptibles a esta vulnerabilidad (especialmente en bases de c贸digo complejas). La herramienta de verificaci贸n de pr茅stamos en Rust no permite que los objetos se destruyan mientras se hace referencia, por lo que estos errores se eliminan en la etapa de compilaci贸n.
Variables no inicializadas
Si la variable se usa antes de la inicializaci贸n, estos datos pueden contener cualquier dato, incluyendo basura aleatoria o datos previamente descartados, lo que conduce a una fuga de informaci贸n (a veces se los llama
punteros no v谩lidos ). Para evitar estos problemas, los lenguajes de administraci贸n de memoria a menudo usan el procedimiento de inicializaci贸n autom谩tica despu茅s de asignar memoria.
Como en C, la mayor铆a de las variables en Rust no se inicializan inicialmente. Pero a diferencia de C, no puede leerlos antes de la inicializaci贸n. El siguiente c贸digo no se compila:
Ejemplo 3 : uso de una variable no inicializada
fn main() { let x: i32; println!("{}", x); }
Punteros nulos
Cuando una aplicaci贸n desreferencia un puntero que resulta ser nulo, generalmente solo accede a la basura y provoca un bloqueo. En algunos casos, estas vulnerabilidades pueden conducir a la ejecuci贸n de c贸digo arbitrario (
1 ,
2 ,
3 ). Rust tiene dos tipos de punteros:
enlaces y punteros sin formato. Los enlaces son seguros, pero los punteros sin formato pueden ser un problema.
Rust evita la desreferenciaci贸n de un puntero nulo de dos maneras:
- Evite punteros anulables.
- Evite desreferenciar punteros sin formato.
Rust evita punteros nulos al reemplazarlos con el
Option
especial. Para cambiar el valor nulo posible en el tipo
Option
, el lenguaje requiere que el programador maneje expl铆citamente el caso con un valor nulo; de lo contrario, el programa no se compilar谩.
驴Qu茅 hacer si no se pueden evitar los punteros que permiten un valor nulo (por ejemplo, al interactuar con el c贸digo en otro idioma)? Intenta aislar el da帽o. La desreferenciaci贸n de punteros sin procesar debe ocurrir en un bloque inseguro aislado.
Afloja las reglas Rust y resuelve algunas operaciones que pueden causar un comportamiento indefinido (por ejemplo, desreferenciar un puntero sin formato).
"Todo sobre el chekcer prestado ... 驴qu茅 pasa con ese lugar oscuro?"
- Este es un bloque inseguro. Nunca vayas, SimbaDesbordamiento de b煤fer
Discutimos vulnerabilidades que pueden evitarse restringiendo el acceso a la memoria indefinida. Pero el problema es que el desbordamiento del b煤fer no accede correctamente a la memoria indefinida, sino legalmente asignada. Al igual que el error use-after-free, dicho acceso puede ser un problema porque accede a la memoria liberada, que a煤n contiene informaci贸n confidencial que ya no deber铆a existir.
Los desbordamientos del b煤fer simplemente significan acceso fuera de los l铆mites. Debido a la forma en que se almacenan las memorias intermedias en la memoria, a menudo filtran informaci贸n que puede contener datos confidenciales, incluidas contrase帽as. En casos m谩s graves, las vulnerabilidades ACE / RCE son posibles sobrescribiendo el puntero de instrucci贸n.
Ejemplo 4: desbordamiento de b煤fer (c贸digo C)
int main() { int buf[] = {0, 1, 2, 3, 4};
La protecci贸n m谩s simple contra los desbordamientos del b煤fer es exigir siempre controles de borde al acceder a los elementos, pero esto conduce a un
bajo rendimiento .
驴Qu茅 hace el 贸xido? Los tipos de b煤fer integrados en la biblioteca est谩ndar requieren controles de bordes para cualquier acceso aleatorio, pero tambi茅n proporcionan API de iterador para acelerar las llamadas secuenciales. Esto asegura que leer y escribir fuera de los l铆mites no sea posible para estos tipos. Rust promueve patrones que requieren controles de borde solo en lugares donde es casi seguro que tenga que colocarlos manualmente en C / C ++.
La seguridad de la memoria es solo la mitad de la batalla
Las infracciones de seguridad conducen a vulnerabilidades como la fuga de datos y la ejecuci贸n remota de c贸digo. Hay varias formas de proteger la memoria, incluidos punteros inteligentes y recolecci贸n de basura. Incluso puede
probar formalmente la seguridad de la memoria . Si bien algunos idiomas han llegado a un acuerdo con la degradaci贸n del rendimiento en aras de la seguridad de la memoria, el concepto de propiedad de Rust proporciona seguridad y minimiza los gastos generales.
Desafortunadamente, los errores de memoria son solo una parte de la historia cuando hablamos de escribir c贸digo seguro. En el pr贸ximo art铆culo, consideraremos la seguridad de subprocesos y los ataques a c贸digo paralelo.
Explotaci贸n de vulnerabilidades de memoria: recursos adicionales