En 2007, escribí un
par de herramientas de modificación para el simulador espacial
Freelancer . Los recursos del juego se almacenan en formato "binario INI" o "BINI". Probablemente, el formato binario se eligió en aras del rendimiento: dichos archivos son más rápidos de cargar y leer que el texto arbitrario en el formato INI.
La mayoría del contenido del juego se puede editar directamente desde estos archivos, cambiando nombres, precios de productos, estadísticas de naves espaciales o incluso agregando nuevas naves. Los archivos binarios son difíciles de modificar directamente, por lo que el enfoque natural es convertirlos a texto INI, hacer cambios en un editor de texto, luego volver a convertirlos al formato BINI y reemplazar los archivos en el directorio del juego.
No analicé el formato BINI, y no soy el primero en aprender a editarlos. Pero no me gustaban las herramientas existentes, y tenía mi propia visión de cómo deberían funcionar. Prefiero una interfaz de estilo Unix, aunque el juego en sí se ejecuta en Windows.
En ese momento, me acabo de familiarizar con las herramientas
yacc (en realidad
Bison ) y
lex (en realidad
flex ), así como con Autoconf, así que las usé exactamente. Fue interesante probar estas utilidades en la práctica, aunque imité servilmente otros proyectos de código abierto, sin entender por qué todo se hizo de esta manera, de ninguna otra manera. Debido al uso de yacc / lex y la creación de scripts de configuración, se requería un sistema completo similar a Unix. Todo esto es visible en la
versión original de los programas .
El proyecto resultó ser bastante exitoso: yo mismo utilicé con éxito estas herramientas, y aparecieron en diferentes colecciones para el modificador Freelancer.
Refactorización
A mediados de 2018, volví a este proyecto. ¿Alguna vez has mirado tu antiguo código con el pensamiento: qué pensaste? Mi formato INI resultó ser mucho más rígido y estricto de lo necesario, los binarios se grabaron de forma dudosa y el ensamblaje ni siquiera funcionó normalmente.
Gracias a diez años de experiencia adicional, sabía con certeza que escribiría estas herramientas mucho mejor ahora. Y lo hice en unos días, reescribiéndolos desde cero. Este nuevo código ahora está en el hilo maestro en Github.
Me gusta hacer que todo sea lo más simple posible , así que me deshice de autoconf a favor de un
Makefile más simple y portátil . No más yacc o lex, pero el analizador está escrito a mano. Solo se usa la C. portátil apropiada. El resultado es tan simple que ensamblo el proyecto con un comando breve
de Visual Studio , por lo que el Makefile no es realmente necesario. Si reemplaza
stdint.h
con
typedef
, incluso puede
construir y ejecutar binitools en DOS .
La nueva versión es más rápida, más compacta, más limpia y más fácil. Es mucho más flexible con respecto a la entrada INI, por lo que es más fácil de usar. ¿Pero es realmente correcto?
Fuzzing
He estado interesado en
fuzzing durante muchos años, especialmente
afl (american fuzzy lop). Pero él nunca lo dominó, aunque probó algunas de las herramientas que uso regularmente. Pero el fuzzing no encontró nada notable, al menos antes de que me diera por vencido. Probé mi biblioteca JSON y, por alguna razón, tampoco encontré nada. Está claro que mi analizador JSON no podría ser
tan confiable, ¿verdad? Pero el difuminado no mostró nada. (Resultó que mi biblioteca JSON es bastante confiable, ¡gracias en gran parte a los esfuerzos de la comunidad!)
Pero ahora tengo un analizador INI relativamente nuevo. Aunque puede analizar con éxito y ensamblar correctamente el conjunto original de archivos BINI en el juego, su funcionalidad
realmente no
ha sido probada. Seguramente aquí fuzzing encontrará algo. Además, no necesita escribir una sola línea para ejecutar afl en este código. Las herramientas predeterminadas funcionan con entrada estándar, lo cual es ideal.
Suponiendo que tiene las herramientas necesarias instaladas (make, gcc, afl), así es como se inicia fácilmente el fuzzing de binitools:
$ make CC=afl-gcc $ mkdir in out $ echo '[x]' > in/empty $ afl-fuzz -i in -o out -- ./bini
La utilidad
bini
acepta INI en la entrada y emite BINI, por lo que es mucho más interesante verificarlo que el procedimiento inverso de
unbini
. Como
unbini
analiza datos binarios relativamente simples, el (probablemente) fuzzer no tiene nada que buscar. Sin embargo, por si acaso, lo revisé de todos modos.

En este ejemplo, cambié el compilador predeterminado al shell GCC para afl (
CC=afl-gcc
). Aquí afl llama a GCC en segundo plano, pero agrega su propio kit de herramientas al binario. Al hacer fuzzing,
afl-fuzz
usa este kit de herramientas para monitorear la ruta de ejecución de un programa.
La documentación afl explica los detalles técnicos.
También creé los directorios de entrada y salida al poner en el directorio de entrada un ejemplo de trabajo mínimo que le da a afl un punto de partida. Cuando comienza, muta la cola de datos de entrada y observa los cambios durante la ejecución del programa. El directorio de salida contiene los resultados y, lo que es más importante, el cuerpo de datos de entrada que provocan rutas de ejecución únicas. En otras palabras, muchas entradas se procesan en la salida del fuzzer, verificando muchos escenarios de borde diferentes.
El resultado más interesante y aterrador es un bloqueo completo del programa. Cuando comencé a usar el fuzzer para binitools,
bini
mostró
muchos accidentes similares. En cuestión de minutos, afl descubrió una serie de errores sutiles e interesantes en mi programa, que fue increíblemente útil. Fazzer incluso encontró un
error poco probable
de un puntero obsoleto , verificando el orden diferente de varias asignaciones de memoria. Este error en particular fue un punto de inflexión que me hizo darme cuenta del valor del fuzzing.
No todos los errores encontrados condujeron a fallas. También estudié el resultado y miré qué entrada dio un resultado exitoso y cuál no, y vi cómo el programa manejó varios casos extremos. Ella rechazó algunos aportes que pensé que procesaría. Y viceversa, procesó algunos datos que consideraba incorrectos e interpretó algunos datos de una manera inesperada para mí. Entonces, incluso después de corregir errores con bloqueos del programa, aún cambié la configuración del analizador para corregir cada uno de estos casos desagradables.
Crea un conjunto de pruebas
Tan pronto como solucioné todos los errores detectados por el fuzzer y ajusté el analizador en todas las situaciones de borde, realicé un conjunto de pruebas desde el paquete de datos del fuzzer, aunque no directamente.
Primero, ejecuté el fuzzer en paralelo, este proceso se explica en la documentación afl, por lo que obtuve muchas entradas redundantes. Por redundancia, quiero decir que la entrada es diferente pero tiene la misma ruta de ejecución. Afortunadamente, afl tiene una herramienta para lidiar con esto:
afl-cmin
, una herramienta para minimizar el shell. Elimina entradas innecesarias.
En segundo lugar, muchas de estas entradas fueron más largas de lo necesario para invocar su ruta de ejecución única.
afl-tmin
, un minimizador de casos de prueba que redujo el caso de prueba, ayudó
afl-tmin
.
Separé entradas válidas e inválidas, y las verifiqué en el repositorio. Eche un vistazo a todas estas entradas estúpidas
inventadas por el fuzzer en base a una sola entrada mínima:
De hecho, aquí el analizador está congelado en un estado, y un conjunto de pruebas asegura que una compilación particular se comporte de una manera
muy específica. Esto es especialmente útil para garantizar que los ensamblados realizados por otros compiladores en otras plataformas se comporten de la misma manera con respecto a su salida. Mi conjunto de pruebas incluso detectó un error en la biblioteca dietlibc porque binitools no pasó las pruebas después de vincularlo. Si fuera necesario realizar cambios no triviales en el analizador, entonces en esencia uno tendría que abandonar el conjunto actual de pruebas y comenzar de nuevo para que genere un cuerpo completamente nuevo para el analizador nuevo.
Por supuesto, el difuminado se ha establecido como una técnica poderosa. Encontró una serie de errores que nunca podría haber descubierto por mi cuenta. Desde entonces, comencé a usarlo de manera más competente para probar otros programas, no solo el mío, y encontré muchos errores nuevos. Ahora fuzzer ha ocupado un lugar permanente entre las herramientas de mi kit de desarrollo.