Saltar al contenido
Nuevo

DeepSeek V4.1 Flash gratis por 7 días: 10M tokens al día tras cualquier recarga. Hasta el 30/11.

Cómo funciona

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.

Intermedio14 min de lecturaPor NotHoly

La ingeniería inversa se aprende mejor con código que tú mismo escribiste. Sabes lo que el programa debería hacer, así que puedes comprobar cada afirmación que la herramienta hace sobre él. Este artículo sigue esa idea. Vas a escribir un programa pequeño en C con una verificación de licencia de juguete, compilarlo de dos maneras y abrir las dos versiones en Ghidra. Después compararás lo que dice el descompilador con el código fuente que escribiste.

La lección principal no está en los botones de Ghidra. Cuando pones la salida del descompilador junto a tu propio código, aprendes rápido qué acierta la herramienta, qué adivina y qué pierde cuando el compilador optimiza.

Si leer código de máquina es nuevo para ti, lee antes ¿Qué es la ingeniería inversa?. No necesitas experiencia previa con ensamblador. Sí necesitas una terminal y cerca de una hora.

Qué es Ghidra

Ghidra es un framework de ingeniería inversa de software desarrollado por la Agencia de Seguridad Nacional de Estados Unidos (NSA) y publicado en GitHub, en el repositorio NationalSecurityAgency/ghidra. Funciona en Windows, macOS y Linux. Para un programa compilado, hace cuatro cosas que importan aquí:

  • Desensambla el código de máquina en instrucciones de ensamblador.
  • Descompila funciones a un pseudocódigo parecido a C, con su descompilador integrado.
  • Encuentra cadenas y referencias cruzadas (XREFs), que indican en qué puntos del programa se usa una cadena o una dirección.
  • Guarda tus cambios en un proyecto, incluidos los nombres que das a funciones y variables.

Ghidra no recupera el código fuente original. Produce una aproximación. El pseudocódigo suele tener una forma parecida a la de tu código, pero los nombres, los tipos y parte del flujo de control pueden ser distintos de lo que escribiste. Esa diferencia es lo que este laboratorio está diseñado para mostrar.

Por qué analizar un programa que tú escribiste

Podrías bajar un binario de ejemplo de un foro y empezar a mirarlo. No lo hagas. Los binarios desconocidos pueden ser malware, y no tendrías forma de verificar la salida de la herramienta, porque no sabes cuál es la respuesta correcta.

Un programa que compilas tú mismo te da tres ventajas:

  1. Verdad de referencia. Sabes que check() compara el argumento con "LAB-1234".
  2. Dos versiones del mismo código. Una conserva información de depuración y símbolos. La otra va sin símbolos y optimizada. Compararlas muestra lo que el compilador eliminó.
  3. Un objetivo seguro. Solo imprime ok o no. No hace llamadas de red ni escribe archivos.

Todo en este laboratorio se queda en tu máquina. Estás analizando un archivo tuyo, que es el único alcance que este ejercicio necesita.

Preparar el laboratorio

Qué necesitas

  • Un compilador de C. En Linux, instala gcc con tu gestor de paquetes. En macOS, sirve clang de las herramientas de línea de comandos de Xcode para este laboratorio, y las banderas son las mismas. En Windows, usa WSL o un contenedor con GCC.
  • Un JDK para Ghidra. Ghidra corre sobre Java, así que el JDK debe estar instalado antes de que arranque el lanzador.
  • Ghidra, descargado de las versiones oficiales en GitHub.
  • Una terminal con sha256sum. Git Bash en Windows lo incluye.

Instalar el JDK y Ghidra

Instala el JDK que pide la versión de Ghidra que descargues, y no confíes en una guía antigua para ese número. Cuando revisé el README del repositorio y la guía en la fecha en que escribí esto, el README y la guía de primeros pasos pedían JDK 25 de 64 bits, y el README enlazaba a las compilaciones Temurin. Lee el README o las notas de la versión que bajes y usa la versión que indiquen. Una versión de Java incompatible es una causa común de que Ghidra no arranque.

Descarga Ghidra solo desde las versiones oficiales en github.com/NationalSecurityAgency/ghidra. El repositorio enlaza a los archivos de cada versión. Descomprime en una carpeta nueva. La guía de primeros pasos indica no extraer Ghidra sobre una instalación existente. Arráncalo con ghidraRun en Linux y macOS, o con ghidraRun.bat en Windows.

Verificar la descarga

Antes de descomprimir nada, compara el archivo con el valor SHA-256 publicado. Ejecuta esto en la carpeta donde guardaste la versión:

bash
sha256sum ghidra_*.zip

Compara el hash impreso con el valor SHA-256 que GitHub muestra junto al archivo en la página de la versión, o que el proyecto publica. Tiene que coincidir exactamente. Si no encuentras un SHA-256 publicado para ese archivo, anótalo en tus notas del laboratorio. No tomes como checksum del publicador un hash copiado de un espejo cualquiera.

Comprobar un hash solo demuestra que el archivo es igual al que publicó el autor. No demuestra que el autor sea confiable. Usa la fuente oficial y guarda el nombre del archivo y su hash en tus notas.

Escribir el programa de juguete

Crea una carpeta vacía para el laboratorio. Mantén juntos el código fuente y los binarios, y guarda los proyectos de Ghidra fuera de esa carpeta para que no se llene de cosas.

bash
mkdir -p ghidra-lab && cd ghidra-lab
cat > toy.c <<'EOF'
#include <stdio.h>
#include <string.h>
int check(const char *k){ return strcmp(k, "LAB-1234") == 0; }
int main(int c, char **v){ if(c>1 && check(v[1])) puts("ok"); else puts("no"); return 0; }
EOF

El programa lee un argumento de la línea de comandos. Si el argumento es igual a LAB-1234, imprime ok. Si no, imprime no. Esa verificación no tiene ningún valor de seguridad. Es una cadena fija dentro de un archivo que cualquiera puede leer, y ese es el punto. Vas a encontrar la cadena en el binario en pocos minutos.

Registra el hash de tu código fuente para poder demostrar después qué archivo analizaste:

bash
sha256sum toy.c

En la máquina que usé, el comando imprimió:

text
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d  toy.c

Tu hash será distinto si tu archivo tiene otros espacios o saltos de línea.

Compilar dos veces

Compila el mismo código fuente en dos binarios con configuraciones distintas:

bash
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.c

Las banderas hacen cosas distintas. Ambas vienen del manual de GCC, que conviene leer una vez:

  • -O0 desactiva la mayoría de las optimizaciones. Es el valor por defecto, y el manual lo describe como el ajuste que mantiene la depuración predecible. El código sigue de cerca al fuente.
  • -O2 activa la mayoría de las optimizaciones que no cambian tamaño por velocidad. El manual dice que también activa la expansión en línea de funciones pequeñas, incluso las que no están declaradas como inline. El código puede verse muy distinto del fuente.
  • -g agrega información de depuración en el formato nativo del sistema operativo. Ghidra lee parte de ella, y los depuradores la usan.
  • -s quita la tabla de símbolos del archivo final, durante el enlazado. Los nombres check y main dejan de estar guardados en el archivo.

Dos binarios con el mismo código y banderas distintas es lo que hace útil el laboratorio. Guarda los dos.

Ejecutar las dos versiones

Ejecuta los binarios antes de abrir Ghidra. Un programa que no has ejecutado es un programa que todavía no entiendes.

bash
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrong

Lo ejecuté dentro de un contenedor Debian con GCC 14.2.0, sobre un host x86-64. La salida fue:

text
ok
no
no
ok
no

Las dos versiones se comportan igual. Quitar símbolos y optimizar no cambió la lógica, que es lo que cabe esperar. Si tu salida es distinta, detente y averigua por qué antes de seguir.

Inspeccionar los binarios desde la línea de comandos

Antes de Ghidra, mira qué hay guardado en cada archivo. Bastan dos herramientas: nm lista símbolos y strings lista textos legibles.

bash
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_stripped

En la máquina que usé, la salida fue:

text
0000000000001149 T check
0000000000001175 T main
nm: toy_stripped: no symbols
LAB-1234
-rwxr-xr-x 1 root root 17384 Nov  8 09:17 toy_debug
-rwxr-xr-x 1 root root 14480 Nov  8 09:17 toy_stripped

Lee esas líneas con atención:

  • El binario de depuración todavía nombra check y main. Esos nombres vienen de tu código fuente.
  • El binario sin símbolos no tiene nada que listar. nm no encuentra ningún símbolo.
  • La cadena LAB-1234 sobrevive en los dos archivos. Una cadena es un dato, y quitar símbolos no elimina datos que el programa necesita en ejecución. Queda en el archivo como constante, y por eso es fácil empezar con este laboratorio.

Ahora desensambla check en el binario de depuración:

bash
objdump -d -M intel toy_debug | grep -A14 "<check>:"

La salida se vio así:

text
0000000000001149 <check>:
    1149:	55                   	push   rbp
    114a:	48 89 e5             	mov    rbp,rsp
    114d:	48 83 ec 10          	sub    rsp,0x10
    1151:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
    1155:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    1159:	48 8d 15 a4 0e 00 00 	lea    rdx,[rip+0xea4]        # 2004 <_IO_stdin_used+0x4>
    1160:	48 89 d6             	mov    rsi,rdx
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 d5 fe ff ff       	call   1040 <strcmp@plt>
    116b:	85 c0                	test   eax,eax
    116d:	0f 94 c0             	sete   al
    1170:	0f b6 c0             	movzx  eax,al
    1173:	c9                   	leave
    1174:	c3                   	ret

Puedes reconocer tu código en estas instrucciones. Una llamada a strcmp, una prueba sobre el valor de retorno y un sete que convierte "igual a cero" en 0 o 1. Ten eso presente cuando leas el descompilador.

Ahora mira el build optimizado. Desensamblé toy_stripped de la misma manera. El compilador puso la comparación directamente en main, que está en 0x1060 en mi build: un cmp edi,0x1 para la prueba c>1, luego mov rdi,[rsi+0x8] para cargar v[1], luego call strcmp@plt. Aun así conservó una copia separada de check en 0x1190, porque la función no es static y otro archivo podría llamarla. Las direcciones cambian según tu toolchain, así que tómalo como un mapa de qué buscar y no como números que deban coincidir.

Importar en Ghidra

Los nombres exactos de los menús cambian según la versión de Ghidra, y no ejecuté la interfaz gráfica en la máquina donde se revisó este artículo. Los pasos siguientes siguen la documentación oficial y lo que la interfaz suele mostrar. Verifica cada nombre con tu versión. Toma una captura de tu propia pantalla al terminar cada paso, y no reutilices capturas de otro tutorial.

  1. Inicia Ghidra con ghidraRun (o ghidraRun.bat).
  2. Elige File > New Project. Selecciona Non-Shared Project y ponle un nombre, como toy-lab. Guárdalo en una carpeta que controles.
  3. Elige File > Import File. Selecciona toy_debug. Acepta el formato detectado si coincide con tu sistema. Un archivo generado en Linux x86-64 debería aparecer como ELF con procesador x86.
  4. Repite la importación con toy_stripped. Ahora el proyecto tiene dos entradas.
  5. Abre cada archivo en la herramienta CodeBrowser. Cuando Ghidra pregunte si debe analizar el programa, acepta el análisis por defecto para este laboratorio.

Espera a que termine el análisis. Ghidra muestra el progreso en la barra inferior. No empieces a renombrar nada antes de que termine.

Encontrar la cadena y seguir sus referencias

Empieza con toy_stripped, que es el caso más difícil. Después compara con toy_debug.

  1. Abre Window > Defined Strings.
  2. Busca LAB-1234 en la lista. Haz doble clic para ir a su dirección en la vista de listado.
  3. Haz clic derecho en la dirección de la cadena y abre el menú de referencias. Busca un elemento como References > Show References to. Cada referencia es un punto del código que lee esa dirección.
  4. Sigue la referencia. Te lleva a la función que compara tu entrada con la cadena.

En el build de depuración, Ghidra también debería mostrar el nombre check de la tabla de símbolos. En el build sin símbolos mostrará un nombre generado, como FUN_00101190. El número depende del build, así que usa el nombre que aparezca en tu pantalla.

Haz lo mismo con las cadenas "ok" y "no". Cada una se usa en main. Seguirlas desde main te da la misma imagen vista desde el otro lado.

Leer el descompilador junto a tu código

Abre el panel del descompilador en la función que compara la cadena. Pon toy.c abierto en otra ventana, al lado.

El resultado que conviene buscar es de este tipo, descrito como ilustración de cómo leerlo y no como salida de una versión concreta de Ghidra:

  • Una función con un argumento que es un puntero a caracteres.
  • Una llamada a una función llamada strcmp, o a un símbolo que la herramienta etiqueta como strcmp.
  • Una comparación que devuelve 1 cuando el resultado es cero.

Ahora anota las diferencias que veas. Las típicas son:

  • Tipos. El descompilador puede mostrar un char * como char *, o usar un tipo más vago como undefined8. Puede que no sepa que la entrada es const.
  • Nombres. Tu parámetro k aparecerá como param_1 o un marcador parecido.
  • Valor de retorno. La expresión strcmp(...) == 0 puede aparecer como una comparación seguida de un retorno condicional. El significado es el mismo, pero la forma no.
  • Manejo de argumentos en main. Tu comprobación c > 1 se convierte en una comparación sobre la cantidad de argumentos. Fíjate en el desfase de uno: argv[0] es el nombre del programa, y v[1] es el primer argumento real.

El objetivo no es calificar al descompilador. El objetivo es notar en qué coinciden su historia y la tuya, y en qué se separan. Cuando se separen, vuelve al desensamblado y al código fuente y decide cuál tiene razón.

Renombrar variables en el build sin símbolos

Ahora haz la parte que vuelve legible un binario sin símbolos.

  1. En el panel del descompilador, haz clic en el parámetro que guarda la cadena de entrada.
  2. Haz clic derecho y elige Rename Variable, o selecciona la variable y pulsa L. Llámala input.
  3. Renombra la función en la vista de listado o en el árbol de símbolos a check_input.
  4. En main, renombra el arreglo de argumentos a argv y el contador a argc.

Ghidra guarda esos nombres en tu proyecto. El binario en disco no cambia. Eso importa: estás documentando tu lectura, no editando el archivo que compilaste.

Fíjate en lo mucho que mejora el código después de renombrar. La primera vez, los nombres de las variables eran suposiciones. Ahora coinciden con tu código fuente. Así trabajan los ingenieros inversos a diario, y por eso los nombres de un programa descompilado deben tratarse como hipótesis hasta que los compruebes.

Comparar los dos builds

Pon la descompilación del build de depuración y la del build sin símbolos una al lado de la otra. Responde estas preguntas en tus notas:

  • ¿Qué nombres de función sobrevivieron en el build sin símbolos?
  • ¿El build -O2 integró check dentro de main, o mantuvo una función separada? Busca en el listado un punto de llamada separado a strcmp y un ret separado.
  • ¿Qué partes de tu código son fáciles de ver en las dos vistas?
  • ¿Qué partes son difíciles de encontrar en la vista sin símbolos?

Escribe una frase por cada respuesta. Si no puedes responder alguna, eso también es un dato útil. Vuelve al listado y busca la instrucción que lo deja confuso.

Errores comunes

Saltarse la ejecución. Si abres el binario en Ghidra antes de ejecutarlo, no sabes cuál es el comportamiento correcto. Ejecútalo primero y registra las salidas.

Confiar en los nombres del descompilador. Un nombre como FUN_00101190 o local_18 es un marcador. Renombrarlo es una afirmación tuya, y necesita evidencia.

Mezclar los builds. Los binarios de depuración y sin símbolos tienen direcciones y nombres distintos. Anota el nombre de cada archivo y revisa en qué entrada del proyecto estás antes de renombrar nada.

Usar una muestra de una fuente no confiable. Tu propio archivo compilado es el punto de partida seguro. Cuando te sientas cómodo con él, el siguiente paso sigue siendo código tuyo o una muestra de una fuente que hayas verificado antes. El artículo sobre hashes de archivos y ética de VirusTotal explica por qué un hash por sí solo no resuelve esa pregunta.

Tratar el laboratorio como una prueba de seguridad. La comprobación de cadena de aquí se rompe de forma trivial. Encontrarla no demuestra nada sobre software real. El código real de licencias y protección es más difícil, y analizarlo debe mantenerse dentro del alcance en el que tienes permiso para trabajar.

Olvidar la versión del JDK. Si Ghidra no arranca, revisa la versión de Java con las notas de la versión antes de tocar cualquier otra cosa.

Cómo seguir

Cuando el juguete funcione, haz el ejercicio más difícil de forma controlada:

  • Cambia "LAB-1234" por una cadena más larga, recompila y vuelve a encontrarla en los dos builds.
  • Agrega una segunda función de verificación que compare la entrada con un número, y busca dónde muestra Ghidra la conversión.
  • Compila con -O0 -s y compáralo con -O2 -s. La diferencia muestra cuánto cambia el optimizador la forma del código sin quitar la lógica.
  • Lee la documentación de Ghidra sobre el descompilador y la vista de listado. El sitio oficial y la carpeta de documentación en GitHub cubren la interfaz con más profundidad de la que puede dar este artículo.

Si quieres entender mejor las banderas del compilador, las secciones del manual de GCC sobre opciones de optimización y de depuración son las fuentes primarias. Léelas antes de confiar en una bandera dentro de un informe.

Guarda tus notas. Un buen cuaderno de laboratorio tiene el hash del código fuente, los comandos de compilación, las salidas de ejecución, la versión de Ghidra, la versión del JDK y los nombres que asignaste, cada uno con la evidencia que lo respalda. Ese registro convierte este ejercicio en una habilidad que puedes reutilizar con el siguiente programa.

Fuentes

Fuentes

  1. Ghidra: Getting Started (GitHub repository)
  2. Ghidra repository README
  3. Ghidra project site
  4. GCC manual: Optimize Options
  5. GCC manual: Debugging Options

Más en Ingeniería inversa