
Herramienta de instrumentación binaria estática para ejecutables de Windows x64
peafl64 es una herramienta de instrumentación estática para PE x64 en Windows.
La instrumentación estática consiste en editar archivos ejecutables y añadir código en ubicaciones específicas de los mismos.
La instrumentación añade código al inicio de cada bloque básico del binario, registrando el flujo de ejecución de forma compatible con AFL.
Nos permite hacer fuzzing de binarios en modo usuario (con WinAFL) y en modo kernel (con kAFL) sin acceso a su código fuente.
Existen otras formas de hacer fuzzing sobre binarios de Windows; en este proyecto decidimos centrarnos en la instrumentación estática porque es el método más rápido.
Este proyecto se basa en la herramienta pe-afl de wmliang, con soporte añadido para x64.
El script de instrumentación requiere un análisis de IDA como entrada.
Para crearlo, ejecuta el script ida_dumper.py proporcionado en IDA.
El script requiere IDA 7+ y python3.8+.
usage: pe_afl.py [-h] [-n] [-cb] [-tf] [-te THREAD_ENTRY] [-nt NTOSKRNL] [-e ENTRY] [-l ENLARGE] [-v] [-lf] pefile ida_dump
positional arguments:
pefile Target PE file for instrumentation
ida_dump dump.json from IDA (created by ida_dumper.py)
optional arguments:
-h, --help show this help message and exit
-n, --nop Instrument with NOPs for testing
-cb, --callback Instrument with a callback, which is in the helper driver that's written in C
-tf, --thread-filter Driver instrumentation that filters only on thread ID (must use "-te" with this option)
-te THREAD_ENTRY, --thread-entry THREAD_ENTRY
The address (RVA) of the thread's initialization function
-nt NTOSKRNL, --ntoskrnl NTOSKRNL
ntoskrnl.exe path for offset extraction (non-optional if instrumenting a driver)
-e ENTRY, --entry ENTRY
Inject code on entry point, ie. -e9090
-l ENLARGE, --enlarge ENLARGE
Enlarge factor for sections, default=4
-v, --verbose Print debug log
-lf, --logfile Print log to pe-afl64.log rather than stream
Instrumentando un binario en modo usuario con NOPs
PS pe-afl-64> python .\pe_afl.py -n C:\Work\cmd.exe C:\Work\cmd.exe.dump.json
[*] User-mode binary is being instrumented
[*] Single-thread instrument is on
[*] Preparing new sections
[*] Added section .text^
[*] Added section .cov
[*] Expanding relative jumps
[*] Expanded 3874 of 14353 branches
[*] Building address map
[*] Updating relative instructions
[*] Updating relocations...
[*] Updating Export table
[*] Updating load config
[*] Updating exception records
[*] Updating the PE headers
[*] Finalizing...
[*] Creating instrumented code
[*] Writing address mapping to C:\Work\cmd.exe.mapping.txt
[*] Updating jump tables
[*] Updated .text
...
[*] Removing temporary PE files
[*] Fixing PE checksum
[*] Instrumented binary saved to: C:\Work\cmd.instrumented.exe
Instrumentando un binario en modo kernel con filtrado por ID de proceso
python .\pe_afl.py -l 6 -nt "C:\Work\ntoskrnl.exe" "C:\Work\driver.sys" "C:\Work\driver.sys.dump.json"
Instrumentando un binario en modo kernel con filtrado por ID de hilo y salida detallada
python .\pe_afl.py -v -tf -te 0x40000 -l 6 -nt "C:\Work\ntoskrnl.exe" "C:\Work\ntoskrnl.exe" "C:\Work\ntoskrnl.exe.dump.json"
Primero tendrás que comprobar si tu máquina arranca con BIOS o UEFI.
Para máquinas Hyper-V: las máquinas de Gen 1 están basadas en BIOS y las de Gen 2 en UEFI.
Si tu máquina está basada en BIOS, necesitas parchear winload.exe:
ImgpValidateImageHash en ellamov eax, edi por xor eax, eax en el último bloque de código de la funciónbcdedit /set path \Windows\system32\winload2.exeSi tu máquina arranca con UEFI, usa la utilidad EfiGuard para parchear winload.efi.
Hoja de referencia de comandos si usas Hyper-V Manager:
FS1:
cd EFI/Boot
Load EfiGuard.Dxe.efi
FS0:
\EFI\Boot\bootx64.efi
A continuación, instrumenta el controlador que elijas. Para cargar un controlador instrumentado en una máquina Windows debe estar firmado, y un certificado de autofirma es suficiente para cumplir los requisitos del sistema operativo:
# In elevated powershell terminal
$c = New-SelfSignedCertificate -Type CodeSigningCert -KeyUsage DigitalSignature -Subject 'CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US'
Set-AuthenticodeSignature .\driver.instrumented.sys -Certificate $c -Force
Si el controlador que instrumentaste ya es utilizado por el sistema, usa estos comandos (como administrador) para reemplazar el archivo del controlador:
set NAME=mydriver.sys
icacls %NAME% /save C:\windows\temp\%NAME%.icacls
takeown /F %NAME%
icacls %NAME% /grant Everyone:F
move %NAME% %NAME%.bak
move instrumented_driver.sys %NAME%
icacls . /restore C:\windows\temp\%NAME%.icacls
Después ejecuta:
bcdedit /set recoveryenabled no
bcdedit /set nointegritychecks on
shutdown -t 0 -r
La integración con WinAFL se realiza compilando un harness con las cabeceras proporcionadas.
Junto a las cabeceras se incluye example.c, un programa de ejemplo que muestra cómo usarlas.
Las cabeceras proporcionadas son una ligera modificación de las que ya incluye WinAFL para integrarse con
otra herramienta de instrumentación estática de binarios llamada Syzygy.
La forma en que nos integramos con kAFL es bastante sencilla.
Normalmente, un harness de kAFL se ejecuta en una máquina virtual y se comunica con el frontend del fuzzer mediante "hypercalls" especiales.
Estas hypercalls le indican al fuzzer que haga muchas cosas, entre ellas cargar los datos de cobertura de IntelPT y parsearlos como un bitmap de AFL.
Debido a que peafl64 hace obsoleto el rastreo con IntelPT, debemos preparar una forma de transmitir los datos de cobertura al fuzzer.
Por lo tanto, ampliamos qemu y kvm con "hypercalls" que permiten que el harness (en modo usuario) que se ejecuta en una VM envíe los datos de cobertura que recopiló mediante el controlador auxiliar.
Esto se refiere específicamente a la configuración en ESXi, pero debería ser relevante para otras plataformas de virtualización como AWS.
La configuración es bastante sencilla:
install.sh qemu, ejecuta install.sh qemu_sbiPara hacer fuzzing con kAFL y peafl64, necesitamos preparar una máquina de fuzzing:
Por lo demás, hacer fuzzing con nuestro fork de kAFL es igual que con el kAFL normal.
Resumen de pasos:
Primero, usamos IDA para encontrar y analizar las instrucciones y ubicaciones de interés.
El análisis crea un archivo dump.json que contiene toda esa información en formato json.
instrument.py contiene la lógica de instrumentación, y el flujo en sí está descrito en la función process_pe.
Para instrumentar el binario, duplicamos sus secciones ejecutables, donde se encontrará todo el código instrumentado.
A continuación, procesamos todas las instrucciones relativas y determinamos cómo y si necesitamos gestionarlas.
Supongamos que tenemos un jmp corto de la dirección X a la dirección Y. Insertamos código de instrumentación entre X e Y, y ahora el destino está en la dirección Z (Z = Y + len(instrumentation)).
Eso significa que tenemos que modificar el salto.
Si la dirección Z queda ahora fuera del rango de los jmp cortos, necesitamos transformar el salto de short a far. (instrument.py:expand_relative_instructions)
Por ejemplo:
short jmp 0x57 (eb 55) -> near jmp 0x604 (e9 ff 05 00 00)Ese también es el caso de instrucciones como call, loop y saltos condicionales.
Otro caso común que requiere tratamiento son las instrucciones relativas a rip introducidas en ensamblador x64:
call [rip+0x1000]Actualizamos los siguientes elementos para que apunten al código instrumentado: tabla de reubicaciones, tabla de exportaciones, configuración de carga, el directorio TLS y los registros de excepciones.
Las actualizaciones se realizan reasignando todas las direcciones de las secciones originales a sus contrapartidas instrumentadas. (instrument.py:update_addr)
Las cabeceras se actualizan para reflejar los cambios en la estructura del PE: secciones añadidas, punto de entrada modificado y tamaño del PE. (instrument.py:update_pe_headers)
Además, peafl64 es capaz de instrumentar el kernel de Windows. Para lograrlo, parsea y actualiza la tabla de reubicación de valores dinámicos (DVRT) y la SSDT, y maneja las particularidades de PatchGuard.
La DVRT es una tabla generada por el compilador que describe las ubicaciones de las direcciones que deben cambiarse al cargar el PE.
Se utiliza para mejorar KASLR y ayuda a mitigar la vulnerabilidad Spectre (1, 2, 3).
La DVRT se parsea mediante un conjunto de clases que imitan la estructura de la tabla (drt.py; instrument.py:get_updated_dynamic_relocs)
Gestionar y actualizar la SSDT no fue tan sencillo.
Su dirección no está exportada, así que tuvimos que depender de símbolos o de heurísticas para localizarla.
Nuestra solución utiliza un enfoque heurístico, usando NtWaitForSingleObject como marcador constante para todas las versiones de Windows 10 y encontrando la dirección de la SSDT de forma relativa a él. (instrument.py:ntoskrnl_update_KiServiceTable)
Este enfoque funciona para todos los kernels de Windows 10, pero deberá ajustarse para otras versiones de kernel.