Ya sea para el análisis de códigos maliciosos, búsqueda de vulnerabilidades en una aplicación, o incluso para analizar la efectividad de un compilador, muchas veces es necesario realizar ingeniería reversa sobre un archivo ejecutable. En este post veremos una introducción a esta idea, ya que es una tarea que todo analista de malware debe dominar.
La necesidad de realizar reversing (o desensamblado) sobre un archivo ejecutable surge en los casos en los que no se cuenta con el código fuente de la aplicación. En definitiva, si queremos entender su funcionamiento, lo normal sería acudir al archivo con el conjunto de instrucciones que los programadores han codificado. Estas instrucciones seguramente estarán expresadas en un lenguaje de programación de alto nivel, de fácil entendimiento para los programadores. Luego, un compilador traducirá estas instrucciones a lenguaje ensamblador (de bajo nivel, más complejo de entender) o directamente a código de máquina, que es el que entienden las computadoras digitales y que consiste en una sucesión de ceros y unos, muy complicado de entender para el ser humano. Justamente, los ejecutables que podemos ver en cualquier computadora no son más que archivos con secuencias de ceros y unos. Por ello, ante las preguntas: ¿es posible realizar el proceso descripto a la inversa? ¿Se puede obtener código con instrucciones a partir de un archivo ejecutable?, la respuesta es el reversing.
La operación de tomar un archivo ejecutable y producir código en ensamblador se conoce como desensamblado. Para que el código que se obtiene sea relevante, es necesario utilizar herramientas que implementen algoritmos eficaces en la detección de instrucciones. Este problema no es menor, dado que, dependiendo de dónde se comience a leer en el archivo y cuántos bytes se lean, será la instrucción que se interpretará. Además, cualquier algoritmo deberá saber distinguir si los bytes leídos corresponden a instrucciones o datos, ya que generalmente estos se encuentran mezclados. Así, algunas herramientas como el desensamblador de WinDbg o gdb, implementan un algoritmo de barrido lineal. Éste obtiene la dirección en que empieza la sección de código, y su tamaño, y lee una instrucción tras otra hasta completar la totalidad de la sección. Por otra parte, herramientas como IDA Pro utilizan principalmente el algoritmo recursive traversal, que tiene en cuenta las instrucciones como saltos o llamadas a subrutinas, ignorando aquellos bytes que no sean alcanzados por el flujo de ejecución.
Luego, la principal ventaja del algoritmo lineal consiste en que no se perderá ninguna instrucción, ya que es barrida toda la sección de código. El problema es que también se interpretarán erróneamente datos como si fuesen instrucciones. Por su parte, el algoritmo recursivo funciona mejor en este aspecto ya que ignora los bytes no alcanzables por el flujo de ejecución, pero en algunos casos podría fallar cuando se produzcan saltos que dependan de un valor calculado en tiempo de ejecución, por ejemplo. A continuación se observa un ejemplo de un ejecutable desensamblado con IDA Pro:
Ahora bien, también es posible tomar código en lenguaje ensamblador y convertirlo a un lenguaje de alto nivel; las aplicaciones que realizan esta tarea se conocen como decompiladores. Este proceso es mucho más complejo, porque los distintos compiladores manejan las estructuras de datos de forma diferente, realizan optimizaciones del código bajo muchos criterios distintos y hay una cierta pérdida de información al compilar que no se puede recuperar al realizar el proceso inverso. Por estas particularidades, estaremos abordando el tema próximamente.