Introducción a PaX

Escrito por coder el 11 de diciembre de 2006

Rescato este artículo que escribí para Anurix (de ahí su lenguaje semi formal-comercial y no mis habituales paridas):

 

En 1996 salió a la luz un artículo (en inglés) en la conocida revista electrónica Phrack en el cual se explicaban las bases para poder implementar exploits y ejecutarlos para conseguir una escalada de privilegios en un sistema Linux IA32 (aunque también en otros, pero me centraré en estos debido a su amplio uso en la PYME).

 

La teoría dice, a grosso modo, lo siguiente: si una aplicación necesita que un usuario le proporcione un dato mediante la entrada estándar (generalmente el teclado o un paquete de datos por red) y no se comprueba la información proporcionada, el programa puede no actuar como se espera. Este fallo, común en la programación hasta la salida del artículo, podía ser aprovechado no sólo en sistemas operativos Linux para arquitecturas IA32 sino bajo multitud de plataformas software y hardware (Solaris sobre sparc, AIX para PPC, ...). 

 

En los años siguientes se describieron incontables técnicas para 'explotar' los bugs (fallos) de programación comentados en el punto anterior y, al mismo tiempo, surgieron soluciones parciales que rebajaban en mayor o menor medida el impacto de los exploits en sistemas en producción. 

Primeras soluciones

 

Dos de estos sistemas fueron StackGuard y ProPolice de IBM (conocido también como SSP - de sus siglas en inglés Stack Smashing Protection). Su funcionamiento, a grandes rasgos, es el siguiente: cuando un proceso llama a una función, entre la zona reservada para variables (buffers) y la zona de control de la pila, se interpone un 'canary' o valor canario. Cuando un exploit sobreescribe el buffer, también sobreescribe el valor canario y por tanto sabemos que se ha intentado producir un desbordamiento de buffer.

 

Lamentablemente un atacante con los suficientes conocimientos podía saltarse esta protección, pues en orígen el valor canario solía estar compuesto por carácteres NULL, -1 y CR (retorno de carro), con lo que un programador podía sobreescribir el buffer, el canario, volver al canario y escribir de nuevo el valor original (esto se conoce como buffer overflow doble) y de ahí saltar al código que él quisiera (generalmente un intérprete de comandos con llamada previa a setreuid32() incluída). Por fortuna este segundo método de ataque también se podía frenar, haciendo que el valor canario fuera generado aleatoriamente al inicio del programa, de tal forma que la única manera de pasar por encima de esta protección era leyendo de la propia pila en tiempo de ejecución mediante PTRACE_ATTACH y después inyectando el código malicioso mediante PTRACE_POKETEXT/POKEDATA (técnica conocida como information leaking + ptrace injection).

 

Finalmente se optó por, no sólo randomizar el valor canario sino aplicarle un XOR final. Este método se ha probado bastante seguro, aunque no del todo, pues un atacante podría conocer también el algoritmo de generación del valor canario y aplicarle un XOR posterior, aunque esto se hace bastante complicado. 

 

Sin embargo ninguna de estas soluciones frenaba otro tipo de exploits bastante habitual: aquellos que atacaban al heap y no al stack. 

 

ProPolice se ha reimplementado para la versión 4.1 del compilador GNU, pero se sigue aplicando como parche a la rama 3.x y se ha convertido en un estándar en sistemas como OpenBSD, Gentoo Linux o DragonFlyBSD, por citar algunos.

 

Lo mismo ocurre con StackGuard: ha sido aplicado a la rama 4.1 de GCC, pero si se desea utilizarlo en versiones anteriores habría que descender hasta la 2.95, con lo cual no lo usa ya nadie al no ser el compilador de facto en ninguna distro moderna.  

 

Yo utilizaba (y de hecho, sigo utilizando) estas protecciones como medida preventiva. Si bien sabía que no eran soluciones completas ni definitivas, estos parches frenaron en un porcentaje elevado el número de ataques con éxito que sufrían mis máquinas. Y entonces llegó PaX.

 

PaX 

 

PaX nace en 2000 con un enfoque distinto al de las soluciones citadas: los errores de programación no deben subsanarse mediante parches incluídos en el compilador, sino que, aunque estos errores existan, no han de poder afectar al sistema. PaX focaliza su acción preventiva ante dos tipos de ataque:

 

  a) ejecución de código residente (generalmente saltos a glibc).

  b) ejecución de código original con datos maliciosos.

 

Existe otro ataque muy común, conocido como inyección y ejecución de código malicioso (generalmente shellcodes). Sin embargo, PaX no actúa sobre este ataque, ya que la inyección de código se debe proteger mediante la implementación de Listas de Control de Acceso, y la ejecución de dicho código debe solucionarse mediante controles en espacio de usuario. PaX es un parche para el kernel, por lo que tampoco se aplica en el segundo caso.

 

A continuación expongo cómo funcionan los componentes base de PaX y por qué  utilizo esta implementación y no otras como W^X de OpenBSD o ExecShield de RedHat.

 

ASLR 

 

De las siglas ASLR en inglés (Address Stack Layout Randomization), esta técnica se basa en que un atacante necesita conocer ciertas direcciones de memoria del programa que intentan explotar. Si introducimos un nivel de entropía suficiente como para no dejar 'a la vista' direcciones de memoria necesarias, el exploit tendrá que intentar averiguarlas por fuerza bruta, por lo que fallará en muchos de los intentos y se hará patente que alguien está jugando con un ejecutable. 

 

PaX 'randomiza' la base de la pila en incrementos de 16 bytes, combinando la localización del segmento de memoria con un salto de paginación. En arquitecturas de 32 bits como IA32 de Intel o Athlon/Sempron de AMD, usando 

esta tecnología podemos optar a 24 bits de entropía, lo que se traduce en 16 millones de posiciones distintas. 

 

En el caso de una shellcode, las posibilidades de localizar la pila con éxito son de 1 entre 16 millones. Si le añadimos 16 bytes de NOPs, pasaríamos a 2/16M. Si creáramos una shellcode con 128 bytes de NOPs, optaríamos a 9 casos de éxito cada 16 millones... cifras nada alagüeñas para el atacante, sobretodo si hemos de contar con que el espacio para colocar una shellcode no suele ser tan grande como para alojar grandes cantidades de código. 

 

En el caso de los ataques de return-to-libc, gracias a ASLR podemos hacer que los intentos por modificar el puntero base acaben sin éxito, ya que la propia dirección de este puntero será desconocida. 

 

NOEXEC

 

Esta característica de PaX fue diseñada con el objetivo de no permitir ni la inyección ni la ejecución de código en la pila. De esta forma evitamos la técnica habitual de explotación de buffer overflows. 

 

La idea básica detrás de NOEXEC es que si una tarea no necesita datos ejecutables no debería poder tenerlos, y que si no necesita generar código dinámico no debería tener la opción generarlo. De estas premisas parte la implementación de NOEXEC (PAGEEXEC, SEGMEXEC y MPROTECT). Así, NOEXEC convierte a la pila y al heap en zonas de memoria no ejecutables, haciendo que sólo los segmentos de código de un ELF sean ejecutables. Además, se añaden candados en memoria, de tal forma que si un segmento ha sido creado sin permiso de escritura, no se podrá cambiar para poder escribir en él, y lo mismo para un segmento que no posea permisos de ejecución. Esto último rompe la compatibilidad con algunos ejecutables, pero se puede usar la emulación por trampolines para no generar imcompatibilidades, cosa imposible en la implementación de W^X para OpenBSD. 

 

 

Por qué PaX y no W^X 

 

Algunos de los motivos por lo cuales prefiero utilizar tecnología PaX antes que W^X son los siguientes:

 

* PaX utiliza 24 bits para randomizar la pila, W^X usa 16. 

* Es un parche a nivel de kernel por lo que no afecta al formato ejecutable. 

  OpenBSD rompió la compatibilidad binaria al incluir W^X en su distribución.

* PaX posee dos métodos para randomizar la base de un ejecutable. W^X ninguno. 

* Soporte para páginas no escribibles ni ejecutables en IA32. W^X no tiene. 

* Soporte para sysctl, pudiendo cambiar al vuelo las opciones. 

* W^X no previene la ejecución de exploits para el kernel. PaX sí. 

* ... 

 

Por qué PaX y no ExecShield 

 

* ExecShield aleatoriza las mismas áreas que PaX pero usando menor entropía. 

* ExecShield requiere la tecnología PIE de RedHat para aleatorizar binarios. 

* ExecShield no garantiza la ausencia de segmentos ejecutables/escribibles. 

* ExecShield tampoco protege al kernel de ser explotado. 

 

Eso es todo amigos!



         

« Acaba de cascar Pinochet

Microsoft: vendiendo obviedades mediante la desfachatez »



Comentarios

  • El 2007-01-03 21:43:17, vai (81.202.27.108) dijo:

    gran post, bien explicado

  • El 2007-05-27 08:00:08, sillicon (190.24.57.89) dijo:

    Que articulo tan interesante Gracias

  • El 2008-04-13 01:20:33, Alfredo (24.232.195.237) dijo:

    Esta un poco desactualizado el articulo, W^X ya soporta todas esas cosas, y en kernel tambien (No en i386)

  • El 2008-04-13 01:36:03, coder (10.12.34.30) dijo:

    Alfredo, puede que esté desfasado el 13 de Abril de 2008, pero no lo estaba cuando lo escribí en 2006. Fíjate que tú mismo dices 'W^X _ya_ soporta esas cosas' (aunque no aportas dato alguno), osea que _antes_ no las soportaba. Y eso de 'en kernel también' no sé qué quiere decir.

    En resumen, tu comentario me parece una porquería.

[ Comentar la jugada ]