Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
blackbox-fuzzing — Fuzzing des appareils IoT avec le routeur TL-WR902AC comme exemple | Kitploit
Outils/GitHubGitHub/otsmr/blackbox-fuzzing
Sécurité IoTAnalyse des VulnérabilitésExploitationRétro-ingénierieFuzzingAnalyse de BinairesArticles et RechercheApprentissage et ÉducationAnalyse de Micrologiciel
GitHubotsmr/blackbox-fuzzing

blackbox-fuzzing

Fuzzing des appareils IoT avec le routeur TL-WR902AC comme exemple

13217il y a 9 moisVérifié par Kitploit

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
Voir le dépôtSite web

Fuzzing en boîte noire d'appareils IoT avec le routeur TL-WR902AC comme exemple

Ceci est la version HTML de mon mémoire qui peut être téléchargé en PDF ici.

Introduction

Le fuzzing est devenu « l'un des moyens les plus efficaces » de trouver des bugs dans les logiciels. C'est par cette affirmation ou des affirmations similaires que commencent de nombreux articles récents liés au fuzzing [google-scholar]. L'objectif principal de notre précédent mémoire sur le thème « Internet of Vulnerable Things » était de trouver un bug lié à la mémoire, puis d'écrire un exploit pour cette vulnérabilité. Nous avons réussi à trouver une vulnérabilité en rétro-ingéniant le firmware, mais aucun bug lié à la mémoire n'a été trouvé. Trouver un débordement de tampon en rétro-ingéniant un binaire à la main est non seulement chronophage, mais nécessite également beaucoup d'expérience. Le fuzzing vise quant à lui à être le « moyen le plus efficace » de trouver ce type de vulnérabilités liées à la mémoire. Google, par exemple, a introduit OSS-Fuzz, qui fuzze en continu des logiciels open source et a déjà trouvé plus de 10 000 vulnérabilités dans 1 000 projets [oss-fuzz].

L'objectif de ce mémoire est à nouveau de trouver une vulnérabilité liée à la mémoire, mais cette fois en utilisant le fuzzing. La vulnérabilité visée doit être exploitable sur le réseau sans connaître les identifiants administrateur. Ce mémoire décrit la manière d'atteindre cet objectif. Pour cela, le mémoire est divisé en deux parties. La première partie se concentre sur la façon de trouver une cible pertinente, quels outils peuvent être utilisés et ce que doit comprendre une bonne cible de fuzzing. La seconde partie décrit ensuite comment développer et déboguer un harness capable de fuzzer une fonction spécifique d'un binaire. Ensuite, le harness développé est utilisé par AFL++ pour fuzzer la fonction cible. Dans ce qui suit, un bref contexte est présenté ainsi que l'état de l'art actuel en matière de fuzzing d'appareils IoT.

Tous les fichiers créés dans le cadre de ce mémoire sont également publiés intégralement sur GitHub et peuvent être consultés via l'URL suivante : otsmr/blackbox-fuzzing.

État de l'art

Fuzzer des appareils IoT n'est pas aussi simple que fuzzer un projet open source. Souvent, le code source est propriétaire, ce qui rend impossible le fuzzing en boîte grise, qui instrumente le code source pour obtenir les meilleures performances de fuzzing [afl-persistent]. De plus, l'architecture du processeur n'est souvent pas prise en charge nativement par les fuzzers, ce qui nécessite un émulateur comme QEMU [qemu], ce qui ralentit également la vitesse de fuzzing [afl-persistent]. Un autre problème concerne les périphériques matériels, ce qui complique le développement d'une approche générale. L'article « Embedded Fuzzing: A Review of Challenges, Tools, and Solutions » [embedded-fuzzing] donne un aperçu des différentes stratégies de fuzzing, comme le fuzzing embarqué basé sur le matériel. La plupart de ces stratégies nécessitent le code source du programme cible, par exemple lors du portage du code source du fuzzer, comme AFL, vers des appareils IoT basés sur ARM afin d'exécuter le fuzzer sur le matériel IoT. L'exécution du fuzzer sur le matériel de l'appareil pose également des problèmes de performances, car ces appareils ont souvent des processeurs bas de gamme, plus lents que les processeurs de bureau classiques. Une autre approche présentée dans cet article est le fuzzing embarqué basé sur l'émulation, où soit un seul programme ciblé est exécuté dans un émulateur pour effectuer un fuzzing guidé par la couverture, soit le système complet.

Les approches mentionnées ci-dessus ciblent toutes un binaire directement en utilisant un émulateur ou en instrumentant le code source. Ces approches nécessitent une configuration de fuzzing qui doit souvent être spécialement conçue pour un seul appareil IoT et sont difficiles à généraliser. Pour cela, les chercheurs ont créé un programme IoTFuzzer qui vise à être un cadre de fuzzing automatisé ayant pour objectif de « trouver des vulnérabilités de corruption mémoire sans accès à leurs images de firmware [iotfuzzer]. » IoTFuzzer repose sur l'observation que la plupart des appareils IoT disposent d'une application mobile pour les contrôler, et que ces applications contiennent des informations sur le protocole utilisé pour communiquer avec l'appareil. Le programme identifie et réutilise ensuite la logique spécifique au programme pour muter les cas de test afin de tester efficacement les cibles IoT [iotfuzzer].

Contexte

Harness

Un harness décrit une séquence d'appels API traitant les entrées fournies par le fuzzer. Contrairement à une application normale, qui ne nécessite souvent pas de harness, une bibliothèque qui implémente des fonctions réutilisables doit être appelée avec les bons paramètres et dans le bon ordre, afin que l'état entre plusieurs appels de fonctions partagées puisse être appelé. Fuzzer aléatoirement la bibliothèque sans construire la machine à états a peu de chances de réussir et, au contraire, créera beaucoup de faux positifs (crashes) lorsque les dépendances de la bibliothèque ne sont pas respectées. Cela peut se produire lorsque, par exemple, une vérification de taille de tampon est ignorée par le fuzzer, ce qui entraîne un débordement de tampon non pertinent.

Dans ce mémoire, des applications normales seront fuzzées, mais en raison des dépendances matérielles liées à l'utilisation des sockets et du multi-threading, nous devons également créer un harness pour elles. Le harness est chargé dans le contexte du binaire et peut appeler des fonctions internes du programme ciblé, comme le montre le Code 10.

Corpus

Le terme « corpus » désigne des échantillons d'entrée valides ou des cas de test et sert de référence fondamentale pour générer de nouvelles données d'entrée au cours du processus de fuzzing. Dans le Code 10, il s'agirait par exemple d'une requête HTTP. Les fuzzers utilisent ensuite ce corpus pour créer des cas de test mutés ou diversifiés, facilitant ainsi la détection de vulnérabilités logicielles grâce à l'exploration de différents scénarios d'entrée.

Trouver une cible pertinente

La partie la plus chronophage du fuzzing en boîte noire consiste à trouver une fonction potentiellement vulnérable dans le firmware. La première étape consiste à trouver des binaires intéressants qui, par exemple, sont accessibles sur le réseau, utilisent des fonctions non sécurisées ou ne disposent pas de fonctionnalités de sécurité comme le stack canary activé, qui est une protection contre les débordements de tampon. Notre précédent article ([iovt]) décrivait déjà comment extraire le firmware du routeur ciblé et comment trouver un binaire potentiellement dangereux. Pour cela, l'outil EMBA [emba] a été utilisé. EMBA classe tous les binaires trouvés dans le firmware en fonction du nombre de fonctions non sécurisées telles que strcpy, de l'accès réseau et des protections de sécurité comme le stack canary ou le bit NX, qui deviennent intéressants lors de l'exploitation d'un débordement de tampon, comme on peut le voir dans le Code 1.

```txt [+] STRCPY - top 10 results: 235 : libcmm.so : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | 77 : wscd : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | Networking | [snip] 28 : httpd : common linux file: yes | RELRO | No Canary | NX enabled | No Symbols | Networking | 27 : cli : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | ```

Code 1 : résultat des EMBAs sur des utilisations non sécurisées de la fonction strcpy.

Parce que l'objectif de cet article est de trouver une vulnérabilité mémoire qui peut être exploitée sur le réseau sans connaissance des identifiants administrateur, la fonction vulnérable doit être appelable sur le réseau et doit interagir directement avec l'entrée utilisateur fournie. Mais avoir une interaction réseau ne signifie pas que le binaire est aussi directement accessible sur le réseau. Pour savoir quels binaires sont à l'écoute, nous pouvons utiliser le shell root UART, qui a déjà été établi dans [iovt].

```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```

Code 2 : Utilisation du shell root UART pour exécuter netstat

Rétro-ingénierie du binaire

Le premier binaire qui semble prometteur est wscd. C'est le binaire qui contient le plus d'appels strcpy dangereux (hormis la bibliothèque libcmm.so) et d'interactions réseau ; dans le cas de wscd, cela signifie qu'il se connecte à un périphérique UPnP et n'écoute pas sur un port spécifique. Comme nous le verrons plus loin, il possède une fonction facile à fuzzer, c'est pourquoi ce binaire a été choisi comme exemple dans cet article pour expliquer la procédure générale. Avant de procéder à la rétro-ingénierie, nous pouvons utiliser le shell root UART pour savoir si le binaire est en cours d'exécution et comment il a été lancé.

```txt $ ps PID USER VSZ STAT COMMAND 962 admin 1096 S wscd -i ra0 -m 1 -w /var/tmp/wsc_upnp/ 1018 admin 1080 S wscd_5G -i rai0 -m 1 -w /var/tmp/wsc_upnp_5G/ ```

Code 3: Utilisation de la commande ps pour afficher tous les programmes en cours d'exécution.

Avec ps, nous ne voyons pas seulement que le binaire est en cours d'exécution, mais aussi quels sont les arguments, qui sont importants pour vérifier si une fonction potentielle est réellement appelée. La signification de ces arguments peut être obtenue à partir de l'aide de la CLI, qui est affichée lors de l'appel du binaire sans aucun argument.

```txt $ chroot root /qemu-mipsel-static /usr/bin/wscd Usage: wscd [-i infName] [-a ipaddress] [-p port] [-f descDoc] [-w webRootDir] -m UPnPOpMode -D [-d debugLevel] -h -i: Interface name this daemon will run wsc protocol(if not set, will use the default interface name - ra0) e.g.: ra0 -w: Filesystem path where descDoc and web files related to the device are stored e.g.: /etc/xml/ -m: UPnP system operation mode 1: Enable UPnP Device service(Support Enrolle or Proxy functions) 2: Enable UPnP Control Point service(Support Registratr function) 3: Enable both UPnP device service and Control Point services. [...] ```

Code 4 : Options du binaire wscd.

Comme le montre le Code 4, wscd est démarré avec « Enabled UPnP Device service », ce qui semble prometteur. Après avoir vérifié que le binaire tourne réellement sur le routeur, il peut ensuite être analysé à l'aide de Ghidra pour rechercher des fonctions suspectes. Pour le fuzzing, les fonctions de parsing sont particulièrement intéressantes car elles sont généralement complexes et, souvent, l'entrée analysée possède des champs de longueur pour les données qu'elle contient, comme le paquet TCP contient la longueur de la charge utile.

Figure 1 : Utilisation de Ghidra pour rechercher des fonctions de parsing.

Un autre avantage des fonctions de parsing est qu'elles n'interagissent souvent pas avec d'autres parties du code ni avec l'utilisateur via le réseau. Ainsi, la fonction de parsing peut être appelée directement avec l'entrée, sans modifier le binaire ni écraser d'autres fonctions, si bien que la fonction peut être fuzzée.

Avant de commencer à fuzzer la fonction, il convient de vérifier si elle est effectivement déclenchée, car elle n'est intéressante que lorsqu'elle est appelée avec une entrée contrôlée par l'utilisateur. Pour cela, Ghidra peut être utilisé pour rechercher les références vers la fonction cible. Dans le cas de la fonction parser_parse, il existe plusieurs possibilités. Comme nous savons comment le programme est démarré, les appels peuvent être réduits à un seul arbre d'appels de fonctions, comme le montre le Code 5.

```c main() if ((WscUPnPOpMode & 1) != 0) // Argument -m 1 WscUPnPDevStart() UpnpDownloadXmlDoc() -> my_http_Download() -> http_Download() if (http_MakeMessage()) http_RequestAndResponse() http_RecvMessage() ```

Code 5 : Arbre d'appel de la fonction parser_parse

Une fois qu'une fonction cible est trouvée, nous pouvons maintenant créer un environnement de fuzzing pour fuzzer la fonction, décrit dans la partie suivante. Mais d'abord, d'autres fonctions potentielles sont présentées.

Autres fonctions potentiellement vulnérables

Pour cet article, plusieurs binaires potentiels ont été analysés manuellement à la recherche de fonctions suspectes. La suite est un bref résumé d'autres cibles possibles qui ont été trouvées.

Le binaire httpd est le backend de l'interface d'administration web. Le binaire est accessible sur le réseau via le port 80. Une fonction intéressante de httpd est la fonction httpd_parser_main. En parcourant l'implémentation du parseur avec Ghidra, plusieurs parties de code suspectes ont pu être identifiées. L'une des parties suspectes est l'analyse du Content-Type. Dans ce qui suit, une requête HTTP de base peut être trouvée.```txt POST / HTTP/1.1\r\n Content-Type: multipart/form-data; boundary=X;\r\n Host: example.com\r\n \r\n \r\n DATA\r\n

root@kitploit:~
Voici un extrait de la fonction `httpd_parser_main` qui analyse le `Content-Type` de la
requête http fournie par l'utilisateur.


<div id="c6"></div>```c
// user_input_ptr points to
//  "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");

if (user_input_ptr == cursor) {
 cursor = strstr(user_input_ptr,"boundary=");
 user_input_ptr = cursor + 9;

 // user_input_ptr points now to "X;\r\nHost: example.com\r\n..."

 if (cursor != (char *)0x0) {

  do {
    while (cursor = user_input_ptr, *cursor == " ") {
      user_input_ptr = cursor + 1;
    }
    user_input_ptr = cursor + 1;
  } while (*cursor == "\t");

  // cursor points now to "X;\r\nHost: example.com\r\n..."

  // strchr returns a pointer to the first occurrence of ";" in the user request.
  // If ";" is not found, the function returns a null pointer.
  user_input_ptr = strchr(cursor, ";");
  if (user_input_ptr != (char *)0x0) {
    // The character ";" is replaced by an null byte to terminate the string
    *user_input_ptr = "\0";
    // cursor points now to "X\0\r\nHost: example.com\r\n..."
  }

  // DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
  strcpy(&DAT_00444050, cursor);
  // DAT_00444050 contains now "X"
 }
}

Code 6 : Arbre d'appel de la fonction parser_parse

La vulnérabilité de ce code réside dans l'appel de la fonction strcpy et dans l'hypothèse que le Content-Type se termine par un point-virgule. Étant donné que strcpy copie le tampon jusqu'au prochain octet nul, et comme le montre le Code 6, l'octet nul n'est ajouté que lorsqu'un point-virgule est trouvé. En supprimant le point-virgule, le prochain octet nul se trouve à la fin du tampon d'entrée, par exemple à la fin de la requête HTTP. Ainsi, on peut faire déborder la variable globale DAT_00444050, ce qui écrase ensuite des données au-delà de l'adresse 0x0044414f. La difficulté ne consiste pas seulement à trouver une variable globale intéressante au-delà de cette adresse qui pourrait être écrasée, mais aussi dans le fait qu'aucun octet nul ne peut être utilisé en raison de strcpy. Mais lorsqu'une telle erreur existe, il y en a probablement d'autres à découvrir.

Le binaire tdpd est utilisé par l'application mobile et est accessible via UDP sur le réseau local. tdpd possède presque les mêmes fonctions que tmpd, qui ne sont pour la plupart jamais appelées. La fonction principale se contente d'écouter les messages sur le port UDP et répond toujours avec des informations de base sur le routeur, comme le nom ou le modèle. Il n'y a pratiquement aucune interaction avec les entrées fournies par l'utilisateur, ce qui n'est donc pas intéressant à fuzzer.

Une autre paire de binaires intéressante est constituée de upnpd et ushare. Ces deux binaires traitent des messages UPnP qui nécessitent donc d'analyser du XML. Comme une chaîne de copyright peut être trouvée dans le binaire, on peut supposer que ces programmes n'ont pas été développés par TP-Link.```sh $ strings usr/bin/ushare | grep "(C)" Benjamin Zores (C) 2005-2007, for GeeXboX Team.

root@kitploit:~
Les deux binaires chargent les bibliothèques partagées `libupnp.so` et `libixml.so` qui ont les mêmes fonctions que le projet open source `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/). Comme l’objet de cet article est le fuzzing en boîte noire, ces binaires sont ignorés. Mais le fuzzing en boîte grise de cette bibliothèque pourrait être prometteur, car en 2021 une fuite de mémoire a été découverte dans `libixml.so` [\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249).

Le binaire **tmpd** est le backend de l’application mobile. Le point intéressant est que le routeur et l’application mobile communiquent via un protocole binaire personnalisé. Dans ce qui suit, un message du client au serveur est présenté.

<div id="c7"></div>```txt
00000000  01 00 05 00 00 08 00 00  00 00 00 17 50 7b 6e fe  |............P{n.|
00000010  01 01 02 00 00 00 00 00                           |........        |

Code 7 : Message de l'application mobile au routeur.

Pour comprendre le protocole binaire, le binaire tmpd a été analysé en rétro-ingénierie à l'aide de Ghidra. Grâce à ces informations, le message dans Code 7 peut être décomposé comme suit :```txt 01 00 05 00 : Version 00 08 00 00 : Size (8 Bytes) 00 00 00 17 : Datatype 50 7b 6e fe : Checksum (CRC32) 01 01 : Options 02 00 : Function id 00 00 00 00 : Function parameters

root@kitploit:~
<p class="text-align: center">Code 8 : protocole binaire personnalisé décomposé.</p>

Cela semble prometteur car de tels protocoles binaires doivent être analysés. Mais la partie la plus suspecte du protocole binaire n'est pas le champ de longueur, mais l'utilisation de l'identifiant de fonction et de ses paramètres.


<figure id="f2">
  <p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
  <figcaption>
    <p style="text-align: center">Figure 2 : fonction rétroconçue de tmpd qui analyse l'identifiant de fonction et ses paramètres.</p>
  </figcaption>
</figure>

La [figure 2](#f2) montre une partie de la fonction d'analyse décompilée du protocole personnalisé. À la ligne 16, l'identifiant de fonction est extrait, puis la fonction correspondante est appelée à la ligne 29. Le comportement suspect est que la fonction est appelée avec des paramètres extraits sans aucune vérification depuis le tampon d'entrée contrôlé par l'utilisateur. Nous pourrions maintenant essayer de trouver une fonction dans la table de saut présentée dans la [figure 3](#f3) où cela pourrait être dangereux, par exemple lorsque le paramètre est utilisé pour indexer un tampon ou interprété comme une chaîne de caractères. Plutôt que de rétro-ingénierer et de rechercher manuellement plus de 100 fonctions, ce qui prendrait du temps, nous pouvons utiliser un fuzzer qui le ferait automatiquement.

<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">Figure 3 : fonction rétroconçue de tmpd qui analyse l'identifiant de fonction
et ses paramètres.</p></figcaption>
</figure>

Malheureusement, le binaire `tmpd` n'est accessible que localement sur le réseau, comme le montre le [Code 2](#c2). Pour se connecter à ce binaire, l'application se connecte d'abord au routeur via SSH en mode `direct-tcpip`, qui se contente de transférer les paquets au processus local. Et la connexion SSH est protégée par les identifiants administrateur. Mais comme décrit dans
[\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf)
la connexion SSH peut être facilement compromise car la clé d'hôte du serveur n'est jamais vérifiée par l'application. En supprimant tous les paquets routés vers Internet, l'administrateur peut être amené à se connecter au routeur pendant qu'une attaque de l'homme du milieu est menée pour voler les identifiants.

## Fuzzing avec AFL++ et QEMU

Dans cette section, un harnais est développé pour cibler l'une des fonctions trouvées précédemment. Une fois le harnais développé, le fuzzer à la pointe de la technologie AFL++
[\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) est utilisé pour fuzzer la fonction cible. Comme les binaires sont compilés pour l'architecture `mipsel`, l'émulateur QEMU est utilisé pour exécuter le binaire. La configuration de fuzzing de base utilisée dans cet article s'inspire principalement de l'article de blog « Firmware Fuzzing 101 » d'Adam Van Prooyen [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101).

### Environnement de fuzzing 

Pour créer facilement un environnement de fuzzing reproductible, Docker est le meilleur choix. Nous avons créé un Dockerfile qui installe tous les outils nécessaires, comme un compilateur croisé pour l'architecture CPU `mipsel` ou `gdb-multiarch` qui peut être utilisé pour déboguer le harnais.

De plus, AFLplusplus est téléchargé et compilé avec QEMU, construit dans une version avec des ajustements mineurs pour permettre aux binaires non instrumentés d'être exécutés sous afl-fuzz.```docker
FROM debian:latest

RUN apt update && apt install -y \
      curl \
      vim \
      gcc-mipsel-linux-gnu \
      openssh-server \
      qemu-user-static \
      gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static

# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh

RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh

WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]

Dockerfile qui installe les outils nécessaires.

L'image peut ensuite être construite à l'aide de docker build.```sh docker build -t fuzz .

root@kitploit:~
Lorsque l'image est construite, elle peut être facilement utilisée avec `docker run` qui démarre ensuite le conteneur.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz

L'utilisation de l'option -d démarre le conteneur en arrière-plan. Avec docker exec, plusieurs shells peuvent être démarrés à l'intérieur du conteneur, ce qui est utile pour démarrer l'exécutable dans une session avec QEMU et dans l'autre session gdb-multiarch.```sh docker exec -it fuzz /bin/bash

root@kitploit:~
### Remplacer la fonction main

Dans la section précédente, une cible de fuzzing puissante a été identifiée. Le problème est que lors de l'exécution du
binaire, nous n'atteindrons jamais l'appel de fonction car la fonction `parser_parse` n'est appelée que si
un paquet TCP est reçu sur une socket. Ce ne serait pas seulement mauvais pour les performances, mais aussi difficile à
mettre en place. C'est pourquoi le point d'entrée du fuzzer devrait être à un endroit différent de la fonction main
normale.  Pour cela, la variable d'environnement `LD_PRELOAD`, qui permet d'injecter un harnais ayant
accès aux fonctions internes, peut être utilisée. Comme la page de manuel de `ld.so`, qui est responsable de la
liaison des bibliothèques partagées nécessaires à un exécutable au moment de l'exécution, le décrit, `LD_PRELOAD` peut être utilisé
"pour remplacer sélectivement des fonctions dans d'autres objets partagés
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)."

La fonction `__uClibc_main` est la mieux adaptée à cet effet. Pour remplacer cette fonction, un fichier C
doit être créé contenant une fonction portant le même nom.```c
void __uClibc_main(void *main, int argc, char** argv) {
    // Harness code, e.g. call the function parser_append
    printf("My custom __uClibc_main was called!");
}

Le fichier C peut ensuite être cross-compilé en un objet partagé pour l'architecture mipsel à l'aide de mipsel-linux-gnu-gcc. L'option -fPIC active le « Position Independent Code », ce qui signifie que le code machine ne dépend pas de sa localisation à une adresse spécifique, en utilisant un adressage relatif plutôt qu'absolu.```txt $ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC

root@kitploit:~
La bibliothèque partagée nouvellement créée peut ensuite être chargée en ajoutant la variable d'environnement `LD_PRELOAD`
à la commande QEMU.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!

Avec la commande chroot, les répertoires courant et racine peuvent être modifiés pour la commande fournie. C'est utile car l'exécutable wscd ouvre d'autres fichiers, comme les bibliothèques partagées du firmware. On peut observer ce comportement en ajoutant l'argument -strace à QEMU.```txt chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt 38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000 38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory) 38180 open("/parser_parse_hook.o",O_RDONLY) = 3 38180 fstat(3,0x7ffff920) = 0 38180 close(3) = 0 38180 munmap(0x7f7e6000,4096) = 0 38180 open("/lib/libpthread.so.0",O_RDONLY) = 3 38180 open("/lib/libc.so.0",O_RDONLY) = 3 [...]

root@kitploit:~
Comme on peut le voir, l'exécutable ouvre plusieurs bibliothèques dans le dossier `/lib/` du firmware et non
sur l'hôte.

### Développer et déboguer le harnais

Une fois la configuration créée, nous pouvons maintenant commencer à développer un harnais. Comme décrit dans la section
contexte, le harnais est le pilote entre le fuzzer et la fonction cible. Le harnais charge l'entrée de
fuzz, qui est stockée par AFL++ dans un fichier. Avec le chemin du fichier comme paramètres, le harnais
appelle ensuite la cible de fuzzing ; dans ce cas, il s'agirait de `parser_append`. Les fonctions peuvent être
appelées en utilisant l'adresse.

<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
  // Verify that a filename is provided
  if (argc != 2) exit(1);

  // Create function pointer to the fuzz target
  int (*parser_request_init)(void *, int) = (void *) 0x00412564;
  int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;

  // Open the fuzz input file
  int fd = open(argv[1], O_RDONLY);
  char fuzz_buf[2048 + 1];
  int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
  if (fuzz_buf_len < 0) exit(1);
  fuzz_buf[fuzz_buf_len] = 0;

  // Call the target functions
  uint8_t parsed_data[220]; 
  parser_request_init(parsed_data, 8);
  int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
  printf("Response is %d\n", status);
  exit(0);
}

Code 10 : Code de harnais avec la cible de fuzzing `parser_append` dans le binaire wscd.

Comme le montre Code 10, la fonction parser_parse n'est pas appelée directement mais en utilisant la fonction parser_append. Avant que cette fonction soit appelée, la fonction d'initialisation parser_request_init doit être appelée, ce qui initialise la structure de sortie de la fonction parser_parse.

Tandis que dans le cas de parser_parse le harnais est assez facile à mettre en place, d'autres cibles nécessitent des harnais plus sophistiqués comme la fonction httpd_parser_main. Par exemple, avant d'appeler la cible, la fonction http_init_main doit être appelée, ce qui se termine par un SIGSEGV. Pour savoir où cette erreur de segmentation est provoquée, il est utile de déboguer le code avec un débogueur comme gdb. Pour cela, QEMU peut être démarré avec l'option -g, qui lance un gdb-server sur le port fourni.```sh chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd corpus/httpd/simple.txt

root@kitploit:~
Comme le binaire est en architecture `mipsel`, `gdb-multiarch` doit être utilisé. Après le
démarrage de gdb, le script d'initialisation suivant peut être chargé avec gdb en utilisant `sources <path to script>`.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc

En raison du chroot, le script modifie d'abord le chemin de préfixe absolu afin que, lorsque le binaire charge un objet partagé, gdb trouve le fichier. Ensuite, le fichier ciblé est défini, car le serveur gdb de QEMU ne prend pas en charge le transfert de fichiers, donc gdb essaie de charger les fichiers depuis le disque à la place. Une fois gdb configuré, le script se connecte au serveur gdb avec target remote et crée un point d'arrêt au début de la fonction cible. Avec display, la sortie est simplement améliorée, de sorte que lors de l'exécution pas à pas, les quatre prochaines lignes d'assembleur seront affichées. En utilisant si, nous pouvons avancer d'une instruction, ce qui est utile lorsque le harnais a une erreur de segmentation avec le corpus par défaut, qui devrait toujours fonctionner. Comme le montre Code 11, le binaire a une erreur de segmentation dans la fonction fprintf.

```sh (gdb) si 0x004059b0 in http_parser_makeHeader () 1: x/4i \$pc => 0x4059b0 : jalr t9 0x4059b4 : addiu a1,a1,16248 0x4059b8 : li v0,200 0x4059bc : lw gp,16(sp) (gdb) ni 0x7f56a8ac in fprintf () from /share/root/lib/libc.so.0 1: x/4i \$pc => 0x7f56a8ac \: bal 0x7f56db80 0x7f56a8b0 \: nop 0x7f56a8b4 \: lw ra,36(sp) 0x7f56a8b8 \: jr ra 0x7f56a8bc \: addiu sp,sp,40 (gdb) n Single stepping until exit from function fprintf, which has no line number information.

Program received signal SIGSEGV, Segmentation fault.

root@kitploit:~
<p style="text-align: center">Code 11 : Erreur de segmentation dans printf.</p>

Pour examiner l'erreur, Ghidra peut être utilisé pour découvrir avec quels paramètres la fonction est
appelée.```c
fprintf(
 *(FILE **)(iVar1 + 0x101c),
 "HTTP/1.1 %d %s\r\n",
 *(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
 (&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);

Le SIGSEGV est probablement dû au fait que le premier paramètre n'est pas un descripteur de fichier mais un pointeur nul. Où iVar1 n'est qu'une référence à l'entrée de la fonction httpd_parser_main. Cela signifie que l'entrée de fuzzing doit avoir un descripteur de fichier à la position 0x101c. Ainsi, l'entrée doit être ajustée à la structure suivante.```c typedef struct { int _a; // 4 Bytes int _b; // 4 Bytes int socket; // 4 Bytes int ip; // 4 Bytes int mac; // 4 Bytes unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes FILE * fd_out; // expected to be a valid file descriptor } HttpMainT;

root@kitploit:~
Comme `fd_out` doit simplement être un pointeur de descripteur de fichier valide, il peut facilement être défini sur `stdout`.
Exécuter à nouveau `httpd_parser_main` produira désormais une sortie HTTP valide.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
    /usr/bin/httpd /httpd_corpus.txt

bind: No such file or directory
[ dm_shmInit ] 086:  shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close

<!DOCTYPE html>
[...]

Le harnais fonctionne désormais et peut être utilisé pour fuzzer la fonction à l’aide d’AFL++, comme expliqué dans la section suivante.

Générer des données de corpus

Comme mentionné dans le contexte, un corpus de départ décrit des échantillons d’entrée valides, qui servent de référence fondamentale pour générer de nouvelles données d’entrée pendant le processus de fuzzing.

Ces entrées sont généralement choisies pour représenter différents aspects des programmes cibles. Le corpus de départ est utilisé par un fuzzer pour générer des cas de test mutés ou évolués, qui sont ensuite exécutés contre le logiciel cible afin de découvrir des bogues, des crashs ou d’autres problèmes. Ce corpus joue un rôle important en orientant le fuzzer vers les zones pertinentes du programme et en augmentant la probabilité de détecter des vulnérabilités ou des comportements inattendus. En fournissant un ensemble d’entrées initiales diversifié et représentatif, le corpus de départ aide le fuzzer à explorer plus rapidement différents chemins dans la cible et augmente ainsi la couverture.

En ce qui concerne les fonctions qui analysent les données réseau, ces entrées peuvent être créées en utilisant Wireshark pour enregistrer différents paquets.

Pour la fonction httpd_parse_main, quatre corpus différents ont été créés. Chacun ciblant différents chemins dans le binaire. Un exemple est la requête de connexion, qui contient le nom d’utilisateur et le mot de passe. Pour ce corpus, le harnais a dû être modifié car TP-Link utilise une cryptographie (faible) pour « protéger » le mot de passe. Pour cela, le mot de passe est chiffré dans le navigateur à l’aide d’AES, puis déchiffré côté backend. Le mot de passe est généré dans le navigateur puis chiffré à l’aide de RSA. Ensuite, les données chiffrées sont signées. Comme un fuzzer ne peut pas créer de signature ni chiffrer de données, certaines fonctions ont été remplacées et se contentent désormais de décoder les données depuis le base64. Pour cela, les données ont d’abord été extraites en clair du navigateur à l’aide du débogueur illustré dans la Figure 4.

Figure 4 : Extraction des données avant chiffrement.

Dans la cible, la fonction rsa_tmp_decrypt_bypart a ensuite été remplacée pour modifier la logique, passant du déchiffrement des données à un simple décodage depuis le base64.```c // Replacing the logic with b64_decode int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00; b64_decode(output, 0x1000, input, input_len); int * seqnumber = (int *) 0x00444db0; *seqnumber = 0x3ac28e29-input_len+12; return 0; // says it was okay }

root@kitploit:~
<p style="text-align: center">Code 12 : la fonction rsa_tmp_decrypt_bypart se contente désormais de décoder le base64
au lieu de déchiffrer les données.</p>

Lors de l'exécution du corpus, la fonction cible renvoie toujours un document HTML avec l'erreur "408 Request Timeout". Grâce à Ghidra et GDB, le problème a pu être identifié. L'erreur se produit toujours après l'appel de la fonction `http_stream_fgets`. La ligne problématique était la vérification du caractère de saut de ligne `\n`.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {

Cette condition impose qu'après chaque saut de ligne, un retour chariot suive. Après avoir ajouté le retour chariot, tous les corpus créés ont fonctionné.

Fuzzer la cible

Dans la dernière section, nous avons développé plusieurs harnais et les avons exécutés avec QEMU. Dans cette section, QEMU est remplacé par AFL++, qui reçoit les corpus générés comme entrée de départ pour fuzzer la fonction cible. Dans la section "Environnement de fuzzing", une image Docker a été créée, qui récupère déjà AFL++ depuis GitHub, puis utilise un script fourni par AFL++ pour construire une version patchée de QEMU. Ainsi, AFL++ peut maintenant être lancé avec la commande suivante, qui accepte différents paramètres, comme -Q, qui indique à AFL++ d'utiliser la version patchée de QEMU.```sh QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@

root@kitploit:~
<p style="text-align: center">Code 13 : Fuzzing du binaire <code>httpd</code> à l'aide du harnais
et d'<code>afl-fuzz</code>.</p>

Contrairement à avant, la commande `chroot` n'est plus nécessaire et est remplacée par la variable
`QEMU_LD_PREFIX`, qui indique à QEMU où chercher les objets partagés. De plus, la variable
`LD_PRELOAD` est remplacée par la version spécifique à AFL, `AFL_PRELOAD`. Le dernier argument de la commande est
constitué des deux caractères `@`. Ils seront remplacés par AFL++ par un chemin de fichier contenant l'entrée de
fuzzing. Une fois lancé, AFL++ affiche la progression via l'interface terminale illustrée dans la [Figure 5](#f5).

<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">Figure 5 : L'écran de statut d'AFL++.</p></figcaption>
</figure>

L'écran de statut d'`AFL++` fournit des informations essentielles sur le processus de fuzzing en cours. La documentation
d'`AFL++` propose un bon aperçu des termes utilisés dans cet écran
[\[afl-screen\]](https://aflplus.plus/docs/status_screen/). Lors du débogage du corpus avec les
variables d'environnement suivantes, l'interface utilisateur peut être désactivée et, avec `AFL_DEBUG`, une journalisation
détaillée est activée, qui affiche l'entrée de fuzzing actuelle ainsi que la `stdout` du programme cible.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI

Comme le montre la Figure 5, fuzzer un binaire peut prendre un certain temps. Selon la documentation, il "devrait s'exécuter pendant des jours ou des semaines" et "certaines tâches seront autorisées à s'exécuter pendant des mois." Pour améliorer le temps nécessaire, la vitesse d'exécution devrait être supérieure à 100 exécutions/seconde. Lorsque, par exemple, la cible httpd_main_parser a été fuzzée, la vitesse d'exécution était au début d'environ 30/seconde. Pour améliorer la vitesse, le binaire cible a été fouillé à la recherche de fonctions suspectes, qui sont probablement à l'origine du ralentissement. L'une des fonctions suspectes était rsa_gdpr_generate_key, car la génération d'une clé RSA est connue pour être lente. Après avoir écrasé la fonction, la vitesse est passée à 600 exécutions par seconde.

Un indicateur qui aide à déterminer quand arrêter le fuzzing est le compteur de cycles. AFL++ surlignera le nombre en vert lorsque "le fuzzer n'a pas observé d'activité depuis un certain temps," ce qui aide à prendre la décision d'arrêter le fuzzer.

Mais le nombre le plus intéressant est probablement "total crashes". Celui-ci indique quand le programme plante à cause de l'entrée de fuzzing actuelle et est probablement un bug lié à la mémoire. Pour vérifier qu'il s'agit d'un vrai bug, gdb peut être utilisé à nouveau pour trouver la position du bug.

Conclusion

Le fuzzing est peut-être le moyen le plus efficace de trouver des vulnérabilités de sécurité. Dans ce travail de session, trois fonctions différentes ont été fuzzées, mais aucune n'a été trouvée. Bien que la mise en place du fuzzing en boîte noire en elle-même soit ni très complexe ni très chronophage, trouver une cible efficace et développer un harness fonctionnel le sont. La plupart du temps, le harness doit être débogué, puis la logique sous-jacente du binaire doit être rétro-ingénierée, ce qui prend à nouveau beaucoup de temps.

Télécharger l’outil