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.objpara obtener el archivo object:-fespecifica 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
-oindica 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 _mainespecifica que el programa iniciará en la etiqueta de códigomain:kernel32.dllpermite realizar una salida del proceso (ExitProcess)user32.dllpermite mostrar una ventana emergente con un mensaje (MessageBox)

- Si ejecutamos
demo1.exeveremos 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:
MOVmueve (copia) los datos de una sección de memoria a otraMOVSXmueve con extensión de signoMOVZXmueve con extensión de ceroXCHGintercambia datosPUSHagregar datos al stackPUSHADagrega todos los registros de 32 bits en el stackPOPsacar datos al stackPOPADsaca todos los registros de 32 bits en el stack
- Aritméticos:
ADDpermite realizar sumasSUBcorresponde a la restaMULrealiza multiplicación sin signoXORoperador lógico XORNOToperador lógico NOTDECresta 1INCincrementa 1
- Control del flujo:
CALLllamada a un procedimientoRETretorno desde un procedimientoLOOPcontrol de loopsJccrealiza un salto en caso de una condición
- Otros:
STIconfigura un flag de interrupciónCLIelimina el flag de interrupciónINentrada desde un puertoOUTsalida 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,16hvalor inmediato a registromov eax,edxregistro a registromov [04030201h],40hinmediato a memoriamov [04030201h],edxregistro a memoria y memoria a registromovsx eax,ebxmueve el valor firmado a un registro y lo extiende con 1movzx eax,ebxmueve un valor sin signo a un registro y lo extiende a cero
- Instrucciones matemáticas:
dec eaxresta 1 al valor deeaxinc eaxsuma 1 al valor deeaxinc 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,ecxsuma operandos y guarda el resultado en el primeroadc ebx,5se suman ambos operandos y se suma el valor de Carry Fsub esi,3resta el segundo operando al primerosbb esi,2se 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 eaxenvía el registro al stackpop edxregistro edx toma el valor de eax desde el stackpushadguarda el contenido de los registros y lo ordena en stackpopadtoma 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
CALLo 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 instruccionesCALLyRETN, 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::BlocksGCC
Ejemplo de uso de GCC:
- Código a compilar:
#include <stdio.h>
int main() {
printf("Hola mundo");
return 0;
}
- Compilación:

Opciones usadas:
-m32específica que es un entorno de 32-bits-odefine 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:
-dindica que vamos a realizar un disassembler del archivoholamundo.exe-Minteldefine 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:
