Cómo funciona el Atheris Python Fuzzer


El viernes, Anunciado que hemos lanzado el Motor de fuzzing Atheris Python como código abierto. En esta publicación, hablaremos brevemente sobre sus orígenes y luego entraremos en muchos más detalles sobre cómo funciona.

La historia del origen

Cada año desde 2013, Google ha realizado un «Fuzzit», un evento interno en el que los empleados de Google escriben fuzzers para su código o software program de código abierto. Para octubre de 2019, sin embargo, ya habíamos escrito fuzzers para la mayoría del código C / C ++ de código abierto que usamos. Entonces, para ese Fuzzit, el autor de esta publicación escribió un motor de fuzzing Python basado en libFuzzer. Desde entonces, se han escrito más de 50 fuzzers de Python en Google y se han informado y solucionado innumerables errores.

Originalmente, este motor de fuzzing solo podía fuzzing extensiones nativas, ya que no admitía la cobertura de Python. Pero con el tiempo, el fuzzer se transformó en Atheris, un motor de fuzzing de alto rendimiento que admite fuzzing nativo y puro de Python.

Un poco de trasfondo

Atheris es un fuzzer guiado por cobertura. Para usar Atheris, especifique un «punto de entrada» a través de atheris.Setup (). Atheris luego llamará rápidamente a este punto de entrada con diferentes entradas, con la esperanza de producir un bloqueo. Mientras lo hace, Atheris supervisará cómo cambia la ejecución del programa en función de la entrada e intentará encontrar rutas de código nuevas e interesantes. Esto permite a Atheris encontrar comportamientos inesperados y con errores de manera muy efectiva.

importar atheris

importar sys

def TestOneInput (datos): # Nuestro punto de entrada

si los datos == b «malos»:

elevate RuntimeError («¡Maldad!»)

atheris.Set up (sys.argv, TestOneInput)

atheris.Fuzz ()

Atheris es una extensión nativa de Python y united states libFuzzer para proporcionar su cobertura de código y capacidades de generación de entrada. El punto de entrada que se pasa a atheris.Set up () se incluye en el punto de entrada de C ++ que en realidad se pasa a libFuzzer. LibFuzzer invocará este contenedor repetidamente y sus datos se enviarán a Python.

Cobertura del código Python

Atheris es una extensión nativa de Python, y normalmente se compila con libFuzzer vinculado. Cuando inicializas Atheris, registra un trazador con CPython para recopilar información sobre el flujo de código de Python. Este rastreador puede realizar un seguimiento de cada línea alcanzada y cada función ejecutada.

Necesitamos llevar esta información de seguimiento a libFuzzer, que es responsable de generar información de cobertura de código. Sin embargo, hay un problema: libFuzzer asume que la cantidad de código se conoce en tiempo de compilación. Los dos mecanismos primarios de cobertura de código son __sanitizer_cov_pcs_init (que registra un conjunto de contadores de programas que pueden ser visitados) y __sanitizer_cov_8bit_counters_init (que registra una matriz de valores booleanos que se incrementarán cuando se visite un bloque básico). Ambos necesitan saber en el momento de la inicialización cuántos contadores de programa o bloques básicos existen. Pero en Python, eso no es posible, ya que el código no se carga hasta mucho después de que Python se inicia. Ni siquiera podemos saberlo cuando iniciamos el fuzzer: es posible importar código dinámicamente más tarde, o incluso generar código sobre la marcha.

Afortunadamente, libFuzzer admite la eliminación de bibliotecas compartidas cargadas en tiempo de ejecución. Tanto __sanitizer_cov_pcs_init como __sanitizer_cov_8bit_counters_init se pueden llamar de forma segura desde una biblioteca compartida en su constructor (llamado cuando la biblioteca está cargada). Entonces, ¡Atheris simula la carga de bibliotecas compartidas! Cuando se inicializa el rastreo, Atheris primero llama a esas funciones con una matriz de contadores de 8 bits y contadores de programa completamente inventados. Luego, cada vez que se alcanza una nueva línea de Python, Atheris asigna una Personal computer y un contador de 8 bits a esa línea Atheris siempre informará esa línea de la misma manera a partir de ese momento. Una vez que Atheris se queda sin Laptop y contadores de 8 bits, simplemente carga una nueva «biblioteca compartida» llamando a esas funciones nuevamente. Por supuesto, el crecimiento exponencial se utiliza para garantizar que la cantidad de bibliotecas compartidas no sea excesiva.

¿Qué tiene de especial Python 3.8+?

En el README, recomendamos a los usuarios que utilicen Python 3.8+ siempre que sea posible. Esto se debe a que Python 3.8 agregó una nueva característica: rastreo de código de operación. No solo podemos monitorear cuándo se visita cada línea y se llama a cada función, sino que también podemos monitorear cada operación que realiza Python y qué argumentos united states. Esto le permite a Atheris encontrar su camino a través de las declaraciones if mucho mejor.

Cuando se encuentra un código de operación Evaluate_OP, que indica una comparación booleana entre dos valores, Atheris inspecciona los tipos de valores. Si los valores son bytes o Unicode, Atheris puede informar la comparación a libFuzzer a través de __sanitizer_weak_hook_memcmp. Para la comparación de enteros, Atheris usa la función apropiada para informar comparaciones de enteros, como __sanitizer_cov_trace_cmp8.

En las versiones recientes de Python, una cadena Unicode en realidad se representa como una matriz de caracteres de 1 byte, 2 bytes o 4 bytes, según el tamaño del carácter más grande de la cadena. La solución obvia para la cobertura es:

  • primero assess dos cadenas para obtener un tamaño de carácter equivalente e infórmelo como una comparación de números enteros con __sanitizer_cov_trace_cmp8
  • En segundo lugar, si son iguales, llame a __sanitizer_weak_hook_memcmp para informar la comparación de cadenas true

Sin embargo, las mediciones de rendimiento descubrieron que la mejor estrategia sorprendente es convertir ambas cadenas a utf-8 y luego compararlas con __sanitizer_weak_hook_memcmp. Incluso con la sobrecarga de rendimiento de la conversión, libFuzzer avanza mucho más rápido.

Construyendo Atheris

La mayor parte del esfuerzo para lanzar Atheris fue simplemente hacer que se compilara fuera del entorno de Google. En Google, la creación de un proyecto de Python genera todo su universo de dependencias, incluido el intérprete de Python. Esto hace que sea trivial para nosotros usar libFuzzer con nuestros proyectos simplemente lo compilamos en nuestro intérprete de Python, junto con Deal with Sanitizer o cualquier otra característica que queramos.

Desafortunadamente, fuera de Google, no es tan easy. Tuvimos muchos comienzos en falso con respecto a cómo vincular libFuzzer con Atheris, incluido convertirlo en un objeto compartido independiente, precargarlo, etc. Finalmente nos decidimos por vincularlo al objeto compartido de Atheris, ya que brinda la mejor experiencia para la mayoría de los usuarios.

Sin embargo, esta estrategia aún requería que hiciéramos cambios menores en libFuzzer, para permitir que se llamara como una biblioteca. Dado que la mayoría de los usuarios no tienen la última versión de Clang y normalmente las distribuciones tardan varios años en actualizar su instalación de Clang, obtener esta nueva versión de libFuzzer sería bastante difícil para la mayoría de las personas, lo que haría que la instalación de Atheris fuera una molestia. Para evitar esto, parcheamos libFuzzer si es demasiado antiguo. Atheris setup.py detectará un libFuzzer desactualizado, hará una copia, marcará su punto de entrada del fuzzer como noticeable, e inyecte un envoltorio pequeño para permitir que se llame a través del nombre LLVMFuzzerRunDriver. Si libFuzzer es suficientemente nuevo, simplemente lo llamamos usando LLVMFuzzerRunDriver directamente.

El verdadero problema proviene de mezclar extensiones nativas con desinfectantes. En teoría, el fuzzing de una extensión nativa con Atheris debería ser trivial – simplemente compílelo con -fsanitize = fuzzer-no-hyperlink, y asegúrese de que Atheris se cargue primero. Esas llamadas a funciones mágicas que inyectó Clang apuntarán a los símbolos libFuzzer dentro de Atheris. Cuando se trata de eliminar una extensión nativa sin desinfectantes, en realidad es así de simple. Todo funciona. Desafortunadamente, los desinfectantes hacen que todo sea más complejo.

Cuando se united states of america un desinfectante como Tackle Sanitizer con Atheris, es necesario LD_PRELOAD el objeto compartido del desinfectante. ASan requiere que se cargue primero, antes que cualquier otra cosa debe estar precargado o enlazado estáticamente en el ejecutable (en este caso, el intérprete de Python). ASan y UBSan definen muchos de los mismos símbolos de cobertura de código que libFuzzer. En el uso típico de libFuzzer, esto no es un problema, ya que ASan / UBSan declara que esos símbolos son débiles los de libFuzzer tienen prioridad. Pero cuando libFuzzer se carga en un objeto compartido más tarde, eso no funciona. Los símbolos de ASan / UBSan ya se han cargado a través de LD_PRELOAD y, por lo tanto, la información de cobertura va a esas bibliotecas, dejando libFuzzer muy dañado.

La única buena manera de resolver esto es vincular libFuzzer en Python, en lugar de Atheris. Dado que, por lo tanto, es parte del ejecutable adecuado en lugar de un objeto compartido que se carga dinámicamente más tarde, la resolución de símbolos funciona correctamente y los símbolos libFuzzer tienen prioridad. Esto no es trivial. Hemos proporcionado documentación sobre esto, y un guión para construir un CPython 3.8.6 modificado. Estos scripts usarán el mismo libFuzzer posiblemente parcheado que Atheris.

¿Por qué se llama Atheris?

Atheris Hispida, o la «víbora de arbustos peludos», es lo más parecido que existe a una pitón difusa.



Enlace a la noticia unique