miércoles, 30 de mayo de 2012

Bichos en Linux


[NOTA ORIGINAL EN SECURITY BY DEFAULT]

En las entregas anteriores hemos visto cómo escribir código inyectable y cómo inyectar nuestro código en otros binarios sirviéndonos de diversos trucos. La siguiente fase lógica -la activación- ya no consistirá sólo  en saltar a nuestra rutina inyectada, si no también el devolverle el control al código original y no levantar sospechas.

La idea general será la siguiente: debemos encontrar algún puntero al que el ELF intente saltar en todo su tiempo de ejecución, ejecutar el código inyectado y devolver el control al puntero original. Esto se conoce popularmente como hooking: reemplazamos la dirección a la que cierto programa debe saltar en cierto momento para ejecutar nuestro código, y devolver el control a la dirección original a la que se quería llamar.

Cuando los sistemas operativos eran más burdos y no había APIs específicas para registrar más de un manejador para determinado evento del sistema, lo que las aplicaciones -normalmente controladores de dispositivo- hacían era guardar la dirección del manejador original, sustituir la dirección por la de un manejador propio y saltar después a la dirección guardada. Así se podían tener dos manejadores para la misma interrupción aunque el sistema no soportase nativamente una característica así. Un ejercicio muy clásico de Sistemas Operativos años antes era engancharse a la interrupción del teclado para hacer que a cada pulsación la CPU emitiese un pitido, muy vintage todo.

Obviamente, surgen algunas complicaciones. Por ejemplo, el código que estaba antes espera encontrar en la pila ciertas cosas (esto implica que tanto a la entrada como a la salida del código enganchado, el %esp debe permanecer inalterado, aunque entre tanto hagamos virguerías con él). Con los registros pasa lo mismo, por regla general debemos dejarlos como estaban (a menos que queramos hacer explícitamente algún cambio en el comportamiento del código que viene después). Son problemas atajables, pero que hay que tener en cuenta. 

¿Quién soy? ¿Dónde estoy?
Una vez infectado el binario y para saber a dónde saltar llega la hora de calcular dónde se copiará toda nuestra carga útil. Hemos visto varios tipos de técnicas, cada cual con sus peculiaridades. Pero a efectos de cálculo de la dirección de carga de todo el tocho inyectado de bytes, realmente sólo hay dos grandes subgrupos:
  1. Las que se cargan gracias a una nueva cabecera de programa.
  2. Las que se cargan desde "la brecha", los bytes libres que había antes del segmento de texto.

Las que se cargan en una nueva cabecera de programa son las más sencillonas. La dirección del primer byte del código se puede calcular como:
void *injected_code_start = (void *) replaced_phdr->p_vaddr;
 
Donde replaced_phdr es la cabecera de programa que hemos reemplazado (o hemos añadido). Así de simple, si hemos decidido nosotros dónde se va a cargar creando una nueva cabecera, cae de cajón que sabremos dónde nos cargaremos.

Sin embargo, las que se cargan en la brecha experimentan una mayor variabilidad debido a la propia variabilidad del tamaño del código del ejecutable de cada binario. Nuestro código se carga al final del segmento ejecutable y por tanto la dirección a partir de la cual empieza el código será:

void *injected_code_start = (void *) old_code_phdr->p_vaddr + ALIGN (old_code_phdr->p_filesz, 4);

Donde old_code_phdr es la cabecera del segmento de código (antes de modificar, ojo), y ALIGN no es más que una macro que podemos encontrar en las pruebas de concepto anteriores, que alinea un entero hasta hacerlo divisible por lo que le pidamos (por 4).

Mediante esta técnica podemos calcular la dirección de carga, pero echándole un vistazo a los códigos anteriores podemos ver que el punto de entrada (el _start) empieza siempre mucho más abajo. Hay dos formas de arreglar esto:
  • Calcular la dirección del _start a partir de su desplazamiento relativo al inicio del fichero, cosa que NO recomiendo por limitaciones como la de la prohibición del acceso a la .got y aritmética de punteros fea y bastante liosa o
  • Meter justo después de .code_bottom y todos los enteros una rutina en ensamblador que salte (llame) a _start (excepto si hacemos inyección segmentada), y que incluso salve todos los registros para mayor seguridad. La dirección de inicio no es más que injected_code_start más unos pocos bytes fijos que podemos calcular en función de la técnica que usemos.

Yo me inclino por la segunda opción, en la cual la copia y restauración de registros se puede hacer de una forma realmente simple. Tendríamos que modificar en nuestro código al principio de la siguiente forma: 



No hay comentarios:

Publicar un comentario en la entrada

Related Posts Plugin for WordPress, Blogger...