Skip to content

Registros x86_64


rax — Accumulator / Syscall Number / Return Value

¿Qué es?

El registro más usado. Tiene tres roles dependiendo del contexto:

  1. Número de syscall (antes de syscall)
  2. Valor de retorno (después de syscall o call)
  3. Acumulador de operaciones aritméticas

Ejemplo 1 — Syscall number

nasm
mov rax, 1      ; le dices al kernel: "quiero hacer write"
syscall         ; el kernel lee rax para saber qué hacer

Ejemplo 2 — Valor de retorno

nasm
; después de un syscall, rax tiene el resultado
mov rax, 1      ; write syscall
mov rdi, 1
mov rsi, message
mov rdx, 13
syscall
; ahora rax contiene cuántos bytes se escribieron (13)
; si rax es negativo → hubo un error

Ejemplo 3 — En aritmética

nasm
mov rax, 100
add rax, 50     ; rax = 150
imul rax, rax   ; rax = 22500

En contexto ofensivo

nasm
; shellcode de execve — rax determina qué syscall ejecutar
xor rax, rax
mov al, 59      ; execve (sin null bytes)
syscall         ; kernel ejecuta /bin/sh

; También se usa para verificar si una syscall falló:
; si rax < 0 después de syscall → error
; por ejemplo, open() retorna -1 si el archivo no existe

Subregistros útiles

nasm
rax     ; 64-bit — operaciones grandes
eax     ; 32-bit — limpiar los bits altos automáticamente
ax      ; 16-bit
al      ; 8-bit  ← más usado en shellcoding (evita null bytes)

rbx — Base Register / Callee Saved

¿Qué es?

Registro "seguro". Las funciones y syscalls garantizan que no lo van a tocar. Si guardas algo en rbx antes de un call, seguirá ahí después.

Ejemplo 1 — Preservar un valor entre funciones

nasm
mov rbx, 0xdeadbeef     ; guardar valor importante

call algunaFuncion      ; puede modificar rax, rcx, rdx...
                        ; pero NO toca rbx

; rbx sigue siendo 0xdeadbeef aquí
cmp rbx, 0xdeadbeef
je sigueBien

Ejemplo 2 — Loop donde necesitas preservar un puntero

nasm
mov rbx, rsi        ; guardar puntero al buffer

mov rcx, 10
loopFib:
    add rax, rbx    ; rbx sigue siendo el puntero
    loop loopFib

; rbx intacto después del loop
mov [rbx], rax      ; escribir resultado en el buffer original

En contexto ofensivo

nasm
; En shellcodes de reverse shell, rbx suele usarse para
; guardar el file descriptor del socket mientras se hacen
; syscalls de dup2 para redirigir stdin/stdout/stderr:

mov rbx, rax        ; guardar fd del socket (retorno de socket())

; ahora hacer dup2(sockfd, 0), dup2(sockfd, 1), dup2(sockfd, 2)
mov rdi, rbx        ; sockfd sigue disponible
mov rsi, 0
mov rax, 33         ; dup2
syscall

mov rdi, rbx        ; sockfd aún intacto
mov rsi, 1
mov rax, 33
syscall

rdi — Destination Index / 1er Argumento

¿Qué es?

Primer argumento de cualquier función o syscall. Su nombre histórico "Destination Index" viene de instrucciones de strings como movs.

Ejemplo 1 — Como argumento de syscall

nasm
; write(fd, buffer, count)
mov rdi, 1          ; 1er arg = fd (1 = stdout)

Ejemplo 2 — Como destino en instrucciones de string

nasm
; copiar memoria de rsi → rdi (como memcpy)
lea rdi, [destino]
lea rsi, [origen]
mov rcx, 100        ; copiar 100 bytes
rep movsb           ; repite movsb 100 veces

En contexto ofensivo

nasm
; En shellcodes, rdi es el pathname de execve:
push rdx            ; null terminator
mov rdi, '/bin//sh'
push rdi
mov rdi, rsp        ; rdi → "/bin//sh"

; En hooking de funciones, rdi contiene el primer argumento
; que la función víctima recibió — útil para interceptar calls

; Ejemplo: hook de open() para loggear qué archivos abre el proceso
; cuando open() es llamado, rdi = pathname del archivo

rsi — Source Index / 2do Argumento

¿Qué es?

Segundo argumento. Históricamente el "Source Index" para operaciones de strings.

Ejemplo 1 — Como argumento de syscall

nasm
; write(fd, buffer, count)
mov rsi, message    ; 2do arg = puntero al buffer

Ejemplo 2 — Puntero a string en el stack (sin variables)

nasm
; Técnica shellcoding: string en stack, rsi apunta a él
xor rbx, rbx
mov bx, 'y!'
push rbx
mov rbx, 'B Academ'
push rbx
mov rbx, 'Hello HT'
push rbx
mov rsi, rsp        ; rsi apunta al string "Hello HTB Academy!"

En contexto ofensivo

nasm
; En un shellcode de lectura de archivo sensible (/etc/passwd):
; open("/etc/passwd", O_RDONLY)
mov rdi, filename   ; 1er arg = pathname
mov rsi, 0          ; 2do arg = O_RDONLY (flags)
mov rax, 2          ; syscall open
syscall
; rax = fd del archivo abierto

rdx — Data Register / 3er Argumento

¿Qué es?

Tercer argumento. También recibe la parte alta en divisiones y multiplicaciones.

Ejemplo 1 — Como argumento

nasm
; write(fd, buffer, count)
mov rdx, 18         ; 3er arg = cantidad de bytes a escribir

Ejemplo 2 — División (rdx contiene el resto)

nasm
mov rax, 17
xor rdx, rdx        ; limpiar rdx antes de dividir (IMPORTANTE)
mov rbx, 5
div rbx             ; rax = 17/5 = 3, rdx = 17%5 = 2

En contexto ofensivo

nasm
; En shellcodes de execve, rdx = envp (variables de entorno)
xor rdx, rdx        ; NULL — sin variables de entorno
mov rax, 59         ; execve
syscall

; Si rdx no está en NULL, execve puede fallar o comportarse
; de forma inesperada — siempre limpiarlo antes

rcx — Counter Register / 4to Argumento

¿Qué es?

Contador natural para loops. En Windows x64, es el 1er argumento (convención diferente a Linux).

Ejemplo 1 — Contador de loop

nasm
mov rcx, 10         ; repetir 10 veces
bucle:
    ; instrucciones
    loop bucle      ; dec rcx + jnz bucle automáticamente

Ejemplo 2 — Con instrucciones de repetición

nasm
; llenar 100 bytes con 0x90 (NOP sled)
mov rdi, buffer
mov al, 0x90
mov rcx, 100
rep stosb           ; almacena al en [rdi] y avanza, rcx veces

En contexto ofensivo

nasm
; NOP sled — técnica clásica de buffer overflow
; Se rellena la memoria con NOPs antes del shellcode
; para aumentar las chances de caer en el shellcode

mov rdi, buffer_addr
xor rax, rax
mov al, 0x90        ; NOP opcode
mov rcx, 200        ; 200 NOPs
rep stosb           ; escribir el NOP sled

; En Windows, rcx es el primer argumento:
; VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
; rcx = lpAddress (NULL = kernel elige la dirección)

r8 y r9 — 5to y 6to Argumentos

¿Qué son?

Registros generales extra añadidos en x86_64. Principalmente usados como 5to y 6to argumentos en funciones complejas.

Ejemplo — Función con muchos argumentos

nasm
; mmap(addr, length, prot, flags, fd, offset)
; en Linux x86_64:
mov rax, 9          ; syscall mmap
xor rdi, rdi        ; addr = NULL (kernel elige)
mov rsi, 4096       ; length = 4096 bytes
mov rdx, 7          ; prot = PROT_READ|PROT_WRITE|PROT_EXEC (rwx)
mov r10, 0x22       ; flags = MAP_PRIVATE|MAP_ANONYMOUS
mov r8, -1          ; fd = -1 (sin archivo)
xor r9, r9          ; offset = 0
syscall
; rax = dirección de la memoria asignada

En contexto ofensivo

nasm
; mmap con permisos rwx es la técnica principal para
; asignar memoria ejecutable y cargar shellcode ahí:
; prot = 7 = PROT_READ(1) | PROT_WRITE(2) | PROT_EXEC(4)

; Después del mmap, copias el shellcode a esa dirección
; y saltas a ella con call rax o jmp rax

rsp — Stack Pointer (Top)

¿Qué es?

Apunta SIEMPRE al tope actual del stack. Se mueve solo con push/pop/call/ret. Nunca estático — cambia constantemente.

Ejemplo 1 — push y pop

nasm
; antes: rsp = 0x7fff1000
push rax            ; rsp = 0x7fff0ff8 (baja 8 bytes)
push rbx            ; rsp = 0x7fff0ff0 (baja 8 más)
pop rbx             ; rsp = 0x7fff0ff8 (sube 8)
pop rax             ; rsp = 0x7fff1000 (restaurado)

Ejemplo 2 — String en el stack

nasm
; técnica para shellcoding sin variables
push 0              ; null terminator
mov rax, '/bin//sh'
push rax
mov rdi, rsp        ; rdi apunta al string en el stack

En contexto ofensivo

nasm
; Buffer overflow — cuando desbordas un buffer en el stack,
; puedes sobreescribir la dirección de retorno que está
; guardada en el stack.
;
; Stack layout típico de una función:
; [variables locales] [saved rbp] [return address] ← rsp apunta aquí al ret
;
; Si sobreescribes la return address con tu shellcode:
; cuando la función hace 'ret', salta a tu shellcode

; Stack pivoting — técnica avanzada:
; controlas rsp para que apunte a donde tú quieras
xchg rsp, rax       ; rsp ahora apunta a tu cadena ROP
ret                 ; ejecuta el primer gadget

rbp — Base Pointer (Frame)

¿Qué es?

Ancla fija que marca el inicio del stack frame de la función actual. No se mueve durante la ejecución de una función — sirve como referencia.

Ejemplo 1 — Prólogo y epílogo de función

nasm
miFuncion:
    push rbp            ; guardar el frame anterior
    mov rbp, rsp        ; rbp = base del frame actual

    ; variables locales accesibles con offsets negativos:
    ; [rbp-8]  = primera variable local
    ; [rbp-16] = segunda variable local

    sub rsp, 16         ; reservar espacio para 2 variables

    mov [rbp-8], rax    ; guardar valor en variable local 1
    mov [rbp-16], rbx   ; guardar valor en variable local 2

    ; ... código de la función ...

    mov rax, [rbp-8]    ; recuperar variable local 1
    pop rbp             ; restaurar frame anterior
    ret

En contexto ofensivo

nasm
; rbp es clave en análisis de binarios:
; - En gdb puedes ver el frame con 'info frame'
; - Cada función tiene su propio [rbp-N] para variables locales
; - Al hacer reversing, rbp te dice dónde están las variables

; Frame pointer overwrite — técnica de explotación:
; si sobreescribes rbp + return address, puedes controlar
; el frame de la función que llamó (leave = mov rsp, rbp; pop rbp)

; 'leave' instruction = mov rsp, rbp + pop rbp
; si controlas rbp, controlas rsp después del leave → stack pivot

rip — Instruction Pointer

¿Qué es?

Siempre apunta a la próxima instrucción a ejecutar. No puedes modificarlo directamente — solo call, ret, jmp y similares lo cambian.

Ejemplo 1 — Relativo a rip (RIP-relative addressing)

nasm
; acceder a datos con dirección relativa a rip
; nasm lo hace automáticamente con labels:
lea rax, [rel message]  ; rax = rip + offset_a_message
; esto evita direcciones absolutas — clave en shellcodes y PIE binaries

Ejemplo 2 — call y ret

nasm
; cuando haces call:
; 1. rip (dirección de la siguiente instrucción) se pushea al stack
; 2. rip salta a la función

call miFuncion
; aquí rip apunta a esta instrucción antes del call
; miFuncion puede acceder a esa dirección con [rsp]

miFuncion:
    ; [rsp] contiene la return address (el rip guardado)
    ret     ; pop rsp → rip (volver)

En contexto ofensivo

nasm
; RIP es el objetivo final en explotación de binarios:
; controlar RIP = controlar el flujo de ejecución del programa

; Técnica clásica:
; 1. Buffer overflow sobreescribe la return address en el stack
; 2. Al hacer 'ret', rip = tu valor = dirección de tu shellcode

; ROP (Return Oriented Programming):
; en vez de shellcode, encadenas 'gadgets' (fragmentos que terminan en ret)
; cada ret carga el siguiente gadget desde el stack
; así ejecutas código arbitrario sin escribir shellcode nuevo

; RIP-relative: útil en shellcodes porque no usa direcciones absolutas
; que cambiarían según dónde se cargue el shellcode en memoria

Resumen aplicado al desarrollo ofensivo

rax → qué syscall ejecutar / qué retornó
      clave: execve=59, write=1, mmap=9, open=2

rbx → guardar valores entre calls (callee-saved)
      clave: guardar fd del socket en reverse shells

rdi → 1er arg: pathname en execve, fd en write/dup2
      clave: /bin/sh en shellcodes

rsi → 2do arg: argv en execve, buffer en write
      clave: puntero al string en el stack

rdx → 3er arg: envp (NULL) en execve, permisos en mmap
      clave: siempre xor rdx, rdx antes de execve

rcx → contador de loops / 4to arg en Linux (1ro en Windows)
      clave: NOP sleds con rep stosb

r8/r9 → 5to/6to arg
      clave: flags y fd en mmap para memoria ejecutable rwx

rsp → tope del stack
      clave: string technique + buffer overflow target

rbp → base del frame
      clave: reversing de variables locales + frame pivot

rip → próxima instrucción
      clave: objetivo final de cualquier exploit (control flow hijack)