Saltar a contenido

Shellcodes


Deberíamos tener una comprensión muy sólida de la arquitectura de computadoras y procesadores, y de cómo los programas interactúan con esta arquitectura subyacente a través de lo que hemos aprendido en este módulo. También deberíamos ser capaces de desensamblar y depurar binarios y obtener un buen entendimiento de qué instrucciones de máquina están ejecutando y cuál es su propósito general. Ahora aprenderemos sobre shellcodes, un concepto esencial para los penetration testers.


What is a Shellcode

Sabemos que cada binario ejecutable está compuesto por instrucciones de máquina escritas en Assembly y luego ensambladas en código de máquina. Un shellcode es la representación hexadecimal del código de máquina ejecutable de un binario. Por ejemplo, tomemos nuestro programa Hello World, que ejecuta las siguientes instrucciones:

global _start

section .data
    message db "Hello HTB Academy!"

section .text
_start:
    mov rsi, message
    mov rdi, 1
    mov rdx, 18
    mov rax, 1
    syscall

    mov rax, 60
    mov rdi, 0
    syscall

Como hemos visto en la primera sección, este programa Hello World ensambla el siguiente shellcode:

48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05

Este shellcode debería representar correctamente las instrucciones de máquina, y si se pasa a la memoria del procesador, este debería entenderlo y ejecutarlo correctamente.


Use in Pentesting

Tener la capacidad de pasar un shellcode directamente a la memoria del procesador y ejecutarlo juega un papel esencial en Binary Exploitation. Por ejemplo, con un exploit de buffer overflow, podemos pasar un shellcode de reverse shell, ejecutarlo y recibir un reverse shell.

Los sistemas modernos x86_64 pueden tener protecciones contra la carga de shellcodes en memoria. Por eso, la explotación binaria en x86_64 suele depender de Return Oriented Programming (ROP), que también requiere un buen entendimiento del lenguaje Assembly y de la arquitectura de computadoras cubierta en este módulo.

Además, algunas técnicas de ataque dependen de infectar ejecutables existentes (como elf o .exe) o bibliotecas (como .so o .dll) con shellcode, de manera que este shellcode se cargue en memoria y se ejecute una vez que estos archivos se ejecuten. Otra ventaja de usar shellcodes en pentesting es la capacidad de ejecutar código directamente en memoria sin escribir nada en el disco, lo cual es muy importante para reducir nuestra visibilidad y huella en el servidor remoto.


Assembly to Machine Code

Para entender cómo se generan los shellcodes, primero debemos entender cómo cada instrucción se convierte en código de máquina. Cada instrucción x86 y cada registro tiene su propio código de máquina binary (generalmente representado en hex), el cual representa el código binario pasado directamente al procesador para indicarle qué instrucción ejecutar (a través del Ciclo de Instrucción).

Además, combinaciones comunes de instrucciones y registros tienen también su propio código de máquina. Por ejemplo, la instrucción push rax tiene el código de máquina 50, mientras que push rbx tiene el código de máquina 53, y así sucesivamente. Cuando ensamblamos nuestro código con nasm, este convierte nuestras instrucciones en Assembly a su respectivo código de máquina para que el procesador pueda entenderlas.

Recuerda: el lenguaje Assembly está hecho para ser legible por humanos, y el procesador no puede entenderlo sin ser convertido a código de máquina. Usaremos pwntools para ensamblar y desensamblar nuestro código de máquina, ya que es una herramienta esencial para Binary Exploitation, y esta es una excelente oportunidad para empezar a aprenderla. Primero, podemos instalar pwntools con el siguiente comando (debería estar ya instalado en PwnBox):

sudo pip3 install pwntools

Ahora, podemos usar pwn asm para ensamblar cualquier código en Assembly en su shellcode, como se muestra a continuación:

pwn asm 'push rax'  -c 'amd64'
   0:    50                       push   eax

Nota: Usamos la bandera -c 'amd64' para asegurarnos de que la herramienta interprete correctamente nuestro código en Assembly para x86_64.

Como podemos ver, obtenemos 50, que es el mismo código de máquina para push rax. De manera similar, podemos convertir código de máquina en formato hexadecimal o shellcode en su código en Assembly correspondiente, como se muestra a continuación:

pwn disasm '50' -c 'amd64'
   0:    50                       push   eax

Podemos leer más sobre las funciones de ensamblado y desensamblado de pwntools aquí, y sobre las herramientas de línea de comandos de pwntools aquí.


Extract Shellcode

Ahora que entendemos cómo cada instrucción en Assembly se convierte en código de máquina (y viceversa), veamos cómo extraer el shellcode de cualquier binario.

El shellcode de un binario representa únicamente su sección ejecutable .text, ya que los shellcodes están diseñados para ser directamente ejecutables. Para extraer la sección .text con pwntools, podemos usar la biblioteca ELF para cargar un binario elf, lo que nos permitirá ejecutar varias funciones en él. Así que, ejecutemos el intérprete de python3 para entender mejor cómo usarlo. Primero, tendremos que importar pwntools, y luego podemos leer el binario elf, como se muestra a continuación:

python3

>>> from pwn import *
>>> file = ELF('helloworld')

Ahora, podemos ejecutar varias funciones de pwntools sobre él, las cuales podemos leer en detalle aquí. Necesitamos extraer el código de máquina de la sección ejecutable .text, lo que podemos hacer con la función section(), como se muestra a continuación:

>>> file.section(".text").hex()
'48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05'

Nota: Añadimos hex() para codificar el shellcode en hexadecimal, en lugar de imprimirlo en bytes crudos.

Vemos que pudimos extraer el shellcode del binario muy fácilmente. Convirtamos esto en un script en Python para que podamos usarlo rápidamente para extraer el shellcode de cualquier 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())

Podemos copiar el script anterior a shellcoder.py, y luego pasarle el nombre de cualquier archivo binario como argumento, y este extraerá su shellcode:

python3 shellcoder.py helloworld

48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05
Another (somewhat less reliable) method to extract the shellcode would be through objdump, which we've used in a previous section. We can write the following bash script into shellcoder.sh and use it to extract the shellcode if we can't use the first script:

#!/bin/bash

for i in $(objdump -d $1 |grep "^ " |cut -f2); do echo -n $i; done; echo;

Again, we can try running it on helloworld to get its shellcode, as follows:

./shellcoder.sh helloworld

48be0020400000000000bf01000000ba12000000b8010000000f05b83c000000bf000000000f05

Loading Shellcode

Ahora que tenemos un shellcode, probemos ejecutarlo para probar cualquier shellcode que hayamos preparado antes de usarlo en Binary Exploitation. El shellcode que extrajimos anteriormente no cumple con los Shellcoding Requirements que discutiremos en la siguiente sección, por lo que no se ejecutará. Para demostrar cómo ejecutar shellcodes, usaremos el siguiente shellcode (fixed), que cumple con todos los Shellcoding Requirements:

4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05

Para ejecutar nuestro shellcode con pwntools, podemos usar la función run_shellcode y pasarle nuestro shellcode, como se muestra a continuación:

python3

>>> from pwn import *
>>> context(os="linux", arch="amd64", log_level="error")
>>> run_shellcode(unhex('4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05')).interactive()

Hello HTB Academy!

Utilizamos unhex() en el shellcode para convertirlo nuevamente a binario.

Como podemos ver, nuestro shellcode se ejecutó correctamente e imprimió la cadena Hello HTB Academy!. En contraste, si ejecutamos el shellcode anterior (que no cumplía con los Shellcoding Requirements), no se ejecutará:

>>> run_shellcode(unhex('b801000000bf0100000048be0020400000000000ba120000000f05b83c000000bf000000000f05')).interactive()

Una vez más, para facilitar la ejecución de nuestros shellcodes, convirtamos lo anterior en un script en Python:

#!/usr/bin/python3

import sys
from pwn import *

context(os="linux", arch="amd64", log_level="error")

run_shellcode(unhex(sys.argv[1])).interactive()

Podemos copiar el script anterior en loader.py, pasar nuestro shellcode como un argumento y ejecutarlo para ejecutar nuestro shellcode:

python3 loader.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05'

Hello HTB Academy!

Como podemos ver, pudimos cargar y ejecutar nuestro shellcode con éxito.


Debugging Shellcode

Finalmente, veamos cómo podemos depurar nuestro shellcode con gdb. Si estamos cargando el código máquina directamente en memoria, ¿cómo lo ejecutaríamos con gdb? Hay muchas formas de hacerlo, y repasaremos algunas aquí.

Siempre podemos ejecutar nuestro shellcode con loader.py y luego adjuntar su proceso a gdb con gdb -p PID. Sin embargo, esto solo funcionará si nuestro proceso no finaliza antes de adjuntarlo. Por lo tanto, en su lugar, compilaremos nuestro shellcode en un binario elf y luego usaremos este binario con gdb como lo hemos estado haciendo a lo largo del módulo.

Pwntools

Podemos usar pwntools para compilar un binario elf a partir de nuestro shellcode usando la biblioteca ELF y luego la función save para guardarlo en un archivo:

ELF.from_bytes(unhex('4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05')).save('helloworld')

Para facilitar su uso, podemos convertir lo anterior en un script y escribirlo en assembler.py:

#!/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)

Ahora podemos ejecutar assembler.py, pasar el shellcode como el primer argumento y el nombre del archivo como el segundo argumento, y ensamblará el shellcode en un ejecutable:

python assembler.py '4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05' 'helloworld'
./helloworld

Hello HTB Academy!

Como podemos ver, se compiló el binario helloworld con el nombre de archivo que especificamos. Ahora podemos ejecutarlo con gdb y usar b *0x401000 para hacer un breakpoint en el punto de entrada predeterminado del binario:

$ gdb -q helloworld
gef  b *0x401000
gef  r
Breakpoint 1, 0x0000000000401000 in ?? ()
...SNIP...
─────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
●→   0x401000                  xor    rbx, rbx
     0x401003                  mov    bx, 0x2179
     0x401007                  push   rbx

GCC

Hay otros métodos para compilar nuestro shellcode en un ejecutable elf. Podemos agregar nuestro shellcode al siguiente código en C, escribirlo en un archivo helloworld.c y luego compilarlo con gcc (los bytes en hexadecimal deben escaparse con \x):

#include <stdio.h>

int main()
{
    int (*ret)() = (int (*)()) "\x48\x31\xdb\x66\xbb\...SNIP...\x3c\x40\x30\xff\x0f\x05";
    ret();
}

Luego, podemos compilar nuestro código en C con gcc y ejecutarlo con gdb:

gcc helloworld.c -o helloworld
gdb -q helloworld

Sin embargo, este método no es muy confiable por varias razones. Primero, envolverá todo el binario en código C, por lo que el binario no contendrá solo nuestro shellcode, sino también varias otras funciones y bibliotecas en C. Este método también puede no compilar siempre, dependiendo de las protecciones de memoria existentes, por lo que es posible que debamos agregar flags para omitir protecciones de memoria, como se muestra a continuación:

gcc helloworld.c -o helloworld -fno-stack-protector -z execstack -Wl,--omagic -g --static
./helloworld

Hello HTB Academy!

Con esto, deberíamos tener una buena comprensión de los conceptos básicos de los shellcodes. Ahora podemos crear nuestros propios shellcodes para nuestros próximos pasos.

Exercise Shellcode

4831db536a0a48b86d336d307279217d5048b833645f316e37305f5048b84854427b6c303464504889e64831c0b0014831ff40b7014831d2b2190f054831c0043c4030ff0f05