
Fuzzer del kernel de Windows basado en instantáneas y guiado por cobertura
Rewind es un fuzzer guiado por cobertura basado en instantáneas que se dirige a componentes del kernel de Windows.
La idea es partir de una instantánea de un sistema en ejecución. Esta instantánea se compone de las páginas de memoria física junto con el estado de la CPU.
Este estado se utiliza para configurar el estado inicial de una CPU virtual. Aprovechando la paginación bajo demanda, solo se leen de la instantánea las páginas necesarias para la ejecución de la función objetivo.
Debido a que utilizamos una máquina virtual dedicada con solo las páginas de memoria física útiles para la ejecución de la función objetivo, restaurar una instantánea es rápido.
Actualmente hay 2 backends disponibles:
WHVP aprovecha la API WHVP (Windows Hypervisor Platform) para proporcionar acceso a una partición de Hyper-V. Consulte
https://docs.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform
para más detalles.Bochs aprovecha el emulador Bochs
(https://bochs.sourceforge.io/)Se está desarrollando un backend KVM y debería estar disponible pronto.
Rewind ofrece 2 funcionalidades principales:
También proporciona una TUI básica (Interfaz de Usuario de Terminal) para reportar información útil sobre el fuzzing.
Ha sido probado en Windows y Linux (por ahora solo el backend bochs para Linux).
Siempre he disfrutado investigando vulnerabilidades de kernel, especialmente en el kernel de Windows. El proceso siempre implica una combinación de análisis estático y dinámico. Hacer análisis dinámico puede volverse tedioso rápidamente. El ciclo depurar / crash / reiniciar / restablecer todos los puntos de interrupción es lento y doloroso. Cuando quieres hacer algo de fuzzing, a menudo requiere configurar una o varias máquinas virtuales además de un depurador de kernel y crear algunos scripts improvisados para manejar la detección de crashes...
Hacer instantáneas con máquinas virtuales ayuda, pero es lento.
Durante 2018, Microsoft presentó un nuevo conjunto de API llamado Windows Hypervisor Platform (WHVP). Estas API permiten configurar una partición (VM en la jerga de Hyper-V) con algunos procesadores virtuales y tener control sobre los VM exits que ocurren en la máquina virtual. Es casi como tener tu propio manejador de VM-exit en el espacio de usuario. Muy útil para hacer cosas interesantes, por ejemplo Simpleator o applepie.
Así que empecé a jugar con WHVP e hice un primer PoC que me permitía ejecutar algo de shellcode en una partición de Hyper-V. Estaba escrito en Python y era bastante lento. Este primer PoC evolucionó rápidamente hacia una especie de trazador basado en instantáneas. Quería algo para arrancar la CPU virtual y que fuera bastante fácil de configurar. Dado que ya estaba usando un depurador de kernel para jugar con mi objetivo, decidí usar volcados de kernel hechos con WinDbg como instantánea. Con eso solo necesitaba configurar una partición con una CPU virtual. El contexto de la CPU virtual se establece con el contexto tomado del volcado. Cada vez que la CPU virtual necesita una página física, uso las del volcado.
Con esto pude bifurcar (fork) el estado del volcado en una partición y luego reanudar la ejecución. Me permitió trazar fácilmente la ejecución de mi función objetivo. Al modificar los argumentos y revertir el estado de memoria de la partición, también era muy fácil hacer fuzzing al objetivo.
Este trabajo se presentó en la conferencia SSTIC en 2020 y se publicó en github.
La herramienta implementa 2 posibilidades para obtener la cobertura. La primera aprovecha el clásico TF (Trap Flag) para tener interrupciones INT1 en cada instrucción. Requiere modificar el objetivo y es lenta. Me habría gustado usar el trap flag MONITOR. Pero WHVP no ofrece esa posibilidad.
Para tener un rendimiento adecuado (requerido para el fuzzing), decidí reducir la precisión de la cobertura y añadir un modo en el que solo se sabe cuando una instrucción se ejecuta por primera vez.
Para ello, parcheo las páginas obtenidas de la instantánea con bytes 0xcc (solo para páginas ejecutables). Cuando la CPU ejecute estas instrucciones parcheadas, el hipervisor atrapará la excepción y reescribirá las instrucciones con el código original.
Es como tener un punto de interrupción de software único establecido en cada instrucción. Funciona el 95% de las veces, pero en piezas de código particulares (como las que tienen tablas de saltos) fallará porque los datos serán reemplazados.
Para superar esto, una opción sería desensamblar el código antes de mapearlo y parchear solo lo necesario (quizás la próxima vez).
Durante mi experimento encontré varias limitaciones al usar WHVP. Es lento, realmente lento. El código fuente de VirtualBox tiene algunos comentarios interesantes :)
Así que para tener un rendimiento adecuado realmente necesitas limitar los VM exits y es incompatible si quieres usar Hyper-V como hipervisor de trazado (ya que requiere muchos VM exits).
Al mismo tiempo empecé a usar bochs (especialmente la parte de instrumentación) para comprobar si los trazados obtenidos por la herramienta eran correctos. Bochs era una especie de oráculo para ver si tenía trazados divergentes.
Bochs es más rápido que WHVP al hacer un trazado completo y también tienes los beneficios de tener accesos a memoria además de otras utilidades útiles.
Decidí añadir bochs como otro backend. whvp ya no era un nombre apropiado y me decidí por rewind.

rewind fue diseñado en torno a mi propio flujo de trabajo cuando realizo evaluaciones de seguridad de controladores de kernel en la plataforma Windows.
El primer paso es instalar el software objetivo dentro de una máquina virtual. Como utilizo una combinación de análisis estático y dinámico, también configuraré un depurador de kernel.
Después de abrir algunos controladores al azar en IDA, rápidamente empezaré a apuntar a algunas funciones. Para ello suelo poner algunos puntos de interrupción con windbg y, combinado con ret-sync, puedo empezar a jugar.
Ahí es donde rewind entra en juego. En lugar de editar buffers aleatorios en memoria, ejecutar paso a paso y anotar el IDB para tener una idea aproximada de lo que está sucediendo, tomaré una instantánea con windbg y usaré rewind.
Facilitará mucho el proceso. Tener una instantánea ofrece muchas ventajas. Todo es determinista. Puedes reproducir una llamada a función hasta la saciedad. Puedes lanzar un fuzzer si la función objetivo parece interesante. Incluso puedes cerrar la VM, ya que ya no es necesaria.
Obviamente necesitas Rust (instalación probada en Windows y Linux con Rust 1.50). CMake también es necesario para algunas dependencias.
Primero clona el repositorio:
$ git clone [email protected]:quarkslab/rewind.git
Continúa con la instalación del backend bochs
Clona el repositorio bochscpu (https://github.com/yrp604/bochscpu) en el directorio vendor:
$ cd vendor
$ git clone https://github.com/yrp604/bochscpu
Descarga los artefactos de bochs precompilados desde bochscpu-build (https://github.com/yrp604/bochscpu-build)
$ curl.exe -L --output bochs-x64-win.zip [artifact_url]
Extrae las carpetas lib y bochs dentro del checkout de bochscpu.
$ Expand-Archive -Path .\bochs-x64-win.zip -DestinationPath .\
$ copy -Recurse .\bochs-x64-win\msvc\* .\bochscpu\
En Windows, WHVP también se compilará como backend.
En una sesión de PowerShell elevada, usa el siguiente comando para comprobar si WHVP está habilitado:
Get-WindowsOptionalFeature -FeatureName HypervisorPlatform -Online
FeatureName : HypervisorPlatform
DisplayName : Windows Hypervisor Platform
Description : Enables virtualization software to run on the Windows hypervisor
RestartRequired : Possible
State : Enabled
CustomProperties :
Si no está habilitado, puedes usar el cmdlet Set-WindowsOptionalFeature para habilitarlo. También necesitarás habilitar Hyper-V.
También necesitas tener instalado un Windows SDK (10.0.19041.0). Puedes descargarlo desde https://developer.microsoft.com/fr-fr/windows/downloads/windows-10-sdk/.
Necesitas instalar LLVM y establecer la variable de entorno LIBCLANG_PATH (requerida por bindgen)
Consulta https://rust-lang.github.io/rust-bindgen/requirements.html para una explicación detallada.
$ $env:LIBCLANG_PATH="C:\Program Files\LLVM\bin"
A partir de ahí deberías poder compilar rewind (se requiere nightly por los unwind_attributes en la crate bochscpu):
$ cd rewind_cli
$ cargo +nightly build --release
El binario rewind estará disponible en el directorio target/release.
También puedes usar cargo para instalar localmente:
$ cd rewind_cli
$ cargo +nightly install --path .
> error: failed to run custom build command for `zydis v3.1.1`
whvp-sys fallará al compilarEn el directorio examples se proporciona un tutorial básico que aprovecha CVE-2020-17087
Consulta TODO.md
hit, el trazador se comporta mal en algunas funciones (es el caso de algunas tablas de switch). La razón es que cada byte es reemplazado por puntos de interrupción de software (incluidos los datos si están presentes en una página ejecutable). Una mejor manera de hacerlo sería obtener la lista de todos los bloques básicos de un desensamblador, por ejemplo.Esta herramienta está actualmente desarrollada y patrocinada por Quarkslab bajo la licencia Apache 2.0.
Un saludo a @yrp604, @0vercl0k, Alexandre Gazet por su ayuda, comentarios y reflexiones. ¡Gracias también a todos mis colegas de Quarkslab!