Hace algunos días estuvimos analizando las estructuras comunes en ingeniería reversa, así como también los beneficios que podemos obtener de la utilización de herramientas adecuadas. En esta oportunidad, volvemos para completar este análisis con más patrones que serán de gran ayuda en la tarea de reversing.
Para esta segunda parte, continuaremos con el ejemplo planteado anteriormente. En la imagen de arriba observamos el programa escrito en C. Por su parte, en la imagen que sigue a continuación recordamos cómo luce ese programa una vez que ha sido desensamblado:
Ya analizamos el significado de las líneas resaltadas en rojo. Pero también hemos marcado unas líneas con color naranja: si prestamos atención, veremos que estas líneas se encuentran después de una llamada a subrutina call. Antes de seguir, debe mencionarse que existen diversas convenciones para la llamada a subrutinas. Es necesario que el código que llama y el que es llamado sepan dónde colocar y buscar los datos, respectivamente. En este sentido, podemos mencionar dos de las convenciones más importantes:
- cdecl: esta es la forma más común de manejar el pasaje de parámetros a una subrutina. Se caracteriza por hacer el pasaje a través del stack, introduciendo los parámetros según aparecen en la llamada, de derecha a izquierda (en nuestro ejemplo, primero argv y luego argc). Además, el resultado devuelto por la subrutina es colocado en eax (o edx:eax, de ser necesario). Pero lo más importante a mencionar es que la rutina que llama (no la que es llamada) es la que debe encargarse de limpiar lo introducido en el stack una vez que se ha finalizado la llamada.
- stdcall: esta convención se utiliza en menor medida; mayormente la vemos en funciones de la API Win32. Es exactamente igual a cdecl, excepto por la limpieza del stack: en este caso es la subrutina invocada quien debe llevar a cabo la tarea.
Entonces, ¿cómo se traduce esto en el código? Si se utiliza cdecl, inmediatamente después de la llamada a la subrutina se observan instrucciones para realizar la limpieza del stack. Esto es justamente lo que se marca con color naranja en nuestro ejemplo: luego de la llamada a atoi, se limpia el espacio que se reservó en el stack para pasar el parámetro (4 bytes); luego de la llamada a sub, se hace lo mismo, pero con los dos parámetros pasados (8 bytes). Si tenemos en cuenta que el registro esp apunta al tope del stack, y que éste crece de direcciones de memoria más grandes a direcciones más chicas, la suma de un valor positivo a esp hará que los valores de la cima queden fuera del stack, logrando el efecto de limpieza o borrado. Pero… ¿qué pasa si se utiliza stdcall?
Al forzar la convención stdcall para la llamada a sub, se observan dos cosas. En primer lugar, que en el cuerpo de main, y después de la llamada a sub, no hay operaciones para limpiar los parámetros introducidos en el stack; si comparamos este código con el anterior, vemos que la línea add esp,8 ya no está. En segundo lugar, al observar el código de sub vemos que la instrucción ret ha cambiado. Ahora es responsable sub de limpiar su propio stack frame, para lo cual ejecuta ret con la cantidad de bytes a limpiar, tomando 4 bytes por parámetro.
Para cerrar este análisis, haremos referencia a las operaciones realizadas sobre el registro ebp y la diferencia entre variables locales y argumentos de llamada a subrutina. En nuestra primer imagen del código desensamblado, hemos marcado en amarillo la operación push ecx. Sin embargo, podemos notar que ecx no está inicializado. Esto nos indica que el valor que está siendo introducido en el stack es indeterminado, lo que significa que se está guardando espacio en el stack. Si además observamos que esta línea no está presente en sub, entendemos que el espacio que se guarda es para variables locales (en el caso de main, una sola variable, a). Entonces, de manera general, podemos decir que si una rutina cuenta con variables locales, luego del código de inicialización encontraremos las líneas que guardan este espacio para las variables en el stack.
Si ahora nos detenemos en el resto de las operaciones realizadas que hacen referencia al registro ebp, notaremos que algunas le suman un valor, y otras se lo restan. En general, nos resultará muy útil en la práctica considerar lo siguiente: cuando a ebp le sumamos un valor para referenciar una posición dentro del stack, estaremos accediendo a uno de los argumentos del código que está siendo ejecutado. Por su parte, si le sustraemos un valor, estamos dentro del stack frame local, con lo cual se hace referencia a variables locales. En otras palabras, si vemos que a ebp se le suma algo, se está tratando de recuperar un parámetro; si se le resta algo, se quiere acceder a una variable local. Esto se observa en la otra parte del código que se ha marcado con color amarillo: se guarda en la pila el valor de la variable a, para la cual antes se había reservado espacio con push ecx.
Si aprendemos a reconocer estos patrones entre las líneas de código, nuestro análisis será mucho más rápido y efectivo. Sin embargo, esto ha sido solo una introducción al tema. En futuros posts estaremos cubriendo cómo reconocer estructuras como bucles, arreglos de datos y sentencias if y switch, entre otras cosas. ¡Será hasta la próxima!