Conceptos básicos de Assembler y Debugger
En este post se presentan los conceptos básicos del lenguaje de programación Assembly, y la utilización de un Debugger (y otras herramientas) para analizar el funcionamiento de los programas sobre el stack.
También, es la continuación del post Conceptos básicos de arquitectura de procesadores x86.
Assembly
Assembly es un lenguaje de programación de muy bajo nivel, el cual, consiste en un código mnemotécnico, también conocido como opcode
(operation code).
Al igual que muchos lenguajes de programación, este debe ser convertido en código de máquina para ser ejecutado.
Para poder lograr esto se usa assembler.
Assembler
Este es un programa que traduce el lenguaje Assembly a código máquina.
Existen varios tipos de assembler y este dependerá del ISA
del sistema, como por ejemplo:
- MASM (Microsoft Macro Assembler): usado en arquitectura x86, el que utiliza sintaxis Intel para MS-DOS y Microsoft Windows.
- GAS (GNU Assembler): usado en el proyecto GNU, y es el back-end por defecto de
GCC
. - NASM (Netwide Assembler): usado en arquitectura x86 para escribir programas de 16, 32 (IA-32) y 64 bits (x86-64).
- FASM (Flat Assembler): usado en arquitectura x86, y soporta el lenguaje assembly en estilo Intel en IA-32 y x86-64.
NOTA: dentro de los más populares se encuentra NASM y MASM. En este post se mostrará el uso básico de NASM.
Cuando el archivo de código es ensamblado, el archivo que se crea es llamado archivo objeto
(object file). Este es la representación binaria de un programa.
Ahora que tenemos el archivo object, se debe utilizar un linker
para poder generar un archivo ejecutable. Para lograr esto, el linker utiliza uno o más archivos object (como kernel32.dll
o user32.dll
) para combinarlos y crear el ejecutable.
NOTA: los archivos kerne32.dll y user32.dll son requeridos en Windows para crear un ejecutable que necesita acceder a ciertas librerías.
El proceso para crear el archivo ejecutable es el siguiente:
NASM
Instalación
Para este caso, se instalará NASM-X
en Windows.
El proyecto NASM-X es una colección de macros, includes y ejemplos que ayudan a desarrollar en NASM software de 32 y 64-bits para BSD, Linux, Windows y XBOX en una fracción inferior al tiempo normal.
Al momento de descargar NASM-X, extraemos el contenido y lo guardamos en el directorio C:\nasmx
(para este ejemplo y los próximos, trabajaré con un Windows XP, pero puede ser replicado en versiones más actuales):
Luego, agregamos la ruta de los binarios de NASM-X a las variables de entorno:
Después de configurar las variables de entorno, validamos que quedó bien instalado. Para esto, abrimos un nuevo terminal, vamos a la carpeta de NASM-X, y corremos el archivo setpaths.bat
, el cual, nos mostrará el mensaje "NASMX Development Toolkit"
en caso de que lo anterior se encuentre bien configurado:
Ahora, para poder trabajar con las demos que este trae, debemos editar un componente del archivo windemos.inc
que se encuentra en C:\nasmx\demos
. En este debemos comentar la linea %include 'nasmx.inc'
usando el símbolo ;
, y agregar la linea %include 'C:\nasmx\inc\nasmx.inc'
:
Si queremos comprobar que todo se encuentra bien configurado, se puede ensamblar el siguiente demo:
- Ir a
C:\nasmx\demos\win32\DEMO1
- Ensamblamos el archivo
demo1.asm
, el cual, abre una ventana con un mensaje. Para lograr esto, usamos el comandonasm -f win32 demo1.asm -o demo1.obj
para obtener el archivo object:-f
especifica el formato del archivo, donde, en Windows puede ser obj, win32 y win64. En Unix se usan los siguientes formatos: aout, as86, coff, elf32, elf64, elfx32, ieee, macho32 y macho64- Esto es seguido del nombre del archivo
-o
indica el nombre del archivo de salida
- Ahora que tenemos el archivo object, debemos utilizar el linker para poder generar el archivo ejecutable. El comando para esto es
GoLink.exe /entry _main demo1.obj kernel32.dll user32.dll
/entry _main
especifica que el programa iniciará en la etiqueta de códigomain:
kernel32.dll
permite realizar una salida del proceso (ExitProcess
)user32.dll
permite mostrar una ventana emergente con un mensaje (MessageBox
)
- Si ejecutamos
demo1.exe
veremos que aparace una ventana con el mensajeHello from the Procedure!
:
ASM Básico
En esta sección no se enseñará a escribir programas en assembly, sólo se mostrarán instrucciones básicas para poder entender el funcionamiento de los programas al momento de verlos en un Debugger.
Dentro de las instrucciones que hablaremos se encuentran las siguientes, las que se dividen en clases:
- Transferencia de datos:
MOV
mueve (copia) los datos de una sección de memoria a otraMOVSX
mueve con extensión de signoMOVZX
mueve con extensión de ceroXCHG
intercambia datosPUSH
agregar datos al stackPUSHAD
agrega todos los registros de 32 bits en el stackPOP
sacar datos al stackPOPAD
saca todos los registros de 32 bits en el stack
- Aritméticos:
ADD
permite realizar sumasSUB
corresponde a la restaMUL
realiza multiplicación sin signoXOR
operador lógico XORNOT
operador lógico NOTDEC
resta 1INC
incrementa 1
- Control del flujo:
CALL
llamada a un procedimientoRET
retorno desde un procedimientoLOOP
control de loopsJcc
realiza un salto en caso de una condición
- Otros:
STI
configura un flag de interrupciónCLI
elimina el flag de interrupciónIN
entrada desde un puertoOUT
salida desde un puerto
Ejemplo de la suma de dos números:
MOV EAX,2 ; almacena 2 en EAX
MOV EBX,5 ; almacena 5 en EBX
ADD EAX,EBX ; equivale a la siguiente operación: EAX = EAX + EBX
Sabemos que PUSH
almacena datos en la parte superior del stack, ajustando el stack en -4-bytes
(-32 bits
o -0x04
). Esto se puede representar de la siguiente forma en assembly (el siguiente ejemplo es lo mismo que usar la instrucción PUSH
):
SUB ESP,4 ; ESP = ESP-4
MOV [ESP],0x12345678 ; almacene el valor 0x12345678 en la ubicación apuntada por ESP
;Los corchetes indican la dirección señalada por el registro.
Ahora si queremos hacer la instrucción inversa (POP
), puede ser representada de la siguiente forma:
MOV EAX,[ESP] ; almance el valor del puntero ESP en EAX
ADD ESP,4 ; agrega 4 a ESP, lo cual, ajusta el valor
; de la parte superior del stack
Otras instrucciones que nos serán útiles de entender son CALL
y RET
.
Las subrutinas son implementadas usando las instrucciones CALL
y RET
. CALL
empuja el puntero de instrucción actual (EIP) al stack y salta a la dirección de la función especificada. Mientras que, cada vez que la función ejecuta la instrucción RET
, el último elemento se saca del stack y la CPU salta a la dirección.
Ejemplo de CALL
:
MOV EAX,1 ; almacena 1 en EAX
MOV EBX,2 ; almacena 2 en EBX
CALL ADD_sub ; llama la subrutina ADD_sub
INC EAX ; incrementa el valor de EAX, quedando en 4
; esto se debe a que al llamar a ADD_sub, se hizo la suma
; EBX+EAX, quedando EAX en 3, y ahora con INC
; se incrementa en 1
JMP end_sample ; salta hasta end_sample
ADD_sub:
ADD EAX,EBX ; EAX = EAX + EBX
RETN ; en este punto termina la función, por lo tanto, retorna
; a la función que lo llamo
end_sample:
Instrucciones de assembly que nos serán útiles (estas instrucciones son parte del curso de Exploiting Básico):
- Acceso a la memoria
[]
:mov edi,[ecx]
accesos a memoria por registromov edi,[ecx+eax]
accesos a memoria por desplazamientomov edi,[ecx+esi+10h]
accesos a memoria por índice y desplazamiento
- Movimientos de datos
MOV
:mov eax,16h
valor inmediato a registromov eax,edx
registro a registromov [04030201h],40h
inmediato a memoriamov [04030201h],edx
registro a memoria y memoria a registromovsx eax,ebx
mueve el valor firmado a un registro y lo extiende con 1movzx eax,ebx
mueve un valor sin signo a un registro y lo extiende a cero
- Instrucciones matemáticas:
dec eax
resta 1 al valor deeax
inc eax
suma 1 al valor deeax
inc dword ptr[04030201h]
incrementa el contenido de posición de memoriainc word ptr[04030201h]
incrementa los últimos 2 bytesinc byte ptr[04030201h]
incrementa el último byteadd eax,ecx
suma operandos y guarda el resultado en el primeroadc ebx,5
se suman ambos operandos y se suma el valor de Carry Fsub esi,3
resta el segundo operando al primerosbb esi,2
se restan ambos operandos y se resta el valor de Carry F
- Movimiento de datos
LEA
:lea ecx,[ebp + 10h]
deja en ecx, el valor de tenga ebp + 10hlea ebx,[edx * 8h]
deja en ebx, el valor que tenga edx * 8hlea esi,[eax + ebx + 20h]
deja en esi, la suma eax+ebx+20hlea ecx,[0x04030201 + 2h]
deja en ecx, el valor de 0x04030204
- Movimiento de datos por tipo:
mov al,byte ptr[edx]
mueve a al un byte al = 20h de 0x04020120mov ax,word ptr[edx]
mueve a al un byte ax = 0120h de 0x04020120mov eax,dword ptr[edx]
mueve a al un byte eax = 0x04020120
- Obteniendo datos del stack:
push eax
envía el registro al stackpop edx
registro edx toma el valor de eax desde el stackpushad
guarda el contenido de los registros y lo ordena en stackpopad
toma los valores del stack y los envía a los registros
Intel vs AT&T
Al momento de trabajar con assembly, es necesario saber que existen dos arquitecturas de su sintaxis: Intel y AT&T.
Se pueden diferenciar porque Intel es la sintaxis por defecto de los entornos Windows. Mientras que AT&T es la sintaxis por defecto para entornos Linux:
- Sintaxis Intel:
<instrucción><destino><origen>
(ejemplo:MOV EAX,8
) - Sintaxis AT&T:
<instrucción><origen><destino>
(ejemplo:MOVL $8,%EAX
)
Como se ve en el ejemplo anterior, en AT&T se utiliza el símbolo %
antes del nombre de un registro, mientras que se usa el símbolo $
antes de un número.
Otra consideración al momento de usar AT&T, es que se le agrega una letra a la instrucción dependiendo del tamaño del operando:
Q
(Quad) para 64-bitsL
(Long) para 32-bitsW
(Word) para 16-bitsB
(Byte) para 8-bits
Debugger
Los debugger son programas que corren otros programas, con la finalidad de poder tomar control sobre ellos y poder testearlos o encontrar fallos en estos.
En este caso, se utilizarán con la finalidad de ayudar en la escritura de exploits, analizar el programa, realizar ingeniería inversa a los binarios, entre otros.
Por lo tanto, este nos permitirá:
- Detener el programa mientras se está ejecutando
- Analizar el stack y sus datos
- Inspeccionar los registros
- Cambiar el programa o variables de este
Los debugger que podemos mencionar son:
- Immunity Debugger (Windows)
- IDA (Windows, Linux y macOS)
- GDB (Unix, Windows)
- x64DBG (Windows)
- EDB (Linux)
- WinDBG (Windows)
- OllyDBG (Windows)
- Hopper (macOS y Linux)
Immunity Debugger
A continuación, se presenta la GUI de este debugger, el cual, se encuentra dividido en 4 paneles:
- Control de ejecución: permite reiniciar, cerrar, iniciar, pausar, ver cada instrucción, saltar entre instrucciones, volver a las instrucciones anteriores, ejecutar hasta un return, y permite que el disassembler sea navegado a una dirección de memoria en particular.
- Panel de disassembler
- Columna 1: direcciones
- Columna 2: código máquina
- Columna 3: assembly
- Columna 4: comentarios del debugger
- Panel de registros
- Nombres de los registros
- El contenido de estos
- En caso de que estos apunten a una cadena ASCII, se mostrará dicho valor
- Dump de la memoria: muestra el contenido del espacio de memoria del proceso como el dump de un binario. Es útil para examinar regiones de memoria.
- Stack
- Columna 1: corresponde a la dirección
- Columna 2: es el valor en el stack en esa dirección
- Columna 3: es una explicación del contenido (una dirección, un UNICODE, etc.)
- Columna 4: comentarios del debugger
- Input de comandos: permite ejecutar plugins.
- Estado: muestra los mensajes de estado.
- Estado del proceso: muestra si el proceso se encuentra corriendo o si está pausado.
Ejemplo del uso del debugger para analizar un programa
Al momento de usar Immunity Debugger, tenemos 3 formas de cargar programas en este:
- La primera es arrastrando el programa sobre el debugger
- La segunda opción es cargarla desde la interfaz del debugger
- La tercera forma es adjuntando el proceso del programa
Para utilizar la segunda opción, debemos ir al icono de directorio
y luego seleccionar el programa a ejecutar:
Para adjuntar el proceso del programa, se debe ir a File > Attach
:
Esto nos abrirá una ventana con los procesos corriendo en el sistema, donde, el programa que necesitemos ver en el debugger debe ser seleccionado y luego dar clic en Attach
:
Para controlar el flujo del programa dentro del debugger, tenemos el panel de control de ejecución:
De este panel, por lo general se usan las primeras 6 instrucciones (de izquierda a derecha):
- Restart: reinicia el programa.
- Close: cierra el programa.
- Run: inicia el programa.
- Pause: pausa el programa.
- Step into: recorre las instrucciones del programa, donde, si hay un
CALL
o unRETN
, este va a dichas direcciones de memoria. - Step over: similar a
Step into
, pero en vez de ir a las direcciones de memoria de las instruccionesCALL
yRETN
, sigue de forma lineal las instrucciones del programa.
Las ventajas que nos entrega un debugger, es que podemos definir breakpoints
; los que son usados para detener el programa en una dirección de memoria específica para realizar un análisis del comportamiento de este. Para configurar o eliminar un breakpoint
, se debe seleccionar la dirección de memoria y presionar la tecla F2
; esto dejará en color Cian
dicha dirección:
Compilador
Varios lenguajes de programación de alto nivel (como es el caso de C
y C++
) necesitan ser compilados para poder crear un archivo de bajo nivel, y poder ser ejecutados.
Al igual que los debugger, existen múltiples opciones al momento de elegir un compilador:
Microsoft Visual C/C++
, también conocido como Visual StudioOrwell Dev-C++
Code::Blocks
GCC
Ejemplo de uso de GCC
:
- Código a compilar:
#include <stdio.h>
int main() {
printf("Hola mundo");
return 0;
}
- Compilación:
Opciones usadas:
-m32
específica que es un entorno de 32-bits-o
define el nombre del archivo de salida
Decompilador
Los decompiladores son herramientas que nos permiten ver cómo funcionan los programas, con los cuales, podemos obtener el disassembler.
Para realizar esta tarea, utilizaremos el software objdump
. Este se encuentra en la carpeta C:\Archivos de programa\Dev-Cpp\MinGW64\bin
de Dev-C++
.
Para obtener el disassembler del programa holamundo.exe
, se utilizará el siguiente comando: objdump -d -Mintel holamundo.exe > holamundo.txt
:
-d
indica que vamos a realizar un disassembler del archivoholamundo.exe
-Mintel
define que el output del disassembler sea en sintaxis Intel>
nos permite reenviar la salida del comando al archivoholamundo.txt
Si vemos el archivo holamundo.txt
, podemos observar que posee las instrucciones de assembly:
Disassembly del archivo holamundo.exe
: