GDB, acrónimo de GNU Debugger, es una herramienta de debugging que permite inspeccionar y controlar la ejecución de programas en tiempo real. Fue una piedra angular en el ecosistema de herramientas de desarrollo de software libre, permitiendo analizar programas escritos en lenguajes tanto de bajo nivel, como Assembly, hasta otros más comúnmente utilizados como Rust o C.
Esta herramienta juega un papel fundamental dentro del campo de la ciberseguridad. Uno de sus principales usos involucra realizar ingeniería inversa de binarios, identificar puntos débiles en el código y desarrollar o sanear exploits.
La capacidad de GDB para modificar el flujo de ejecución de un programa y manipular su estado en tiempo real lo convierte en una herramienta indispensable para la investigación de malware y la evaluación de la seguridad del software. Además, su integración con otras herramientas de análisis de seguridad y su soporte para scripting permiten automatizar muchas tareas repetitivas, mejorando la eficiencia del análisis de seguridad.
Otro aspecto fundamental de GDB es su portabilidad: si bien es una herramienta nativa y preinstalada de sistemas operativos Linux, esta puede ser utilizada también en sistemas Windows y MacOS.
Si bien la herramienta no tiene interfaz gráfica, existen varios plugins y add-ons que facilitan la visualización de información como GDBgui o Data Display Debugger. A lo largo de este post, utilizaremos GDB Dashboard, una variación del archivo de configuración inicial de GDB que colorea y categoriza la información arrojada por la herramienta.
Ejecutando un programa
En primer lugar, y para comenzar la herramienta, llamamos dentro de una terminal con el comando gdb (archivo ejecutable), situados en el directorio del ejecutable a analizar. Luego, utilizamos el comando run junto a los argumentos que toma el programa, si lo hace.
Supongamos que contamos con un ejecutable en Linux llamado suma, y queremos observar su operatoria. Para ello, inicializamos la herramienta y corremos el programa con dos números como sus argumentos:
Como este programa no tiene interrupciones o esperas, podemos ver la finalización de su ejecución en la misma herramienta.
Hasta ahora, ejecutamos el programa suma con éxito. Sin embargo, el punto más interesante de la herramienta es la capacidad de observar el programa con su ejecución detenida en alguna instrucción o función.
Y si nuestro programa no cuenta con expresiones que causen una interrupción temporal, como la espera de entrada del usuario, o si queremos detener la ejecución en funciones puntuales, ¿cómo podemos hacerlo desde GDB?
Breakpoints y watchpoints
Dentro de un debugger cualquiera, los breakpoints y watchpoints son piezas esenciales para observar el contenido de un programa, comprender la lógica, encontrar funciones conocidas como vulnerables y mucho más.
Puntualmente, los breakpoints son puntos de interrupción que se establecen en el código fuente. Cuando la ejecución del programa alcanza un breakpoint, el programa se detiene, permitiendo a la herramienta inspeccionar el estado actual del programa, como el valor de las variables, el flujo de control y el contenido de la memoria.
Por otro lado, los watchpoints se enfocan en la observación de cambios en valores de variables específicas. Un watchpoint detiene la ejecución del programa cada vez que el valor de una variable observada cambia, independientemente de dónde ocurra este cambio en el código. Estos permiten monitorear el estado de variables específicas, lo cual es crucial para detectar y entender cómo y cuándo cambian los valores de estas variables durante la ejecución del programa. Esto es particularmente útil para encontrar errores que involucran corrupción de memoria o valores inesperados.
Para complementar el uso de breakpoints y watchpoints, GDB proporciona comandos de navegación y control como step, next o continue. Profundizaremos sobre estos en la siguiente sección.
Continuando con nuestro ejemplo del ejecutable suma, colocaremos un breakpoint en la función main con el comando break main. Si iniciamos la ejecución de GDB, podremos ver el estado del programa de forma integral. En este caso, podemos observar tanto el desensamblado del ejecutable junto a valores correspondientes a la memoria, como registros:
Así como también su código fuente, la línea que será ejecutada a continuación, variables y más:
Alterando el flujo del programa
Para sacar el máximo provecho de las interrupciones colocadas en ejecución, GDB proporciona varios comandos para controlar la ejecución del programa de manera detallada. A continuación, definiremos las funciones clave: step, next, continue, finish.
El comando step permite avanzar la ejecución del programa una línea de código a la vez, entrando en cualquier función llamada en esa línea. Este comando es extremadamente útil para examinar el comportamiento detallado de funciones anidadas y seguir el flujo de ejecución paso a paso.
El comando next también avanza la ejecución del programa una línea a la vez, pero a diferencia de step, no entra en las funciones llamadas en esa línea. En su lugar, ejecuta la función completa y se detiene en la siguiente línea del código principal, lo cual acelera el proceso de debugging si estamos analizando secciones de código menos críticas.
En otra categoría tenemos a los comandos continue y finish. El comando continue reanuda la ejecución del programa hasta que se alcance el siguiente breakpoint o watchpoint, o hasta que el programa termine; mientras que finish permite continuar la ejecución del programa hasta que la función actual termine, y se retorne al contexto y línea en donde se llamó a la misma.
En nuestro ejemplo anterior, y ejecutando next para dejar pasar algunas líneas, llegamos al llamado de una función que podría ser de interés.
Y si accedemos a la función con el comando step, podemos ver cómo se realiza el manejo de la entrada de números a sumar. En este caso, se utiliza la función atoi en C, que resulta interesante ya que no es una elección segura por parte del desarrollador.
Interactuando con la memoria
Manipular y visualizar la memoria observada por un programa es, también, una de las fortalezas de GDB. Esta proporciona una serie de comandos que permiten modificar variables y valores de retorno, visualizar el stack, y ver el contenido de direcciones de memoria específicas. Veamos algunos de los comandos más utilizados.
Para cambiar el valor de una variable, se utiliza el comando set. Este comando permite asignar un nuevo valor a una variable específica durante la ejecución del programa, lo cual nos facilita probar diferentes escenarios sin cambiar el código fuente.
GDB también permite modificar el valor de retorno de una función antes de que finalice su ejecución. Esto es útil para probar cómo el programa maneja diferentes resultados de función sin modificar el código. Por ejemplo, el comando return 42 forzará a la función actual a retornar 42, sin importar el cálculo que estaba realizando. Es particularmente útil cuando se analizan funciones críticas cuyos resultados afectan significativamente el flujo del programa.
También contamos con comandos para observar la pila (o stack), como backtrace. Este muestra la pila de llamadas actual, detallando todas las funciones que se han llamado hasta el punto de interrupción.
Volviendo a nuestro ejecutable, y recordando que encontramos una función que puede causar fallos en el programa que estábamos analizando, podemos modificar la variable input para verificarlo:
Y así forzar un error por overflow aritmético en el programa, descrito como un Segmentation Fault.
GDB también ofrece algunos comandos más avanzados, pero útiles sobre todo para analizar binarios. Uno de ellos es el comando x, que permite examinar el contenido de la memoria en una dirección específica. Este comando es altamente configurable y permite visualizar la memoria en diferentes formatos. Por ejemplo, el comando x/4xw 0x7fffffffe000 muestra cuatro palabras (de la unidad Word, equivalente a 2 bytes) en hexadecimal, desde la dirección de memoria 0x7fffffffe000.
Conclusiones
GDB es una herramienta extremadamente poderosa y versátil, ampliamente utilizada tanto en ciberseguridad como en programación: Desde el análisis de malware, pasando por la explotación de vulnerabilidades, hasta la resolución de un programa defectuoso.
Las funcionalidades van mucho más allá de las básicas mencionadas en este post. Entre ellas, encontramos el debugging remoto, inspección de estructuras de datos complejas, análisis código ensamblador y mucho más.
A pesar de su poder, esta herramienta puede ser intimidante para principiantes debido a la cantidad de comandos y opciones disponibles, y el manejo por terminal. Y si bien requiere tiempo y práctica para aprender a usarla de manera efectiva, recomendamos usar las llamadas “cheat sheets” para una rápida referencia a la sintaxis del programa.