Cómo lanzar pruebas continuas de fuzzing autoservicio
No es la primera vez que escribo sobre las pruebas de fuzzing, que introducen de manera inteligente datos modificados a medida en un programa concreto con el fin de desencadenar casos inesperados y encontrar errores, haciendo hincapié en cómo Fastly utiliza American Fuzzy Lop para encontrar y mitigar de forma proactiva errores en algunos de los servidores con los que trabajamos.
OSS-Fuzz es una iniciativa dirigida por Google para contribuir a aumentar la seguridad y la estabilidad del software de código abierto (OSS, por sus siglas en inglés) sometiendo los proyectos a pruebas de fuzzing continuas. En resumen, los ingenieros de seguridad de Google han recurrido a la colaboración abierta distribuida para llevar a cabo tareas de desarrollo de integraciones de pruebas de fuzzing de proyectos de código abierto ubicuos y aplicar recursos de computación masiva para encontrar (y notificar de manera privada) errores en ellos. Este enfoque distribuido es similar al empleado por el ciclo de vida de desarrollo seguro (self-service secure development lifecycle [SDL]) en modo autoservicio que el equipo de seguridad de Slack ha desplegado en su organización en rápido desarrollo. (Profundizaremos en esto más adelante).
OSS-Fuzz es un proyecto innovador que supone un avance en la ya puntera ingeniería de seguridad OSS y que mejora la calidad general del software que presta servicio a Internet. Puedes obtener más información aquí y aquí. Hace poco, incluso, han empezado a ofrecerincentivos por parches para integraciones de proyectos. En un esfuerzo continuo por ofrecer una plataforma segura a nuestros clientes, hemos bifurcado el proyecto OSS-Fuzz de código abierto para lanzar nuestra propia capacidad interna de pruebas de fuzzing continuas. Las pruebas de fuzzing continuas nos permiten encontrar y mitigar proactivamente errores para proteger a nuestros clientes y contribuir, al mismo tiempo, a mantener la seguridad general en Internet.
En esta publicación explicaré cómo utilizar los componentes de código abierto de google/oss-fuzz para lanzar, en modo de autoservicio, pruebas de fuzzing continuas para software público y privado utilizando h2o, el proxy HTTP/2 de Fastly, como ejemplo de ejecución.
Ventajas de la ejecución continua de pruebas de fuzzing
Una de las cuestiones más complicadas en torno a las pruebas de fuzzing es decidir cuándo dejar de dedicar recursos a las pruebas de fuzzing de un objetivo determinado y seguir adelante.
OSS-Fuzz responde a esta cuestión ofreciendo un marco de pruebas de fuzzing continuas de la última versión de un programa objetivo hasta que está disponible la siguiente versión del mismo. Se trata de una solución pragmática que asume que el nuevo código no sometido previamente a pruebas de fuzzing contiene con más probabilidad fallos detectables, y que protege contra las regresiones que podrían surgir en la base de código.
Una vez llevado a cabo el trabajo de integración de un proyecto, OSS-Fuzz carga los objetivos de las pruebas de fuzzing en ClusterFuzz, que ejecutará iteraciones del objetivo en marcos de pruebas como LibFuzzer y AFL.
Diagrama de la arquitectura ClusterFuzz en torno a 2015. Fuente: Abhishek Arya / nullcon
Uno de los aspectos más interesantes de OSS-Fuzz es que cualquiera puede solicitar escribir una integración para un proyecto de código abierto. Manteniendo un marco de integración de proyectos utilizable, los desarrolladores del proyecto pueden encargarse de partes que son sencillas para ellos pero que, a menudo, requieren mucho tiempo de los ingenieros de seguridad (como aprenderse el sistema de construcción y generación de ejecutables del proyecto para escribir una sencilla integración del fuzzer). De este modo, los ingenieros de seguridad pueden centrarse en el marco y en las innovaciones generalizadas de pruebas de fuzzing que benefician a todos los proyectos objetivo.
Flujo de trabajo OSS-Fuzz. Fuente: google/oss-fuzz.
Hemos añadido objetivos de pruebas de fuzzing para la versión pública de h2o, el proxy HTTP/2 de Fastly, a Google OSS-Fuzz. Puedes comprobar el código de integración de OSS-Fuzz aquí; puedes encontrar la fuente objetivo de pruebas de fuzzing aquí. En el transcurso de la tarea, hemos recopilado las entradas de las pruebas unitarias de h2o, modificado manualmente los archivos y extraído los resultados de algunas ejecuciones de pruebas de fuzzing para elaborar baterías de valores para nuestros objetivos de pruebas de fuzzing.
En general, trabajar con OSS-Fuzz de Google ha sido una magnífica experiencia para el equipo de h2o; hemos descubierto errores en h2o y obtenido seguridad en torno al código base, y la batería de valores originales HTTP/2 que publicamos para esta iniciativa se ha utilizado para detectar fallos en nghttp2.
¿Qué ocurre con el código privado?
Si bien Fastly ha utilizado tradicionalmente las pruebas de fuzzing para proteger periódicamente el código crítico, el equipo de seguridad de Fastly está desplegando una capacidad de pruebas de fuzzing autoservicio continuas para ampliar la cobertura de los fuzzer y garantizar que se ejecutan sistemáticamente como parte del desarrollo y de los procesos de desarrollo de Fastly.
Como parte de nuestro código no es open source, era necesario desarrollar una capacidad de realización de pruebas de fuzzing continuas de proyectos privados como apoyo a esta iniciativa. Hasta ahora hemos alcanzado dos usos de alto nivel para esta capacidad:
Contribuir a la seguridad del software de código cerrado y las bifurcaciones privadas
Realizar fácilmente pruebas de funcionalidad (smoke) de los cambios de software que van a enviarse a Google OSS-Fuzz
El primer caso de uso era un objetivo perseguido hace tiempo pero el segundo no se nos ocurrió hasta el anuncio de Google OSS Fuzz.
Como muchas empresas favorables al OSS, el equipo de Fastly realiza periódicamente mejoras en las bifurcaciones de software de código abierto y contribuye a los cambios de la fuente de origen aprobados en producción. En el caso de h2o, solemos hacer cambios internos en el proxy y someterlos a estrictas pruebas y despliegues antes de incluir los parches en el repositorio público. Las pruebas de fuzzing continuas a las que sometemos internamente nuestras bifurcaciones de h2o detectan los errores más rápidamente cuando se cometen, pero además nos ofrecen un espacio de experimentación con los cambios en la integración pública de OSS-Fuzz de h2o.
Fuzzer mínimo viable
El ciclo de vida de desarrollo seguro (SDL, por sus siglas en inglés) en modo autoservicio es un potente concepto para ampliar la seguridad en equipos de desarrollo de rápido crecimiento, una idea sobre la que aprendimos gracias a Leigh y Ari del equipo de seguridad de Slack. Una técnica que utilizan en su SDL en modo autoservicio es la creación de servicios automatizados que los ingenieros pueden utilizar para asegurar sistemas, haciendo un uso eficaz de los recursos finitos de equipos de seguridad e ingeniería.
Ante los éxitos cosechados gracias a la utilización de este modelo en otras partes del SDL en Fastly, decidimos aplicar el mismo enfoque autoservicio a la pruebas de fuzzing. Sin embargo, lanzar un sistema de pruebas de fuzzing continuas autoservicio es un proyecto muy exigente ya que requiere:
Desarrollo de un marco de integración de proyectos.
Servir de guía para los equipos de desarrollo sobre cómo (y por qué) integrar las pruebas de fuzzing.
Construir y gestionar la infraestructura de las pruebas de fuzzing.
Garantizar al equipo de seguridad que el sistema está aportando valor.
Cerrar las puertas, bajar las persianas y construir un grupo de pruebas de fuzzing continuas era un punto de partida muy tentador, pero nos dimos cuenta de que este planteamiento era muy arriesgado: ¿Utilizarían realmente este sistema los ingenieros? ¿Cuánto trabajo supondría? ¿Expandir las pruebas de fuzzing es hacer un buen uso de los recursos limitados de AppSec? ¿Revelarán los fuzzers errores relevantes? ¿Aportará definitivamente el sistema garantía de seguridad constante?
Para garantizar el éxito del este proyecto, decidimos seguir el ejemplo del Startup Playbook y adoptar un enfoque de "producto mínimo viable" (MVP, por sus siglas en inglés) para desarrollar nuestra propia capacidad de pruebas de fuzzing continuas. La idea era concentrarnos en las tareas dirigidas a las partes más arriesgadas del proyecto, a expensas de la automatización bien entendida que podría aplicarse después:
1. Diseño de cómo los desarrolladores van a interactuar con el servicio
2. Integración con proyectos clave
3. Detección de algunos errores importantes (es decir, probar que funciona)
4. Comenzar a integrar los comentarios
Plan de MVP
FTW OSS-Fuzz
El proyecto OSS-Fuzz ha realizado un buen trabajo de diseño de un marco de integración de pruebas de fuzzing bien documentadas y utilizables. Llegados a este punto, probablemente sepas dónde queremos llegar: decidimos modificar Google OSS-Fuzz para crear un sistema de pruebas de fuzzing continuas MVP para proyectos privados Fastly.
Google OSS-Fuzz ya ofrece un proceso eficaz y probado de integración de proyectos, de modo que logramos los objetivos número 1 y 2 de la lista anterior básicamente "a coste cero". Con unas sencillas indicaciones y formación específicas, los desarrolladores pudieron apoyarse en la documentación y los ejemplos de código abierto para la integración de proyectos.
Marco de integración de proyectos documentado y aprobado para producción, disponible engoogle/oss-fuzz
Pero aún quedaba trabajo que hacer para llegar al MVP: google/oss-fuzz no incluye ClusterFuzz, la parte del sistema de fuzzing testing de Google que gestiona realmente los trabajos de fuzzing de producción.
[x] Diseño de cómo los desarrolladores van a interactuar con el servicio
[x] Integración con proyectos clave
[ ] Detección de algunos errores importantes (es decir, probar que funciona)
[ ] Comenzar a integrar los comentarios
Tareas 1 y 2 abordadas
El suficiente ClusterFuzz
google/oss-fuzz incluye herramientas para realizar pruebas de funcionalidad (smoke) a las integraciones de pruebas de fuzzing localmente. Por ejemplo, se puede construir y probar el fuzzer de la URL de h2o público de la siguiente manera:
$ git clon http://github.com/google/oss-fuzz
$ cd oss-fuzz
$ python infra/helper.py build_fuzzers h2o
$ python infra/helper.py run_fuzzer h2o ./h2o-fuzzer-url
Después de la clonación de rigor, estos comandos orquestarán los contenedores docker para:
Elaborar desde cero los objetivos de pruebas de fuzzing h2o.
Ejecutar el objetivo de pruebas de fuzzing del fuzzer de la URL de h2o.
Está diseñado para permitir al integrador de proyecto OSS-Fuzz comprobar que los objetivos de las pruebas de fuzzing se construyen, se enlazan y no fallan inmediatamente cuando llegan a ClusterFuzz. Aunque no son la solución ideal para trabajos a largo plazo, estas herramientas de prueba pueden readaptarse: ofrecen la capacidad básica de ejecutar objetivos de pruebas de fuzzing compatibles con ClusterFuzz producidos por la herramienta de pruebas de fuzzing google/oss-fuzz.
Fuzzer de pruebas de fuzzing MVP
Decidimos que, con unos retoques, esta herramienta podría funcionar lo suficientemente bien para ejecutar trabajos de pruebas de fuzzing en contenedores dentro del MVP de pruebas de fuzzing continuas. Para probar esta idea, solicitamos un servidor de repuesto (potente) de almacenamiento en caché en el edge de Fastly a nuestro laboratorio, hackeamos un poco la herramienta OSS-Fuzz (más información abajo) e invocamos un conjunto de contenedores de run_fuzzer utilizando las opciones -worker
y -jobs
compatibles de LibFuzzer para equilibrar los cores entre todos los objetivos de pruebas de fuzzing.
for project, targets in projects.items(): # Build fuzzers
if args.build:
build(project)
# Run fuzzers
for target in targets:
p = run_fuzzer(project, target,
["-workers=%d" % n_workers,
" -jobs=%d" % n_workers,
" -artifact_prefix=%s" % target.strip("./")])
Código simplificado para el ejecutor de pruebas de fuzzing
Acceso a repositorios de código privado
Por defecto, OSS-Fuzz clona proyectos desde servidores de control de versión de código abierto públicos, como github.com, y luego construye los objetivos de pruebas de fuzzing desde ellos. Para que el MVP fuera compatible con repositorios privados, decidimos enviar al contenedor de construcción de objetivos de pruebas de fuzzing la invocación del ssh-agent (SSH_AUTH_SOCK
) de la shell y bajar el código privado mediante el build.sh del proyecto OSS-Fuzz en lugar de utilizar el Dockerfile.
def build_fuzzers(build_args):
" " " Build fuzzers."" "
parser = argparse.ArgumentParser('helper.py build_fuzzers')
[...]
parser.add_argument('-A', dest='forward_ssh', default=Falso,
action='store_const', const=True, help='Forward SSH to container.')
[...]
if args.forward_ssh:
ssh_auth_sock = os.environ['SSH_AUTH_SOCK']
command += ['-v', '%s:/ssh-agent' % ssh_auth_sock, '-e',
'SSH_AUTH_SOCK=/ssh-agent']
[...]
command += [
'-v', '%s:/out' % os.path.join(BUILD_DIR, 'out', project_name),
'-v', '%s:/work' % os.path.join(BUILD_DIR, 'work', project_name),
'-t', 'gcr.io/oss-fuzz-base/%s' % project_name
]
Snippet que añade el envío de ssh-agent infra/helper.py’s build_fuzzers
Aunque no es una solución ideal a largo plazo, este método resolvió dos problemas rápidamente:
El equipo de seguridad pudo automatizar la reconstrucción de los objetivos de pruebas de fuzzing privados sin aprovisionar (o gestionar) secretos de acceso al repositorio adicionales.
Los desarrolladores pudieron construir y probar las integraciones en sus entornos de desarrollo personales sin necesidad de realizar ningún cambio administrativo en sus proyectos.
Previmos realizar modificaciones en la arquitectura del proceso de construcción del objetivo de las pruebas de fuzzing para conseguir una mejor alineación con los flujos de trabajo de despliegue de proyecto existentes de Fastly, lo que también nos hacía sentir algo mejor respecto a haber tomado este atajo inicialmente.
Conservar las baterías de valores
Una de las ventajas de las pruebas de fuzzing continuas es que, con el tiempo, se crean unas baterías de valores originales (o colas) que pueden utilizarse para campañas de fuzzing posteriores. Por defecto, las invocaciones del contenedor del ejecutor de pruebas de fuzzing de google/oss-fuzz fuzz-runner no recuerdan los progresos que han realizado en pruebas de fuzzing; eso queda para ClusterFuzz. Para solucionar esto para nuestro MVP, simplemente modificamos google/oss-fuzz para que conservara su directorio de baterías de valores originales entre invocaciones del contenedor. A continuación, compartimos y extrajimos, manual y periódicamente, las baterías de valores originales entre objetivos de pruebas de fuzzing similares para mejorar la eficiencia.
OSS-Fuzz ofrece una interfaz compatible con LibFuzzer a los marcos de pruebas de fuzzing que utiliza, lo que significa que los archivos que aumentan la cobertura en el objetivo (es decir, el objetivo del fuzzer) se almacenan en el directorio enviado al fuzzer a través de su argumento CORPUS_DIR
. Los cambios necesarios en esta fase dependen de tu entorno, pero, en nuestro caso, simplemente modificamos el comando run_fuzzer
para utilizar el directorio seed_corpus
para el proyecto objetivo, en vez de un directorio efímero en /tmp
.
SEED_CORPUS="${FUZZER}_seed_corpus.zip"
if [ -f $SEED_CORPUS ]; then
echo "Using seed corpus: $SEED_CORPUS"
- rm -rf /tmp/seed_corpus/ && mkdir /tmp/seed_corpus/
- unzip -d /tmp/seed_corpus/ $SEED_CORPUS > /dev/null
- CMD_LINE="$CMD_LINE /tmp/seed_corpus/"
+ CORPUS_DIR=$OUT/seed_corpus
+ mkdir -p $CORPUS_DIR
+ unzip -od $CORPUS_DIR $SEED_CORPUS > /dev/null
+ CMD_LINE="$CMD_LINE $CORPUS_DIR"
fi
echo $CMD_LINE
Cambios en un infra/base-images/base-runner/run_fuzzer
heredado para conservar la batería de valores originales
Gestión de recursos
Aunque resultan excelentes para integraciones de fuzzer de proyectos, utilizar los contenedores para aislar trabajos de pruebas de fuzzing de ejecución larga puede ser arriesgado: las pruebas de fuzzing pueden consumir recursos en exceso o, de otro modo, crear inestabilidad en el sistema operativo subyacente, lo que puede producir interacciones accidentales entre trabajos de pruebas de fuzzing, falsos positivos, etc. Google ClusterFuzz, por ejemplo, construye binarios de objetivos de pruebas de fuzzing en contenedores, pero, en último término, programa y ejecuta los trabajos de pruebas de fuzzing en máquinas virtuales, que ofrecen un aislamiento mucho más consistente.
Control de MVP básico a través de scripts de Perl y herramientas de sistema como htop
Para nuestro MVP, decidimos evitarnos directamente este caso límite: atendiendo al interés por adoptar atajos inteligentes con el fin de obtener informes de vuelta rápidamente (y reducir el riesgo general del proyecto), controlamos regularmente el uso de los recursos del sistema y los fallos, para que fuera más fácil detectar cualquier fallo que surgiera del uso contenedores. Además, pensamos que adaptaríamos el motor de ejecución fuzzer para mejorar la eficiencia y tratar cualquier peculiaridad de nuestro entorno más tarde; y que este tipo de cambios serían sencillos (no implicarían la modificación de integraciones de proyecto o tener que volver a formar a los desarrolladores, por ejemplo).
MVP desplegado
Con la capacidad de construir objetivos de pruebas de fuzzing de repositorios privados y ejecutarlos en nuestra infraestructura, lográbamos cumplir los objetivos n.º 3 y n.º 4 del proyecto MVP.
[x] Diseño de cómo los desarrolladores van a interactuar con el servicio
[x] Integración con proyectos clave
[x] Detección de algunos errores importantes (es decir, probar que funciona)
[x] Comenzar a integrar los comentarios
¡Conseguido!
La lista anterior recoge los principales cambios que realizamos para que OSS-Fuzz funcionara para repositorios privados, no obstante, he omitido muchas modificaciones más pequeñas y específicas del entorno que también aplicamos (y pusimos en cola) durante el proceso. Si quieres saber más sobre las funciones que pueden incorporarse a un sistema de pruebas de fuzzing continuas, consulta la presentación de Abhishek Arya de ClusterFuzz (2015).
Desarrollo continuo y agradecimientos
Hemos avanzado un largo camino desde las primeras fases del MVP de pruebas de fuzzing continuas, pero nos queda aún mucho por recorrer. Aunque se trata de un trabajo aún en proceso, gracias a las pruebas de fuzzing continuas hemos descubierto ya errores de alto calado en nuestros proyectos privados y detectado fallos antes de que llegaran a los repositorios de código abierto, aprovechando los recursos finitos del equipo de ingeniería y de seguridad.
Espero que esta publicación os dé ideas sobre cómo lanzar pruebas de fuzzing continuas en vuestra empresa o, al menos, que arroje algo de luz sobre los conceptos de pruebas de fuzzing continuas, Google OSS-Fuzz y SDL autoservicio. ¡Gracias por leerme!
Quiero agradecer también a Frederik Deweerdt y a Kazuho Oku su apoyo constante para hacer posible la integración OSS-Fuzz de la h2o pública, a todos los Fastlyans que hayan arrimado el hombro para desarrollar integraciones de pruebas de fuzzing continuas y al equipo de Google OSS-Fuzz (y al resto de contribuidores ajenos a Google) su continuo trabajo en pro de la seguridad del software ubicuo y de la mejora de la resiliencia de Internet.