Al comenzar a visualizar el código desensamblado de un programa es usual sentir que se está frente a un conjunto de instrucciones sin estructura o sentido aparente. Sin embargo, si se analiza en mayor profundidad, dichas estructuras comienzan a emerger y el código comienza a cobrar sentido. Por lo tanto, conocerlas y saber cómo identificarlas puede ser de gran ayuda para acelerar y facilitar nuestras tareas de ingeniería inversa sobre programas compilados para una determinada arquitectura.
En este post nos enfocaremos en mostrar cómo suelen observarse en lenguaje ensamblador algunas estructuras de control de flujo presentes en la mayoría de los lenguajes de alto nivel. En los ejemplos se utilizará sintaxis de C++, instrucciones de Assembly x86-64 y compilador GCC.
If
Los condicionales están fuertemente basados en instrucciones de comparación (CMP) y de salto (Jxx). La idea detrás de esto será aplicar una comparación entre dos elementos y, según el resultado de dicha comparación, saltar a otro punto del código Assembly o seguir a la siguiente instrucción. Luego, al depender del resultado de una comparación, las instrucciones de salto utilizadas serán las de salto condicional y la instrucción utilizada dependerá del tipo de comparación presente en la condición del if.
While/for
Los ciclos son similares a los ifs, ya que contienen una condición que será la que determine durante cuánto tiempo se estará dentro del ciclo. Mientras esta condición no se cumpla (negación de la guarda), no se tomará el salto condicional y simplemente se continuará la ejecución en la siguiente instrucción. Si la condición se cumple, se activará el salto condicional que apuntará a una instrucción por fuera del ciclo. Lo único que falta será la instrucción que permita que la ejecución efectivamente sea un ciclo. Esto se logrará colocando al final del código del cuerpo del ciclo una instrucción de salto no condicional JMP que apunte a la primera instrucción del ciclo, es decir, a la condición inicial.
Switch
Al igual que los ciclos y los ifs, los switchs también utilizarán la combinación de instrucciones CMP y un salto condicional Jxx; sin embargo, éste hará un uso mucho más intensivo, ya que intuitivamente un switch puede ser visto como una serie de ifs anidados. De esta manera, habrá muchas comparaciones y saltos condicionales seguidos donde cada instrucción de salto llevará a su correspondiente etiqueta, donde estará el código a ejecutar si se cumple la condición. Luego, al final del código de cada caso habrá un salto no condicional JMP que apuntará a la siguiente instrucción luego del switch para retomar el flujo de ejecución.
Funciones
Si bien esto podría variar según el lenguaje y el compilador, las funciones generalmente tienen una estructura particular bien definida. Estas suelen comienzar con dos instrucciones cuya finalidad es armar el Stack Frame para la función. Posteriormente, podría haber instrucciones encargadas de reservar espacio en el stack para ubicar allí el valor de las variables locales de la función. A continuación, estarán presentes las instrucciones correspondientes a las tareas que realiza la función y, finalmente, la misma terminará con una instrucción RET. Teniendo en cuenta esta estructura es muy sencillo identificar el comienzo y el fin de una función.
Observaciones generales
Es importante tener en cuenta que, si bien los compiladores suelen respetar estructuras similares a las aquí mostradas, estas podrían llegar a variar o a tener modificaciones. Esto se debe a que cada compilador puede funcionar de manera diferente o aplicar distintos tipos de optimizaciones sobre el código con el fin de que este tenga mejor performance (por ejemplo, loop unrolling de ciclos). Por lo tanto, si se quisiera conocer la forma puntual en la que un compilador específico compila cierta estructura, pueden utilizarse herramientas como Compiler Explorer o incluso realizar pruebas uno mismo compilando y desensamblando código hasta encontrarla. De todas maneras, aunque puedan variar las instrucciones utilizadas o el orden de las mismas, la idea detrás de cada estructura suele mantenerse.