Skip to content

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 rdi

Eficiencia 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 control

xchg — 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 loopFib

Contexto 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 dup2

inc / 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 byte

Contexto 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íficas

add / 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 loopFib

Contexto 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 address

imul — 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 = 0x100000000

Contexto 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 hijacking

not / 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 → 1

Ejemplos 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