J'ai commencé à écrire des notes sur les vidéos liées à la sécurité que je regarde (comme moyen de rappel rapide).
Ceux-ci pourraient être plus utiles aux débutants.
L'ordre des notes ici n'est pas par ordre de difficulté, mais dans l'ordre chronologique inverse de la façon dont je les écris (c'est-à-dire le plus tard en premier).
Ce travail est concédé sous licence Creative Commons Attribution-NonCommercial-Sharealike 4.0 International Licence.
Écrit le 12 août 2017
Influencé par la confiance de Gynvael CTF 2017 en direct ici et ici; Et par son Google CTF Quals 2017 Livestream ici
Parfois, un défi peut mettre en œuvre une tâche compliquée en mettant en œuvre une machine virtuelle. Il n'est pas toujours nécessaire de rétrécir complètement la machine virtuelle et de travailler sur la résolution du défi. Parfois, vous pouvez être un peu un peu, et une fois que vous savez ce qui se passe, vous pouvez vous connecter à la machine virtuelle et avoir accès à des choses dont vous avez besoin. De plus, les attaques du canal latéral basées sur le timing deviennent plus faciles dans les machines virtuelles (principalement en raison de plus de nombre d'instructions "réelles" exécutées.
Les fonctions cryptographiquement intéressantes dans les binaires peuvent être reconnues et rapidement simplement en recherchant les constantes et en les recherchant en ligne. Pour les fonctions de crypto standard, ces constantes sont suffisantes pour deviner rapidement une fonction. Les fonctions de crypto plus simples peuvent être reconnues encore plus facilement. Si vous voyez beaucoup de Xors et des trucs comme ça se passe, et pas de constantes facilement identifiables, c'est probablement une crypto roulée à la main (et aussi éventuellement cassée).
Parfois, lors de l'utilisation d'IDA avec des raies hexores, la vue de démontage peut être meilleure que la vue de décompilation. Cela est particulièrement vrai si vous remarquez qu'il semble y avoir beaucoup de complications en cours dans la vue de décompilation, mais vous remarquez des modèles répétitifs dans la vue de démontage. (Vous pouvez rapidement changer de b / w les deux à l'aide de la barre d'espace). Par exemple, s'il y a une bibliothèque à grande intervalle (de taille fixe) implémentée, la vue de décompilation est terrible, mais la vue de désassemblage est facile à comprendre (et facilement reconnaissable en raison des instructions répétitives "avec la gare" telles que adc ). De plus, lors de l'analyse comme celle-ci, l'utilisation de la fonctionnalité de "nœuds de groupe" dans la vue du graphique d'IDA est extrêmement utile pour réduire rapidement la complexité de votre graphique, car vous comprenez ce que fait chaque nœud.
Pour des architectures étranges, avoir un bon émulateur est extrêmement utile. Surtout, un émulateur qui peut vous donner un vidage de la mémoire peut être utilisé pour déterminer rapidement ce qui se passe et reconnaître des parties intéressantes, une fois que vous avez la mémoire de l'émulateur. De plus, l'utilisation d'un émulateur implémenté dans un langage confortable (comme Python) signifie que vous pouvez exécuter les choses exactement comme vous le souhaitez. Par exemple, s'il y a une partie intéressante du code, vous voudrez peut-être exécuter plusieurs fois (par exemple, à une force brute ou quelque chose), puis en utilisant l'émulateur, vous pouvez rapidement coder quelque chose qui ne fait que la partie du code, plutôt que d'avoir à exécuter le programme complet.
Être paresseux est bon, quand Reing. Ne perdez pas de temps à tout inverser l'ingénierie, mais passez suffisamment de temps à faire Recon (même dans un défi RE!), Afin de pouvoir réduire le temps consacré à faire la tâche la plus difficile de Reing. Ce que Recon, dans une telle situation, est de simplement prendre des regards rapides sur différentes fonctions, sans passer trop de temps à analyser soigneusement chaque fonction. Vous évaluez rapidement ce que pourrait être la fonction (par exemple "ressemble à une chose cryptographique", ou "ressemble à une chose de gestion de la mémoire", etc.)
Pour un matériel ou une architecture inconnu, passez suffisamment de temps à le rechercher sur Google, vous pourriez avoir de la chance avec un tas d'outils ou de documents utiles qui pourraient vous aider à créer des outils plus rapidement. Souvent, vous trouverez des implémentations d'émulateur de jouets, etc. qui pourraient être utiles comme point rapide pour commencer. Alternativement, vous pouvez obtenir des informations intéressantes (comme la façon dont les bitmaps sont stockés, ou comment les cordes sont stockées, ou quelque chose) avec lesquelles vous pouvez écrire un script "correctif" rapide, puis utiliser des outils normaux pour voir si des trucs intéressants sont là.
GIMP (l'outil de manipulation d'image), a une fonctionnalité ouverte / charge très cool pour voir les données de pixels bruts. Vous pouvez l'utiliser pour rechercher rapidement des actifs ou des structures répétitives dans les données binaires brutes. Passez du temps à jouer avec les paramètres pour voir si plus d'informations peuvent en être glanées.
Écrit le 2 juillet 2017
Influencé par une discussion avec @ P4N74 et @ H3RCUL35 sur le chat infoseciitr #bin. Nous avons discuté de la façon dont parfois les débutants ont du mal à commencer par un plus grand défi binaire, surtout lorsqu'il est éliminé.
Pour résoudre le défi RE, soit pour pouvoir le PWN, il faut d'abord analyser le binaire donné, afin de pouvoir l'exploiter efficacement. Étant donné que le binaire pourrait être dépouillé, etc. (trouvé en utilisant file ), il faut savoir par où commencer l'analyse, pour prendre pied pour s'accumuler.
Il y a quelques styles d'analyse, lorsque vous recherchez des vulnérabilités dans les binaires (et d'après ce que j'ai rassemblé, différentes équipes CTF ont des préférences différentes):
1.1. Transplipant le code complet à C
Ce type d'analyse est en quelque sorte rare, mais est très utile pour les petits binaires. L'idée est d'aller dans un ingénieur à la recherche de l'intégralité du code. Chaque fonction est ouverte dans IDA (en utilisant la vue de décompilateur), et le renommer (raccourci: n) et la retypage (raccourci: y) sont utilisés pour rendre rapidement le code décompilé beaucoup plus lisible. Ensuite, tout le code est copié / exporté dans un fichier .c séparé, qui peut être compilé pour obtenir un binaire équivalent (mais pas le même) vers l'original. Ensuite, l'analyse du niveau de code source peut être effectuée, pour trouver des vulns, etc. Une fois le point de vulnérabilité, alors l'exploit est construit sur le binaire d'origine, en suivant la source bien décompilée dans IDA, côte à côte avec la vue de désassemblage (utilisez un onglet pour basculer rapidement entre les deux; et utiliser l'espace pour basculer rapidement entre le graphique et la vue de texte pour le démontage).
1.2. Analyse minimale de la décompilation
Cela est fait assez souvent, car la plupart des binaires sont relativement inutiles (du point de vue de l'attaquant). Il vous suffit d'analyser les fonctions suspectes ou qui pourraient vous conduire à la vulne. Pour ce faire, il existe certaines approches pour commencer:
1.2.1. Commencez de Main
Maintenant, généralement, pour un binaire dépouillé, même le principal n'est pas étiqueté (IDA 6.9 le marque cependant pour vous), mais au fil du temps, vous apprenez à reconnaître comment atteindre le principal à partir du point d'entrée (où IDA s'ouvre par défaut). Vous sautez à cela et commencez à analyser à partir de là.
1.2.2. Trouver des chaînes pertinentes
Parfois, vous connaissez des chaînes spécifiques qui pourraient être sorties, etc., que vous savez peut être utile (par exemple "Félicitations, votre drapeau est% s" pour un défi RE). Vous pouvez sauter à la vue des chaînes (raccourci: shift + f12), trouver la chaîne et travailler en arrière à l'aide de xrefs (raccourci: x). Les Xrefs vous permettent de trouver le chemin de fonction des fonctions à cette chaîne, en utilisant Xrefs sur toutes les fonctions de cette chaîne, jusqu'à ce que vous atteigniez le principal (ou un point que vous connaissez).
1.2.3. De une fonction aléatoire
Parfois, la chaîne non spécifique peut être utile, et vous ne voulez pas commencer de Main. Donc, au lieu de cela, vous parcourez rapidement la liste des fonctions entières, à la recherche de fonctions qui semblent suspectes (comme avoir beaucoup de constantes ou beaucoup de XOR, etc.) ou appeler des fonctions importantes (xrefs de malloc, gratuit, etc.), et vous commencez à partir de là, et allez les deux (fonctions suivantes qu'il appelle) et en arrière (xrefs de la fonction)
1.3. Analyse de démontage pur
Parfois, vous ne pouvez pas utiliser la vue de décompilation (en raison de l'architecture étrange, ou des techniques anti-décompilation, ou de l'assemblage écrit à la main, ou de la décompilation qui semble trop inutilement complexe). Dans ce cas, il est parfaitement valable de regarder uniquement la vue de démontage. Il est extrêmement utile (pour de nouvelles architectures) d'allumer les commentaires automobiles, qui montre un commentaire expliquant chaque instruction. De plus, les fonctionnalités de colorisation des nœuds et de nœuds de groupe sont extrêmement utiles. Même si vous n'utilisez aucun de ces éléments, marquer régulièrement les commentaires dans le démontage aide beaucoup. Si je fais personnellement cela, je préfère écrire des commentaires de type Python, afin que je puisse rapidement transformer manuellement dans Python (particulièrement utile pour les défis RE, où vous pourriez avoir à utiliser Z3, etc.).
1.4. Utilisation de plates-formes comme BAP, etc.
Ce type d'analyse est (semi-) automatisé et est généralement plus utile pour des logiciels beaucoup plus grands et est rarement directement utilisé dans les CTF.
Le fuzzing peut être une technique efficace pour se rendre rapidement à la vulne, sans avoir à le comprendre initialement. En utilisant un Fuzzer, on peut obtenir beaucoup de style vulns à faibles fruits, qui doivent ensuite être analysés et triés pour accéder à la vulne réelle. Voir mes notes sur les bases du fuzzing et du fuzzing génétique pour plus d'informations.
Une analyse dynamique peut être utilisée après avoir trouvé une vulne en utilisant une analyse statique, pour aider à construire rapidement les exploits. Alternativement, il peut être utilisé pour trouver la vulne elle-même. Habituellement, on démarre l'exécutable à l'intérieur d'un débogueur et essaie d'aller le long des chemins de code qui déclenchent le bogue. En plaçant des points d'arrêt aux bons emplacements et en analysant l'état des registres / tas / pile / etc., on peut avoir une bonne idée de ce qui se passe. On peut également utiliser des débogueurs pour identifier rapidement les fonctions intéressantes. Cela peut être fait, par exemple, en définissant initialement des points d'arrêt temporaires sur toutes les fonctions; puis procéder à 2 promenades - une à travers tous les chemins de code sans intérêt; et un à travers un seul chemin intéressant. La première marche déclenche toutes les fonctions inintéressant et désactive ces points d'arrêt, laissant ainsi ceux intéressants apparaissant comme points d'arrêt pendant la deuxième marche.
Mon style personnel pour l'analyse est de commencer par une analyse statique, généralement à partir des applications principales (ou pour les applications non condamnées, à partir de chaînes), et de travailler rapidement à trouver une fonction qui semble étrange. Je passe ensuite du temps et je me diverste des avantages et en arrière à partir d'ici, je réduis régulièrement des commentaires, et le renommage et la repensage en continu pour améliorer la décompilation. Comme d'autres, j'utilise des noms comme Apple, Banana, Carrot, etc. pour apparemment utile, mais des fonctions / variables encore inconnues / etc., pour le rendre plus facile à analyser (garder une trace de Func_123456 Le style de noms est trop difficile pour moi). J'utilise également régulièrement la vue des structures en IDA pour définir les structures (et les énumériques) pour rendre la décompilation encore plus agréable. Une fois que je trouve la Vuln, je passe généralement à l'écriture d'un script avec PWNTools (et je l'utilise pour appeler un gdb.attach() ). De cette façon, je peux obtenir beaucoup de contrôle sur ce qui se passe. À l'intérieur de GDB, j'utilise généralement un GDB ordinaire, bien que j'aie ajouté une commande peda qui charge instantanément PEDA si nécessaire.
Mon style évolue définitivement, car je suis devenu plus à l'aise avec mes outils, et aussi avec des outils personnalisés, j'ai écrit pour accélérer les choses. Je serais heureux d'entendre d'autres styles d'analyse, ainsi que des modifications possibles de mon style qui pourraient m'aider à aller plus vite. Pour tous les commentaires / critiques / louanges que vous avez, comme toujours, je peux être joint sur Twitter @ Jay_F0xtr0t.
Écrit le 4 juin 2017
Influencé par ce génial Stream en direct de Gynvael Coldwind, où il discute des bases de ROP, et donne quelques conseils et astuces
La programmation orientée vers le retour (ROP) est l'une des techniques d'exploitation classiques, qui est utilisée pour contourner la protection NX (mémoire non exécutable). Microsoft a incorporé NX en tant que DEP (prévention de l'exécution des données). Même Linux, etc., ayez-le efficace, ce qui signifie qu'avec cette protection, vous ne pouvez plus placer ShellCode sur un tas / pile et le faire exécuter simplement en sautant. Alors maintenant, pour pouvoir exécuter du code, vous passez à un code préexistant (binaire principal, ou ses bibliothèques - libc, ldd, etc. sur Linux; Kernel32, ntdll etc sur Windows). Le ROP naît en réutilisant les fragments de ce code qui est déjà là, et en trouvant un moyen de combiner ces fragments pour faire ce que vous voulez faire (ce qui est bien sûr, pirater la planète !!!).
À l'origine, ROP a commencé avec RET2LIBC, puis est devenu plus avancé au fil du temps en utilisant beaucoup plus de petits morceaux de code. Certains pourraient dire que ROP est maintenant "mort", en raison de protections supplémentaires pour l'atténuer, mais elle peut toujours être exploitée dans de nombreux scénarios (et certainement nécessaire pour de nombreux CTF).
La partie la plus importante de la ROP est les gadgets. Les gadgets sont des "pièces de code utilisables pour ROP". Cela signifie généralement des pièces de code qui se terminent par un ret (mais d'autres types de gadgets peuvent également être utiles; comme ceux qui se terminaient par pop eax; jmp eax etc.). Nous enchaînons ces gadgets ensemble pour former l'exploit, qui est connu sous le nom de chaîne ROP .
L'une des hypothèses les plus importantes de ROP est que vous avez le contrôle de la pile (c'est-à-dire le pointeur de pile pointe vers un tampon que vous contrôlez). Si ce n'est pas vrai, vous devrez appliquer d'autres astuces (telles que la pile pivot) pour obtenir ce contrôle avant de construire une chaîne ROP.
Comment extraire les gadgets? Utilisez des outils téléchargeables (tels que Ropgadget) ou un outil en ligne (tel que Ropshell) ou écrivez vos propres outils (peut être plus utile pour des défis plus difficiles, car vous pouvez le modifier au défi spécifique si nécessaire). Fondamentalement, nous avons juste besoin des adresses auxquelles nous pouvons sauter pour ces gadgets. C'est là qu'il pourrait y avoir un problème avec ASLR, etc. (auquel cas, vous obtenez une fuite de l'adresse, avant de passer à la ROP).
Alors maintenant, comment utilisons-nous ces gadgets pour faire un ropchain? Nous recherchons d'abord des "gadgets de base". Ce sont des gadgets qui peuvent faire des tâches simples pour nous (comme pop ecx; ret , qui peut être utilisé pour charger une valeur dans ECX en plaçant le gadget, suivi de la valeur à charger, suivi du reste de la chaîne, qui est renvoyé après la valeur de la valeur). Les gadgets de base les plus utiles sont généralement "Définir un registre", "Store Register Value à l'adresse indiquée par le registre", etc.
Nous pouvons construire à partir de ces fonctions primitives pour gagner des fonctionnalités de niveau supérieur (similaire à mon post intitulé l'abstraction d'exploitation). Par exemple, en utilisant les gadgets de set-registre et le magasin-valeur à l'adresse, nous pouvons trouver une fonction "poke", qui nous permet de définir une adresse spécifique avec une valeur spécifique. En utilisant cela, nous pouvons créer une fonction "Poke-String" qui nous permet de stocker une chaîne particulière à un endroit particulier en mémoire. Maintenant que nous avons la corde, nous sommes presque presque terminés, car nous pouvons créer toutes les structures que nous voulons en mémoire, et nous pouvons également appeler toutes les fonctions que nous voulons avec les paramètres que nous voulons (car nous pouvons définir un enregistrement et peut placer des valeurs sur la pile).
L'une des raisons les plus importantes de construire de ces primitives d'ordre inférieur à des fonctions plus grandes qui font des choses plus complexes, est de réduire les chances de faire des erreurs (ce qui est courant dans le ROP autrement).
Il y a des idées, des techniques et des conseils plus complexes pour ROP, mais c'est peut-être un sujet pour une note séparée, pour un moment différent :)
PS: Gyn a un blog sur l'exploitation axée sur le retour qui pourrait valoir la peine d'être lu.
Écrit le 27 mai 2017; prolongé le 29 mai 2017
Influencé par ce incroyable flux en direct de Gynvael Coldwind, où il parle de la théorie de base derrière le fuzzing génétique, et commence à construire une fuzzer génétique de base. Il procède ensuite à la mise en œuvre de ce flux en direct.
Fuzzing "avancé" (par rapport à un fuzzer aveugle, décrit dans ma note "Basics of Fuzzing"). Il modifie / mute également les octets, etc., mais il le fait un peu plus intelligent que le flou de "muet" aveugle.
Pourquoi avons-nous besoin d'un fuzzer génétique?
Certains programmes pourraient être "méchants" envers les fuzzers muets, car il est possible qu'une vulnérabilité puisse nécessiter tout un tas de conditions pour être satisfaites pour être atteintes. Dans un fuzzer muet, nous avons une très faible probabilité que cela se produise car il n'a aucune idée s'il fait des progrès ou non. Comme exemple spécifique, si nous avons le code if a: if b: if c: if d: crash! (Appelons cela le code de crash), alors dans ce cas, nous avons besoin de 4 conditions pour être satisfaits pour écraser le programme. Cependant, un flou muet pourrait être incapable de dépasser la condition a , simplement parce qu'il y a de très faibles chances que les 4 mutations a , b , c , d , se produisent en même temps. En fait, même si cela progresse en faisant juste a , la prochaine mutation pourrait revenir !a juste parce qu'elle ne sait rien du programme.
Attendez, quand ce genre de programme "mauvais cas" apparaît-il?
Il est assez courant dans les analyseurs de format de fichiers, de prendre un exemple. Pour atteindre certains chemins de code spécifiques, il faudra peut-être passer plusieurs vérifications "Cette valeur doit être la suivante, et cette valeur doit être cela, et une autre valeur doit être autre chose" et ainsi de suite. De plus, presque aucun logiciel du monde réel n'est "simple", et la plupart des logiciels ont de nombreux chemins de code possibles, dont certains ne peuvent être accessibles qu'après que beaucoup de choses dans l'État se sont configurées correctement. Ainsi, bon nombre de ces chemins de code de ces programmes sont fondamentalement inaccessibles aux fuzzers muets. De plus, parfois, certains chemins peuvent être complètement inaccessibles (plutôt que simplement très improbables) en raison des mutations pas assez faites. Si l'un de ces chemins a des bogues, un fuzzer stupide ne pourrait jamais les trouver.
Alors, comment faire mieux que les fuzzers muets?
Considérez le graphique de flux de contrôle (CFG) du code de crash mentionné ci-dessus. Si par hasard, un fuzzer muet devenait soudainement a , alors il ne reconnaîtrait pas qu'il a atteint un nouveau nœud, mais il continuerait à ignorer cela, en rejetant l'échantillon. D'un autre côté, ce que font AFL (et d'autres fuzzers génétiques ou "intelligents"), c'est qu'ils reconnaissent cela comme une nouvelle information ("un chemin nouvellement atteint") et stockent cet échantillon comme un nouveau point initial dans le corpus. Cela signifie que maintenant le Fuzzer peut commencer à partir du bloc a et se déplacer plus loin. Bien sûr, parfois, cela peut revenir au !a de l'échantillon a , mais la plupart du temps, ce ne sera pas le cas, et pourrait plutôt atteindre le bloc b Ceci est encore un nouveau nœud atteint, donc ajoute un nouvel échantillon dans le corpus. Cela continue, permettant de vérifier de plus en plus de chemins possibles et atteint enfin l' crash! .
Pourquoi cela fonctionne-t-il?
En ajoutant des échantillons mutés dans le corpus, qui explorent davantage le graphique (c'est-à-dire des pièces à atteindre non explorées auparavant), nous pouvons atteindre des zones auparavant inaccessibles et peut ainsi être fuzz de telles zones. Étant donné que nous pouvons fuzz ces zones, nous pourrions être en mesure de découvrir des bogues dans ces régions.
Pourquoi s'appelle-t-il le fuzzing génétique?
Ce type de fuzzing "intelligent" est un peu comme des algorithmes génétiques. La mutation et le croisement des échantillons provoquent de nouveaux échantillons. Nous gardons des spécimens mieux adaptés aux conditions testées. Dans ce cas, la condition est "Combien de nœuds dans le graphique a-t-il atteint?". Ceux qui traversent plus peuvent être conservés. Ce n'est pas exactement comme des algos génétiques, mais c'est une variation (car nous gardons tous les spécimens qui traversent un territoire inexploré, et nous ne faisons pas de croisement) mais est suffisamment similaire pour obtenir le même nom. Fondamentalement, le choix de la population préexistante, suivi d'une mutation, suivi des tests de fitness (qu'il ait vu de nouvelles zones) et répéter.
Attendez, alors nous gardons simplement une trace des nœuds non atteints?
Non, pas vraiment. AFL garde une trace des traversées de bord dans le graphique, plutôt que des nœuds. De plus, il ne dit pas que "le bord a voyagé ou non", il garde une trace du nombre de fois où un bord a été traversé. Si un bord est traversé 0, 1, 2, 4, 8, 16, ... fois, il est considéré comme un "nouveau chemin" et mène à l'ajout dans le corpus. Cela se fait parce que le fait de regarder les bords plutôt que les nœuds est un meilleur moyen de faire la distinction entre les états d'application, et l'utilisation d'un nombre croissant de traversées de bord exponentielle donne plus d'informations (un bord traversé une fois est très différent de traverser deux fois, mais le 10 traversé n'est pas trop différent de 11 fois).
Alors, de quoi et tout avez-vous besoin dans un fuzzer génétique?
Nous avons besoin de 2 choses, la première partie est appelée traceur (ou instrumentation de traçage). Il vous indique essentiellement quelles instructions ont été exécutées dans la demande. AFL le fait de manière simple en sautant entre les étapes de compilation. Après la génération de l'assemblage, mais avant d'assembler le programme, il recherche des blocs de base (en recherchant des fins, en vérifiant le type d'instructions de saut / de branche), et ajoute du code à chaque bloc qui marque le bloc / bord tel qu'il est exécuté (probablement dans une mémoire d'ombre ou quelque chose). Si nous n'avons pas de code source, nous pouvons utiliser d'autres techniques de traçage (comme la broche, le débogueur, etc.). Il s'avère que même Asan peut fournir des informations de couverture (voir les documents pour cela).
Pour la deuxième partie, nous utilisons ensuite les informations de couverture fournies par le traceur pour garder une trace de nouveaux chemins tels qu'ils apparaissent, et ajoutant ces échantillons générés dans le corpus pour une sélection aléatoire à l'avenir.
Il existe plusieurs mécanismes pour faire le traceur. Ils peuvent être basés sur un logiciel ou un matériel. Pour le matériel basé, il existe, par exemple, certaines fonctionnalités Intel CPU existent où, étant donné un tampon en mémoire, il enregistre les informations de tous les blocs de base traversés dans ce tampon. C'est une fonctionnalité du noyau, donc le noyau doit le prendre en charge et le fournir en API (ce que Linux fait). Pour les logiciels, nous pouvons le faire en ajoutant du code, ou en utilisant un débogueur (en utilisant des points d'arrêt temporaires, ou via un pas unique), ou utilisez les capacités de traçage de l'adresse de l'adresse, ou utilisez des crochets, ou des émulateurs, ou tout un tas d'autres façons.
Une autre façon de différencier les mécanismes est soit par le traçage de la boîte noire (où vous ne pouvez utiliser que le binaire non modifié), soit un tracé de boîte à boîte blanche logicielle (où vous avez accès au code source et modifier le code lui-même pour ajouter dans le code de traçage).
AFL utilise l'instrumentation logicielle pendant la compilation comme méthode de traçage (ou par émulation QEMU). Honggfuzz prend en charge les méthodes de traçage basées sur les logiciels et le matériel. D'autres fuzzers intelligents pourraient être différents. Celui que Gyn construit utilise le traçage / la couverture fournis par Address Sanitizer (ASAN).
Certains fuzzers utilisent des «speedhacks» (c'est-à-dire augmenter la vitesse de fuzzing), comme en faisant un service de fourche ou d'autres idées de ce type. Peut-être la peine de les examiner à un moment donné :)
Écrit le 20 avril 2017
Influencé par ce génial Stream en direct de Gynvael Coldwind, où il parle de ce qu'est Fuzzing, et construit également un flou de base à partir de zéro!
Qu'est-ce qu'un fuzzer, en premier lieu? Et pourquoi l'utilisons-nous?
Considérez que nous avons une bibliothèque / programme qui prend des données d'entrée. L'entrée peut être structurée d'une manière ou d'une autre (par exemple, un PDF, un PNG, ou XML, etc., mais il n'a pas besoin d'être un format "standard"). Du point de vue de la sécurité, il est intéressant s'il existe une frontière de sécurité entre l'entrée et le processus / bibliothèque / programme, et nous pouvons passer une "entrée spéciale" qui provoque un comportement inattendu au-delà de cette frontière. Un fuzzer est une telle façon de le faire. Il le fait en «mutant» les choses dans l'entrée (ce qui peut le corrompre peut-être), afin de conduire à une exécution normale (y compris des erreurs gérées en toute sécurité) ou un crash. Cela peut se produire en raison de la logique du cas de bord qui n'est pas bien gérée.
L'écrasement est le moyen le plus simple pour les conditions d'erreur. Il pourrait y en avoir d'autres aussi. Par exemple, l'utilisation d'ASAN (Adress Dasitizer), etc. peut conduire à détecter plus de choses, ce qui pourrait être des problèmes de sécurité. Par exemple, un débordement d'octet d'un seul octet d'un tampon peut ne pas provoquer de crash seul, mais en utilisant Asan, nous pourrions être en mesure de l'attraper même avec un fuzzer.
Une autre utilisation possible pour un Fuzzer est que les entrées générées par Fuzzing One Program peuvent également être utilisées dans une autre bibliothèque / programme et voir s'il y a des différences. Par exemple, certaines erreurs de bibliothèque de mathématiques de haute précision ont été remarquées comme celle-ci. Cela ne mène généralement pas à des problèmes de sécurité, donc nous ne nous concentrerons pas sur autant.
Comment fonctionne un Fuzzer?
Un Fuzzer est essentiellement une boucle mutée-répétition exécutive qui explore l'espace d'état de l'application pour essayer de trouver "au hasard" des états d'une vulne de crash / de sécurité. Il ne trouve pas d'exploit, juste une vulne. La partie principale du Fuzzer est le mutateur lui-même. Plus à ce sujet plus tard.
Sorties d'un fuzzer?
Dans le Fuzzer, un débogueur est (parfois) attaché à l'application pour obtenir une sorte de rapport du crash, pour pouvoir l'analyser plus tard en tant que Vuln de sécurité par rapport à un crash bénin (mais éventuellement important).
Comment déterminer les domaines des programmes qui sont les meilleurs à fuzz en premier?
Lors du fuzzing, nous voulons généralement nous concentrer sur une seule pièce ou un petit ensemble de morceaux du programme. Cela se fait généralement principalement pour réduire la quantité d'exécution à faire. Habituellement, nous nous concentrons uniquement sur l'analyse et le traitement. Encore une fois, la limite de sécurité est importante pour décider quelles pièces nous importent.
Types de fuzzers?
Les échantillons d'entrée donnés au Fuzzer sont appelés le corpus . Dans les fuzzers oldschool (alias "aveugles" / "muets" Fuzzzers), il y avait une nécessité pour un grand corpus. Les plus récents (alias les fuzzers "génétiques", par exemple AFL) n'ont pas nécessairement besoin d'un corpus aussi grand, car ils explorent l'État par eux-mêmes.
Comment les fuzzers sont-ils utiles?
Les fuzzers sont principalement utiles pour les «fruits à faible suspension». Il ne trouvera pas de bogues logiques compliqués, mais il peut trouver des bugs faciles à trouver (qui sont en fait parfois faciles à manquer pendant l'analyse manuelle). Bien que je puisse dire les entrées tout au long de cette note, et se référer généralement à un fichier d'entrée , il ne faut pas être juste cela. Les fuzzers peuvent gérer les entrées qui peuvent être STDIN ou un fichier d'entrée ou une prise de réseau ou bien d'autres. Sans trop de perte de généralité, nous pouvons le considérer comme un simple fichier pour l'instant.
Comment écrire un (basique) Fuzzer?
Encore une fois, il faut simplement être une boucle de répétition mutée. Nous devons être en mesure d'appeler souvent la cible ( subprocess.Popen ). Nous devons également être en mesure de transmettre des entrées dans le programme (par exemple: les fichiers) et de détecter les plantages ( SIGSEGV , etc. provoquent des exceptions qui peuvent être capturées). Maintenant, nous devons simplement écrire un mutateur pour le fichier d'entrée et continuer à appeler la cible sur les fichiers mutés.
Mutateurs? Quoi?!?
Il peut y avoir plusieurs mutateurs possibles. Celles faciles (c'est-à-dire simples à implémenter) peuvent être de muter des bits, de muter les octets ou de muter sur des valeurs "magiques". Pour augmenter les chances de crash, au lieu de changer un seul bit ou quelque chose, nous pouvons changer plusieurs (peut-être un pourcentage paramétré d'entre eux?). Nous pouvons également (au lieu de mutations aléatoires), modifier les octets / mots / dwords / etc. à certaines valeurs "magiques". Les valeurs magiques peuvent être 0 , 0xff , 0xffff , 0xffffffff , 0x80000000 (32 bits INT_MIN ), 0x7fffffff (32 bits INT_MAX ) etc. Fondamentalement, choisissez ceux qui sont communs à causer des problèmes de sécurité (car ils peuvent déclencher certains cas de bord). Nous pouvons écrire des mutateurs plus intelligents si nous savons plus d'informations sur le programme (par exemple, pour les entiers basés sur des chaînes, nous pourrions écrire quelque chose qui modifie une chaîne entière en "65536" ou -1 etc). Les mutateurs à base de morceaux peuvent déplacer des pièces (en gros, réorganiser l'entrée). Les mutateurs additifs / appuyés fonctionnent également (par exemple, provoquant une entrée plus importante dans le tampon). Les tronçons peuvent également fonctionner (par exemple, l'EOF peut parfois ne pas être bien géré). Fondamentalement, essayez tout un tas de façons créatives de mangager des choses. Plus il y a d'expérience en ce qui concerne le programme (et l'exploitation en général), plus les mutateurs sont possibles.
Mais qu'est-ce que ce fuzzing "génétique"?
C'est probablement une discussion pour une période ultérieure. Cependant, quelques liens vers certains fuzzers modernes (open source) sont AFL et Honggfuzz.
Écrit le 7 avril 2017
Influencé d'un bon défi dans PICOCTF 2017 (nom du défi retenu, car le concours est toujours en cours)
AVERTISSEMENT: Cette note peut sembler simple / évidente pour certains lecteurs, mais il faut dire, car la superposition n'a été claire que très récemment.
Bien sûr, lors de la programmation, nous utilisons tous des abstractions, qu'il s'agisse de classes et d'objets, ou de fonctions, ou de méta-fonctions, ou de polymorphisme, ou de monades, ou de functeurs, ou de tout ce jazz. Cependant, pouvons-nous vraiment avoir une telle chose pendant l'exploitation? De toute évidence, nous pouvons exploiter les erreurs qui sont commises dans la mise en œuvre des abstractions susmentionnées, mais ici, je parle de quelque chose de différent.
Sur plusieurs CTF, chaque fois que j'ai écrit un exploit auparavant, il s'agit d'un script d'exploitation ad hoc qui laisse tomber un shell. J'utilise l'incroyable PWNTools comme cadre (pour me connecter au service, et convertir les choses, et dynelf, etc.), mais c'est tout. Chaque exploit avait tendance à être un moyen ad hoc de travailler vers l'objectif de l'exécution de code arbitraire. Cependant, ce défi actuel, en plus de penser à ma note précédente sur l'exploitation de chaîne de format "avancé", m'a fait réaliser que je pouvais superposer mes exploits de manière cohérente et se déplacer à travers différentes couches d'abstraction pour atteindre enfin l'objectif requis.
Par exemple, considérons la vulnérabilité comme une erreur logique, qui nous permet de faire une lecture / écrire de 4 octets, quelque part dans une petite gamme après un tampon. Nous voulons abuser de cela jusqu'à obtenir l'exécution du code, et enfin le drapeau.
Dans ce scénario, je considérerais cette abstraction comme une primitive short-distance-write-anything . Avec cela lui-même, nous ne pouvons évidemment pas faire grand-chose. Néanmoins, je fais une petite fonction Python vuln(offset, val) . Cependant, comme juste après le tampon, il peut y avoir des données / métadonnées qui pourraient être utiles, nous pouvons abuser de ceci pour construire à la fois des primitives read-anywhere et write-anything-anywhere Cela signifie que j'écris de courtes fonctions Python qui appellent la fonction vuln() précédemment définie. Ces fonctions get_mem(addr) et set_mem(addr, val) sont faites simplement (dans cet exemple actuel) simplement en utilisant la fonction vuln() pour écraser un pointeur, qui peut ensuite être déréféré ailleurs dans le binaire.
Maintenant, après avoir ces abstractions get_mem() et set_mem() , je construis une abstraction anti-ASLR, en divulguant essentiellement 2 adresses de Get via get_mem() et en comparant à une base de données LIBC (merci @niklasb pour la fabrication de la base de données). Les décalages de ceux-ci me donnent une libc_base de manière fiable, ce qui me permet de remplacer toute fonction dans le GOT par un autre de LiBC.
Cela m'a essentiellement donné le contrôle de EIP (au moment où je peux "déclencher" l'une de ces fonctions exactement quand je veux). Maintenant, tout ce qui reste est pour moi d'appeler le déclencheur avec les bons paramètres. J'ai donc configuré les paramètres en tant qu'abstraction distincte, puis j'appelle trigger() et j'ai un accès à la coquille sur le système.
TL; DR: On peut construire de petites primitives d'exploitation (qui n'ont pas trop de puissance), et en les combinant et en construisant une hiérarchie de primitives plus fortes, nous pouvons gagner une exécution complète.
Écrit le 6 avril 2017
Influencé par ce génial Stream en direct de Gynvael Coldwind, où il parle de l'exploitation des chaînes de format
Exploits de chaînes de format simples:
Vous pouvez utiliser le %p pour voir ce qu'il y a sur la pile. Si la chaîne de format elle-même est sur la pile, alors on peut placer une adresse (disons foo ) sur la pile, puis la chercher en utilisant le spécificateur de position n$ (par exemple, AAAA %7$p peut renvoyer AAAA 0x41414141 , si 7 est la position sur la pile). We can then use this to build a read-where primitive, using the %s format specifier instead (for example, AAAA %7$s would return the value at the address 0x41414141, continuing the previous example). We can also use the %n format specifier to make it into a write-what-where primitive. Usually instead, we use %hhn (a glibc extension, iirc), which lets us write one byte at a time.
We use the above primitives to initially beat ASLR (if any) and then overwrite an entry in the GOT (say exit() or fflush() or ...) to then raise it to an arbitrary-eip-control primitive, which basically gives us arbitrary-code-execution .
Possible difficulties (that make it "advanced" exploitation):
If we have partial ASLR , then we can still use format strings and beat it, but this becomes much harder if we only have one-shot exploit (ie, our exploit needs to run instantaneously, and the addresses are randomized on each run, say). The way we would beat this is to use addresses that are already in the memory, and overwrite them partially (since ASLR affects only higher order bits). This way, we can gain reliability during execution.
If we have a read only .GOT section, then the "standard" attack of overwriting the GOT will not work. In this case, we look for alternative areas that can be overwritten (preferably function pointers). Some such areas are: __malloc_hook (see man page for the same), stdin 's vtable pointer to write or flush , etc. In such a scenario, having access to the libc sources is extremely useful. As for overwriting the __malloc_hook , it works even if the application doesn't call malloc , since it is calling printf (or similar), and internally, if we pass a width specifier greater than 64k (say %70000c ), then it will call malloc, and thus whatever address was specified at the global variable __malloc_hook .
If we have our format string buffer not on the stack , then we can still gain a write-what-where primitive, though it is a little more complex. First off, we need to stop using the position specifiers n$ , since if this is used, then printf internally copies the stack (which we will be modifying as we go along). Now, we find two pointers that point ahead into the stack itself, and use those to overwrite the lower order bytes of two further ahead pointing pointers on the stack, so that they now point to x+0 and x+2 where x is some location further ahead on the stack. Using these two overwrites, we are able to completely control the 4 bytes at x , and this becomes our where in the primitive. Now we just have to ignore more positions on the format string until we come to this point, and we have a write-what-where primitive.
Written on 1st April 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he explains about race conditions
If a memory region (or file or any other resource) is accessed twice with the assumption that it would remain same, but due to switching of threads, we are able to change the value, we have a race condition.
Most common kind is a TOCTTOU (Time-of-check to Time-of-use), where a variable (or file or any other resource) is first checked for some value, and if a certain condition for it passes, then it is used. In this case, we can attack it by continuously "spamming" this check in one thread, and in another thread, continuously "flipping" it so that due to randomness, we might be able to get a flip in the middle of the "window-of-opportunity" which is the (short) timeframe between the check and the use.
Usually the window-of-opportunity might be very small. We can use multiple tricks in order to increase this window of opportunity by a factor of 3x or even up to ~100x. We do this by controlling how the value is being cached, or paged. If a value (let's say a long int ) is not aligned to a cache line, then 2 cache lines might need to be accessed and this causes a delay for the same instruction to execute. Alternatively, breaking alignment on a page, (ie, placing it across a page boundary) can cause a much larger time to access. This might give us higher chance of the race condition being triggered.
Smarter ways exist to improve this race condition situation (such as clearing TLB etc, but these might not even be necessary sometimes).
Race conditions can be used, in (possibly) their extreme case, to get ring0 code execution (which is "higher than root", since it is kernel mode execution).
It is possible to find race conditions "automatically" by building tools/plugins on top of architecture emulators. For further details, http://vexillium.org/pub/005.html
Written on 31st Mar 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he is experimenting on the heap
Use-after-free:
Let us say we have a bunch of pointers to a place in heap, and it is freed without making sure that all of those pointers are updated. This would leave a few dangling pointers into free'd space. This is exploitable by usually making another allocation of different type into the same region, such that you control different areas, and then you can abuse this to gain (possibly) arbitrary code execution.
Double-free:
Free up a memory region, and the free it again. If you can do this, you can take control by controlling the internal structures used by malloc. This can get complicated, compared to use-after-free, so preferably use that one if possible.
Classic buffer overflow on the heap (heap-overflow):
If you can write beyond the allocated memory, then you can start to write into the malloc's internal structures of the next malloc'd block, and by controlling what internal values get overwritten, you can usually gain a read-what-where primitive, that can usually be abused to gain higher levels of access (usually arbitrary code execution, via the GOT PLT , or __fini_array__ or similar).