
Outil d'instrumentation binaire statique pour exécutables Windows x64
peafl64 est un outil d'instrumentation statique pour les PE x64 sous Windows.
L'instrumentation statique consiste à modifier des fichiers exécutables en ajoutant du code à des emplacements précis.
L'instrumentation ajoute du code au début de chaque bloc de base du binaire, enregistrant le flux d'exécution d'une manière compatible avec AFL.
Elle permet de fuzzer des binaires en mode utilisateur (avec WinAFL) et en mode noyau (avec kAFL) sans avoir accès à leur code source.
Il existe d'autres méthodes pour fuzzer les binaires Windows ; dans ce projet, nous avons choisi de nous concentrer sur l'instrumentation statique car c'est la méthode la plus rapide.
Ce projet s'appuie sur l'outil pe-afl de wmliang, avec l'ajout du support x64.
Le script d'instrumentation nécessite une sortie d'analyse IDA.
Pour la créer, exécutez le script ida_dumper.py fourni dans IDA.
Le script requiert IDA 7+ et 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
Instrumentation d'un binaire en mode utilisateur avec des 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
Instrumentation d'un binaire en mode noyau avec filtrage par ID de processus
python .\pe_afl.py -l 6 -nt "C:\Work\ntoskrnl.exe" "C:\Work\driver.sys" "C:\Work\driver.sys.dump.json"
Instrumentation d'un binaire en mode noyau avec filtrage par ID de thread et sortie détaillée
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"
D'abord, vous devez vérifier si votre machine démarre avec BIOS ou UEFI.
Pour les machines Hyper-V : les machines de génération 1 utilisent le BIOS, et les machines de génération 2 utilisent l'UEFI.
Si votre machine utilise le BIOS, vous devez patcher winload.exe :
ImgpValidateImageHashmov eax, edi par xor eax, eax dans le dernier bloc de code de la fonctionbcdedit /set path \Windows\system32\winload2.exeSi votre machine démarre avec UEFI, utilisez l'utilitaire EfiGuard pour patcher winload.efi.
Aide-mémoire des commandes si vous utilisez le Gestionnaire Hyper-V :
FS1:
cd EFI/Boot
Load EfiGuard.Dxe.efi
FS0:
\EFI\Boot\bootx64.efi
Instrumentez ensuite le pilote de votre choix. Pour charger un pilote instrumenté sur une machine Windows, il doit être signé, et un certificat auto-signé suffit pour satisfaire aux exigences de l'OS :
# 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 le pilote que vous avez instrumenté est déjà utilisé par le système, utilisez ces commandes (en tant qu'administrateur) pour remplacer le fichier du pilote :
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
Puis exécutez :
bcdedit /set recoveryenabled no
bcdedit /set nointegritychecks on
shutdown -t 0 -r
L'intégration avec WinAFL se fait en compilant un harnais à l'aide des en-têtes fournis.
En plus des en-têtes, il y a example.c, un programme d'exemple qui montre comment les utiliser.
Les en-têtes fournis sont une légère modification des en-têtes déjà fournis par WinAFL pour intégrer un autre outil d'instrumentation statique de binaires appelé Syzygy.
La manière dont nous nous intégrons avec kAFL est assez simple.
Normalement, un harnais kAFL s'exécute sur une machine virtuelle et communique avec le frontend du fuzzer via des « hypercalls » spéciaux.
Ces hypercalls indiquent au fuzzer de faire de nombreuses choses, parmi lesquelles charger les données de couverture d'IntelPT et les analyser sous forme de bitmap AFL.
Parce que peafl64 rend le traçage IntelPT obsolète, nous devons préparer une méthode pour transmettre les données de couverture au fuzzer.
Par conséquent, nous avons étendu qemu et kvm avec des « hypercalls » qui permettent au harnais (en mode utilisateur) exécuté dans une VM d'envoyer les données de couverture qu'il a collectées via le pilote auxiliaire.
Ceci concerne spécifiquement la configuration sur ESXi, mais devrait être pertinent pour d'autres plateformes de virtualisation comme AWS.
La configuration est assez simple :
install.sh qemu, exécutez install.sh qemu_sbiPour fuzzer avec kAFL et peafl64, nous devons configurer une machine de fuzzing :
À part cela, le fuzzing avec notre fork de kAFL est identique au fuzzing normal avec kAFL.
Aperçu des étapes :
D'abord, nous utilisons IDA pour trouver et analyser les instructions et les emplacements d'intérêt.
L'analyse crée un fichier dump.json qui contient toutes ces informations au format json.
instrument.py contient la logique d'instrumentation, et le flux lui-même est décrit dans la fonction process_pe.
Pour instrumenter le binaire, nous dupliquons ses sections exécutables, où tout le code instrumenté se trouvera.
Ensuite, nous traitons toutes les instructions relatives et déterminons comment, et si, il faut les gérer.
Supposons un saut court de l'adresse X vers l'adresse Y. Nous insérons du code d'instrumentation entre X et Y, et la cible se trouve maintenant à l'adresse Z (Z = Y + len(instrumentation)).
Cela signifie que nous devons modifier le saut.
Si l'adresse Z est maintenant hors de la portée des sauts courts, nous devons transformer le saut de short en far. (instrument.py:expand_relative_instructions)
Par exemple :
short jmp 0x57 (eb 55) -> near jmp 0x604 (e9 ff 05 00 00)C'est également le cas pour les instructions comme call, loop et les sauts conditionnels.
Un autre cas courant à gérer est celui des instructions relatives à RIP introduites en assembleur x64 :
call [rip+0x1000]Nous mettons à jour les éléments suivants pour pointer vers le code instrumenté : table de relocalisation, table d'exportation, configuration de chargement, répertoire TLS et enregistrements d'exceptions.
Les mises à jour sont effectuées en remplaçant toutes les adresses des sections d'origine par leurs homologues instrumentés. (instrument.py:update_addr)
Les en-têtes sont mis à jour pour refléter les modifications de la structure PE : sections ajoutées, point d'entrée modifié et taille PE. (instrument.py:update_pe_headers)
De plus, peafl64 est capable d'instrumenter le noyau Windows. Pour y parvenir, il analyse et met à jour la Dynamic Value Relocation Table (DVRT) et la SSDT, et gère les spécificités de PatchGuard.
La DVRT est une table générée par le compilateur qui décrit les emplacements des adresses qui doivent être modifiées lors du chargement du PE.
Elle est utilisée pour améliorer KASLR et aide à atténuer la vulnérabilité Spectre (1, 2, 3).
La DVRT est analysée à l'aide d'un ensemble de classes qui imitent la structure de la table (drt.py; instrument.py:get_updated_dynamic_relocs)
La gestion et la mise à jour de la SSDT n'étaient pas aussi simples.
Son adresse n'est pas exportée, nous avons donc dû nous appuyer soit sur les symboles, soit sur des heuristiques pour la localiser.
Notre solution utilise une approche heuristique, en utilisant NtWaitForSingleObject comme marqueur constant pour toutes les versions de Windows 10 et en trouvant l'adresse de la SSDT relativement à celui-ci. (instrument.py:ntoskrnl_update_KiServiceTable)
Cette approche fonctionne pour tous les noyaux Windows 10, mais devra être ajustée pour les autres versions de noyau.