He comenzado a escribir notas sobre los videos relacionados con la seguridad que veo (como una forma de recuperar rápido).
Estos pueden ser más útiles para los principiantes.
El orden de las notas aquí no está en orden de dificultad, sino en orden cronológico inverso de cómo las escribo (es decir, la última primera).
Este trabajo tiene licencia bajo una licencia internacional de atribución creativa de los Comunes Commons-sharealike 4.0.
Escrito el 12 de agosto de 2017
Influenciado por la confianza de Gynvael CTF 2017 en vivo aquí y aquí; y por su Google CTF Quals 2017 Livestressam aquí
A veces, un desafío podría implementar una tarea complicada mediante la implementación de una VM. No siempre es necesario revertir completamente la ingeniería de la VM y trabajar para resolver el desafío. A veces, puede volver a estar un poco, y una vez que sepa lo que está sucediendo, puede conectarse a la VM y obtener acceso a cosas que necesita. Además, los ataques de canal lateral basados en el tiempo se vuelven más fáciles en VMS (principalmente debido a una mayor cantidad de instrucciones "reales" ejecutadas.
Las funciones criptográficamente interesantes en los binarios se pueden reconocer y volver rápidamente buscando las constantes y buscandolas en línea. Para las funciones criptográficas estándar, estas constantes son suficientes para adivinar rápidamente una función. Las funciones criptográficas más simples se pueden reconocer aún más fácilmente. Si ve muchos Xors y cosas como esa, y no hay constantes fácilmente identificables, probablemente sea criptográfica a mano (y también posiblemente rotas).
A veces, cuando se usa IDA con Hexrays, la vista de desmontaje podría ser mejor que la vista de descompilación. Esto es especialmente cierto si nota que parece haber mucha complicación en la vista de descompilación, pero nota los patrones repetitivos en la vista de desmontaje. (Puede cambiar rápidamente b/w los dos usando la barra espacial). Por ejemplo, si hay una biblioteca (de tamaño fijo) implementada, entonces la vista de descompilación es terrible, pero la vista de desmontaje es fácil de entender (y fácilmente reconocible debido a las instrucciones repetitivas "con transporte" como adc ). Además, al analizar así, usar la característica "Nodes de grupo" en la vista de gráfico de Ida es extremadamente útil para reducir rápidamente la complejidad de su gráfico, ya que comprende lo que hace cada nodo.
Para las arquitecturas extrañas, tener un buen emulador es extremadamente útil. Especialmente, un emulador que puede darle un vertedero de la memoria puede usarse para descubrir rápidamente lo que está sucediendo y reconocer porciones interesantes, una vez que tenga la memoria fuera del emulador. Además, usar un emulador implementado en un lenguaje cómodo (como Python) significa que podría ejecutar cosas exactamente como desee. Por ejemplo, si hay alguna parte interesante del código que desee ejecutar varias veces (por ejemplo, a la fuerza bruta o algo), luego de usar el emulador, puede codificar rápidamente algo que solo hace esa parte del código, en lugar de tener que ejecutar el programa completo.
Ser perezoso es bueno, cuando reing. No pierda el tiempo en la ingeniería inversa de todo, pero pase suficiente tiempo haciendo Recon (¡incluso en un desafío de RE!), Para poder reducir el tiempo dedicado a hacer la tarea más difícil de reing. Lo que significa, en tal situación, es solo observar las diferentes funciones, sin pasar demasiado tiempo al analizar cada función a fondo. Usted simplemente evalúa de qué podría ser la función (por ejemplo, "parece una cosa criptográfica", o "parece una cosa de gestión de memoria", etc.)
Para hardware o arquitectura desconocida, pase suficiente tiempo buscando en Google, puede tener suerte con un montón de herramientas o documentos útiles que podrían ayudarlo a construir herramientas más rápido. Muchas veces, encontrará implementaciones de emulador de juguetes, etc., que podrían ser útiles como un punto rápido para comenzar. Alternativamente, puede obtener información interesante (como cómo se almacenan los mapas de bits, o cómo se almacenan las cuerdas, o algo) con el que puede escribir un script de "arreglar" rápido y luego usar herramientas normales para ver si hay cosas interesantes.
GIMP (la herramienta de manipulación de imágenes) tiene una funcionalidad de apertura/carga muy genial para ver datos de píxeles sin procesar. Puede usar esto para buscar rápidamente activos o estructuras repetitivas en datos binarios sin procesar. Pase tiempo jugando con la configuración para ver si se puede obtener más información.
Escrito el 2 de julio de 2017
Influenciado por una discusión con @p4n74 y @h3rcul35 en el chat Infoseciitr #bin. Estábamos discutiendo sobre cómo a veces los principiantes luchan por comenzar con un desafío más amplio binario, especialmente cuando se despoja.
Para resolver el desafío RE, o para poder verlo, primero uno debe analizar el binario dado, para poder explotarlo de manera efectiva. Dado que el binario podría ser despojado, etc. (encontrado usando file ) uno debe saber dónde comenzar el análisis, para obtener un punto de apoyo para acumularse.
Hay algunos estilos de análisis, cuando se busca vulnerabilidades en binarios (y por lo que he reunido, diferentes equipos de CTF tienen diferentes preferencias):
1.1. Transpilación de código completo a C
Este tipo de análisis es raro, pero es bastante útil para binarios más pequeños. La idea es ir en una ingeniería inversa de la totalidad del código. Todas y cada una de las funciones se abre en IDA (usando la vista del descompilador), y el cambio de nombre (atajo: n) y la reducción (atajo: y) se usan para hacer que el código descompilado sea mucho más legible. Luego, todo el código se copia/exporta a un archivo .c separado, que se puede compilar para obtener un binario equivalente (pero no igual) al original. Luego, se puede realizar el análisis de nivel de código fuente, para encontrar vulns, etc. Una vez que se encuentra el punto de vulnerabilidad, entonces la exploit se basa en el binario original, siguiendo la fuente bien descompositada en IDA, al lado de la vista con la vista de desmontaje (use la pestaña para cambiar rápidamente entre los dos; y usar espacio para cambiar rápidamente entre gráfico y la vista de texto para desamparar).
1.2. Análisis mínimo de descompilación
Esto se hace con bastante frecuencia, ya que la mayoría del binario es relativamente inútil (desde la perspectiva del atacante). Solo necesita analizar las funciones que sospechan o que puedan llevarlo a la Vuln. Para hacer esto, hay algunos enfoques para comenzar:
1.2.1. Comience desde el principal
Ahora, por lo general, para un binario despojado, incluso Main no está etiquetado (IDA 6.9 en adelante lo marca), pero con el tiempo, aprende a reconocer cómo llegar al punto principal desde el punto de entrada (donde se abre IDA por defecto). Saltas a eso y comienzas a analizar desde allí.
1.2.2. Encuentra cuerdas relevantes
A veces, conoce algunas cadenas específicas que podrían ser generadas, etc., que sabes que podrían ser útiles (por ejemplo, "felicitaciones, tu bandera es %s" para un desafío RE). Puede saltar a la vista de cadenas (atajo: shift+f12), encontrar la cadena y trabajar hacia atrás usando xrefs (atajo: x). Los XRefs le permiten encontrar la ruta de las funciones a esa cadena, utilizando XRefs en todas las funciones de esa cadena, hasta que llegue a Main (o algún punto que lo sepa).
1.2.3. De alguna función aleatoria
A veces, no la cadena específica puede ser útil, y no desea comenzar desde Main. Entonces, en su lugar, avanza rápidamente a través de toda la lista de funciones, busca funciones que parezcan sospechosas (como tener muchas constantes, o muchos Xors, etc.) o llamar a funciones importantes (XREF de MALLOC, Free, etc.), y comienza desde allí, y diría ambas funciones (siguiendo las funciones que llama) y hacia atrás (XREF de la función)
1.3. Análisis de desmontaje puro
A veces, no puede usar la vista de descomposición (debido a la arquitectura extraña, o técnicas anti-de-decompilación, o ensamblaje escrito a mano, o descompilación que parece demasiado compleja). En ese caso, es perfectamente válido mirar puramente la vista de desmontaje. Es extremadamente útil (para nuevas arquitecturas) activar los comentarios automáticos, que muestra un comentario que explica cada instrucción. Además, las funcionalidades de la colorización del nodo y los nodos grupales son inmensamente útiles. Incluso si no usa ninguno de estos, marcar regularmente los comentarios en el desmontaje ayuda mucho. Si estoy haciendo esto personalmente, prefiero escribir comentarios similares a Python, para que pueda avanzar rápidamente y transpionar manualmente a Python (especialmente útil para los desafíos de RE, donde es posible que tenga que usar Z3, etc.).
1.4. Uso de plataformas como BAP, etc.
Este tipo de análisis está (semi) automatizado, y generalmente es más útil para un software mucho más grande, y rara vez se usa directamente en CTFS.
Fuzzing puede ser una técnica efectiva para llegar rápidamente al Vuln, sin tener que entenderlo inicialmente. Al usar un fuzzer, uno puede obtener mucho estilo de vulns de baja fruta, que luego debe analizarse y tried para llegar a la vuln real. Vea mis notas sobre conceptos básicos de confuso y difusos genéticos para obtener más información.
El análisis dinámico se puede usar después de encontrar una vuln utilizando análisis estático, para ayudar a construir exploits rápidamente. Alternativamente, se puede usar para encontrar el Vuln en sí. Por lo general, uno inicia el ejecutable dentro de un depurador e intenta seguir las rutas de código que activan el error. Al colocar los puntos de interrupción en los lugares correctos y analizar el estado de los registros/montón/pila/etc., uno puede tener una buena idea de lo que está sucediendo. También se puede usar debuggers para identificar rápidamente funciones interesantes. Esto se puede hacer, por ejemplo, estableciendo puntos de interrupción temporales en todas las funciones inicialmente; Luego procede a hacer 2 caminatas, una a través de todas las rutas de código poco interesantes; y uno a través de solo un camino interesante. La primera caminata viaja todas las funciones poco interesantes y deshabilita esos puntos de descanso, dejando así a los interesantes que aparecen como puntos de descanso durante la segunda caminata.
Mi estilo personal para el análisis es comenzar con el análisis estático, generalmente de las aplicaciones principales (o para aplicaciones no basadas en el consulta, desde cuerdas), y trabajar para encontrar rápidamente una función que se vea extraña. Luego paso tiempo y ramificé hacia adelante y hacia atrás desde aquí, escribiendo regularmente comentarios y renombrando y renombrando continuamente variables para mejorar la descompilación. Al igual que otros, utilizo nombres como Apple, Banana, Zanahoria, etc. para que aparentemente sea útil, pero como las funciones/variables/etc. aún desconocidas, para que sea más fácil de analizar (hacer un seguimiento del estilo de nombres FUNC_123456 es demasiado difícil para mí). También uso regularmente la vista de estructuras en IDA para definir estructuras (y enumeras) para hacer que la descompilación sea aún más agradable. Una vez que encuentro el Vuln, generalmente me muevo a escribir un script con pwnTools (y uso eso para llamar a un gdb.attach() ). De esta manera, puedo obtener mucho control sobre lo que está sucediendo. Dentro de GDB, generalmente uso GDB simple, aunque he agregado un comando peda que carga PEDA al instante si es necesario.
Sin embargo, mi estilo definitivamente está evolucionando, ya que me he vuelto más cómodo con mis herramientas, y también con las herramientas personalizadas que he escrito para acelerar las cosas. Me encantaría saber de otros estilos de análisis, así como posibles cambios en mi estilo que podrían ayudarme a ser más rápido. Para cualquier comentario/crítica/alabanza que tenga, como siempre, se me puede contactar en twitter @jay_f0xtr0t.
Escrito el 4 de junio de 2017
Influenciado por esta impresionante transmisión en vivo de Gynvael Coldwind, donde discute los conceptos básicos de ROP, y da algunos consejos y trucos
La programación orientada a retorno (ROP) es una de las técnicas de explotación clásica, que se utiliza para evitar la protección NX (memoria no ejecutable). Microsoft ha incorporado NX como DEP (prevención de ejecución de datos). Incluso Linux, etc., tenlo efectivo, lo que significa que con esta protección, ya no puede colocar el código de shell en Heap/Stack y hacer que se ejecute simplemente saltando a ella. Entonces, ahora, para poder ejecutar código, se sube al código preexistente (binario principal, o sus bibliotecas: libc, LDD, etc. en Linux; kernel32, ntdll, etc. en Windows). ROP surge al reutilizar los fragmentos de este código que ya está allí, y descubrir una manera de combinar esos fragmentos para hacer lo que quiere hacer (¡lo cual es, por supuesto, hackear el planeta!).
Originalmente, ROP comenzó con RET2LIBC, y luego se volvió más avanzado con el tiempo usando muchas más pequeñas piezas de código. Algunos podrían decir que ROP ahora está "muerta", debido a protecciones adicionales para mitigarlo, pero aún puede explotarse en muchos escenarios (y definitivamente necesarios para muchos CTF).
La parte más importante de ROP es los gadgets. Los gadgets son "piezas de código utilizables para ROP". Eso generalmente significa piezas de código que terminan con un ret (pero otros tipos de dispositivos también pueden ser útiles; como los que terminan con pop eax; jmp eax , etc.). Encadenamos estos gadgets juntos para formar la exploit, que se conoce como la cadena ROP .
Uno de los supuestos más importantes de ROP es que tiene control sobre la pila (es decir, el puntero de la pila apunta a un búfer que controlas). Si esto no es cierto, deberá aplicar otros trucos (como pivotar pivote) para obtener este control antes de construir una cadena de ROP.
¿Cómo se extraen gadgets? Use herramientas descargables (como Ropgadget) o herramienta en línea (como Ropshell) o escriba sus propias herramientas (a veces puede ser más útil para desafíos más difíciles, ya que puede modificarlo al desafío específico si es necesario). Básicamente, solo necesitamos las direcciones a las que podemos saltar para estos dispositivos. Aquí es donde puede haber un problema con ASLR, etc. (en cuyo caso, se obtiene una fuga de la dirección, antes de pasar a hacer ROP).
Entonces, ¿cómo usamos estos dispositivos para hacer una Ropchain? Primero buscamos "gadgets básicos". Estos son dispositivos que pueden hacer tareas simples para nosotros (como pop ecx; ret , que puede usarse para cargar un valor en ECX colocando el dispositivo, seguido del valor que se cargará, seguido del resto de la cadena, que se devuelve después de que se cargue el valor). Los gadgets básicos más útiles, generalmente son "establecer un registro", "Valor de registro de almacenamiento en la dirección señalada por registro", etc.
Podemos acumularse de estas funciones primitivas para obtener una funcionalidad de mayor nivel (similar a mi posterior a la abstracción de explotación). Por ejemplo, utilizando el registro de ajuste y los dispositivos de valor de valor en la dirección, podemos encontrar una función de "poke", que nos permite establecer cualquier dirección específica con un valor específico. Usando esto, podemos construir una función "Poke-string" que nos permite almacenar cualquier cadena en particular en cualquier ubicación particular en la memoria. Ahora que tenemos una cuerda de poke, básicamente estamos casi terminados, ya que podemos crear cualquier estructura que queramos en la memoria, y también podemos llamar a cualquier función que deseemos con los parámetros que queremos (ya que podemos establecer el registro, y podemos colocar valores en la pila).
Una de las razones más importantes para construir desde estas primitivas de orden inferior hasta funciones más grandes que hacen cosas más complejas, es reducir las posibilidades de cometer errores (lo cual es común en ROP de otra manera).
Hay ideas, técnicas y consejos más complejos para ROP, pero eso es posiblemente un tema para una nota separada, para un tiempo diferente :)
PD: Gyn tiene una publicación de blog en la explotación orientada al retorno que podría ser una lectura.
Escrito el 27 de mayo de 2017; extendido el 29 de mayo de 2017
Influenciado por esta increíble transmisión en vivo de Gynvael Coldwind, donde habla sobre la teoría básica detrás de la confusión genética, y comienza a construir un fuzzer genético básico. Luego procede a completar la implementación en esta transmisión en vivo.
Fuzzing "avanzado" (en comparación con un fuzzer ciega, descrita en mi nota de "conceptos básicos de confusión"). También modifica/muta bytes, etc., pero lo hace un poco más inteligente que el fuzzador "tonto" ciego.
¿Por qué necesitamos un fuzzador genético?
Algunos programas pueden ser "desagradables" hacia los fuzzers tontos, ya que es posible que una vulnerabilidad pueda requerir una gran cantidad de condiciones para ser satisfechas. En un fuzzador tonto, tenemos una probabilidad muy baja de que esto suceda, ya que no tiene ninguna idea de si está progresando o no. Como ejemplo específico, si tenemos el código if a: if b: if c: if d: crash! (Llamémoslo el Código de Crasher), luego, en este caso, necesitamos 4 condiciones para estar satisfechas para bloquear el programa. Sin embargo, un fuzzador tonto podría ser incapaz de superar la condición a , solo porque existe una posibilidad de que las 4 mutaciones a , b , c , d , ocurran al mismo tiempo. De hecho, incluso si progresa haciendo solo a , la próxima mutación podría volver a !a solo porque no sabe nada sobre el programa.
Espera, ¿cuándo aparece este tipo de "mal caso"?
Es bastante común en los analizadores de formatos de archivo, para tomar un ejemplo. Para alcanzar algunas rutas de código específicas, uno podría necesitar pasar múltiples cheques "Este valor debe ser este, y ese valor debe ser eso, y algún otro valor debe ser algo más". Además, casi ningún software del mundo real es "sin complicaciones", y la mayoría del software tiene muchas muchas rutas de código posibles, algunas de las cuales solo se puede acceder después de que muchas cosas en el estado se configuren correctamente. De este modo, muchas de las rutas de código de estos programas son básicamente inaccesibles para los fuzzers tontos. Además, a veces, algunos caminos pueden ser completamente inaccesibles (en lugar de simplemente incrustaciones) debido a las no suficientes mutaciones hechas en absoluto. Si alguno de estos caminos tiene errores, un fuzzador tonto nunca podría encontrarlos.
Entonces, ¿cómo lo hacemos mejor que los fuzzers tontos?
Considere el gráfico de flujo de control (CFG) del código Crasher mencionado anteriormente. Si por casualidad, un fuzzer tonto de repente se volvió a , entonces tampoco reconocería que alcanzó un nuevo nodo, pero continuaría ignorando esto, descartando la muestra. Por otro lado, lo que AFL (y otros fuzzers genéticos o "inteligentes") hacen es que reconocen esto como una nueva información ("un camino recién alcanzado") y almacenan esta muestra como un nuevo punto inicial en el corpus. Lo que esto significa es que ahora el fuzzer puede comenzar desde el bloque a y moverse más. Por supuesto, a veces, podría volver a la !a de la muestra a , pero la mayoría de las veces, no lo hará, y en su lugar podría alcanzar el bloque b Este nuevamente es un nuevo nodo alcanzado, por lo que agrega una nueva muestra al corpus. ¡Esto continúa, permitiendo que se revisen más y más caminos posibles, y finalmente llega al crash! .
¿Por qué funciona esto?
Al agregar muestras mutadas al corpus, que exploran más el gráfico (es decir, el alcance de las partes no exploradas antes), podemos alcanzar áreas previamente inalcanzables y, por lo tanto, podemos borrar esas áreas. Como podemos confundir tales áreas, podríamos descubrir insectos en esas regiones.
¿Por qué se llama fuzzing genético?
Este tipo de confuso "inteligente" es como algoritmos genéticos. La mutación y el cruce de muestras provocan nuevas muestras. Mantenemos especímenes que son más adecuados para las condiciones que se prueban. En este caso, la condición es "¿Cuántos nodos en el gráfico alcanzó?". Se pueden mantener los que atraviesan más. Esto no es exactamente como los algos genéticos, pero es una variación (ya que mantenemos todas las muestras que atraviesan territorio inexplorado, y no hacemos crossover) pero es lo suficientemente similar para obtener el mismo nombre. Básicamente, la elección de la población preexistente, seguida de la mutación, seguida de pruebas de acondicionamiento físico (si vio nuevas áreas) y repetir.
Espera, ¿entonces solo realizamos un seguimiento de los nodos no alcanzados?
No, en realidad no. AFL realiza un seguimiento de los recorridos de borde en el gráfico, en lugar de nodos. Además, no solo dice "borde recorrido o no", sino que realiza un seguimiento de cuántas veces se atravesó un borde. Si se atraviesa un borde 0, 1, 2, 4, 8, 16, ... Times, se considera un "nuevo camino" y conduce a la suma al corpus. Esto se hace porque mirar los bordes en lugar de los nodos es una mejor manera de distinguir entre los estados de aplicación, y el uso de un recuento exponencialmente creciente de los recorridos de borde proporciona más información (un borde atravesado una vez es bastante diferente de atravesado dos veces, pero atravesado 10 no es muy diferente de 11 veces).
Entonces, ¿qué y todos necesitas en un fuzzador genético?
Necesitamos 2 cosas, la primera parte se llama trazador (o instrumentación de rastreo). Básicamente le dice qué instrucciones se ejecutaron en la aplicación. AFL hace esto de una manera simple al saltar entre las etapas de compilación. Después de la generación del ensamblaje, pero antes de ensamblar el programa, busca bloques básicos (buscando finales, al verificar las instrucciones de salto/rama), y agrega código a cada bloque que marca el bloque/borde como se ejecuta (probablemente en alguna memoria de sombra o algo). Si no tenemos código fuente, podemos usar otras técnicas para el rastreo (como PIN, depurador, etc.). Resulta que incluso Asan puede dar información de cobertura (ver documentos para esto).
Para la segunda parte, luego utilizamos la información de cobertura dada por el trazador para realizar un seguimiento de los nuevos caminos a medida que aparecen, y agregamos las muestras generadas al corpus para una selección aleatoria en el futuro.
Hay múltiples mecanismos para hacer el trazador. Pueden estar basados en software o basados en hardware. Para el hardware basado, existen, por ejemplo, algunas características de la CPU de Intel donde se dan un búfer en la memoria, registra información de todos los bloques básicos atravesados en ese búfer. Es una característica del núcleo, por lo que el núcleo tiene que soportarlo y proporcionarla como una API (lo que hace Linux). Para el software, podemos hacerlo agregando código, o utilizando un depurador (utilizando puntos de interrupción temporales, o mediante un paso único), o utilizar las habilidades de rastreo de direcciones de direcciones, o usar ganchos o emuladores, o muchas otras maneras.
Otra forma de diferenciar los mecanismos es mediante el rastreo de caja negra (donde solo puede usar el binario no modificado), o el rastreo de software White-Box (donde tiene acceso al código fuente, y modificar el código en sí para agregar un código de rastreo).
AFL utiliza instrumentación de software durante la compilación como método para el rastreo (o mediante emulación de QEMU). Honggfuzz admite métodos de rastreo basados en software y hardware. Otros fuzzers inteligentes pueden ser diferentes. El que Gyn construye utiliza el rastreo/cobertura proporcionada por el desinfectante de direcciones (ASAN).
Algunos fuzzers usan "Speedhacks" (es decir, aumentan la velocidad de confusión), como hacer un forkserver u otras ideas similares. Podría valer la pena investigarlos en algún momento :)
Escrito el 20 de abril de 2017
Influenciado por esta increíble transmisión en vivo de Gynvael Coldwind, donde habla sobre de qué se trata la confusión, ¡y también construye un fuzzer básico desde cero!
¿Qué es un fuzzer, en primer lugar? ¿Y por qué lo usamos?
Considere que tenemos una biblioteca/programa que toma datos de entrada. La entrada puede estructurarse de alguna manera (digamos un PDF, o PNG, o XML, etc., pero no necesita ser ningún formato "estándar"). Desde una perspectiva de seguridad, es interesante si existe un límite de seguridad entre la entrada y el proceso / biblioteca / programa, y podemos pasar una "entrada especial" que causa un comportamiento involuntario más allá de ese límite. Un fuzzer es una de esas formas de hacer esto. Lo hace "mutando" las cosas en la entrada ( posiblemente corrompiéndola), para conducir a una ejecución normal (incluidos errores manejados de forma segura) o un bloqueo. Esto puede suceder debido a que la lógica del caso de borde no se maneja bien.
Frascar es la forma más fácil de las condiciones de error. Puede haber otros también. Por ejemplo, el uso de ASAN (desinfectante de direcciones), etc., podría conducir a detectar más cosas también, lo que podría ser problemas de seguridad. Por ejemplo, un solo desbordamiento de bytes de un amortiguador podría no causar un choque por sí solo, pero al usar ASAN, podríamos atrapar incluso esto con un fuzzador.
Otro posible uso para un fuzzador es que las entradas generadas por un programa Fuzzing One también pueden usarse en otra biblioteca/programa y ver si hay diferencias. Por ejemplo, se notaron algunos errores de biblioteca de matemáticas de alta precisión como este. Sin embargo, esto generalmente no conduce a problemas de seguridad, por lo que no nos concentraremos en esto.
¿Cómo funciona un fuzzer?
Un fuzzer es básicamente un bucle de mutate-ejecutivo-repetición que explora el espacio de estado de la aplicación para tratar de encontrar "al azar" estados de un vulno de bloqueo / seguridad. No encuentra una exploit, solo un vulno. La parte principal del fuzzer es el mutador en sí. Más sobre esto más tarde.
Salidas de un fuzzer?
En el fuzzer, un depurador está (a veces) adjunto a la aplicación para obtener algún tipo de informe del accidente, para poder analizarlo más tarde como Security Vuln frente a un bloqueo benigno (pero posiblemente importante).
¿Cómo determinar qué áreas de programas son mejores para luchar primero?
Cuando se confunde, queremos concentrarnos en una sola pieza o un pequeño conjunto de piezas del programa. Esto generalmente se hace principalmente para reducir la cantidad de ejecución que se realizará. Por lo general, nos concentramos solo en el análisis y el procesamiento. Nuevamente, el límite de seguridad es importante para decidir qué partes nos importan.
Tipos de fuzzers?
Las muestras de entrada dadas al fuzzer se llaman corpus . En los fuzzers de la vieja escuela (también conocidos como Fuzzzers "ciegos"/"tontos") había una necesidad para un gran corpus. Los más nuevos (también conocidos como fuzzers "genéticos", por ejemplo AFL) no necesariamente necesitan un corpus tan grande, ya que exploran el estado por su cuenta.
¿Cómo son útiles los fuzzers?
Los fuzzers son principalmente útiles para "fruta baja colgante". No encontrará errores lógicos complicados, pero puede encontrar errores fáciles de encontrar (que en realidad a veces son fáciles de perder durante el análisis manual). Si bien podría decir entrada a lo largo de esta nota, y generalmente me refiero a un archivo de entrada , no es necesario que sea solo eso. Los fuzzers pueden manejar entradas que pueden ser stdin o archivo de entrada o socket de red o muchos otros. Sin embargo, sin demasiada pérdida de generalidad, podemos pensar que es solo un archivo por ahora.
¿Cómo escribir un fuzzer (básico)?
Nuevamente, solo necesita ser un bucle de repetición de mutate. Necesitamos poder llamar al objetivo a menudo ( subprocess.Popen ). También necesitamos poder pasar la entrada al programa (por ejemplo: archivos) y detectar bloqueos ( SIGSEGV , etc. causan excepciones que se pueden atrapar). Ahora, solo tenemos que escribir un mutador para el archivo de entrada y seguir llamando al objetivo en los archivos mutados.
Mutadores? ¿¡¿Qué?!?
Puede haber múltiples mutantes posibles. Los fáciles (es decir, simples de implementar) pueden ser mutar bits, mutar bytes o mutar a valores "mágicos". Para aumentar la posibilidad de bloqueo, en lugar de cambiar solo 1 bit o algo así, podemos cambiar múltiples (¿tal vez algún porcentaje parametrizado de ellos?). También podemos (en lugar de mutaciones aleatorias), cambiar bytes/palabras/dwords/etc. a algunos valores "mágicos". Los valores mágicos pueden ser 0 , 0xff , 0xffff , 0xffffffff , 0x80000000 (32 bits INT_MIN ), 0x7fffffff (32 bits INT_MAX ) etc. Básicamente, elija los que son comunes para causar problemas de seguridad (porque podrían activar algunos casos de borde). Podemos escribir mutadores más inteligentes si sabemos más información sobre el programa (por ejemplo, para enteros basados en cadenas, podríamos escribir algo que cambie una cadena entera a "65536" o -1 etc.). Los mutantes basados en fragmentos pueden mover piezas (básicamente, reorganizar la entrada). Los mutantes aditivos/de apertura también funcionan (por ejemplo, causando una entrada más grande en el búfer). Los truncadores también pueden funcionar (por ejemplo, a veces EOF podría no ser manejado bien). Básicamente, prueba un montón de formas creativas de destrozar cosas. Cuanta más experiencia con respecto al programa (y la explotación en general), los mutantes más útiles podrían ser posibles.
Pero, ¿qué es este borde "genético"?
Esa es probablemente una discusión para un momento posterior. Sin embargo, un par de enlaces a algunos fuzzers modernos (de código abierto) son AFL y Honggfuzz.
Escrito el 7 de abril de 2017
Influenciado de un buen desafío en Picoctf 2017 (nombre del desafío retenido, ya que el concurso aún está en marcha)
ADVERTENCIA: Esta nota puede parecer simple/obvia para algunos lectores, pero requiere decir, ya que las capas no eran claras para mí hasta hace muy poco.
Por supuesto, cuando se programamos, todos usamos abstracciones, ya sean clases y objetos, o funciones, metafunciones, o polimorfismo, móntas, o functores, o todo ese jazz. Sin embargo, ¿podemos realmente tener tal cosa durante la explotación? Obviamente, podemos explotar los errores que se cometen al implementar las abstracciones antes mencionadas, pero aquí, estoy hablando de algo diferente.
En múltiples CTF, cada vez que he escrito un exploit anteriormente, ha sido un script de exploit ad-hoc que deja caer un shell. Utilizo los increíbles pwntools como marco (para conectarme al servicio, y convertir cosas, y dinfer, etc.), pero eso es todo. Cada exploit tendía a ser una forma ad-hoc de trabajar hacia el objetivo de la ejecución del código arbitrario. Sin embargo, este desafío actual, así como pensar en mi nota anterior sobre la explotación de cadenas de formato "avanzada", me hizo darme cuenta de que podría colocar mis hazañas de manera consistente y moverse a través de diferentes capas de abstracción para finalmente alcanzar el objetivo requerido.
Como ejemplo, consideremos que la vulnerabilidad es un error lógico, que nos permite hacer una lectura/escritura de 4 bytes, en algún lugar en un rango pequeño después de un búfer. Queremos abusar de esto hasta obtener la ejecución del código, y finalmente el indicador.
En este escenario, consideraría que esta abstracción es una primitiva short-distance-write-anything . Con esto mismo, obviamente no podemos hacer mucho. Sin embargo, hago una pequeña función de pitón vuln(offset, val) . Sin embargo, dado que justo después del búfer, puede haber algunos datos/metadatos que podrían ser útiles, podemos abusar de esto para construir tanto read-anywhere como las primitivas write-anything-anywhere . Esto significa que escribo funciones cortas de Python que llaman a la función vuln() previamente definida. Estas funciones get_mem(addr) y set_mem(addr, val) se realizan simplemente (en este ejemplo actual) simplemente usando la función vuln() para sobrescribir un puntero, que luego se puede desferenciar en otra parte del binario.
Ahora, después de tener estas abstracciones get_mem() y set_mem() , construyo una abstracción anti-ASLR, básicamente filtrando 2 direcciones desde la get_mem() y comparando con una base de datos libc (gracias @niklasb por hacer la base de datos). Las compensaciones de estos me dan una libc_base de manera confiable, lo que me permite reemplazar cualquier función en el GOT con otra de LibC.
Esto esencialmente me ha dado control sobre EIP (en el momento en que puedo "activar" una de esas funciones exactamente cuando quiero). Ahora, todo lo que queda es que yo llame el disparador con los parámetros correctos. Entonces configuré los parámetros como una abstracción separada, y luego llamo a trigger() y tengo acceso de shell en el sistema.
Tl; dr: uno puede construir pequeñas primitivas de explotación (que no tienen demasiada potencia), y al combinarlas y construir una jerarquía de primitivas más fuertes, podemos obtener una ejecución completa.
Escrito el 6 de abril de 2017
Influenciado por esta impresionante transmisión en vivo de Gynvael Coldwind, donde habla sobre la explotación de cuerdas de formato
Expotencias de cadena de formato simple:
Puede usar el %p para ver qué hay en la pila. Si la cadena de formato en sí está en la pila, entonces se puede colocar una dirección (digamos Foo ) en la pila, y luego buscarla usando el especificador de posición n$ (por ejemplo, AAAA %7$p podría devolver AAAA 0x41414141 , si 7 es la posición en la pila). Luego podemos usar esto para construir una lectura de lugar primitivo, utilizando el especificador de formato %s en su lugar (por ejemplo, AAAA %7$s devolvería el valor en la dirección 0x41414141, continuando con el ejemplo anterior). We can also use the %n format specifier to make it into a write-what-where primitive. Usually instead, we use %hhn (a glibc extension, iirc), which lets us write one byte at a time.
We use the above primitives to initially beat ASLR (if any) and then overwrite an entry in the GOT (say exit() or fflush() or ...) to then raise it to an arbitrary-eip-control primitive, which basically gives us arbitrary-code-execution .
Possible difficulties (that make it "advanced" exploitation):
If we have partial ASLR , then we can still use format strings and beat it, but this becomes much harder if we only have one-shot exploit (ie, our exploit needs to run instantaneously, and the addresses are randomized on each run, say). The way we would beat this is to use addresses that are already in the memory, and overwrite them partially (since ASLR affects only higher order bits). This way, we can gain reliability during execution.
If we have a read only .GOT section, then the "standard" attack of overwriting the GOT will not work. In this case, we look for alternative areas that can be overwritten (preferably function pointers). Some such areas are: __malloc_hook (see man page for the same), stdin 's vtable pointer to write or flush , etc. In such a scenario, having access to the libc sources is extremely useful. As for overwriting the __malloc_hook , it works even if the application doesn't call malloc , since it is calling printf (or similar), and internally, if we pass a width specifier greater than 64k (say %70000c ), then it will call malloc, and thus whatever address was specified at the global variable __malloc_hook .
If we have our format string buffer not on the stack , then we can still gain a write-what-where primitive, though it is a little more complex. First off, we need to stop using the position specifiers n$ , since if this is used, then printf internally copies the stack (which we will be modifying as we go along). Now, we find two pointers that point ahead into the stack itself, and use those to overwrite the lower order bytes of two further ahead pointing pointers on the stack, so that they now point to x+0 and x+2 where x is some location further ahead on the stack. Using these two overwrites, we are able to completely control the 4 bytes at x , and this becomes our where in the primitive. Now we just have to ignore more positions on the format string until we come to this point, and we have a write-what-where primitive.
Written on 1st April 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he explains about race conditions
If a memory region (or file or any other resource) is accessed twice with the assumption that it would remain same, but due to switching of threads, we are able to change the value, we have a race condition.
Most common kind is a TOCTTOU (Time-of-check to Time-of-use), where a variable (or file or any other resource) is first checked for some value, and if a certain condition for it passes, then it is used. In this case, we can attack it by continuously "spamming" this check in one thread, and in another thread, continuously "flipping" it so that due to randomness, we might be able to get a flip in the middle of the "window-of-opportunity" which is the (short) timeframe between the check and the use.
Usually the window-of-opportunity might be very small. We can use multiple tricks in order to increase this window of opportunity by a factor of 3x or even up to ~100x. We do this by controlling how the value is being cached, or paged. If a value (let's say a long int ) is not aligned to a cache line, then 2 cache lines might need to be accessed and this causes a delay for the same instruction to execute. Alternatively, breaking alignment on a page, (ie, placing it across a page boundary) can cause a much larger time to access. This might give us higher chance of the race condition being triggered.
Smarter ways exist to improve this race condition situation (such as clearing TLB etc, but these might not even be necessary sometimes).
Race conditions can be used, in (possibly) their extreme case, to get ring0 code execution (which is "higher than root", since it is kernel mode execution).
It is possible to find race conditions "automatically" by building tools/plugins on top of architecture emulators. For further details, http://vexillium.org/pub/005.html
Written on 31st Mar 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he is experimenting on the heap
Use-after-free:
Let us say we have a bunch of pointers to a place in heap, and it is freed without making sure that all of those pointers are updated. This would leave a few dangling pointers into free'd space. This is exploitable by usually making another allocation of different type into the same region, such that you control different areas, and then you can abuse this to gain (possibly) arbitrary code execution.
Double-free:
Free up a memory region, and the free it again. If you can do this, you can take control by controlling the internal structures used by malloc. This can get complicated, compared to use-after-free, so preferably use that one if possible.
Classic buffer overflow on the heap (heap-overflow):
If you can write beyond the allocated memory, then you can start to write into the malloc's internal structures of the next malloc'd block, and by controlling what internal values get overwritten, you can usually gain a read-what-where primitive, that can usually be abused to gain higher levels of access (usually arbitrary code execution, via the GOT PLT , or __fini_array__ or similar).