Escribí este artículo en coautoría con Elvia Morales Turruviates, como parte de su Tesis de Maestría que yo asesoré. Estábamos haciendo ingenieŕia inversa a código fuente de C y ASM de la GUI GEM.
1. Ingeniería Inversa de Código Fuente
José Enrique Alvarez Estrada (jeae@prodigy.net.mx)
Elvia Morales Turrubiates (emorales@pampano.unacar.mx)
Resumen
Nadie pone en duda la importancia de la ingeniería como disciplina del conocimiento humano. Pero pocas
personas saben que esta disciplina posee una contraparte: la llamada ingeniería inversa.
El objetivo de la ingeniería, es desarrollar la solución a un problema a partir de la nada. En cambio, la inge-
niería inversa propone solucionar ese mismo problema, pero a partir de alguna tecnología existente, cuyo fun-
cionamiento no se conoce (al menos no totalmente), pero que se sabe tiene el potencial de resolverlo total o
parcialmente.
Si bien no existe una licenciatura en ingeniería inversa, es un hecho que muchos ingenieros dedican su vida
profesional a esta disciplina. Pero, curiosamente, muy poco o nada se habla de ella en las currículas universi-
tarias, y jamás se enseña formalmente. De hecho, en muchos círculos el hablar de ingeniería inversa todavía
es un tabú, y si bien se sabe y se acepta que la mayor parte de las empresas la utilizan, nadie quiere reconocer-
lo abiertamente.
El objetivo de esta ponencia es divulgar la importancia de esta disciplina, presentando su estado del arte para
la rama particular de la ingeniería inversa de código fuente.
Las técnicas de transformación de programas se
Introducción utilizan en muchas áreas de la ingeniería de
Se puede definir a la ingeniería inversa como el software incluyendo la construcción de
proceso mediante el cual una tecnología o producto compiladores, la visualización de software, la
se analiza, con el fin de conocer los componentes generación de documentación y la actualización
que lo integran y la forma en que éstos interactúan, automatizada del software. En todas estas
para lograr finalmente una comprensión cabal de aplicaciones podemos distinguir dos aspectos
su modo de funcionamiento, con el objetivo principales, por ejemplo, uno donde el lenguaje
probable de construir una tecnología similar. La fuente y destino son diferentes (las traducciones) y
ingeniería inversa toma un producto cuyo formato otro aspecto cuando son iguales (la reformulación).
presenta un bajo grado de abstracción, y obtiene Estos panoramas principales se pueden refinar en
una nueva presentación del mismo, con una un número de aspectos secundarios típicos
abstracción mayor. basados en su efecto sobre el nivel de
Sin duda, cada rama del conocimiento ha abstracción de un programa y en la medida
desarrollado sus propias técnicas de ingeniería que preservan la semántica de un programa.
inversa. Por ejemplo, en el caso de la química, el
análisis espectrográfico de los componentes que
forman una substancia (digamos, un fármaco) Decompilación
puede utilizarse para crear una substancia similiar.
En la mecánica, el desensamble de un mecanismo La decompilación es lo inverso de la compilación:
permite al ingeniero ver las partes que lo integran, traducir un archivo ejecutable a un archivo en un
sus medidas, los materiales de que está hecho, etc. lenguaje de más alto nivel. La decompilación es
de modo que esté en condiciones de crear un clon útil si el código fuente original no está disponible.
del mismo. La decompilación completamente automatizada no
En el caso del software, la transformación de es posible, pero aunque se pudiera llevar a cabo el
programas es el acto de cambiar un programa en resultado obtenido no sería totalmente objetivo:
otro. El lenguaje en el cual el programa es este problema es teóricamente equivalente a “The
transformado y el programa que resulta se Halting Problem”, y por tanto la decompilación es
denominan lenguaje fuente y lenguaje destino, computacionalmente irresoluble.
respectivamente. La decompilación no puede ser exitosa para todos
los programas, pues es difícil alcanzar una
2. completa separación de datos y del código en “dependiente o específica del compilador en
máquinas con arquitectura Von Neumann. cuestión” y otra forma “genérica o general”.
Además, si se logra un cierto grado de éxito, el
La primera aproximación intenta ir del ejecutable
programa generado carece de nombres de
al fuente, basándose en el análisis de la salida
funciones y variables significativos, pues éstos no
generada por un compilador especifico. Esto por lo
se almacenan normalmente en un archivo
general da mejores resultados con respecto a
ejecutable (excepto cuando son almacenados con
generar código fuente que se asemeje al código
propósitos de depuración, situación que se
fuente original. Esta aproximación es limitada, en
denomina “almacenar la tabla de símbolos en el
el sentido de que tiene que construirse un
código objeto”).
decompilador para cada compilador individual.
Algunas personas creen que solamente es posible
lograr que la decompilación recupere los códigos
fuentes en lenguaje ensamblador, pero ya en sí éste Decompilación Dependiente
no es un problema trivial, nuevamente debido a su
equivalencia al problema de la detención de la En la decompilación dependiente o específica del
máquina de Turing. Sin embargo, existen en la compilador, se puede seguir el siguiente proceso:
práctica aproximaciones que se ocupan del 1. El ejecutable tiene que examinarse para ver si
desensamblaje y la decompilación. Los más está comprimido y si es así, tiene que
acertados hasta la fecha hacen uso de información descomprimirse. O simplemente excluir ese
adicional -conocimiento acerca del compilador tipo de ejecutable, advirtiéndole la razón al
empleado- o requieren la intervención humana en usuario. Revisar el aviso de copyright para
las partes más complejas del proceso del verificar que el compilador y la versión sean
desensamblaje. Puede incluso ser posible para las correctas. Una sola versión del
ciertos decompiladores, el decompilar decompilador puede ampliarse a múltiples
automáticamente una fracción grande de versiones del mismo compilador usando los
programas de código máquina provenientes del archivos que contengan los datos críticos para
mundo real, no sólo ejercicios académicos. cada uno.
Sea como fuere, esta traducción se alcanza Se debe examinar el ejecutable para depurar
generalmente en varias fases en las cuales un información y conservar esa información,
lenguaje de alto nivel primero se traduce a una posiblemente en un archivo temporal. Esto
representación intermedia. La selección de la puede ayudar a detectar librerías y recuperar
instrucción entonces traduce la representación nombres de funciones y variables. Se puede
intermedia a instrucciones de máquina. El proceso construir una lista ligada que permita observar
de la compilación generalmente también implica qué partes del programa se conocen
un número de pasadas al código fuente. Por (completamente o parcialmente), y por lo
ejemplo, normalizaciones al programa, tales como tanto, cuáles no se conocen. Cada nodo debe
poner las instrucciones en forma canónica, tener el tipo de memoria (código/dato),
simplificaciones algebraicas y varias formas de nombre de la función, contenido si está
optimización de programas. disponible y tipo de código (librería/usuario).
Las técnicas de la decompilación fueron utilizadas 2. Como siguiente paso se debe determinar el
inicialmente en los años 60 para ayudar en la punto de inicio del ejecutable, esto podría ser
migración de programas de una plataforma a otra. el código de inicio de C que se traduciría como
Desde entonces, se han utilizado para ayudar en la
recuperación del código fuente perdido, en la int main (int argc,
depuración (debugging) para eliminar errores de char *argv[],...)
programas, en la localización de virus, compresión Más importante, el código de inicio indica dónde
de programas, recuperación de vistas de alto nivel se han inicializado las variables y cuáles son sus
–diagramas de análisis y diseño- de programas, y valores. Como no se puede conocer, inicialmente,
más. el tamaño o tipo de tales variables, examinar estos
datos puede ayudar a encontrar los tipos flotantes,
los dobles, las cadenas de caracteres, y quizá los
Aproximaciones a la enteros. Buscando por patrones de repetición de
Decompilación entradas de tipo/tamaño, se podría (tentativamente)
identificar arreglos de tipo estándar y de
Existen dos formas distintas de atacar el proceso de estructuras. Se puede diferenciar entre arreglos y
decompilación: una forma que pudiéramos llamar
3. variables múltiples escalares en una línea 5. Concentrarse en una función a la vez, no
examinando el código que las referencia. siguiendo las llamadas a funciones. El código
de entrada a la función informa acerca de las
3. El siguiente paso es desensamblar, siguiendo
variables automáticas, y el examen del código
el flujo del programa principal, observando las
puede revelar el tamaño y probablemente
direcciones de las funciones llamadas y las
también el tipo. El mismo tipo de análisis
direcciones de inicio de las instrucciones
aplicado al área de las variables inicializadas
ejecutadas. Las áreas de confusión (donde un
se puede utilizar para localizar estructuras.
goto es dirigido a la mitad de una
instrucción) pueden ser ensambladas en línea El segundo paso para una función es
(y marcarlas como tal) siguiendo siempre la identificar llamadas a funciones y el código
ejecución del programa. Se emite una que le pasa parámetros a la función. Para el
advertencia cuando es probable que lo paso de valores que no son solamente
realizado no sea correcto. Luego se puede ver operaciones de carga/descarga, se puede crear
si las funciones invocadas corresponden a un nuevo nodo de código que contenga el
cualquiera en la lista de información. código que genera el valor que es cargado.
Entonces, se repite recursivamente para cada Para funciones donde los tipos de parámetros
rutina llamada, revisando la lista de son conocidos, se puede ajustar la información
información, y comparando el código con la acerca de las variables involucradas (por
biblioteca para ese compilador. Cualquier ejemplo, identificar una variable de archivo
módulo de bilbioteca conocido puede “FILE *”). Esta es una forma de
eliminarse previa consideración, excepto concordancia de patrones, pero debe realizarse
cuando una función de biblioteca llama a otras antes de avanzar para que sea más fácil.
funciones; entonces, se puede también
El paso tres es la identificación de plantillas,
eliminar las funciones llamadas, etc.
para cualquiera de las plantillas que se han
Conociendo qué es lo que hacen las diferentes
determinado para el compilador. Conforme se
interrupciones (INT), se pueden delimitar
van identificando los segmentos de código y el
áreas que sean de datos. Cuando este proceso
código fuente, se crea un nodo de código, el
se haya realizado (y éste involucra varios
cuál agrega el código fuente generado al
pasos), se desarrolla un enorme árbol de
binario. La identificación de plantillas debe
información acerca del ejecutable, y se puede
realizarse recursivamente para identificar
producir un archivo de lenguaje ensamblador
ciclos anidados, etc. El código ajustado para
intermedio que tenga la biblioteca descubierta
los ciclos los identifica como: for, while ó
(incluso si no había información depurada),
variables estáticas inicializadas identificadas do, si no el ciclo no interesa y puede elegirse
junto con sus valores iniciales. de forma arbitraria. Esto conducirá a
identificar sentencias switch, case, if y
4. Una vez que se tiene la versión llamadas de funciones, convertidas en código
desensamblada, junto con nuestra lista de en línea por las instrucciones del
información, se puede comenzar el proceso preprocesador “#pragma inline”. Cuando
real de decompilación. Se puede usar una lista se encuentra código controlado por #pragma,
ligada de ramificación; cada nodo indica el debe colocarse el enunciado pragma en el
nombre de la función (determinada de la código fuente; La localización actual depende
información depurada o sintetizada), y un de que si el pragma puede utilizarse como un
apuntador a la lista de código para esa función.
bloque de código local o para el archivo entero
El nodo de la lista de código indica el tipo de
(y hay un #pragma correspondiente que lo
código contenido (fuente, binario, o
ensamblado en línea), un apuntador a memoria desactiva).
que contiene el fuente del ASCII, y un El paso cuatro busca el código en línea que
apuntador a un binario no desensamblado. tiene una representación directa del fuente y
Como el código es desensamblado, las agrega el código fuente al nodo de código. Ese
entradas en la lista de código se dividen y se es el código que reúne una o más variables, las
mezclan; cuando la función completa es modifica, y pone uno o más valores de retorno.
desensamblada, hay una única entrada en la Se debe poner atención a las fuentes de
lista de código. El binario se retiene hasta que variables de registro (desde la lista de
la función ha sido desensamblada automáticas) de manera que el fuente pueda
completamente. identificar correctamente la variable original.
El condicional en las instrucciones if puede
determinarse en este punto, ya que ahora es
4. posible identificar el código (inmediato) de la equivalencias. Se generan problemas cuando en el
comparación. código original se fuerza el cambio de tipo
(casting). También los tipos enumerados y otros
6. Cualquier cosa que se haya dejado en este
tipos que son equivalentes a los tipos estándares,
punto probablemente puede convertirse en
no pueden recuperarse.
código en línea del ensamblador. En cualquier
caso, ésta es la última posición para retornar. La detección de los tipos arreglos y tipos apuntador
no es difícil: los tipos estructurados, especialmente
cuando se utilizan uniones, generan problemas
Decompilación Independiente extra.
La segunda aproximación se caracteriza por
analizar la semántica del ejecutable, y de este
análisis se deriva un archivo fuente equivalente, sin
Reglas de Traducción de
hacer uso de información acerca de qué compilador Código para Lenguajes de
fue utilizado para generar el ejecutable. El código
fuente generado no puede asemejarse en todo al Alto Nivel
original. La ventaja es que el método trabaja para
cualquier compilador que pudiera haberse El ensamblador es, a fin de cuentas, un lenguaje de
utilizado. programación como cualquier otro. Teóricamente,
si se cuenta con la definición de la gramática del
1. Se asume que se inicia con la salida de mismo, pudiera construirse un parser que
ensamblador del desensamblador, que también reconociera el código fuente, lo estructurase en
contiene las etiquetas propias. Se podría leer el forma de un árbol de análisis sintáctico, y
archivo entero, dentro de la memoria, y posteriormente recorrer dicho árbol para generar
agrupar instrucciones en bloques, basados en código en C o algún otro lenguaje de alto nivel.
las etiquetas.
En la práctica, para poder realizar la ingeniería
2. Decodificar cada instrucción en más inversa de código de Ensamblador, se deben hallar
operaciones primitivas, para así evitar todos las estructuras de control básicas presentes en el
los diferentes modos de direccionamiento. La código fuente de ensamblador, y traducirlas a un
mayoría de las instrucciones sólo se modifican lenguaje de mayor nivel. El problema es que la
en registros o direcciones de memoria. gramática de ningún ensamblador toma en
3. A partir de esto, debe ser posible determinar cuenta dichas estructuras de control. Esto se
qué registros se leen y escriben por cada debe a que la mayor parte de las instrucciones del
bloque (asumiendo que las direcciones de ensamblador se mapean directamente a
memoria se refieren a variables C). Basado en instrucciones de la capa ISA, y por ello la
ello se puede hacer un análisis de flujo, para estructura gramatical del lenguaje ensamblador
encontrar si hay algún registro de variables tiende a tomar la forma de un conjunto de líneas,
detectadas (mapped). cada una de las cuáles está compuesta por una
etiqueta opcional (para identificarla en caso de
4. Si esto se ha llevado a cabo, se puede saltos o llamados a subrrutinas), una instrucción de
determinar cuáles son las expresiones que son ensamblador directamente traducible a lenguaje
calculadas por las instrucciones para cada máquina, y un comentario opcional.
almacén a una variable (también las basadas
en registros o memoria). Se necesitara la Bajo estas circunstancias, es responsabilidad
normalización de esta expresión. completa del programador de lenguaje
Ensamblador, instrumentar las estructuras de
5. Ahora, se tiene que reconocer las instrucciones control secuenciales, selectivas y repetitivas
de alto nivel que resultaron en la estructura del mediante combinaciones más o menos extensas de
bloque, y los saltos entre ellos. Desde aquí es instrucciones de este lenguaje, usando para ello
posible empezar a generar el código que tantas líneas de código como sea necesario♦. La
solamente tratará operaciones con bytes, problemática estriba en traducir un código que no
words, longwords y flotantes (doubles). necesariamente se pensó y programó con buenas
Para la reconstrucción de tipos, primero se técnicas estructuradas ni modulares, a un código
necesitan técnicas de resolución de tipos, siempre y
cuando los parámetros sean pasados en llamadas a
En ello estriba la dificultad de aprender a
funciones, o cuando se asignan de uno a otro. La programar en lenguaje Ensamblador: la creación de
manera de hacer esto es asignar un tipo único a las estructuras de control es responsabilidad del
cada variable y parámetro, y derivar reglas de programador, y no del compilador.
5. estructurado y modular en un lenguaje de alto • Bertelsons, Boris y Mathias Rasch. PC al
nivel. Límite, Programación Avanzada. Ed.
A diferencia del Ensamblador, un programa en Computec-Marcombo. ISBN 970-15-0085-7.
lenguaje C puede construirse utilizando • Cifuentes, Cristina. Reverse Compilation
cualquier combinación sintácticamente correcta Techniques. Tesis Doctoral, Australia, 1994.
de estructuras for, while, do..while, if,
switch, etc. La gramática de C cuenta con clases • Godfrey, J. Terry. Lenguaje Ensamblador
sintácticas para describir y reconocer cada una de para Microcomputadoras IBM. Ed.
ellas, y crear el respectivo árbol de análisis Prentice-Hall. ISBN 968-880-204-2.
sintáctico para después traducirlas a Ensamblador o • Lemone, Karen A. Fundamentos de
a lenguaje máquina. Compiladores. Ed. CECSA. ISBN 968-26-
Y aquí es donde se encuentra el reto del estado del 1297-7.
arte de esta disciplina: • O’Gorman, John. Systematic Decompilation.
Describir gramáticas que sean un Tesis Doctoral. Irlanda, 1991.
superconjunto de las gramáticas habituales de
los lenguajes ensambladores, pero que agrupen • Pratt, Terrence y Marvin V. Zelkowitz.
las instrucciones de éstos con el objeto de Lenguajes de Programación, Diseño e
reconocer estructuras de control secuenciales, Implementación. Ed. Prentice-Hall. ISBN 0-
selectivas y repetitivas, llamados a funciones y 13-678012-1.
procedimientos, operaciones aritméticas y • Teufel, Schmidt y Teufel. Compiladores,
lógicas, todo ello traducible posteriormente a un Conceptos Fundamentales. Ed. Addison-
lenguaje de alto nivel. Wesley Iberoamericana. ISBN 0-201-65365-6.
Para ello, es necesario revisar la relación entre • Tischer, Michael y Bruno Jennrich. PC
instrucciones Ensamblador y estructuras de control Interno, Programación de Sistemas. Ed.
de alto nivel, con el afán de definir reglas de Computec-Marcombo. ISBN 970-15-0079-2.
reconocimiento de patrones estructurales, que
posteriormente puedan usarse para reconocer tales
patrones y traducir el código a lenguaje de alto
nivel.
Conclusiones
Se necesita formalizar la enseñanza de la ingeniería
inversa en las aulas de las Facultades e Institutos
de México, puesto que las empresas líderes interna-
cionales emplean a discreción tales técnicas, y no
conocerlas nos pone en desventaja.
Cuanto mayor sea el número de profesionales que
conozcan y apliquen la ingeniería inversa de códi-
go fuente, tanto mayor será nuestro conocimiento
respecto a las reglas de reconocimiento de patrones
estructurales que es necesario aplicar, en distintos
tipos de compiladores.
Bibliografía
• Abel, Peter. Lenguaje Ensamblador y
Programación para PC IBM y Compatibles.
Ed. Pearson. ISBN 968-880-708-7.
• Aho, Sethi y Ullman. Compiladores,
Principios, Técnicas y Herramientas. Ed.
Pearson. ISBN 968-444-333-1.