Ingeniería inversa
Introducción a Ghidra con un binario inofensivo que tú compilas
Compila una verificación de licencia de juguete en C de dos formas y usa Ghidra para hallar la cadena, seguir referencias y comparar con tu código.
Entiende qué descarta la compilación, en qué se diferencian desensambladores y descompiladores y los límites legales antes de analizar un binario.
La ingeniería inversa es el trabajo de averiguar cómo se construyó algo examinando el producto terminado. En software, ese producto suele ser un programa compilado: un ejecutable, una biblioteca o una imagen de firmware. El código fuente no está disponible o nunca se publicó, así que empiezas desde el código de máquina y vuelves atrás hasta entender qué hace el programa y por qué.
La gente hace esto por motivos muy variados, y la mayoría son cotidianos. Un ingeniero de seguridad revisa el binario de un proveedor antes de confiar en él. Un desarrollador intenta que un formato de archivo cerrado funcione con una herramienta abierta. Un estudiante quiere ver qué produce realmente un compilador. Este artículo explica qué descarta la compilación, la diferencia entre análisis estático y dinámico, desensambladores frente a descompiladores, las razones legítimas para hacer este trabajo y los límites legales que conviene conocer antes de empezar. El siguiente artículo de la serie hace un primer análisis real.
Un compilador convierte código legible para personas en instrucciones que un procesador puede ejecutar. El resultado es correcto para la máquina y dice poco sobre la intención. Varias cosas desaparecen en el camino:
for se convierte en una comparación y un salto. Un if se convierte en un salto condicional a otro punto del archivo.Cada pérdida hace más incierto el paso siguiente. El ingeniero inverso reconstruye parte de esos detalles a partir de pistas: las cadenas de texto que usa el programa, las funciones que importa del sistema operativo y la forma en que los datos pasan por los registros.
Puedes ver una versión pequeña de esto con Python 3.12. El ejemplo siguiente es una función de tres líneas con un comentario. El bytecode de Python no es código de máquina nativo, así que tómalo como una analogía, no como un modelo de la salida de C. Aun así, muestra el punto.
# Teaching constant, not real access control.
def check_pin(user_pin):
training_pin = 2468 # the comment never reaches the bytecode
return user_pin == training_pinCompílala y desensámblala con estos comandos:
python -c "import py_compile; py_compile.compile('check_pin.py', cfile='check_pin.pyc', doraise=True)"
python -c "import marshal, dis; data = open('check_pin.pyc','rb').read()[16:]; code = marshal.loads(data); fn = [c for c in code.co_consts if hasattr(c,'co_code')][0]; print('name:', fn.co_name, '| constants:', fn.co_consts, '| locals:', fn.co_varnames); dis.dis(fn)"En la máquina donde se revisó este artículo, la salida fue:
name: check_pin | constants: (None, 2468) | locals: ('user_pin', 'training_pin')
2 0 RESUME 0
3 2 LOAD_CONST 1 (2468)
4 STORE_FAST 1 (training_pin)
4 6 LOAD_FAST 0 (user_pin)
8 LOAD_FAST 1 (training_pin)
10 COMPARE_OP 40 (==)
14 RETURN_VALUEHay dos cosas que llaman la atención. El comentario desapareció: una búsqueda de su texto en el archivo compilado no encuentra nada. Los nombres sobrevivieron, porque Python los conserva en el objeto de código. Un programa C compilado normalmente no conserva ninguno de los dos, así que el mismo ejercicio sobre un binario nativo es más difícil. Úsalo como modelo mental, no como promesa para todos los lenguajes.
Los métodos de ingeniería inversa se dividen en dos grandes grupos.
El análisis estático examina el archivo sin ejecutarlo. Lees las instrucciones, listas las importaciones y cadenas, mapeas funciones y sus referencias cruzadas y construyes un modelo del programa. El archivo nunca se ejecuta, pero tus herramientas sí lo interpretan, así que mantenlas actualizadas y trabaja en una máquina aislada cuando la muestra pueda ser hostil. El trabajo estático es más lento, pero es repetible, y la mayoría de la gente lo hace primero.
El análisis dinámico ejecuta el programa bajo observación. Un depurador lo pausa en puntos de interrupción para que leas registros y memoria. Un trazador registra llamadas al sistema, accesos a archivos o conexiones de red. Un sandbox registra qué cambia en el disco. El trabajo dinámico muestra el comportamiento real, incluidos valores que solo existen en tiempo de ejecución, pero implica ejecutar código en el que quizá no confías. Hazlo en una máquina virtual desechable, sin cuentas reales, sin carpetas compartidas, sin datos sensibles y, idealmente, sin red.
La mayor parte del trabajo real combina ambos. Una pasada estática encuentra la función que verifica una contraseña. Luego, una ejecución en el depurador muestra cuál es realmente la entrada y cuál la salida.
Un desensamblador convierte bytes en instrucciones de ensamblador para una arquitectura de procesador específica. La traducción es mecánica: un patrón de bytes se convierte en un mnemónico, como mov, cmp o jne, con sus operandos. El resultado coincide exactamente con la máquina, pero sigue siendo de bajo nivel. Leer miles de instrucciones para entender una función lleva mucho tiempo.
Un descompilador va más allá. Intenta producir pseudocódigo de nivel más alto, normalmente parecido a C, a partir de los mismos bytes. Reconstruye los límites de las funciones, el flujo de control como if y while, y tipos de datos aproximados. Su salida es una interpretación, no una copia del código fuente original. Puede equivocarse con los tipos, fusionar cosas que estaban separadas o inventar una estructura que nunca existió. Un buen hábito es leer el código descompilado junto al desensamblado y confiar en el ensamblador cuando ambos no coincidan.
Ghidra es una de las herramientas gratuitas más usadas que hacen las dos cosas. Es un framework de ingeniería inversa de software creado y mantenido por la Research Directorate de la Agencia de Seguridad Nacional de Estados Unidos, con licencia Apache-2.0, y funciona en Windows, macOS y Linux. Soporta muchas arquitecturas de procesador y formatos de archivo, y se puede extender con Java o Python. Su README pide JDK 25 (64 bits) para ejecutar una versión oficial. Existen otras herramientas, comerciales y abiertas, y las ideas de este artículo sirven para todas.
La ingeniería inversa tiene varios usos legítimos.
Interoperabilidad. Tu software necesita intercambiar datos con un programa o formato cerrado, y el proveedor no publica los detalles. La Unión Europea trata este caso de forma directa, como explica la sección legal.
Investigación de vulnerabilidades. Examinas software que tienes autorización para probar, encuentras fallos y los reportas para que el proveedor los corrija. Normalmente sigue la divulgación coordinada: un reporte privado, un plazo para la corrección y solo después los detalles públicos.
Análisis de malware. Los defensores desarman un archivo sospechoso para saber qué hace, qué sistemas toca y cómo detectarlo. Buena parte del conocimiento público sobre amenazas sale de este trabajo.
Aprendizaje. Leer un programa compilado es una de las mejores formas de entender cómo las computadoras ejecutan código: distribución de memoria, convenciones de llamada y la distancia entre lo que escribes y lo que realmente se ejecuta.
Verificar afirmaciones. Quizá quieras comprobar qué envía un programa por la red o qué guarda localmente, sobre todo cuando la privacidad está en juego. Esto toca las mismas cuestiones legales que cualquier otro análisis.
Esto es una visión general, no asesoría legal. Las leyes cambian de país a país, y los contratos pueden añadir condiciones que valen en todas partes. Hay tres capas que importan más.
1. Términos de licencia. La mayoría de las licencias de software comercial prohíben la ingeniería inversa, salvo cuando la ley lo permite. Las licencias de código abierto funcionan de otra manera y suelen permitir leer y modificar el código, pero también tienen condiciones. Leer código fuente que te entregaron no es lo mismo que hacer ingeniería inversa de un binario, y ninguno de los dos es lo mismo que entrar en un servicio. Antes de analizar un producto que no escribiste, lee su licencia.
2. Unión Europea. La Directiva 2009/24/CE sobre la protección jurídica de los programas de ordenador incluye en su artículo 6 una excepción de interoperabilidad. En resumen, un usuario legítimo, o alguien que actúa en su nombre, puede descompilar un programa cuando es indispensable para obtener la información necesaria para que un programa creado de forma independiente funcione con otros. Hay condiciones. La información no debe estar ya fácilmente disponible, el trabajo debe limitarse a las partes del programa necesarias para la interoperabilidad y la información no puede usarse para otros fines ni compartirse más allá de lo que exige la interoperabilidad. Tampoco puede servir para desarrollar un programa sustancialmente similar, y la excepción no puede perjudicar sin razón los intereses legítimos del titular de los derechos. El artículo 6 trata de interoperabilidad, no de curiosidad general.
3. Estados Unidos. La sección 1201 del Digital Millennium Copyright Act prohíbe eludir las medidas tecnológicas que controlan el acceso a obras protegidas. Esa prohibición es distinta de la infracción de derechos de autor, así que una persona puede romper la regla sin copiar nada. La ley incluye excepciones, entre ellas la sección 1201(j), que cubre ciertas pruebas de seguridad bajo condiciones establecidas. La Oficina de Derechos de Autor también realiza cada tres años un proceso de regulación que puede añadir excepciones temporales. Su página sobre la sección 1201, enlazada en las fuentes, describe el proceso. Al momento de la revisión, ya había comenzado la décima ronda de regulación, y las excepciones renovadas en ella siguen vigentes por tres años, de octubre de 2027 a octubre de 2030. Lee la ley y la normativa vigente antes de depender de cualquier excepción. Manda el texto, no un resumen.
Otros países tienen sus propias reglas. Trata las tres capas anteriores como ejemplos de cómo se plantean las preguntas, no como un mapa de tu jurisdicción.
De aquí salen tres puntos prácticos. Analiza software que te pertenece, tus propias compilaciones o material que tienes permiso para probar. Mantén el trabajo de pruebas dentro de sistemas que controlas o para los que tienes autorización por escrito. Y si un proyecto tiene riesgo legal, consulta a un abogado antes de empezar, no después de publicar.
La forma más segura de aprender es con tu propio código. Escribe un programa pequeño, compílalo y analiza el resultado. Conoces el código fuente, así que puedes comparar cada suposición con la verdad. También evitas las cuestiones legales que vienen con software de terceros y el riesgo de ejecutar una muestra desconocida.
Dos reglas mantienen limpia la práctica. Nunca ejecutes binarios desconocidos en tu máquina de uso diario. Nunca apuntes herramientas de análisis ni escaneos a sistemas que no te pertenecen o para los que no tienes permiso escrito de prueba, incluidos sitios que parecen inofensivos. El siguiente artículo, un primer análisis con Ghidra usando un binario que compilas tú, recorre un primer proyecto con un programa que construyes tú mismo.
Aprende el procesador que estás estudiando, normalmente x86-64 o ARM64 en equipos modernos, y cómo el sistema operativo pasa argumentos a las funciones. Compila dos veces el mismo archivo C pequeño con un compilador de C en el que confíes, una con información de depuración y otra sin símbolos, y compara lo que queda. Lee la documentación de Ghidra y el README del proyecto. Luego sigue con el artículo práctico que viene a continuación.
Ingeniería inversa
Compila una verificación de licencia de juguete en C de dos formas y usa Ghidra para hallar la cadena, seguir referencias y comparar con tu código.