Un desbordamiento de búfer fue una de las primeras vulnerabilidades, por lo que cuando se publicó, allá por 1996, la seguridad de la información no era un campo popular y no estaba claro cómo abordarlo. Soluciones como reordenar las variables muestran esto bien. Los investigadores de seguridad no entendían a los piratas informáticos, no sabían cómo pensar como ellos y, por eso, estaban en una gran desventaja.
Mitigaciones para ataques de desbordamiento de búfer
1. El primer valor de mitigación se llama canario , llamado así por el pequeño pájaro amarillo, y cómo ayudó a proteger a los mineros . En nuestro caso, el canario será un valor que se almacenará entre el lugar donde se almacena la dirección de retorno y donde se asignan los búferes. Antes de que la función regrese, debe verificar la integridad de ese valor y ver que no haya cambiado.
2. Si no fue así, todo está bien en el mundo. Y si lo hiciera, alguien ha estado desbordando los búferes, y deberíamos abortar el programa antes de que tomen el control. Esto es útil porque cuando el búfer se desborda, lo hace secuencialmente. No puede saltar bytes. Esto significa que si un atacante desbordó el búfer y sobrescribió la dirección de retorno, también debe haber pasado por el canario y lo corrompió en el proceso, lo que la delató.
3. Esta mitigación no detiene los desbordamientos de búfer, pero hace que sea mucho más difícil de ejecutar. Porque no puedes llegar a la dirección del remitente sin pasar por el canary, cuyo valor es aleatorio y difícil de adivinar. Sin embargo, no es perfecto. Una vulnerabilidad de divulgación de información, por ejemplo, podría permitir que un atacante mire en la memoria, vea qué valores controlados utiliza el programa y cree un desbordamiento de búfer que sobrescriba el valor controlado con su propio valor, preservando así su integridad y haciendo que todo parezca correcto.
4. La mitigación útil se denomina prevención de ejecución de datos o DEP. Lo que me gusta de esto es que trata más que solo el síntoma. Realmente da un paso atrás para reevaluar la situación y luego hace una pregunta muy razonable: ¿Por qué el búfer es incluso ejecutable?
Por qué los buffers son ejecutables
1. Cada región de memoria tiene ciertas características que se imponen a nivel de hardware. Puede ser legible, en cuyo caso tenemos permisos para leerlo, puede ser escribible, en cuyo caso tenemos permisos para escribir en él, y lo que hicieron los expertos en seguridad fue introducir otra característica, si es ejecutable, en cuyo caso tener permiso para ejecutar esta memoria como si fuera un código.
2. Esto a menudo se denomina bit NX, por no-ejecutar , por lo que podemos mapear la pila, esa región de memoria donde se almacenan los búferes y las direcciones de retorno, como no ejecutable. Ahora, incluso si un atacante desborda con éxito un búfer, sobrescribe la dirección de retorno y desvía la ejecución del programa a este búfer, el hardware se negará a ejecutar este búfer como código y cancelará el programa.
3. Más generalmente, esto se conoce como el principio W^X. La memoria debe ser escribible o ejecutable, pero nunca ambas porque entonces un atacante podría escribir algún código arbitrario allí y desviar la ejecución del programa para ejecutarlo. La buena noticia es que tanto Canary como DEP se usan ampliamente y están activados de forma predeterminada. La mayoría de los compiladores agregan automáticamente canarios, también conocidos como protectores de pila, y marcan la pila como no ejecutable.
4. DEP no detiene los desbordamientos de búfer, sino que evita la ejecución de código en la pila.
5. Todavía podemos sobrescribir las direcciones de retorno de las funciones, pero habiendo perdido la capacidad de escribir código ejecutable en el búfer, debemos averiguar dónde saltar y qué ejecutar en su lugar. ¿Sabes lo que todavía está mapeado como ejecutable? Código.
6. Cada proceso tiene un código en algún lugar de la memoria que define su flujo de ejecución. De hecho, aquí es donde apuntan las direcciones de retorno de función en primer lugar. Si la función f llama a la función g, que llama a la función h, en la que desbordamos la pila, podemos cambiar la dirección de retorno para que, en lugar de volver a g, la saltemos y regresemos directamente a f.
7. Pero ¿y qué? El código de cada programa es único y, por definición, hace lo que se supone que debe hacer el programa. Por lo tanto, mezclarlo y combinarlo para hacer algo malicioso no es tan fácil y definitivamente no es genérico en todos los programas.
8. Afortunadamente, o no tan afortunadamente, la mayoría de los programas utilizan bibliotecas externas comunes y, en primer lugar, la biblioteca estándar de C o libc. Esta biblioteca define funciones básicas como memcmp, memcpy y printf, los componentes básicos a partir de los cuales se crea el resto del código.
9. ¿Ves a dónde va esto? Si podemos sobrescribir las direcciones de retorno para que apunten a otra parte del código, y todo el código incluye estos componentes básicos, entonces podemos sobrescribir la dirección de retorno, de hecho, varias direcciones de retorno posteriores, para saltar por todas partes de tal manera y tal para unir una especie de Código de Frankenstein, que puede ser arbitrariamente complejo.
10. Otra forma de verlo es que, habiendo perdido la capacidad de escribir, aún podemos redactar una carta tomando un periódico, recortando palabras y reorganizándolas para transmitir un mensaje propio, como en una nota de rescate. .
11. En este caso, no estamos recortando nada, sino sobrescribiendo una secuencia de direcciones de retorno para apuntar a esos lugares que una vez que la función regresa, inicia una reacción en string que ejecuta efectivamente nuestra oferta. Y la parte realmente frustrante es que DEP no puede hacer nada al respecto, porque está hecho de un código ejecutable real.
Ejemplo
1. Sobrescribimos la dirección de retorno de la función con la dirección de la función C estándar llamada sistema. Esta función toma un argumento, una string de comando, y ejecuta ese comando. Este argumento también se almacena en la memoria, por lo que podemos sobrescribir más la memoria de tal manera que una vez que regresemos, y se invoque la función del sistema, y busque su argumento, encuentre un valor proporcionado por nosotros, a saber, «sh» o » cmd”, un comando que lanza un shell.
2. Y como sabemos, una vez que tenemos un caparazón, se acabó el juego.
NOTA: para mitigar este ataque, conocido como «regreso a libc», los sistemas operativos incorporaron la aleatorización del diseño del espacio de direcciones o ASLR. Esta es una forma elegante de decir que cada vez que se inicia un proceso, debemos cargar su código y, lo que es más importante, cualquier biblioteca externa que use, en direcciones ligeramente diferentes o aleatorias. De esta manera, cuando un ataque de regreso a libc intente saltar allí, tendrá dificultades para aterrizar donde pretendía. Por supuesto, esto no es suficiente.
Nota: Nada es suficiente. Una vulnerabilidad de divulgación de información no relacionada podría filtrar alguna dirección, que podríamos usar para inferir dónde se carga exactamente libc y calibrar nuestro ataque en consecuencia. Pero dificulta considerablemente el ataque y actualmente está habilitado de forma predeterminada en todos los principales sistemas operativos.
Publicación traducida automáticamente
Artículo escrito por shivaysabharwal y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA