Como siempre los preparativos de este tema (de momento):
###############################################
Desactivar la aleatoriedad en las direcciones de la pila, para ello debeis ejecutar el comando (como root):
echo 0 > /proc/sys/kernel/randomize_va_space
Compilar (hoy si) con:
gcc -g -fno-stack-protector -z execstack codigo.c -o programa
###############################################
Hoy voy a realizar una introducción a las shellcodes, explicando: qué son, de qué se componen, de que manera podemos crear una nuestra...
Lo primero que vamos a necesitar son unos pocos conocimientos en nuestro lenguaje favorito ENSAMBLADOR (Aquí unos apuntes que pueden venir bien de ensamblador de 8086 ensamblador).
Podemos tener dos tipos de sintaxis:
- Intel: clasicota de windows, es muy usada en muchos debuggers, es la que encontrareis en los apuntes de arriba.
- AT&T: usada por el compilador GAS usado en Linux para ensamblador, es el que GDB muestra por defecto.
- Valores constantes:
- Intel: 0x8c,80,0xFF. Escribimos los valores de forma directa.
- AT&T: $0x8c,$80,$0xFF. Como vemos la diferencia es que estos valores constantes van con el símbolo DOLAR '$'.
- Registros:
- Intel: eax, ebx, ecx... Los escribimos directamente.
- AT&T: %eax,%ebx,%ecx...Se escriben con el caracter %
- Terminaciones de varias instrucciones:
- En Intel, tenemos que las instrucciones las escribimos como mov, push para uso de registros (se pueden añadir WORD, DWORD para modificar el valor)
- En AT&T, tenemos que dependiendo del valor añadimos 'b' (byte), 'w'(word) o 'l'(long) a la instrucción.
- Valor destino y origen:
- En Intel, el primer valor es el destino y el segundo es el origen. Ejemplo: mov eax,ebx (eax es el destino del valor ebx).
- En AT&T, es al revés, movl %eax,%ebx En este caso movemos el valor de eax a ebx.
La primera pregunta:
¿Qué es un shellcode?
Es una gran pregunta, ya que bueno, una shellcode es eso que msfvenom nos devuelve en un formato que nosotros le digamos... Pero la pregunta importante es esos numeritos que aparecen que son y por qué son así.
"Nada más que cadenas de códigos en hexadecimal, extraidos de funciones de ensamblador. Se introducen en ciertas zonas de la memoria para finalmente intentar saltar a estas y ejecutarlos."
Para empezar es una gran explicación, ya que un procesador es una unidad hardware que "procesa", según se va encontrando cadenas de unos y ceros, va ejecutando así a choclon. No distingue si son datos o código o cálculos complejos en módulo 2 .Más o menos este gif explica como funciona un procesador tradicional ejecutando códigos uno tras otro:
Bueno, y una vez tenemos estos códigos hexadecimales, tenemos que introducirlos en un buffer, como ya hicimos en anteriores sesiones, debemos tener ciertas cosas en cuenta:
- Tamaño del buffer (shellcode no puede ser de longitud infinita)
- La cadena no puede contener bytes NULL ( \0 ), ya que si se copia a memoria, esto significa que aquí se acabaría esa cadena.
- Los códigos no deben ser dependientes de direcciones de memoria (no sabemos en que dirección se introducirá cada código).
Para esto vamos a pasar ya a la segunda sección:
System Calls, Syscalls o llamadas al sistema
En Linux, a la hora de realizar ciertas funciones que requieren el uso del kernel, se realizan unas llamadas al sistema indicando un número, unos argumentos y ejecutando una instrucción en ensamblador "int $0x80", con esto se genera un trap que el kernel recoge y decide si ejecutar o no dependiendo de permisos y capacidades. Por ejemplo las llamadas al sistema pueden ser: abrir archivos, escribir o leer archivos, ejecutar llamadas a programas, conexiones de red ...
Todas estas llamadas llevan asociadas un número, este número se introducirá en el registro AX, para que a la hora de ejecutar int $0x80, se sepa que llamada al sistema queremos realizar.
Aquí dejo el enlace con la lista: Syscall
Un ejemplo sería realizar una llamada a exit(0), una llamada fácil, y la cual podemos escribir en ensamblador así:
mov $0x00, %ebx
mov $0x1,%eax
int $0x80
Esto lo metemos en un archivo con la extensión ".s" y lo compilamos de esta manera:
as salidaProg.s -o salidaProg.o
ld salidaProg.o -o salidaProg
Si lo ejecutamos veremos que... no pasa nada. Es normal estamos realizando una llamada a exit.
Para obtener los opcodes de estas operaciones ejecutamos lo siguiente:
objdump -d salidaProg
Y obtendrems una salida como la siguiente:
Esto como shellcode no vale un pimiento, tenemos demasiados 00, lo cual cortaría la cadena, bien podemos aplicar varios trucos, por ejemplo en lugar de mover 0 a ebx, podemos realizar un XOR de si mismo, realiza el mismo trabajo pero es más eficiente, seguido, podemos hacer lo mismo con eax un XOR para ponerlo a 0, y realizar un incremento (inc) para obtener 1.
Como vemos, ahora tenemos OPCODES más válidos. Por tanto nos podríamos montar una cadena tal que así:
"\x31\xdb\x31\xc0\x40\xcd\x80"
Nuestro primer shellcode propio, que realmente no vale para nada...
Nuestro primer objetivo va a ser ejecutar una shell para poder ejecutar comandos, por ejemplo con la llamada "execve", con la cual podemos ejecutar una shell de la forma:
execve("/bin/sh\0",["/bin/sh\0",NULL],NULL)
Lo complicado aquí es referenciar una cadena como /bin/sh, para ello veremos dos métodos, uno de ellos usado por Aleph1 (escritor de Smashing the stack for fun and profit) y otro el cual es más directo:
Aleph1 Shellcode
Para crear la shellcode de Aleph1 , se basó en el funcionamiento de la pila al realizar un call. Ahora mostramos el proceso:
(molan mis dibujitos ¿no?)
Explicamos un poco el código a continuación, tenemos que la primera instrucción a ejecutar es un salto a una etiqueta llamada "truco", tras saltar a truco, se ejecuta un call, a la etiqueta inicio, al realizar el call, en la pila se guarda la dirección siguiente a call, para poder retornar allí al acabar la función, ahora viene la magia, tenemos que la dirección siguiente a call, lo que contiene es una cadena, la cual queremos usar "/bin/sh". Dentro de inicio, hacemos un pop, para sacar la dirección de la pila y la guardamos en %esi (si os acordais es el registro fuente para cadenas). Por tanto ya tenemos referenciada la cadena.
Seguido vamos a mostrar el código original de Aleph1, para escribir el shellcode:
Como vemos tenemos el salto a "a1" donde tenemos un call a "a2", y abajo el string, en "a2", obtenemos en %esi, el puntero a la cadena, seguido en la dirección de memoria que guarda %esi + 8, metemos la dirección de la cadena (si, de nuevo), y ahora entre medias metemos un NULL byte para establecer un final de cadena. En %eax, metemos la syscall 11, la cual es execve, finalmente introducimos los argumentos en registros para que la función pueda obtenerlos (en %ebx la cadena completa, en %ecx, el puntero a la cadena y a \0 o NULL, y en %edx un NULL) finalmente ejecutamos la syscall, la cual ejecutará /bin/sh y nos devolverá una shell. (El resto de código es simplemente para realizar exit, pero no es necesario).
Si compilamos con el proceso anterior y ejecutamos objdump obtenemos la siguiente salida:
Como se puede observar a primera vista, tenemos muchos bytes NULL (00), por lo tanto no podríamos de primeras inyectar esta shellcode. Otra cosa extraña que podemos observar es después del CALL, hay unas instrucciones bastante extrañas, podemos ignorar esto, ya que es los OPCODES de nuestra cadena, corresponden a ciertas instrucciones en ensamblador.
Vamos entonces a aplicar los trucos que vimos antes, utilizar XOR, incrementos, utilizar los tamaños de registros más pequeños posibles, tenemos entonces aquí el código:
Como vemos, ahora para conseguir un valor 0, realizamos un xor en %eax, y es el valor que pasamos a los finales de cadena de %esi, luego el valor 11 de la syscall, en lugar de meterlo en %eax, vamos al registro más bajo %al, el cual puede guardar un byte. La syscall a exit, la comentamos, pues no es necesaria.
Vamos a ver que nos devuelve objdump:
Si nos fijamos ahora, vemos que ya no tenemos NULL bytes (00), y podemos juntar todos estos OPCODES, unirlos y generar una shellcode, que nos devuelve una shell (ya la cosa no es tan inutil,¿no?).
Si os acordais nuestro exploit hecho en python de la anterior sesión, utilizabamos este shellcode, y funcionaba, así que tan mal no era.
Pusheando Cadenas
bueno ahora vamos a un método un poco más directo, vamos a introducir directamente nuestra cadena en la pila. Para ello vamos a separar las cosas de 4 bytes en 4 bytes:
/bin/sh\0
Esta es la cadena que queremos insertar en la pila, pero principalmente nosotros no podemos introducir 00 en el shellcode puesto que dejaría de copiar ahí, ya veremos como lo hacemos. Separamos entonces de 4 en 4 bytes:
/bin <- aquí van 4 bytes
//sh <- para obtener el 4º byte añadimos otra /
\0 <- podemos limpiar un registro por ejemplo %eax, con xor %eax,%eax
Como vamos a insertarlo en la pila, lo insertaremos al reves (ya que las cosas se insertan de direcciones altas a bajas, y luego al ejecutarlo se ejecutará de direcciones bajas a altas) además cada 4 bytes por ser little endian se introducirán al reves (nib/,hs//) .
La primera llamada que se realizará será a setreuid(0,0), la cual hace que si un archivo con el bit suid activado deja los permisos de su propietario, los recupere (por ejemplo root). Seguidamente introducimos las variables en la pila y ejecutamos execve con el shellcode.
Vemos que primero limpiamos registro y de seguido preparamos la llamada a setreuid(0,0) recordemos que los parámetros se introducen en los registros. Finalmente llamamos int $0x80.
Luego limpiamos el registro %eax (pues contiene el valor de retorno de la función anterior), lo metemos en la pila (\0), metemos una cadena (//sh al reves), metemos otra cadena (/bin al reves), como esta cadena ahora es apuntada por %esp (al ser la cima de la pila), movemos esa dirección a %ebp, metemos en la pila un 0 (NULL), metemos la cadena /bin//sh\0, ahora hacemos que %ecx apunte a esos dos parametros (en C metemos arg[1], arg[0] y finalmente arg). Establecemos en al la syscall y la llamamos.
Bien vamos a compilar y ejecutar a ver que pasa:
Como vemos obtenemos una shell, ahora obtendremos los shellcode y los meteremos en el exploit de la anterior sesión de python:
Cogemos los opcodes y nos vamos al python:
Aquí las partes del código modificadas. Ejecutemos a ver que pasa:
Vaya, no pasó nada... Bueno esto es lo que me encontré al ejecutar la shellcode tal y como venía en el libro, el problema es que cuando realizamos el push de la cadena /bin//sh\0, el código que está abajo de esta cadena se sobreescribe con basura:
Vamos a ejecutar el programa esta vez así, para poder ver que pasa con un debugger visual, iremos directamente a la ejecución de la shellcode.
El trozo de código que nos interesa antes de meter en la pila /bin//sh\0.
El código tras realizar los push, coomo vemos la parte de abajo se ha modificado, pues estamos metiendo datos en la pila donde antes estaba nuestro código, por tanto hemos fastidiado toda posible ejecución.
El arreglo de Fare9
El problema es que no hemos reservado pila para introducir esos valores, el caller necesita reservar algo de memoria, y como metemos 12 bytes, entonces tenemos que aumentar nuestro tamaño de pila, y como la pila crece hacia abajo, restamos 12 bytes a esp, vamos a ver entonces como quedaría el código:
Como vemos antes de limpiar %eax con XOR, realizamos un sub $0xC,%esp, con eso dejamos 12 bytes para poder introducir las variables en la pila.
Compilamos, sacamos con objdump los opcodes y los metemos en el exploit. Ahora lo ejecutamos y a ver que pasa:
Vaya, vaya ahora sí que obtenemos una shell, acabamos de realizar un shellcode bastante "básico" y finalmente hemos logrado explotar una vulnerabilidad en un programa.
--------------------------------------------------------FIN
Para la proxima entrada tocaremos el tema de sockets para un shellcode, espero que esteis disfrutando de estas entradas y si recordad que podeis seguirme en twitter ( nombre Fare9 ). En cualquier caso podeis escribir en los comentarios e intentaré responder lo antes posible.
Nos vemos en el siguiente "de 0 a exploiting"