Instrucciones Básicas
mov — Mover / Copiar dato
¿Qué hace?
Copia un valor de un lado a otro. El origen no se modifica. Es la instrucción más usada en cualquier programa.
Cuándo se usa
- Cargar valores inmediatos en registros
- Copiar entre registros
- Leer/escribir en memoria
Ejemplos explicados
nasm
; 1. Valor inmediato → registro
mov rax, 1 ; rax = 1 (número directo)
; 2. Registro → registro (copia)
mov rbx, rax ; rbx = rax (rax no cambia)
; 3. Memoria → registro (leer de RAM)
mov rax, [rbp-8] ; rax = valor guardado en [rbp-8]
; 4. Registro → memoria (escribir en RAM)
mov [rbp-8], rax ; guarda rax en la variable local
; 5. Valor inmediato → memoria
mov byte [rdi], 0x90 ; escribe 1 byte en la dirección rdiEficiencia de subregistros (crítico para shellcoding)
nasm
; Ineficiente — genera null bytes en shellcode
mov rax, 1 ; → b8 01 00 00 00 (5 bytes, 3 son 00)
; Eficiente — sin null bytes
xor rax, rax ; limpiar primero
mov al, 1 ; → b0 01 (2 bytes, sin 00) ✓Contexto ofensivo
nasm
; Escribir un NOP (0x90) en memoria — preparar NOP sled
mov byte [rdi], 0x90
; Sobreescribir return address en el stack
; (después de obtener la dirección via leak)
mov qword [rsp+8], 0x401337 ; apuntar al shellcode
; Cargar shellcode byte a byte en memoria ejecutable
; (técnica de inyección manual)
mov byte [rdi], 0x48 ; primer byte del shellcode
mov byte [rdi+1], 0x31 ; segundo byte
mov byte [rdi+2], 0xc0 ; ...lea — Load Effective Address
¿Qué hace?
Calcula una dirección (con offset) y la guarda en un registro. NO accede a memoria — solo hace la aritmética de la dirección.
Cuándo se usa
- Calcular punteros con offsets
- Aritmética rápida de direcciones
- Cargar puntero a string/buffer sin acceder al valor
Ejemplos explicados
nasm
; lea vs mov con corchetes — la diferencia clave:
lea rax, [rsp+10] ; rax = rsp + 10 (la DIRECCIÓN, no el valor)
mov rax, [rsp+10] ; rax = *(rsp+10) (el VALOR en esa dirección)
; Ejemplo concreto:
; si rsp = 0x7fff1000 y en 0x7fff100a hay el valor 0xdeadbeef:
lea rax, [rsp+10] ; rax = 0x7fff100a (la dirección)
mov rax, [rsp+10] ; rax = 0xdeadbeef (el valor)
; Aritmética rápida — más eficiente que add:
lea rax, [rbx+rcx*4] ; rax = rbx + (rcx * 4) en 1 instrucción
; equivale a:
mov rax, rcx
imul rax, 4
add rax, rbx ; 3 instrucciones — menos eficiente
; Puntero a string en stack (shellcoding sin variables)
push 0
mov rbx, '/bin//sh'
push rbx
lea rdi, [rsp] ; rdi → "/bin//sh" en el stack
; (mismo que mov rdi, rsp pero más explícito)Contexto ofensivo
nasm
; RIP-relative addressing — acceder a datos sin dirección absoluta
; Crítico en shellcodes y binarios con PIE (Position Independent Executable)
lea rdi, [rel binsh] ; rdi = dirección de "/bin/sh" relativa a rip
; funciona sin importar dónde se cargue el binario
; Calcular offsets en buffer overflows
lea rax, [rbp-0x100] ; rax = inicio del buffer vulnerable
; si sabes que el buffer está 0x100 bytes debajo de rbp,
; puedes calcular exactamente cuántos bytes de padding necesitas:
; padding = 0x100 + 8 (saved rbp) = 0x108 bytes antes del return address
; Stack pivoting
lea rsp, [rbp-0x200] ; mover rsp a donde tenemos controlxchg — Intercambiar valores
¿Qué hace?
Swap atómico entre dos registros o registro y memoria. Sin necesidad de registro temporal.
Cuándo se usa
- Intercambiar dos valores sin usar un tercero
- Stack pivoting
- Shellcodes compactos (opcode corto)
Ejemplos explicados
nasm
; Swap básico
mov rax, 5
mov rbx, 10
xchg rax, rbx ; rax = 10, rbx = 5
; Equivale a (pero con 1 instrucción):
mov rcx, rax
mov rax, rbx
mov rbx, rcx
; En el proyecto Fibonacci:
xor rax, rax ; rax = 0 (F0)
xor rbx, rbx
inc rbx ; rbx = 1 (F1)
loopFib:
add rax, rbx ; rax = F0 + F1 = siguiente número
xchg rax, rbx ; rbx = nuevo F, rax = F anterior
cmp rbx, 10
js loopFibContexto ofensivo
nasm
; Stack pivot — técnica de ROP
; Si controlas el valor de rax (por ejemplo, via overflow),
; puedes pivotar el stack a donde quieras:
xchg rsp, rax ; rsp ahora apunta a tu ROP chain
ret ; ejecuta el primer gadget de tu cadena
; xchg rsp, rax es uno de los gadgets más buscados en ROP
; se busca con: ROPgadget --binary ./vuln | grep "xchg rsp"
; También útil para intercambiar fd en reverse shells:
; después de socket(), rax tiene el fd del socket
mov rbx, rax ; guardar en rbx (callee-saved)
; ... más syscalls ...
xchg rdi, rbx ; poner el fd en rdi para dup2inc / dec — Incrementar / Decrementar
¿Qué hacen?
Suman o restan 1. Más corto y eficiente que add rax, 1.
Cuándo se usan
- Contadores de loops
- Avanzar por memoria byte a byte
- Inicializar valores (0 → 1)
Ejemplos explicados
nasm
; Inicializar Fibonacci
xor rax, rax ; rax = 0
xor rbx, rbx ; rbx = 0
inc rbx ; rbx = 1 (más corto que mov rbx, 1)
; Contador manual
mov rcx, 0
miLoop:
; hacer algo
inc rcx
cmp rcx, 10
jl miLoop ; repetir mientras rcx < 10
; Avanzar puntero byte a byte
mov rdi, buffer
inc rdi ; rdi apunta al siguiente byteContexto ofensivo
nasm
; Evasión de null bytes — inc es más corto que add
; y a veces evita null bytes donde add no puede:
xor rax, rax
inc rax ; rax = 1 (syscall write) — sin null bytes
; vs:
mov rax, 1 ; → b8 01 00 00 00 — tiene null bytes
; Decodificador XOR en shellcodes polimórficos:
; para evadir AV/EDR que buscan firmas estáticas,
; el shellcode se XOR-ea y se autodescodifica en runtime
decode:
xor byte [rdi], 0xAA ; desencriptar byte
inc rdi ; siguiente byte
dec rcx ; counter--
jnz decode ; repetir hasta desencriptar todo
; Bypass de protecciones — modificar flags
; inc/dec afectan ZF, SF, PF pero NO CF (carry flag)
; a veces se usa esto para manipular condiciones específicasadd / sub — Suma / Resta
¿Qué hacen?
Operaciones aritméticas básicas. El resultado va en el destino.
Cuándo se usan
- Aritmética general
- Ajustar punteros (stack alignment, offsets)
- Modificar contadores
Ejemplos explicados
nasm
; Suma básica
mov rax, 100
mov rbx, 50
add rax, rbx ; rax = 150 (rbx no cambia)
; Resta básica
sub rax, rbx ; rax = 100 (150 - 50)
; Ajustar el stack para alineación (CRÍTICO con funciones libc)
sub rsp, 8 ; bajar rsp 8 bytes para alinear a 16
call printf
add rsp, 8 ; restaurar rsp después del call
; Calcular offset en un buffer
lea rdi, [buffer]
add rdi, 0x10 ; avanzar 16 bytes dentro del buffer
; Fibonacci
loopFib:
add rax, rbx ; rax = rax + rbx (siguiente Fibonacci)
xchg rax, rbx
cmp rbx, 10
js loopFibContexto ofensivo
nasm
; Stack alignment — OBLIGATORIO antes de llamar funciones libc
; Si rsp no es múltiplo de 16 al momento del call → crash
sub rsp, 8 ; ajustar si es necesario
call system ; "/bin/sh"
add rsp, 8
; Shellcode encoding/decoding — add para XOR alternativo
; (a veces add es menos detectado que xor por algunos AV)
add byte [rdi], 0x41 ; "sumar" para desencriptar
; Calcular dirección de retorno en buffer overflow
; padding = distancia_buffer_a_retaddr
; típicamente: rbp + 8 = return address
; sub para ir hacia atrás en el stack frame
lea rax, [rbp+8] ; dirección del return addressimul — Multiplicación con signo
¿Qué hace?
Multiplica dos operandos. Resultado en el destino.
Cuándo se usa
- Cálculos de offsets (índice × tamaño)
- Operaciones matemáticas
- Shellcodes compactos de loops
Ejemplos explicados
nasm
; Multiplicación básica
mov rax, 4
mov rbx, 8
imul rax, rbx ; rax = 32
; Calcular offset de array
; array de int (4 bytes cada elemento), acceder al índice 5:
mov rax, 5
imul rax, 4 ; rax = 20 (offset en bytes)
add rdi, rax ; rdi apunta a array[5]
; Loop de potencias (del ejercicio HTB)
mov rax, 2
mov rcx, 5
loop:
imul rax, rax ; rax = rax²
loop loop
; resultado: 2^32 = 0x100000000Contexto ofensivo
nasm
; Calcular offsets en estructuras de memoria
; Por ejemplo, en una tabla de punteros a funciones (vtable):
; cada puntero es 8 bytes, índice 3:
mov rax, 3
imul rax, 8 ; rax = 24
add rdi, rax ; rdi apunta al 4to puntero en la vtable
call [rdi] ; llamar a la función en esa posición
; técnica usada en vtable hijackingnot / and / or / xor — Operaciones Bit a Bit
¿Qué hacen?
Operan a nivel de bit individual. Cada bit se procesa independientemente.
NOT: invierte 0→1, 1→0
AND: ambos 1 → 1, cualquier 0 → 0
OR: alguno 1 → 1, ambos 0 → 0
XOR: iguales → 0, distintos → 1Ejemplos explicados
nasm
; NOT — invertir todos los bits
mov rax, 0xFF00
not rax ; rax = 0xFFFFFFFFFFFF00FF
; AND — máscara de bits (aislar ciertos bits)
mov rax, 0b11001010
and rax, 0b00001111 ; rax = 0b00001010 (solo los 4 bits bajos)
; útil para verificar flags de permisos:
and rax, 7 ; aislar los 3 bits de permisos (rwx)
; OR — activar bits específicos
mov rax, 0b00000001
or rax, 0b10000000 ; rax = 0b10000001 (activar bit 7)
; OR se usa para combinar flags:
or rdi, 0x40 ; activar O_CREAT en open()
; XOR — cifrado simple / limpiar registros
xor rax, rax ; rax = 0 (truco eficiente sin null bytes)
xor rax, 0xAA ; cifrar/descifrar con clave 0xAA
xor rax, 0xAA ; aplicar dos veces = valor original (simétrico)XOR en profundidad — el más importante
nasm
; 1. Poner registro en 0 (más eficiente que mov rax, 0)
xor rax, rax ; 2-3 bytes vs 5-7 bytes con mov
; 2. Toggle de bits (activar/desactivar)
xor rax, 0b00000001 ; invierte el bit 0
; 3. Cifrado/descifrado de shellcode (polimorfismo)
; Primero, XOR-ear el shellcode con una clave:
; shellcode[i] ^= 0x41
; Luego, al ejecutarse, el decoder lo restaura:
xor byte [rdi], 0x41
inc rdi
dec rcx
jnz decode_loop
; 4. Evitar null bytes con XOR
; en vez de mov rax, 0 (que genera 00s):
xor rax, rax ; sin null bytes ✓Contexto ofensivo
nasm
; AND para verificar permisos de archivo
; stat() retorna mode en rax — verificar si es ejecutable:
and rax, 0x49 ; S_IXUSR | S_IXGRP | S_IXOTH
jnz es_ejecutable
; OR para setear flags en open()
xor rsi, rsi
or rsi, 0x41 ; O_WRONLY(1) | O_CREAT(0x40)
; abrir archivo para escritura, creándolo si no existe
; XOR encoder — evadir detección de AV/EDR
; el shellcode se guarda XOR-eado con una clave
; solo se desencripta en memoria al momento de ejecutarse
; los AV que escanean el binario ven bytes encriptados → no hay firma
mov rdi, shellcode_encriptado
mov rcx, shellcode_len
mov al, 0x55 ; clave XOR
decode:
xor [rdi], al ; desencriptar byte
inc rdi
dec rcx
jnz decode
jmp shellcode_encriptado ; ejecutar el shellcode ya desencriptado
; NOT para ofuscación de valores
; en vez de poner 0xFFFFFFFF directamente (detectable):
xor rax, rax
not rax ; rax = 0xFFFFFFFFFFFFFFFF (-1)Resumen — Cuándo usar cada instrucción
mov → copiar datos (lo más común)
lea → calcular dirección con offset (sin acceder a memoria)
xchg → swap / stack pivot (ROP)
inc/dec → contadores, inicializar, avanzar punteros
add/sub → aritmética, ajuste de rsp (stack alignment)
imul → multiplicar para calcular offsets de arrays/structs
not → invertir todos los bits
and → aislar bits (máscaras, verificar flags)
or → activar bits (combinar flags de open/mmap)
xor → limpiar registros + cifrado/descifrado de shellcodes
Regla de oro para shellcoding:
- Siempre usar subregistros del tamaño exacto del dato
- xor reg, reg para poner en 0 (no mov reg, 0)
- Verificar null bytes con: pwn asm 'instrucción' | xxd