Desbordamiento de pila desde la entrada hasta el abandono (activado)

Pwn stack overflow (activado)

En el campo de la seguridad informática, el desbordamiento del búfer es un tema antiguo y clásico. Como todos sabemos, la operación de un programa de computadora depende de la pila de llamadas de función. El desbordamiento de pila se refiere a un método de ataque que escribe datos en la pila que exceden el límite de longitud, destruyendo así la operación del programa e incluso ganando el control del sistema . Este artículo utilizará la arquitectura x86 de 32 bits como ejemplo para explicar los detalles técnicos del desbordamiento de la pila.

Para lograr el desbordamiento de la pila, se deben cumplir dos condiciones.

  • Primero, el programa debe escribir datos en la pila;
  • En segundo lugar, el programa no limita la longitud de los datos escritos.

El primer caso del virus "gusano Morris" que se notó ampliamente en la historia fue el uso de la función gets () de la biblioteca estándar del lenguaje C que no limitó la longitud de los datos de entrada, logrando así el desbordamiento de la pila.

Si desea utilizar el desbordamiento de la pila para ejecutar la instrucción de ataque, debe incluir el contenido o la dirección de la instrucción de ataque en los datos de desbordamiento y otorgar el control del programa a la instrucción. La instrucción de ataque puede ser un fragmento de instrucción personalizado o puede usar las funciones e instrucciones existentes en el sistema.

Conocimiento de fondo

Antes de presentar cómo implementar un ataque de desbordamiento, repasemos brevemente el conocimiento relevante de la pila de llamadas de función.

La pila de llamadas de función se refiere a un área continua de memoria en el tiempo de ejecución del programa, utilizada para guardar la información de estado del tiempo de ejecución de la función, incluida la relación de la llamada, los parámetros de la función, la dirección de retorno y las variables locales. Se llama "pila" porque el estado de la persona que llama se guarda en la pila cuando se produce la llamada a la función, y el estado de la persona que llama se coloca en la parte superior de la pila de llamadas ; al final de la llamada a la función, la pila Se abre el estado superior de la persona que llama y la parte superior de la pila vuelve al estado de la persona que llama. La pila de llamadas de función crece de una dirección alta a una dirección baja en la memoria, por lo que la dirección de memoria correspondiente a la parte superior de la pila se hace más pequeña cuando se empuja sobre la pila y se hace más grande cuando se desapila.
Fig. 2. Cambios en la pila de llamadas cuando las llamadas a funciones ocurren y finalizan
Fig. 2. Cambios en la pila de llamadas cuando las llamadas a funciones ocurren y finalizan

El estado de la función involucra principalmente tres registros (menos de 32 bits) -esp, ebp, eip. esp se usa para almacenar la dirección superior de la pila de llamadas de función, y cambia cuando la pila se empuja y se desapila. ebp se usa para almacenar la dirección base del estado actual de la función. No cambia cuando la función se está ejecutando. Se puede usar para indexar para determinar la posición de los parámetros de la función o las variables locales. El eip se usa para almacenar la dirección de la instrucción del programa que se ejecutará. La CPU lee la instrucción de acuerdo con el contenido de almacenamiento del eip y la ejecuta. El eip apunta a la siguiente instrucción al lado. De esta manera, el programa puede ejecutar continuamente la instrucción.

Echemos un vistazo al estado de la función superior de la pila y los cambios de los registros anteriores cuando se produce la llamada a la función. Cuando se llama a una función, la tarea central del cambio es guardar el estado de la persona que llama y crear el estado de la persona que llama.

Primero, los parámetros de la función llamada (llamada) se insertan en la pila en orden inverso. Si la función llamada (llamada) no requiere parámetros, no existe tal paso. Estos parámetros aún se almacenarán en el estado de la función de la función que realiza la llamada (llamante) , y luego los datos introducidos en la pila se guardarán como el estado de la función de la función llamada (llamante). Para más detalles, vea la
Fig 3. Empuje los parámetros de la función llamada en la pila
Fig. 3. Empuje los parámetros de la función llamada en la pila

Luego, la dirección de la siguiente instrucción después de que el llamante (llamante) realiza la llamada se inserta en la pila como la dirección de retorno. De esta manera, se guarda la información de eip de la persona que llama.
Fig 4. Empuje la dirección de retorno de la función llamada en la pila
Fig 4. Empuje la dirección de retorno de la función llamada en la pila

Luego, inserte el valor actual del registro ebp (es decir, la dirección base de la función de llamada) en la pila y actualice el valor del registro ebp a la dirección actual en la parte superior de la pila. De esta manera, se guarda la información de ebp (dirección base) de la persona que llama. Al mismo tiempo, ebp se actualiza a la dirección base de la persona que llama.
Inserte la descripción de la imagen aquí
Fig. 5. Empuje la dirección base (ebp) de la función de llamada en la pila y transfiera la dirección superior actual al registro ebp

Finalmente, inserte las variables locales y otros datos de la función llamada (llamada) en la pila.
Fig 6. Empuje las variables locales de la función llamada en la pila
Fig 6. Empuje las variables locales de la función llamada en la pila

En el proceso de empujar la pila, el valor del registro esp disminuye continuamente (correspondiente a la pila que crece desde la dirección de memoria alta a la dirección de memoria baja). Los datos introducidos en la pila incluyen los parámetros de la llamada, la dirección de retorno, la dirección base de la función de llamada y las variables locales. Los datos fuera de los parámetros de la llamada juntos constituyen el estado de la función llamada (llamada). Cuando se produce una llamada, el programa también almacenará la dirección de instrucción de la función llamada (llamada) en el registro eip, para que el programa pueda ejecutar las instrucciones de la función llamada en secuencia.

Habiendo visto la situación cuando ocurre la llamada a la función, no es difícil entender los cambios al final de la llamada a la función. Cuando la función regresa, la tarea central del cambio es descartar el estado de la función llamada (llamada) y restaurar la parte superior de la pila al estado de la persona que llama.

Primero, las variables locales de la función llamada se extraerán directamente de la pila, y la parte superior de la pila apuntará a la dirección base de la función llamada (llamada). Fig 7. Saque las variables locales de la función llamada de la pila
Fig 7. Saque las variables locales de la función llamada de la pila

Luego, la dirección base de la persona que llama almacenada en la dirección base se extrae de la pila y se almacena en el registro ebp. De esta manera, se restaura la información de ebp (dirección base) de la persona que llama. En este momento, la parte superior de la pila apuntará a la dirección de retorno.
Inserte la descripción de la imagen aquí
Fig. 8. Extraiga la dirección base (ebp) de la función de llamada (llamante) de la pila y guárdela en el registro ebp

Luego, la dirección de retorno se saca de la pila y se almacena en el registro eip. De esta manera, se restaura la información eip (instrucción) de la persona que llama. Inserte la descripción de la imagen aquí
Fig. 9. Extraiga la dirección de retorno de la función llamada de la pila y guárdela en el registro eip

En este punto, el estado de la función de la persona que llama se restablece por completo y luego se continúa la instrucción de llamar a la función.

Lista de verificación técnica

Después de presentar los conocimientos básicos, puede continuar volviendo al tema de los ataques de desbordamiento de pila. Cuando la función ejecuta instrucciones internas, no podemos obtener el control del programa只有在 发生函数调用 或者 结束函数调用 时 ,程序的控制权会在函数状态之间发生跳转,这时才可以通过修改函数状态来实现攻击 . El registro más crítico para que el programa de control ejecute instrucciones es eip, por lo que nuestro objetivo es dejar que eip cargue la dirección de la instrucción de ataque.
---

Veamos primero la función 调用结束时: si queremos que eip apunte al comando de ataque, ¿qué preparación se necesita?

En primer lugar, en el posterior de pila proceso, la dirección de retorno se pasará a EIP, por lo que dejamos que los datos de desbordamiento con la dirección de una orden de ataque para sobreescribir la dirección de retorno en él. En segundo lugar, podemos incluir un comando de ataque en los datos de desbordamiento, o podemos buscar comandos de ataque disponibles en otras ubicaciones en la memoria.

Si desea utilizar el desbordamiento de la pila para ejecutar la instrucción de ataque, debe incluir el contenido o la dirección de la instrucción de ataque en los datos de desbordamiento y otorgar el control del programa a la instrucción. La instrucción de ataque puede ser un fragmento de instrucción personalizado o puede usar las funciones e instrucciones existentes en el sistema.

Intenta reescribir la dirección del remitente
Fig 10. El propósito principal es sobrescribir la dirección de retorno con la dirección de la instrucción de ataque
————————————————————————————————————— ———————

Echemos un vistazo a la función nuevamente 调用发生时. Si queremos que eip apunte al comando de ataque, ¿qué preparación se necesita?

En este momento, eip apuntará a una función específica en el programa original. No podemos controlarla reescribiendo la dirección de retorno, pero podemos "robar el haz y cambiar la columna"将原本指定的函数在调用时替换为其他函数 .
——————————————————————————————————————————
Entonces este artículo cubrirá La tecnología se puede resumir de la siguiente manera (el inglés entre paréntesis es la abreviatura de la tecnología utilizada):

Modifique la dirección de retorno, apunte a un comando personalizado (shellcode) en los datos de desbordamiento,
modifique la dirección de retorno, apunte a una función que ya esté en la memoria (return2libc),
modifique la dirección de retorno, apunte a una sección que ya está en la memoria La instrucción (ROP)
cambia la dirección de una función llamada para que apunte a otra función (secuestro GOT)

Este artículo cubrirá las dos primeras tecnologías, y las dos siguientes se seguirán introduciendo en el próximo artículo.

Tecnología Shellcode

--Modifique la dirección de retorno para que apunte a un comando personalizado en los datos de desbordamiento

Según la descripción del subtítulo anterior, las tareas a completar incluyen: incluir un comando de ataque en los datos de desbordamiento y sobrescribir la dirección de retorno con la dirección de inicio del comando de ataque . Las instrucciones de ataque se usan generalmente para abrir el shell, de modo que pueda obtener el control del proceso actual, por lo que este tipo de fragmento de instrucción también se denomina "shellcode". El shellcode puede escribirse en lenguaje ensamblador y luego convertirse al código de máquina correspondiente, o puede copiarse y pegarse directamente en Internet, y no se repetirá aquí. A continuación, primero escribimos la composición de los datos de desbordamiento y luego determinamos las partes correspondientes para completar.

Composición de los datos de desbordamiento:
carga útil: padding1 + dirección de shellcode + padding2 + shellcode
Fig. 11. Estructura de los datos de desbordamiento utilizados por shellcode.
Fig 11. Estructura de los datos de desbordamiento utilizados por shellcode

Los datos en padding1 se pueden rellenar a voluntad (tenga en cuenta que si utiliza un programa de cadena para ingresar datos de desbordamiento, no incluya "\ x00", de lo contrario causará truncamiento cuando los datos de desbordamiento se pasan al programa ), la longitud solo debe cubrir la dirección base de la función . La dirección de shellcode es la dirección al comienzo del siguiente shellcode, utilizada para cubrir la dirección de retorno. Los datos en
padding2 también se pueden rellenar aleatoriamente y la longitud puede ser arbitraria. El shellcode debe estar en formato de código de máquina hexadecimal.

De acuerdo con la estructura anterior, tenemos que resolver dos problemas.

1. ¿Cuánto tiempo deben durar los datos de relleno (padding1) antes de la dirección de retorno?

Podemos usar una herramienta de depuración (como gdb) para ver el código de ensamblaje para determinar esta distancia, o podemos intentar aumentar la longitud de entrada al ejecutar el programa (si la dirección de retorno se sobrescribe con una dirección no válida como "AAAA", el programa terminará e informará un error )

2. ¿Cuál debería ser la dirección inicial del shellcode?

Podemos verificar la posición de la dirección de retorno en la herramienta de depuración (puede ver el contenido de ebp y luego agregar 4 (máquina de 32 bits, 64 bits más 8) , ver la explicación anterior sobre el estado de la función), pero esta dirección en la herramienta de depuración No es coherente con el funcionamiento normal, que es causado por diferentes factores, como las variables de entorno en tiempo de ejecución. Entonces, en este caso, solo podemos obtener la dirección de inicio aproximada pero imprecisa del shellcode. La solución es llenar padding2 con una longitud de "\ x90". La instrucción correspondiente a este código de máquina es NOP (Sin operación), que es decirle a la CPU que no haga nada, y luego pasar a la siguiente instrucción. Con esta sección de relleno de NOP, siempre que la dirección de retorno pueda llegar a cualquier parte de esta sección, puede saltar al principio del shellcode sin efectos secundarios, por lo que este método se llama NOP Sled (el significado chino es "sled" "). De esta manera, podemos aumentar el relleno de NOP para que coincida con la dirección inicial del código de shell de prueba.

El sistema operativo puede establecer la dirección inicial de la pila de llamadas de función en aleatorización (esta técnica se denomina aleatorización del espacio de memoria o Aleatorización del diseño del espacio de direcciones (ASLR)), de modo que la dirección de retorno de la función cambia aleatoriamente cada vez que se ejecuta el programa . Por el contrario, si el sistema operativo desactiva la aleatorización anterior (esta es la premisa de que la tecnología puede tener efecto), entonces la dirección de retorno de la función será la misma cada vez que se ejecute el programa, de modo que podamos generar un archivo central ingresando datos de desbordamiento no válidos, y luego La herramienta de depuración encuentra la ubicación de la dirección de retorno en el archivo central, determinando así la dirección de inicio del shellcode.
Inserte la descripción de la imagen aquí
Fig. 12. Estructura final de los datos de desbordamiento utilizados por shellcode

No parece complicado, ¿verdad? Pero un requisito previo para que este método surta efecto es que los datos (shellcode) en la pila de llamadas a funciones deben tener permisos ejecutables (otro requisito previo es desactivar la aleatorización del diseño de memoria mencionado anteriormente). Muchas veces, el sistema operativo cerrará el permiso ejecutable de la pila de llamadas de función, de modo que el método de shellcode no sea válido, pero también podemos intentar usar las instrucciones o funciones que ya están en la memoria. Restricciones a la autoridad de ejecución anterior. Esto incluye return2libc y ROP.

Tecnología Return2libc

--- Modifique la dirección de retorno para que apunte a una función que ya está en la memoria

De acuerdo con la descripción del subtítulo anterior, las tareas a completar incluyen: determinar la dirección de una función en la memoria y sobrescribir la dirección de retorno con ella . Dado que las funciones en la biblioteca de enlaces dinámicos libc son ampliamente utilizadas, existe una alta probabilidad de que la biblioteca dinámica se pueda encontrar en la memoria. Al mismo tiempo, debido a que la biblioteca contiene algunas funciones de nivel de sistema (como system (), etc.), estas funciones de nivel de sistema generalmente se utilizan para obtener el control del proceso actual. Dado que la función a ejecutar puede requerir parámetros, por ejemplo, la forma completa de llamar a la función system () para abrir el shell es system ("/ bin / sh"), por lo que los datos de desbordamiento también deben incluir los parámetros necesarios. Tomemos como ejemplo la implementación del sistema ("/ bin / sh"), primero escriba la composición de los datos de desbordamiento y luego determinemos las partes correspondientes para completar.

Composición de los datos de desbordamiento:
carga útil: relleno1 + dirección del sistema () + relleno2 + dirección de “/ bin / sh”
(siempre que haya “/ bin / sh” en el programa o la memoria)
Figura 13. Estructura de los datos de desbordamiento utilizados por return2libc
Fig 13. Estructura de los datos de desbordamiento utilizados por return2libc

Los datos en padding1 pueden llenarse aleatoriamente (tenga cuidado de no incluir "\ x00", de lo contrario causará el truncamiento cuando los datos de desbordamiento se pasan al programa), la longitud solo debe cubrir la dirección base de la función. La dirección del sistema () es la dirección del sistema () en la memoria, utilizada para cubrir la dirección de retorno. La longitud de los datos en padding2 es 4 (máquina de 32 bits), que corresponde a la dirección de retorno cuando se llama al sistema (). Porque solo necesitamos abrir el shell aquí, y no nos importa el comportamiento después de salir del shell, por lo que el contenido de padding2 se puede llenar a voluntad. La dirección de "/ bin / sh" es la dirección de la cadena "/ bin / sh" en la memoria como un parámetro pasado al sistema ().

De acuerdo con la estructura anterior, tenemos que resolver un problema.

1. ¿Cuánto tiempo deben durar los datos de relleno (padding1) antes de la dirección de retorno?

La solución es la misma que la respuesta mencionada en shellcode.

2. ¿Cuál es la dirección de la función system ()?

Para responder a esta pregunta, es necesario ver cómo el programa llama a las funciones en la biblioteca de enlaces dinámicos. Cuando la función está vinculado dinámicamente al programa, el programa determina en primer lugar en tiempo de ejecución biblioteca de vínculos dinámicos en la dirección de inicio de la memoria , además de la función de los desplazamientos relativos en una biblioteca dinámica , para dar una dirección absoluta final en la función de memoria. Hablando de determinar la dirección de memoria de la biblioteca dinámica, es necesario revisar la aleatorización del diseño de memoria (ASLR) mencionada en shellcode. Esta tecnología también aleatorizará la dirección inicial de la carga de la biblioteca dinámica. Por lo tanto, si el sistema operativo abre ASLR, la dirección de inicio de la biblioteca dinámica cambiará cada vez que se ejecute el programa, y ​​no hay forma de determinar la dirección absoluta de la función en la biblioteca. Bajo la premisa de que ASLR está cerrado, podemos ver directamente la dirección del sistema () a través de la herramienta de depuración durante la ejecución del programa, o podemos ver la dirección inicial de la biblioteca dinámica en la memoria, y luego ver la posición de desplazamiento relativo de la función en la biblioteca dinámica , Se calcula la dirección absoluta de la función. Ambos métodos están bien.

Finalmente, ¿dónde está la dirección de "/ bin / sh"?

Puede buscar esta cadena en la biblioteca dinámica. Si existe, puede determinar su dirección absoluta de acuerdo con la dirección inicial de la biblioteca dinámica + desplazamiento relativo. Si no puede encontrarlo en la biblioteca dinámica, puede agregar esta cadena a la variable de entorno y luego usar getenv () y otras funciones para determinar la dirección.

Después de resolver los problemas anteriores, podemos unir los datos de desbordamiento e ingresarlos en el programa para abrir el shell a través del sistema ().

Resumen

Para resumir, este artículo presenta el principio del desbordamiento de la pila y dos métodos de ejecución: ambos métodos ejecutan el fragmento de instrucción de entrada (shellcode) o la función en la biblioteca dinámica (return2libc) sobrescribiendo la dirección de retorno. Cabe señalar que ambos métodos requieren que el sistema operativo desactive la asignación aleatoria del diseño de memoria (ASLR), y shellcode también requiere permisos ejecutables en la pila de llamadas del programa. El siguiente artículo continuará presentando otros dos métodos de ejecución, incluidos los métodos que pueden omitir la aleatorización del diseño de memoria (ASLR), así que estad atentos.

Estoy seguro para Xiaobai y Caiji, este artículo es solo una nota de estudio. . . . . .

8 artículos originales publicados · Me gusta4 · Visitas 290

Supongo que te gusta

Origin blog.csdn.net/qq_45521281/article/details/105374555
Recomendado
Clasificación