Hoy en día muchos lenguajes de programación modernos abstraen el uso de la memoria a través de mecanismos como asignación dinámica de memoria, recolector de basura, estructuras dinámicas, etc.

Lo cuál por un lado facilita el trabajo de los programadores, y por el otro hace que muchos programadores ignoren completamente el funcionamiento de la misma y terminan introduciendo fallas de seguridad en sus programas.

La memoria de acceso aleatorio (RAM, por sus siglas en inglés) es el tipo de memoria para computadoras más conocido, incluso una persona que no se dedica a la computación ha oído de y sabe qué es la RAM.

Sin embargo, tú trabajo como programador, no es solo de saber que RAM significa Random Access Memory, sino el de entender como funciona a bajo y alto nivel.

La RAM es una memoria considerada de acceso aleatorio porque puedes leer cualquier región de la memoria siempre y cuando conozcas la ubicación exacta de la información en ella.

El funcionamiento de la RAM a bajo nivel es relativamente sencillo. Sin entrar demasiado en detalles la RAM es un circuito integrado compuesto de millones de transistores y capacitores.

Los transistores son resistores de transferencia que permiten regular el flujo de la corriente de un lado a otro.

Un capacitor es un componente que sirve para almacenar energía y/o como filtros de frecuencias.

Cuando se conecta un transistor con un capacitor, se creas una celda de memoria que puede almacenar 1 bit de información. El capacitor "almacena" ese bit de información de la siguiente manera:

  • Cuando el capacitor se encuentra al 50% o más de capacidad máxima el bit equivale a 1.
  • Cuando la capacidad es menor a 50% de energía el bit equivale a 0.

He ahí el origen del uso del sistema binario en las computadoras.

El problema de los capacitores es que la información es muy volátil. En algunas ocasiones, el capacitor solamente puede mantener la carga por unos nanosegundos; por lo tanto se necesita de otro mecanismo que recargue todos los capacitores que tengan un nivel de energía equivalente a un bit de valor 1 antes de que se descarguen, de lo contrario se puede perder la información.

Este mecanismo frecuentemente es administrado automáticamente por el CPU miles de veces por segundo.

La RAM provee un arreglo de "palabras", que de ahora en adelante me referiré a ellas como words. Hasta hace relativamente poco, el tamaño de un word era de 16 bits; es decir 16 arreglos de capacitores y transistores.

Cada word tiene su propia dirección que generalmente está compuesta por bitlines y wordlines, o el equivalente a columnas y renglones en la RAM. Cuando una columna (bitline) se interescta con un renglón (wordline), se forma una dirección de memoria.

De una manera muy simplifcada, la interacción entre el CPU y la memoria es muy sencilla, y generalmente ocurre mediante 2 instrucciones:

  • La instrucción load que transfiere un word de la RAM a un registro del CPU.
  • La instrucción store que transfiere un word del registro del CPU a la RAM.

El ciclo de ejecución de instrucciones más común es el de la arquitectura Von Neumann: fetch, decode y execute.

Eso quiere decir que el CPU va a extraer una instrucción de la memoria y almacena esa instrucción en el registro de instrucciones, posteriormente descifra la instrucción, la cual puede ocasionar que otros operadores sean extraídos de la memoria y almacenados en algún registro interno, y una vez que la instrucción original haya sido ejecutada el resultado puede que sea almacenado nuevamente en la memoria.

Como podrás darte cuenta, la unidad de memoria solamente puede "ver" un flujo de direcciones de memoria, pero no sabe (ni le importa) como se generan o para que sirven esas direcciones de memoria.

Y es precisamente ahí donde la mayoría de los exploits de memoria ocurren, ya que si logras escribir las direcciones de memoria de instrucciones maliciosas en la unidad de memoria, el CPU va a extraer esas instrucciones, descifrarlas y ejecutarlas.

Para prevenir que un proceso cargue instrucciones maliciosas en otro proceso, el sistema operativo se tiene que asegurar que cada proceso tenga un espacio de memoria único.

Para asignar ese espacio de memoria, el sistema operativo debe determinar el rango de direcciones validas que los procesos pueden leer, para que el sistema operativo pueda asegurar que ese proceso, solamente pueda leer direcciones de memoria validas.

Éste mecanismo funciona de una manera muy sencilla haciendo uso de 2 registros del CPU, a éstos registros los llamaremos: base y limite.

El registro base almacena la dirección física de memoria más pequeña, mientras que el registro límite almacena el tamaño del rango de direcciones válidas.

Por ejemplo, si el registro límite almacena la dirección 300040 y el registro limite almacena 120900, entonces el proceso puede leer/escribir legalmente en todas las direcciones de memoria entre 3000400 y 420939.

El CPU se encarga de proteger el espacio de memoria de cada proceso generado por el usuario. Cualquier intento de ingresar un área de memoria no autorizada para ese proceso, resultará en un error fatal.

Y es precisamente ese diseño el que previene que un programa deliberada o accidentalmente modifique el código o las estructuras de datos que le pertenezcan al sistema operativo o a otros usuarios. Sin embargo, éste mecanismo no es perfecto y puede ser explotado por un usuario malicioso ;)

Para muestra basta un botón: En Marzo 9 de 2015, el equipo Project Zero de Google explotó una vulnerabilidad física en ciertos tipos de memoria DRAM de tal manera que se podía invertir el estado de un bit en la memoria, y lograr una elevación de privilegios en el sistema operativo.

Básicamente la hipótesis de la vulnerabilidad row hammer era que a medida de que las memorias van disminuyendo en tamaño, los espacios de memoria están más juntos uno con respecto al otro, por lo tanto, si se leen ésos espacios de memoria agresivamente, es posible crear una turbulencia en la memoria que cambié un bit de 0 a 1  (o viceversa) en los espacios de memoria adjuntos.

Es por eso que reitero que la memoria es uno de los componentes de la computadora más conocidos, pero menos comprendidos entre los programadores.

La información que se puede encontrar en la memoria de una computadora es invaluable, en muchas ocasiones la ciencia forense ha sido capaz de resolver crímenes a través de análisis forense de memoria, ya que la memoria generalmente almacena información valiosa como:

  1. Procesos
  2. Controladores
  3. Passwords
  4. Passphrases
  5. Archivos descifrados
  6. Usuarios logueados
  7. Archivos abiertos
  8. Conversaciones privadas

Así que vas a aprender una sola cosa de como funciona una computadora, aprende como funciona la memoria.