Vamos a ver una demostración práctica de los errores vinculados al Stagefright bug, el agujero de seguridad que ha hecho temblar a Android.
Todos habéis oído hablar del Stagefright bug, y si no es así os recomiendo que os paséis por aqui. A modo de resumen, y para centrar un poco el contexto, diremos que es un bug que afecta a la biblioteca Stagefright, escrita en C++, encargada de la gestión multimedia a bajo nivel.
El exploit para Stagefright se basa fundamentalmente en desbordamientos tanto superiores como inferiores. Un desbordamiento superior, de ahora en adelante overflow, se da cuando un proceso de cálculo da como resultado un número de mayor magnitud que el número máximo que puede representar el soporte donde vaya a ser almacenado. Por su parte, un desbordamiento inferior, de ahora en adelante underflow, se da cuando un proceso de cálculo da como resultado un número de menor magnitud que el número nínimo que puede representar el soporte donde vaya a ser almacenado.
A continuación vamos a ver una serie de ejemplos básicos en C donde podemos ver como ocasionar los fallos que hacen posible el exploit de Stagefright. Todos ellos se apoyan en el desbordamiento, concepto aclarado anteriormente.
Asignando memoria
Una parte fundamental de toda pieza de código programada en C/C++ es la asignación de memoria. Para almacenar cualquier dato, necesitamos alojarlo en memoria, lo cual hacemos declarando variables. Por ejemplo, para almacenar un número:
int a = 5;
Le estamos diciendo al compilador que estamos creando un número entero entero, por lo que éste reservará una cantidad de memoria suficiente para almacenar un número entero estándar. También estamos nombrando esa región de memoria con un nombre (a) para podernos referir a ella posteriormente. Por último le estamos diciendo que almacene en dicha región el valor 5 pero este paso es opcional, si no asignamos nada esta región de memoria tendrá un valor residual impredecible fruto de otro programa o proceso anterior que la utilizara. Vamos a ver un ejemplo práctico:
#include
int main ()
{
int herpDerp[255];
int myArray[10];
for(int i = 0;i < 10;i++)
{
printf ( "%d n " , myArray[i]);
}
return 0 ;
}
En este caso estamos reservando espacio para arrays, que no son otra cosa que una sucesión de elementos del mismo tipo alineados en memoria. En nuestro ejemplo, estos arrays son una secuencia de números enteros que serán reservados en regiones contiguas de memoria. El primer array, herpDerp, únicamente reserva el espacio de 255 números enteros en memoria, con el proposito de ver más claro todo esto. El segundo array, que contendrá 10 números enteros alineados en memoria, lo recorreremos mediante el bucle para acceder a las diferentes regiones que ocupa cada entero e imprimiremos el contenido de las mismas mediante el comando printf.
Tras compilar y ejecutar este programa, vemos que el resultado es la impresión en pantalla de los valores residuales que contenía la memoria:
$ ./program
- 1010740764
32765
0
0
2130837504
0
1642289244
32535
1642288456
32535
Lo interesante es volver a ejecutar el programa, exáctamente el mismo código y el mismo ejecutable, y ver la salida:
$ ./program
355598324
32767
0
0
-1090387968
0
-980453284
32522
-980454072
32522
Más valores residuales... pero distintos. Esto se debe a que la memoria no estaba inicializada y nunca introdujimos valores dentro del myArray. En cada ejecución, se está asignando porciones diferentes de memoria, por lo que cada vez tendrá un valor distinto.
Empecemos a desbordar
Hasta aquí sólo hemos demostrado que C no inicializa las variables con un valor por defecto, nada espectacular, pero este principio es importante. Vamos a imaginar un programa más complicado donde manejemos varios tipos de números que pedimos al usuario que introduzca. Para no desviarnos y simplificar, introduciremos directamente estos números en el código.
Vamos a hacer un programa que su única funcion sea leer los números de un array y almacenarlos en otro. Aaunque este programa no tiene mucho sentido, nos servirá para ver el funcionamiento (NOTA: esta no es la manera correcta de implementar la funcionalidad que describimos, ya que hay métodos y constructores concretos para esta situación. Se han utilizado opercaiones inseguras para ilustrar cómo pueden aparecer vulnerabilidades en el código).
#include
int main()
{
int lengthOfData = 5;
int myArray [5];
int a = 100;
for (int i=0; i < lengthOfData+6; i++)
{
myArray[i] = i;
}
// El array está ahora lleno con algún contenido
for (int i=0; i < lengthOfData; i++)
{
printf("%dn", myArray[i]);
}
printf("a = %dn", a);
return 0;
}
Este código imprimiría los valores de 0 a 4 y después una línea con un 100. Hablo en pasado porque cómo os habréis percatado hemos incrementado el índice lengthOfData en 6, por lo que estamos intentando establecer el contenido de direcciones de memoria no reservadas (sólo hemos reservado 5). Veamos lo que ocurre:
$ ./program
0
1
2
3
4
5
6
7
8
a = 8
[1] 8144 segmentation fault (core dumped) ./program
Aquí pasa algo raro, ¿verdad? Esperábamos ver sólo 5 valores (del 0 al 4) en caso de que no se detuviese la ejecución (ya que estabamos asignando valores a direcciones no reservadas), sin embargo vemos que se han imprimido 9 valores... ¿Qué está pasando?
Pues estamos, ni más ni menos, que desbordando un array. Cuando reservamos el espacio de memoria, reservamos espacio únicamente para 5 elementos, sin ebargo a la hora de establecer los valores lo hicimos para 11 elementos (lengthOfData+6). Por tanto hemos escrito en memoria que no se debería haber tocado.
Si nos fijamos, en el proceso hemos sobreescrito 2 variables importantes: la variable a que debería valer 100 y ha sido sobreescrita con el valor 8, y la variables lengthOfData que ha hecho que imprimamos más valores de los que esperábamos, en concreto 9 (por lo que su valor, 5, ha sido sobreescrito con el 9). Como vemos ambas variables han sido sobreescritas con valores consecutivos, por lo que podemos deducir que estaban alineadas en memoria. En la siguiente sección probaremos esto.
Como vemos el programa se ha detenido con un fallo "segmentation fault", que por lo general nos viene a indicar que se ha producido un desbordamiento u otra condición inesperada. Este sencillo programa nos ha llevado a una situación de desbordamiento. Aunque la corrupción de zonas aleatoria no es muy interesante, si logramos cambiar regiones de memoria con sentido empieza a cobrar interés.
Modificando datos significativos
Vamos a realizar unos pequeños cambios en el código anterior. Cambiando las siguientes líneas de codigo, podemos lograr establecer nuestro propio valor de la variable a:
if (i == 8)
{
myArray[i] = 0;
}
else
{
myArray[i] = i;
}
Esto lo único que hace es que en la novena iteraciónl, cuando el valor es 8, establecemos el valor de la posición coreespondiente a 0. Esta posición de memoria empieza 3 direcciones después del último valor reservado de myArray, y corresponde justamente con la dirección de la variable a.
Cuando ejecutamos este programa, obtenemos lo siguiente:
$ ./program
0
1
2
3
4
5
6
7
0
a = 0
[1] 8607 segmentation fault (core dumped) ./program
Como podéis ver, hemos logrado cambiar el valor de una variable con el desbordamiento de un array,
Igualmente, podemos cambiar el valor del contenido de la variable lengthOfData modificando el contenido de la dirección de memoria myArray[9] añadiendo:
else if (i == 9)
{
myArray[i] = 40;
}
Si nuestra teroría es correcta, ahora se mostrarán 40 elementos en lugar de los 11 anteriores. No vamos a comprobarlo (aunque ya os digo que es cierto) pero podéis hacerlo como ejercicio de práctica. Esto nos permite modificar otras secciones del programa accediendo a memoria a la que no deberíamos tener acceso. Supongo que ya os estáis dando cuenta de por donde van los tiros.
¿Qué relación tiene esto con Stagefright?
En Stagefright el desbordamiento ocurría con los números enteros (overflow y underflow). Esto es algo distinto al desbordamiento de arrays que hemos visto, pero podemos ilustrarlo brevemente de la siguiente manera:
#include
int main()
{
unsigned int a = 5;
int b = 10;
a = a - b;
printf("%un", a);
return 0;
}
Aquí únicamente hacemos operaciones matemáticas como hace Stagefright. Para calcular el espacio que necesita reservar en memoria a la hora de almacena videos u otros datos, Stagefright hace cálculos con metadatos para determinar qué partes deben ser alojadas y cuales no, para solo consumir la memoria necesaria.
En el ejemplo hemos puesto una suma simple (5-10), pero imaginad que hemos puesto intencionadamente el valor 10 aquí mediante un archivo MP4 o 3GP modificado. O imadinad que hemos cambiado el valor de esta variable con un desbordamiento de array como el del ejemplo anterior. Cuando restamos al número natural 5 el valor 10, C me dice que el valor es 4294967291 y esto es debido a que al trabajar con números naturales (unsigned int) un valor negativo es interpretado como un número positivo muy alto. Ocurre lo mismo cuando sumas un número entero a otro número entero de muy alto, el valor será un número muy pequeño (y probablemente no asigne la suficiente memoria). Esto se conoce como desbordamiento de enteros.
Modifiquemos el código anterior para ilustrarlo:
unsigned int a = 4294967291;
int b = 10;
a = a + b;
Esto nos da como resultado que a vale 5. Aunque esperábamos un número enorme, el número alojado es demasiado grande para la memoria reservada y como consecuencia solo almacena el 5 en el espacio disponible. Esto es el desbordamiento de enteros.
Aclaración y conclusión
Mediante estos ejemplos hemos vistos casos reales donde se nos permite acceder y modificar zonas de memoria únicamente variando el tamaño de los datos. El error de Stagefright se basa en estos fundamentos y aunque son mucho más complejos, son ocasionados por estos motivos.
Como podéis ver la gestión de memoria a bajo nivel es un aspecto muy muy difícil de controlar. Fijaos que Google, contando con uno de los mejores equipos de programadores del mundo, comete estos errores. La solución está o en poner muchísimo cuidad (más que el equipo de Google) a la hora de programar en lenguajes de bajo nivel, o elegir lenguajes de alto nivel que, aunque ofercen menor aprovechamiento de recursos, no tienen estos problemas.
Espero que os haya resultado tan interesante como a mi. Podéis probar todos los ejemplos para practicar. ¿Qué os parece?
Vía XDA-Developers.