GNU Debugger (GDB)
Debugging es una habilidad importante tanto para desarrolladores como para pentesters. Debugging es un término utilizado para encontrar y eliminar problemas (es decir, bugs) en nuestro código, de ahí el nombre de-bugging. Cuando desarrollamos un programa, con mucha frecuencia nos encontraremos con bugs en nuestro código. No es eficiente seguir cambiando nuestro código hasta que haga lo que esperamos. En su lugar, realizamos debugging configurando breakpoints y observando cómo actúa nuestro programa en cada uno de ellos y cómo cambia nuestra entrada entre ellos, lo que debería darnos una idea clara de lo que está causando el bug
.
Los programas escritos en lenguajes de alto nivel pueden establecer breakpoints en líneas específicas y ejecutar el programa a través de un debugger para monitorear cómo actúan. Con Assembly, trabajamos con código máquina representado como instrucciones en ensamblador, por lo que nuestros breakpoints se establecen en la ubicación de memoria donde se carga nuestro código máquina, como veremos.
Para depurar nuestros binarios, usaremos un debugger bien conocido para programas Linux llamado GNU Debugger (GDB
). Existen otros debuggers similares para Linux, como Radare y Hopper, y para Windows, como Immunity Debugger y WinGDB. También hay debuggers potentes disponibles para muchas plataformas, como IDA Pro y EDB. En este módulo, utilizaremos GDB. Es el más confiable para binarios Linux ya que está desarrollado y mantenido directamente por GNU, lo que le da una excelente integración con el sistema Linux y sus componentes.
Installation
GDB está instalado en muchas distribuciones de Linux, y también viene instalado por defecto en Parrot OS y PwnBox. En caso de que no esté instalado en tu máquina virtual, puedes usar apt
para instalarlo con los siguientes comandos:
sudo apt-get update
sudo apt-get install gdb
Una de las grandes características de GDB
es su soporte para plugins de terceros. Un excelente plugin que está bien mantenido y tiene buena documentación es GEF. GEF es un plugin gratuito y de código abierto para GDB diseñado específicamente para ingeniería inversa y explotación de binarios. Esto lo convierte en una gran herramienta para aprender.
Para agregar GEF a GDB, podemos usar los siguientes comandos:
wget -O ~/.gdbinit-gef.py -q https://gef.blah.cat/py
echo source ~/.gdbinit-gef.py >> ~/.gdbinit
Getting Started
Ahora que tenemos ambas herramientas instaladas, podemos ejecutar gdb para depurar nuestro binario HelloWorld
usando los siguientes comandos, y GEF se cargará automáticamente:
gdb -q ./helloWorld
...SNIP...
gef➤
Como podemos ver en gef➤
, GEF se carga cuando GDB se ejecuta. Si alguna vez tienes problemas con GEF
, puedes consultar la Documentación de GEF, y probablemente encontrarás una solución.
En adelante, con frecuencia estaremos ensamblando y enlazando nuestro código en ensamblador y luego ejecutándolo con gdb
. Para hacerlo rápidamente, podemos usar el script assembler.sh
que escribimos en la sección anterior con el flag -g
. Este ensamblará y enlazará el código, y luego lo ejecutará con gdb
, de la siguiente manera:
./assembler.sh helloWorld.s -g
...SNIP...
gef➤
Info
Una vez que GDB
esté iniciado, podemos usar el comando info
para ver información general sobre el programa, como sus funciones o variables.
Consejo: Si queremos entender cómo funciona cualquier comando dentro de GDB
, podemos usar el comando help CMD
para obtener su documentación. Por ejemplo, podemos probar ejecutando help info
Functions
Para comenzar, usaremos el comando info
para verificar qué functions
están definidas dentro del binario:
gef➤ info functions
All defined functions:
Non-debugging symbols:
0x0000000000401000 _start
Como podemos ver, encontramos nuestra función principal _start
.
Variables
También podemos usar el comando info variables
para ver todas las variables disponibles dentro del programa:
gef➤ info variables
All defined variables:
Non-debugging symbols:
0x0000000000402000 message
0x0000000000402012 __bss_start
0x0000000000402012 _edata
0x0000000000402018 _end
Como podemos ver, encontramos el message
, junto con algunas otras variables predeterminadas que definen segmentos de memoria. Podemos hacer muchas cosas con las funciones, pero nos centraremos en dos puntos principales: Disassembly y Breakpoints.
Disassemble
Para ver las instrucciones dentro de una función específica, podemos usar el comando disassemble
o disas
junto con el nombre de la función, de la siguiente manera:
gef➤ disas _start
Dump of assembler code for function _start:
0x0000000000401000 <+0>: mov eax,0x1
0x0000000000401005 <+5>: mov edi,0x1
0x000000000040100a <+10>: movabs rsi,0x402000
0x0000000000401014 <+20>: mov edx,0x12
0x0000000000401019 <+25>: syscall
0x000000000040101b <+27>: mov eax,0x3c
0x0000000000401020 <+32>: mov edi,0x0
0x0000000000401025 <+37>: syscall
End of assembler dump.
Como podemos ver, la salida que obtuvimos se asemeja mucho a nuestro código en ensamblador y a la salida de desensamblado que obtuvimos con objdump
en la sección anterior. Debemos centrarnos en lo principal de este desensamblado: las direcciones de memoria para cada instrucción y operandos (es decir, argumentos).
Tener la dirección de memoria es fundamental para examinar las variables/operandos y configurar breakpoints para una determinada instrucción.
Podrás notar durante el debugging que algunas direcciones de memoria están en la forma de 0x00000000004xxxxx
, en lugar de sus direcciones crudas en memoria 0xffffffffaa8a25ff
. Esto se debe al $rip-relative addressing
en los ejecutables independientes de posición PIE
, en los que las direcciones de memoria se utilizan en relación con su distancia desde el puntero de instrucción $rip
dentro de la RAM virtual del programa, en lugar de usar direcciones crudas de memoria. Esta característica puede desactivarse para reducir el riesgo de explotación de binarios.
A continuación, revisaremos los conceptos básicos de debugging con GDB utilizando breakpoints, examinando datos y avanzando paso a paso a través del programa.