
Suite de fuzzing in-process basée sur Frida avec proxy AFL++, modes autonomes actif/passif, et communication par mémoire partagée pour une découverte de vulnérabilités guidée par la couverture et haute performance sur plusieurs plateformes.
fpicker est une suite de fuzzing basée sur Frida qui offre une variété de modes de fuzzing pour le fuzzing in-process, comme un mode AFL++ ou un mode de traçage passif. Il devrait fonctionner sur toutes les plateformes supportées par Frida.
Des informations de contexte ainsi que les réflexions et idées derrière fpicker peuvent être trouvées dans un article de blog que j'ai écrit.
Fpicker est basé sur des travaux antérieurs sur ToothPicker, développé pendant mon mémoire de master. La majeure partie de fpicker a été développée pendant les heures de travail chez mon employeur (ERNW).
Requis pour exécuter fpicker :
frida-core-devkit pour la plateforme respective disponible sur les versions de Frida sur GitHub.
libfrida-core-ios.a, libfrida-core-macos.a ou libfrida-core-linux.a.frida-core.h). Stockez-les sous frida-core-linux.h ou frida-core-ios.h selon la plateforme.update_frida_version.sh avant la compilation.Requis uniquement en mode AFL++ :
CFLAGS="-DUSEMMAP=1".CFLAGS="-DUSEMMAP=1 -DTARGET_OS_IOS".Fpicker peut être compilé pour macOS, iOS ou Linux. Le Makefile ne supporte actuellement que la compilation pour iOS sur macOS, mais il devrait être tout à fait possible de compiler fpicker en utilisant une chaîne d'outils iOS sous Linux.
Selon la cible souhaitée, exécutez :
make fpicker-macos
make fpicker-ios
make fpicker-linux
pour compiler fpicker.
Une fois fpicker compilé, le harnais de fuzzing doit être construit ensuite :
Voir le dossier examples pour différents exemples de cas de fuzzing. L'approche générale est la suivante :
examples/test/test.js) (voir ici pour plus d'informations sur les harnais)frida-compile test.js -o harness.jsMaintenant fpicker peut commencer le fuzzing. La commande exacte dépend fortement de la configuration et de la mise en place. Ci-dessous, quelques exemples de cas sont donnés. Ceux-ci correspondent principalement aux exemples du dossier examples.
afl-fuzz -i examples/test-network/in -o ./examples/test-network/out -- \\
./fpicker --fuzzer-mode afl -e attach -p test-network -f ./examples/test-network/harness.js
./fpicker --fuzzer-mode standalone -e attach -p server-process -f harness.js --input-mode cmd \\
--command "./client-send @@" -i indir -o outdir
./fpicker --fuzzer-mode active --communication-mode shm -e attach -p server-process -f harness.js \\
-i indir -o outdir --standalone-mutator cmd --mutator-command "radamsa"
./fpicker --fuzzer-mode passive --communication-mode send -e attach -p server-process -o outdir -f harness.js
./fpicker --fuzzer-mode active -e attach -p test -D remote -o examples/test/out/ -i examples/test/in/ \\
-f fuzzer-agent.js --standalone-mutator cmd --mutator-command "radamsa"
Chaque cible nécessite son propre harnais de fuzzing. La partie la plus importante de ce harnais est la définition de la fonction d'entrée du Stalker de Frida, qui détermine effectivement à quel point l'instrumentation est insérée. En mode in-process, c'est simple. La fonction est généralement celle qui est appelée à chaque itération de fuzzing. Cependant, ce pourrait être une autre.
Un exemple d'implémentation minimaliste de harnais (en mode command) pourrait être celui-ci :
// Import the fuzzer base class
const Fuzzer = require("harness/fuzzer.js");
// The custom fuzzer needs to subclass the Fuzzer class to work properly
class TestFuzzer extends Fuzzer.Fuzzer {
constructor() {
// The constructor needs to specify the address of the targeted function and a NativeFunction
// object that can later be called by the fuzzer.
const FUZZ_FUNCTION_ADDR = Module.getExportByName(null, "FUZZ_FUNCTION");
const FUZZ_FUNCTION = new NativeFunction(
FUZZ_FUNCTION_ADDR,
"void", ["pointer", "int64"], {
});
super("test", FUZZ_FUNCTION_ADDR, FUZZ_FUNCTION);
}
}
const f = new TestFuzzer();
exports.fuzzer = f;
Ce harnais configure l'instrumentation pour suivre la fonction FUZZ_FUNCTION. L'instrumentation démarre lorsque cette fonction est entrée et s'arrête lorsque la fonction retourne. Cette fonction doit être choisie avec soin car elle est coûteuse et plus les parties (potentiellement sans importance) du processus sont instrumentées, plus le fuzzer est lent. Bien sûr, c'est un compromis entre vitesse et couverture souhaitée. De plus, le fuzzer ne supporte actuellement que les fonctions qui ne sont entrées qu'une seule fois par itération de fuzzing, c'est-à-dire que la fonction ne doit pas être appelée plus d'une fois par cas de fuzz, sinon les informations de couverture pourraient devenir peu fiables.
Lorsque le mode in-process est utilisé, une autre fonction est requise dans le script du fuzzer. La méthode fuzz. Elle sera appelée à chaque itération. Elle sera appelée avec deux paramètres : un pointeur vers un tampon et la longueur du tampon. Notre fonction cible exemple prend deux paramètres : un pointeur vers un tampon et sa longueur. Ainsi, nous pouvons simplement passer les paramètres que nous recevons dans la méthode fuzz.
fuzz(payload, len) {
this.target_function(payload, parseInt(len));
}
En mode passif, un callback doit être spécifié pour traiter les données requises. Le fuzzer s'attend à recevoir un tampon de charge utile et sa longueur. Selon la fonction cible fuzzée, ces données doivent être extraites. Dans l'exemple suivant, nous avons à nouveau une fonction qui a deux paramètres : un pointeur vers un tampon et sa longueur. Le paramètre args contient tous les paramètres potentiels que la fonction cible reçoit, donc le paramètre de longueur (qui est le deuxième dans notre cas) peut être accédé avec args[1]. Nous lisons ensuite le tampon comme Uint8Array et le renvoyons au fuzzer en utilisant la méthode sendPassiveCorpus.
passiveCallback(args) {
const len = args[1];
const data = new Uint8Array(Memory.readByteArray(args[0], parseInt(len)));
// this encodes the data and sends it back to the fuzzer
this.sendPassiveCorpus(data, len);
}
Si la cible a besoin d'une sorte de préparation avant que le fuzzer ne démarre, fpicker fournit une méthode prepare qui est appelée lors de l'initialisation du fuzzer. La préparation pourrait être l'établissement d'un état, par exemple en instanciant un objet. Une telle fonction de préparation pourrait ressembler à ceci :
prepare() {
// the object can be attached to the fuzzer instance so that it can be used within the
// fuzz() method later on.
this.required_object = call_native_function_that_creates_object();
}
fpicker offre un large ensemble de modes et de configurations qui sont expliqués ci-dessous. La plupart de ces modes peuvent être combinés de différentes manières. À la fin de cette section se trouve un tableau indiquant quelles options peuvent être combinées et leur statut d'implémentation.
Fpicker dispose de trois modes de fuzzing différents : le mode AFL++, le mode Actif Autonome et le mode Passif Autonome :
Mode AFL++ : En mode AFL++, fpicker agit comme un proxy entre AFL++ et le processus cible. En utilisant les capacités d'instrumentation de Frida, le bitmap de couverture d'AFL est rempli pendant que la cible est fuzzée avec des données d'entrée générées par AFL++.
Mode Actif Autonome : En mode actif autonome, le fuzzer utilise les résumés d'appels Stalker de Frida pour collecter la couverture sous forme de blocs de base exécutés pendant une itération. Ce n'est pas nouveau et a été implémenté sous diverses formes auparavant. Cependant, en combinaison avec certains autres réglages du fuzzer, cela peut avoir divers avantages. C'est aussi une bonne alternative si AFL++ n'est pas applicable ou souhaité dans un environnement ou un cas donné.
Mode Passif Autonome : Le mode passif est moins un fuzzer qu'un traceur. Essentiellement, il fait la même chose que le mode actif autonome. Cependant, il n'envoie pas ses propres entrées. Il s'attache simplement à une certaine fonction et collecte la couverture. Une fois qu'une nouvelle couverture est observée, la couverture et l'entrée sont stockées.
Bien que fpicker soit conçu principalement comme un fuzzer in-process, il supporte également le fuzzing via une commande externe. Pour cela, fpicker offre deux modes d'entrée.
Mode d'entrée In-Process : En mode d'entrée in-process, le harnais appelle directement une fonction spécifiée dans le processus cible. Le fuzzer envoie la charge utile au harnais et le harnais prépare la charge utile de manière à pouvoir appeler la fonction ciblée.
Mode d'entrée CMD : En mode d'entrée par commande, la charge utile est redirigée vers une commande externe. Ceci est utile lorsqu'il est trop complexe de préparer les paramètres ou un autre état en appelant directement la fonction cible. La collecte de couverture doit toujours être attachée à une certaine fonction. Peut-être y a-t-il un client qui peut recevoir une charge utile qui déclenche ensuite la fonction cible.
Le mode de communication détermine comment le harnais injecté communique avec le fuzzer. Cela dépend largement de l'application cible. Frida offre une API pour envoyer et recevoir des messages depuis le script agent injecté. Ce type de communication est assez coûteux. L'un des facteurs est que le message transporté doit être encodé en JSON. Envoyer des données binaires est donc simple. Pour cette raison, fpicker propose un second mode de communication via la mémoire partagée. Cependant, cela ne fonctionne que s'il est possible d'établir une mémoire partagée entre le fuzzer et l'application cible, ce qui signifie que ce mode ne peut pas être utilisé lorsque la cible est attachée à l'hôte du fuzzer via USB. En mode d'entrée CMD, le mode de communication ne concerne que la manière dont les informations de couverture sont renvoyées au fuzzer, et non la manière dont la charge utile est envoyée, car cela est délégué à une commande externe.
Mode de communication Envoi : En mode de communication par envoi, la charge utile est envoyée en utilisant le mécanisme d'appel RPC de Frida. Cela permet au fuzzer d'exécuter une fonction JavaScript dans le script du harnais injecté. Cette fonction dans le harnais peut alors effectuer toutes les préparations nécessaires pour appeler la fonction cible. Une fois que la fonction cible est retournée, la collecte de couverture s'arrête et le harnais peut signaler au fuzzer que l'itération est terminée. Cela se fait en renvoyant les informations de couverture au fuzzer via l'API send de Frida.
Mode de communication SHM : En mode de communication SHM, le fuzzer et le script du harnais communiquent via la mémoire partagée et des sémaphores. Un tampon en mémoire partagée est utilisé pour envoyer la charge utile et recevoir les informations de couverture. Au lieu d'envoyer et de recevoir, les deux composants utilisent l'attente et le signalement du sémaphore. Selon le système et la cible, cela apporte des gains de performance non négligeables. Surtout parce que la charge utile binaire est écrite en mémoire une fois et n'a pas besoin d'être encodée, décodée ou copiée dans d'autres emplacements mémoire. Malheureusement, ce mode entraîne parfois une faible stabilité lors de l'exécution avec AFL++. On ne sait pas encore pourquoi.
Le mode d'exécution peut être soit spawn (lancement) soit attach (attachement). C'est assez explicite. fpicker peut soit s'attacher à un processus en cours, soit lancer un processus. Une différence majeure entre les deux modes est que, si la cible attachée plante, fpicker n'essaiera pas de la relancer.
En mode autonome, fpicker propose trois stratégies de mutation d'entrée différentes. Pour le dire gentiment, la mutation d'entrée a certainement beaucoup de marge d'amélioration.
Mutateur autonome NULL : Ce mutateur ne mute pas la charge utile et renvoie simplement une copie de la même charge utile. Principalement à des fins de test. Sinon, pas vraiment utile.
Mutateur autonome Rand : Un très mauvais mutateur aléatoire. Tout ce qu'il fait est de remplacer aléatoirement des valeurs à des emplacements aléatoires dans la charge utile originale. Il ne modifie pas la longueur de la charge utile.
Mutateur autonome personnalisé : Ce mutateur peut appeler une commande externe pour muter les charges utiles. Il écrit la charge utile sur stdin et reçoit la charge utile mutée depuis stdout. En raison de son implémentation superficielle, il a un impact non négligeable sur les performances.
En utilisant l'option de périphérique -D usb, Frida sélectionnera le premier périphérique USB local, par exemple un iPhone ou un téléphone Android.
Avec l'option -D remote, il est possible de fuzzer un processus tournant sur un périphérique réseau. Pour cela, le périphérique distant doit exécuter frida-server. Comme exemple de configuration, utilisez SSH avec redirection de port pour lier le port d'écoute par défaut 27042 de frida-server sur le périphérique distant à une socket sur le client local.
ssh -N [email protected] -L 127.0.0.1:27042:127.0.0.1:27042
Sur un iPhone, on peut aussi utiliser iproxy pour rediriger le port depuis une connexion USB. Cela peut être particulièrement utile si l'on exécute Frida sur un port non standard sur un appareil non jailbreaké avec le gadget Frida. Lorsqu'on travaille avec le gadget Frida, le seul processus disponible aura le nom Gadget, quel que soit le nom de l'application cible.
iproxy 27042 27042
Utilisez ensuite frida-ps pour valider la configuration en listant les processus sur le périphérique distant :
frida-ps -R