martes, 24 de junio de 2014

Programación dirigida por retorno. (II) - Un ejemplo práctico

Finalicé el post anterior con el compromiso de continuar con un ejemplo práctico de Programación dirigida por retorno.

Pongámonos en un caso extremadamente simple, un programa lee un fichero que contiene dos líneas, la primera es un identificador de sesión (numérico) y la siguiente es una contraseña válida para ese identificador de sesión.

Hay una matriz en la pila que se encarga de recibir ambos valores y coloca en el elemento apuntado por el identificador de sesión el valor la contraseña asociada.

El resto es demasiado sencillo se pregunta si el valor introducido es igual a la entrada que hay en una tabla de contraseñas. Si es igual el procedimiento sigue, si no lo es da un mensaje de error y finaliza.


{
FILE *fd;
int in1,in2;
int arr[20];
char var[20];

if (argc !=2){
printf(mensaje0,*argv);
return -1;
}
fd = fopen(argv[1],"r");
if(fd == NULL)
{
fprintf(stderr,mensaje1);
return -2;
}
memset(var,7,sizeof(var));
memset(arr,6,20*sizeof(int));
while(fgets(var,20,fd))
{
in1 = atoll(var);
fgets(var,20,fd);
in2 = atoll(var);
/* fill array */
arr[in1]=in2;
//printf("%d - %d\n", arr[in1], tabla[in1]);
if (arr[in1] != tabla[in1])
{
printf("La coordenada no es correcta\n");
return ;
}
printf("La coordenada es correcta\n");
printf(mensaje4);
return;
}
}



Esto está así por simplicidad. En un sistema más complejo la tabla de valores estaría fuera del programa y al menos procesada mediante un algoritmo de hashing. Pero todo este tema nos da un poco igual porque lo que a nosotros nos interesa simplemente dirigir el retorno.





He tomado algunas libertades para hacer más sencillo y comprensible el programa el programa:

Unos campos de memoria de relleno para simplificar los cálculos:



Unas inicializaciones de memoria raras. Lo correcto habría sido inicializarlas a 0x00 pero las he inicializado con los valores 6 y 7 para que nos sea más fácil de identificar en la pila.



Arrancamos Ollydbg y cargamos el programa compilado. Con la información que nos suministra este depurador podemos localizar en que punto está la verificación de la clave correcta: 

if (arr[in1] != tabla[in1])




Y colocamos un breakpoint en este punto.

Como los datos los tenemos en un fichero lo primero que tenemos que hacer es indicar a Ollydbg cuales el argumento de entrada. Lo realizamos desde el menú Debug -> Arguments

Ponemos el nombre del fichero que queramos  que previamente habremos creado con los siguientes valores:

2
93

Ejecutamos hasta que se pare y comprobamos los valores de los registros EAX y EDX. Al ser iguales el programa continúa en la direccion 0x004017F2 (la contraseña es correcta).



Si cargamos el fichero con otros valores:

2
95

Y ejecutamos, cuando se pare el programa podemos ver que los  valores de los registros EAX y EDX sin distintos. El programa da el mensaje de aviso y llega a la instrucción return.

El objetivo, que veremos en la próxima entrada es alterar el contenido de la pila para que nos lleve a la instrucción 0x004017F2 y poder continuar le ejecución del programa como si la contraseña hubiera sido correcta.







miércoles, 18 de junio de 2014

Programación dirigida por retorno. (I) - La teoría

Antes de entrar en lo que es la Programación dirigida por retorno (en adelante la nombraré por sus siglas en inglés: Return Oriented Programming) vamos a refrescar un poco lo que es el el desbordamiento de buffer y su posible utilización para ejecutar código arbitrario.

En anteriores post hemos entrado en este tema a fondo  pero vamos a recordar un poco para quienes no sigan este blog en que consiste.


Buffer Overflow y sus contramedidas.


Consiste alterar mediante un bloque datos la información de la pila, introduciendo código y modificando una dirección de retorno para redirigirla a ese código introducido.

Los compiladores, desde hace tiempo, usan diversas técnicas para  luchar contra estos ataques. Sin entrar en detalles estas técnicas se suele basar en cargar en la pila encima de la dirección de retorno un valor aleatorio y también colocarlo en el heap de memoria. Antes de hacer el return se comprueba que ambos valores coinciden. Para más información aconsejo esta lectura.

El popular compilador gcc tiene también su opción para proteger el stack con la opción -fstack-protector.

Otra solución  ha sido la implantanción en los procesadores de un mecanismo que impidiera la ejecución de código ubicado en la pila. Esta solucion tiene varios nombres XD, NX según el fabricante de ordenadores y a veces se habla indistintamente de esta capacidad hardware con que el sistema operativo la use y hablamos de Data Execution Prevention que es una capacidad software del sistemo operativo que incluso puede simularse en procesadores sin NX.

El conocer un poco más estos mecanismos da para mucho y en este artículo, en lo que nos sirve nos hemos de quedar con la idea de que existe una proteccion que impide la ejecución de código desde la pila

Bien DEP, impide que se pueda ejecutar un código que previamente he metido de forma aviesa en la pila. Pero DEP no impide que coloque lo que quiera en la pila y por lo tanto no va a impedir que sobreescriba la dirección de retorno correcta por una de mi interés.
Lo único que tengo que hacer es que ese código que quiero de que ejecute no esté en la pila. Podemos hacer entonces dos cosas: llamar a un función que nos interese del sistema, por ejemplo construir una llamada al sistema  o bien podemos hacer que el retorno fuera a otro punto del programa comprometido con lo que estamos alterando su comportamiento.

Hasta aquí la teoría, en el siguiente post veremos mediante un sencillo ejemplo como simplemente alterando al dirección de retorno de una función podríamos comprometer sofisticados sistemas de verificación.