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
zipdefrag — Presented at Recon Montreal 2018 | Kitploit
Outils/GitHubGitHub/nccgroup/zipdefrag
Embedded Systems SecurityMemory ForensicsReverse EngineeringData RecoveryDigital ForensicsFirmware Analysis
GitHubnccgroup/zipdefrag

zipdefrag

Presented at Recon Montreal 2018

Voir le dépôt
74il y a 8 ansPas encore vérifié

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

Ce Dump Est Un Puzzle

ou Analyse Avancée au Fusil à Pompe pour Imbéciles et Rebelles

il était une fois

Il était une fois, cher lecteur, par une nuit sombre et orageuse, votre fidèle auteur est tombé sur une situation perplexe et mystérieuse — un système où la seule chose empêchant l'analyse mémoire chip-off et le reversing était un système de fichiers propriétaire fragmenté inconnu, combiné à de la compression due à l'utilisation de Java embarqué, réduisant l'efficacité des outils existants de carving de fichiers.

Une solution ad hoc avait peut-être été bricolée à l'époque en concaténant manuellement les blocs qui semblaient s'assembler, avec un travail de console Python horrible et d'affreux scripts bash ad hoc. C'était assez bon, mais très chronophage.

Si extraire des données simples non compressées lors d'une analyse chip-off est un travail assez banal dans la journée d'un hacker matériel, la compression pose de sérieux problèmes lorsque ses morceaux sont éparpillés un peu partout, de manière irrationnelle et désagréable, même lorsqu'aucune autre protection réelle n'est en place contre l'extraction.

Mais il doit sûrement y avoir une meilleure solution ?

Il y a des choses intéressantes à propos des fichiers Zip en particulier (qui est le format de base utilisé pour les fichiers JAR). En tant qu'étudiant assidu du International Journal of PoC||GTFO depuis pas mal de temps maintenant, et en suivant particulièrement les travaux sur les acrobaties de formats de fichiers d'Ange Albertini, j'ai estimé qu'il pourrait y avoir assez de données à l'intérieur d'un fichier zip à propos du fichier zip lui-même pour pouvoir faire un travail correct de reconstruction.

Bon, maintenant que les références sont en place, passons aux détails techniques.

Tout d'abord, nous ne connaissons peut-être pas les spécificités du système de fichiers (et du point de vue de ma recherche, indépendamment du système sur lequel j'avais rencontré le problème, j'ai décidé qu'il était simplement préférable de ne pas s'en soucier). Mais nous savons une chose ou deux sur la façon dont la plupart des systèmes de fichiers sont implémentés. En particulier, nous savons qu'ils ont tendance à être écrits par blocs. Ces blocs ont une taille minimale, appelée pages, et nous pouvons identifier cette taille de page en parcourant le dump et en repérant la taille de bloc minimale écrite.

Certains de ces blocs peuvent être contigus, d'autres non, sans schéma clair pour savoir quand les blocs sont contigus.

Tout cela pour dire que le problème qui se pose est de réordonner les pages de données de manière à obtenir des images valides (ou suffisamment proches) des fichiers que nous voulons extraire.

Les fichiers Zip sont écrits de manière à implémenter une sorte de hiérarchie inversée. D'abord les données de fichiers compressées (enveloppées dans des en-têtes de fichier locaux qui les décrivent). Ensuite un répertoire central (qui liste les offsets des en-têtes de fichier locaux), puis un enregistrement de fin de répertoire central (qui décrit, entre autres, le nombre de fichiers stockés dans le zip, l'offset où commence le répertoire central et la taille du répertoire central).

Prenons cela à l'envers, en creusant un peu plus les détails :

  • La fin du répertoire central (EOCD) nous indique :

    • L'emplacement exact, dans le fichier Zip, de l'enregistrement EOCD (l'offset du CD, plus la longueur du CD, qui précède l'EOCD)
    • Combien de fichiers (et donc d'enregistrements CD) chercher.
    • L'emplacement précis dans le fichier Zip du premier enregistrement CD.
  • Chaque enregistrement du répertoire central nous indique :

    • Le CRC32 des données de fichier compressées
    • L'horodatage
    • Beaucoup d'autres métadonnées (méthode de compression, drapeaux, version de l'OS utilisée/nécessaire...)
    • Un index dans le fichier pour le bloc LF correspondant
    • Point crucial : suffisamment de données pour reconstruire une image du bloc LF correspondant.
  • Chaque enregistrement de fichier local nous indique :

    • L'emplacement dans notre dump du début d'un fichier
    • Si le fichier est assez petit, on obtient tout le fichier dans la même page, ou grâce à l'en-tête de fichier suivant apparaissant dans le prochain bloc paginé du fichier zip !
    • si assez de petits fichiers sont regroupés dans assez de pages, on peut utiliser l'emplacement des pages et les valeurs connues du répertoire pour créer un ordre pour les pages (avec des trous connus !)

Tout ce qui précède nous permet d'avoir reconstruit la grande majorité du fichier.

Retournement de situation — il faut réellement gérer plus d'un firmware JAR !

Tout d'abord, nous avons besoin d'une étape pour distinguer les données des différents firmwares. La raison est que tous les offsets ne sont pertinents qu'au sein de leurs fichiers zip respectifs — tout conflit mènera à des flux zip incompatibles et à de la corruption, et nous voulons absolument récupérer le plus de données non corrompues possible. Nous voulons aussi de bonnes garanties que, par exemple, les éventuelles vulnérabilités que nous diagnostiquons dans le firmware cible affectent bien celui que nous voyons normalement s'exécuter, et pas un autre fichier qui traîne simplement quelque part.

La solution nécessaire ici est l'algorithme kmeans (alias « algorithme de Lloyd »). Il y a une excellente vidéo ici qui explique son fonctionnement. SciPy avait une bonne version prête à l'emploi, mais j'ai dû identifier/ patcher le seul crate de clustering/analyse implémentant l'algorithme pour que cela fonctionne avec l'implémentation Rust. Heureusement, je n'ai pas eu à l'écrire de zéro.

Après ça, le travail est bien engagé.

Nous pouvons utiliser plusieurs caractéristiques pour cela. Les champs Flags, Method et Version varient tous selon la pile Zip utilisée pour compresser le fichier. De plus, les en-têtes contiennent des horodatages, et il est généralement peu probable que tous les firmwares aient été compilés et compressés exactement au même moment.

Soit dit en passant, il convient de noter que les fichiers Zip utilisent des horodatages au format MS-DOS, qui sont des entiers courts compressés en bits représentant année-mois-jour et heure-minute-deux-secondes. Si ceux-ci n'étaient pas convertis en une valeur scalaire absolue avant d'être utilisés comme données de classification, vous pourriez tout aussi bien accorder autant de poids à une différence d'année qu'à une différence d'une seconde, et ce n'est pas bien du tout !

Nous convertissons ceux-ci en un vecteur euclidien (ce qui est un mot savant pour désigner un tableau à n dimensions de valeurs dans ℝ, ou de coordonnées flottantes, mais la vidéo liée ci-dessus est probablement l'explication la plus simple) et l'algorithme de clustering fait à peu près tout le reste pour nous, rassemblant tous les en-têtes analysés dans le nombre de buckets attendu.

Une petite remarque sur le parsing

Bien que j'aie écrit cela dans un script Python assez bancal pour prototyper cette méthode, étant parvenu à un taux de récupération du contenu JAR d'environ 70-80 % avec la PoC, j'ai décidé de m'arrêter là et de passer à l'implémentation d'une version rapide en Rust.

Rust a un crate appelé nom qui est absolument fantastique pour écrire des parseurs-vérificateurs. C'était l'une des principales raisons de le réécrire en Rust, pour ce que ça vaut. La possibilité d'écrire des parseurs clairs et extrêmement stricts rend cela bien plus facile, à certains égards, que d'essayer de tout gérer en Python (qui a tendance à être beaucoup plus tolérant, à tel point qu'il est parfois un peu difficile d'être certain de ne pas négliger des échecs erronés et de rater un cas limite.

Si des parseurs rapides, lisibles et géniaux t'intéressent, va voir :

  • Writing Parsers like it's 2017
  • Nom Benchmarks - où quelqu'un a écrit un parseur HTTP de zéro en Rust, légèrement plus rapide qu'une implémentation C très rapide, sans débordements de buffer.

En plus de tout le reste, exécuter une analyse de ce type en Python est intrinsèquement plutôt lent, et n'a jamais vraiment été destiné à être beaucoup plus qu'un chemin vers une PoC pour explorer la viabilité de cette approche.

Bref, assez parlé de ça...

Poursuivons

Les approches pour reconstruire les blocs restants consistent notamment à filtrer d'abord les pages restantes pour trouver les candidates à haute entropie, à combler d'abord les petites lacunes (en éliminant autant de pages que possible de la liste de recherche, car tester des permutations sur celles-ci est une tâche à temps exponentiel dans le pire des cas ; résoudre rapidement les cas faciles est donc une priorité et simplifie exponentiellement notre problème au fur et à mesure !).

Nous pouvons aussi obtenir une victoire rapide en repérant les cas où un en-tête de fichier local est impossible à analyser à cause d'une limite de page (nous devrions pouvoir le faire correspondre à un homologue aligné de manière identique, au moins pour les cas où les alignements similaires sont uniques et n'entrent pas en collision avec d'autres artefacts de corruption).

Comment vérifier les candidates pour les pages manquantes ? Eh bien, nous avons nos sommes de contrôle CRC32 pour les fichiers, juste là dans notre répertoire central ! Plutôt que de calculer le CRC32 sur le fichier entier, la meilleure façon de procéder est probablement de calculer le CRC32 sur les blocs que nous connaissons déjà (vers l'avant à partir des données à la fin de la page avant que notre lacune ne commence, et vers l'arrière à partir des données (ou du bloc DataDescriptor après le flux deflate) et de déterminer à partir de cela quel CRC32 intermédiaire nous devrions attendre pour chaque bloc de pages manquantes.

Essentiellement, chaque fois que nous choisissons de résoudre le problème le plus simple/le plus rapide, nous rendons les problèmes plus difficiles nettement plus simples en éliminant le superflu. C'est la raison pour laquelle on utilise l'entropie de Shannon pour simplement éliminer carrément les pages vides ou presque vides — il n'est pas garanti que nous n'ayons que des pages zip à haute entropie, mais même s'il y a des valeurs aberrantes, c'est une énorme accélération d'éviter d'avoir à gérer cette complication au départ.

Je voulais juste construire ce foutu truc, c'est quoi ce bordel ?

Si vous voulez bricoler avec, installez Rust (recommandé avec l'excellent rustup nightly. Ensuite :

root@kitploit:~
$ git clone [repo]
...
$ cd zipdefrag
...
$ cargo build --release

Vous pouvez ne pas utiliser l'option release pour activer le débogage.

Les artefacts de compilation seront dans /target/{debug,release}

Générez la documentation avec cargo doc (Ce crate est très documenté. J'aime écrire.)

Actuellement, il n'y a aucune sortie terminal par défaut pour la version Rust ; si vous voulez exécuter le harnais CLI, vous devez définir la variable d'environnement RUST_LOG=zipdefrag, ce qui active une journalisation terminale verbeuse montrant l'analyse en cours.

À venir :

  • Un exécutable natif rapide et portable (avec des hooks Python) pour résoudre les dumps zip puzzle issus de systèmes de fichiers inconnus.

  • Un dump de démonstration

Problèmes connus

  • Les performances sont actuellement cassées pour l'implémentation Rust en raison d'un comportement inefficace autour de la recherche des blocs LFH correspondants. Je vais corriger ça et j'en tirerai la leçon.

  • Cette technique ne fonctionne pas bien lorsque beaucoup de fichiers du JAR sont nettement plus grands que la taille de page. Comme elle repose sur une utilisation intensive de la structure inhérente aux fichiers zip, les fichiers riches en données ne s'en sortent pas très bien.

Par chance, les fichiers de classes ont tendance à être assez petits en général pour les midlets J2ME, mais les binaires volumineux empaquetés à l'intérieur seront probablement irrécupérables.

De plus, la PoC Python contient un certain nombre de bugs arithmétiques.

Télécharger l’outil