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
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