Y a-t-il de toute façon cela peut être fait? J'ai utilisé objdump mais cela ne produit pas de sortie d'assembly qui sera acceptée par tous les assembleurs que je connaisse. Je voudrais pouvoir changer des instructions dans un exécutable puis le tester ensuite.
Je ne pense pas qu'il existe un moyen fiable de le faire. Les formats de code machine sont très compliqués, plus compliqués que les fichiers d'assemblage. Il n'est pas vraiment possible de prendre un binaire compilé (par exemple, au format ELF) et de produire un programme d'assemblage source qui compilera vers le même binaire (ou assez similaire). Pour mieux comprendre les différences, comparez la sortie de la compilation GCC directement à l'assembleur (gcc -S
) par rapport à la sortie de objdump sur l'exécutable (objdump -D
).
Il y a deux complications majeures auxquelles je peux penser. Premièrement, le code machine lui-même n'est pas une correspondance 1 à 1 avec le code Assembly, en raison de choses comme les décalages de pointeur.
Par exemple, considérez le code C pour Hello world:
int main()
{
printf("Hello, world!\n");
return 0;
}
Cela se compile dans le code d'assemblage x86:
.LC0:
.string "hello"
.text
<snip>
movl $.LC0, %eax
movl %eax, (%esp)
call printf
Où .LCO est une constante nommée et printf est un symbole dans une table de symboles de bibliothèque partagée. Comparez avec la sortie de objdump:
80483cd: b8 b0 84 04 08 mov $0x80484b0,%eax
80483d2: 89 04 24 mov %eax,(%esp)
80483d5: e8 1a ff ff ff call 80482f4 <printf@plt>
Premièrement, la constante .LC0 est maintenant juste un décalage aléatoire en mémoire quelque part - il serait difficile de créer un fichier source d'assemblage qui contient cette constante au bon endroit, car l'assembleur et l'éditeur de liens sont libres de choisir des emplacements pour ces constantes.
Deuxièmement, je ne suis pas entièrement sûr de cela (et cela dépend de choses comme le code indépendant de la position), mais je pense que la référence à printf n'est pas du tout codée à l'adresse du pointeur dans ce code, mais les en-têtes ELF contiennent un table de recherche qui remplace dynamiquement son adresse au moment de l'exécution. Par conséquent, le code désassemblé ne correspond pas tout à fait au code assembleur source.
En résumé, l'assembly source a symboles tandis que le code machine compilé a adresses qui sont difficiles à inverser.
La deuxième complication majeure est qu'un fichier source d'assembly ne peut pas contenir toutes les informations qui étaient présentes dans les en-têtes de fichier ELF d'origine, comme les bibliothèques avec lesquelles lier dynamiquement et les autres métadonnées placées là par le compilateur d'origine. Il serait difficile de reconstruire cela.
Comme je l'ai dit, il est possible qu'un outil spécial puisse manipuler toutes ces informations, mais il est peu probable que l'on puisse simplement produire du code d'assemblage qui peut être réassemblé à l'exécutable.
Si vous souhaitez modifier juste une petite section de l'exécutable, je recommande une approche beaucoup plus subtile que de recompiler l'application entière. Utilisez objdump pour obtenir le code d'assemblage pour la ou les fonctions qui vous intéressent. Convertissez-le à la "syntaxe d'assemblage source" à la main (et ici, je souhaite qu'il y ait un outil qui ait réellement produit le désassemblage dans la même syntaxe que l'entrée) et modifiez-le comme vous le souhaitez. Lorsque vous avez terminé, recompilez uniquement ces fonctions et utilisez objdump pour déterminer le code machine de votre programme modifié. Ensuite, utilisez un éditeur hexadécimal pour coller manuellement le nouveau code machine au-dessus de la partie correspondante du programme d'origine, en veillant à ce que votre nouveau code soit précisément le même nombre d'octets que l'ancien code (ou tous les décalages seraient incorrects ). Si le nouveau code est plus court, vous pouvez le compléter à l'aide des instructions NOP. S'il est plus long, vous pouvez avoir des problèmes et devoir créer de nouvelles fonctions et les appeler à la place.
@mgiuca a correctement répondu à cette réponse d'un point de vue technique. En fait, désassembler un programme exécutable en une source d'assembly facile à recompiler n'est pas une tâche facile.
Pour ajouter quelques éléments à la discussion, il existe quelques techniques/outils qui pourraient être intéressants à explorer, bien qu'ils soient techniquement complexes.
-g
offre souvent de meilleurs résultats. Vous voudrez peut-être essayer Retargetable Decompiler .La plupart de ces informations proviennent des domaines de recherche d'évaluation de la vulnérabilité et d'analyse d'exécution. Ce sont des techniques complexes et souvent les outils ne peuvent pas être utilisés immédiatement hors de la boîte. Néanmoins, ils fournissent une aide inestimable lors de la tentative de rétro-ingénierie de certains logiciels.
Pour changer le code à l'intérieur d'un assembly binaire, il y a généralement 3 façons de le faire.
Bien entendu, seul le 2e fonctionnera, si l'Assemblée procède à une quelconque vérification d'auto-intégrité.
Edit: Si ce n'est pas évident, jouer avec des assemblages binaires est un truc de développeur de très haut niveau, et vous aurez du mal à le demander ici, à moins que ce ne soit vraiment des choses spécifiques que vous demandez.
Je le fais avec hexdump
et un éditeur de texte. Vous devez être vraiment à l'aise avec le code machine et le format de fichier le stockant, et flexible avec ce qui compte comme "démonter, modifier, puis remonter" .
Si vous pouvez vous contenter de faire des "changements ponctuels" (réécriture d'octets, mais sans ajouter ni supprimer d'octets), ce sera facile (relativement parlant).
Vous vraiment ne voulez pas déplacer les instructions existantes, car alors vous devrez ajuster manuellement tout décalage relatif effectué dans le code machine, pour les sauts/branches/charges/magasins par rapport au compteur de programme, tous deux en valeurs codées en dur immédiates et ceux calculés via registres .
Vous devriez toujours pouvoir vous en tirer sans supprimer les octets. L'ajout d'octets peut être nécessaire pour des modifications plus complexes et devient beaucoup plus difficile.
Après avoir réellement désassemblé le fichier correctement avec objdump -D
ou tout ce que vous utilisez normalement en premier pour le comprendre et trouver les points que vous devez modifier, vous devrez prendre note des éléments suivants pour vous aider à localiser les octets corrects à modifier:
--show-raw-insn
option pour objdump
est vraiment utile ici).Vider la représentation hexadécimale brute du fichier binaire avec hexdump -Cv
.
Ouvrez le fichier hexdump
ed et recherchez les octets à l'adresse que vous souhaitez modifier.
Cours accéléré rapide en hexdump -Cv
production:
objdump
fournit).|
caractères) est juste une représentation "lisible par l'homme" des octets - le caractère ASCII correspondant à chaque octet y est écrit, avec un .
remplace tous les octets qui ne correspondent pas à un caractère imprimable ASCII.Attention: contrairement à objdump -D
, qui vous donne l'adresse de chaque instruction et affiche l'hex brut de l'instruction en fonction de la façon dont elle est documentée comme étant encodée, hexdump -Cv
sauvegarde chaque octet exactement dans l'ordre où il apparaît dans le fichier. Cela peut être un peu déroutant car d'abord sur les machines où les octets d'instructions sont dans l'ordre inverse en raison de différences d'endianité, ce qui peut également être désorientant lorsque vous attendez un octet spécifique comme adresse spécifique.
Modifiez les octets qui doivent être modifiés - vous devez évidemment comprendre le codage des instructions machine brutes (et non les mnémoniques d'assemblage) et écrire manuellement les octets corrects.
Remarque: Vous pas devez changer la représentation lisible par l'homme dans la colonne la plus à droite. hexdump
l'ignorera lorsque vous le "déchargerez".
"Annuler le vidage" du fichier hexdump modifié à l'aide de hexdump -R
.
objdump
votre nouveau fichier hexdump
ed et vérifiez que le démontage que vous avez modifié est correct. diff
par rapport au objdump
de l'original.
Sérieusement, ne sautez pas cette étape. Je fais une erreur le plus souvent lors de l'édition manuelle du code machine et c'est ainsi que j'attrape la plupart d'entre eux.
Voici un exemple concret et concret de la récente modification d'un binaire ARMv8 (petit endian). (Je sais, la question est balisée x86
, mais je n'ai pas d'exemple x86 à portée de main, et les principes fondamentaux sont les mêmes, juste les instructions sont différentes.)
Dans ma situation, je devais désactiver une vérification de prise en main spécifique "vous ne devriez pas faire cela": dans mon exemple binaire, dans objdump --show-raw-insn -d
la sortie de la ligne qui m'intéressait ressemblait à ceci (une instruction avant et après donnée pour le contexte):
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
Comme vous pouvez le voir, notre programme quitte "utilement" en sautant dans une fonction error
(qui termine le programme). Inacceptable. Nous allons donc transformer cette instruction en un no-op. Nous recherchons donc les octets 0x97fffeeb
à l'adresse/décalage de fichier 0xf44
.
Voici la hexdump -Cv
ligne contenant ce décalage.
00000f40 e3 03 15 aa eb fe ff 97 f7 13 40 f9 e8 02 40 39 |..........@...@9|
Remarquez comment les octets pertinents sont réellement inversés (le petit codage endian dans l'architecture s'applique aux instructions de la machine comme à toute autre chose) et comment cela se rapporte de manière peu intuitive à quel octet est à quel décalage d'octet:
00000f40 -- -- -- -- eb fe ff 97 -- -- -- -- -- -- -- -- |..........@...@9|
^
This is offset f44, holding the least significant byte
So the *instruction as a whole* is at the expected offset,
just the bytes are flipped around. Of course, whether the
order matches or not will vary with the architecture.
Quoi qu'il en soit, je sais en regardant d'autres démontages que 0xd503201f
se démonte en nop
ce qui semble être un bon candidat pour mon instruction sans opération. Je modifie la ligne dans le fichier hexdump
ed en conséquence:
00000f40 e3 03 15 aa 1f 20 03 d5 f7 13 40 f9 e8 02 40 39 |..........@...@9|
Reconverti en binaire avec hexdump -R
, a démonté le nouveau binaire avec objdump --show-raw-insn -d
et vérifié que la modification était correcte:
f40: aa1503e3 mov x3, x21
f44: d503201f nop
f48: f94013f7 ldr x23, [sp, #32]
Ensuite, j'ai exécuté le binaire et obtenu le comportement que je voulais - la vérification pertinente n'a plus provoqué l'arrêt du programme.
Modification du code machine réussie.
Ou ai-je réussi? Avez-vous repéré ce que j'ai manqué dans cet exemple?
Je suis sûr que vous l'avez fait - puisque vous demandez comment modifier manuellement le code machine d'un programme, vous savez probablement ce que vous faites. Mais pour le bénéfice de tous les lecteurs qui pourraient lire pour apprendre, je développerai:
J'ai seulement changé l'instruction last dans la branche cas d'erreur! Le saut dans la fonction qui sort du problème. Mais comme vous pouvez le voir, enregistrez x3
était en cours de modification par le mov
juste au-dessus! En fait, un total de quatre (4) registres ont été modifiés dans le cadre du préambule pour appeler error
, et un registre l'était. Voici le code machine complet pour cette branche, en commençant par le saut conditionnel sur le bloc if
et en terminant là où le saut va si le if
conditionnel n'est pas pris:
f2c: 350000e8 cbnz w8, f48
f30: b0000002 adrp x2, 1000
f34: 91128442 add x2, x2, #0x4a1
f38: 320003e0 orr w0, wzr, #0x1
f3c: 2a1f03e1 mov w1, wzr
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
Tout le code après la branche a été généré par le compilateur en supposant que l'état du programme était tel qu'il était avant le saut conditionnel ! Mais en faisant juste le saut final au code de fonction error
un no-op, j'ai créé un chemin de code où nous atteignons ce code avec un état de programme incohérent/incorrect !
Dans mon cas, cela ne semblait pas causer de problème. J'ai donc eu de la chance. Très chanceux: seulement après avoir déjà exécuté mon binaire modifié (qui, soit dit en passant, était un binaire critique pour la sécurité : il avait la capacité de setuid
, setgid
, et de changer le contexte SELinux !) Est-ce que je me suis rendu compte que j'avais oublié de suivre réellement les chemins de code pour savoir si ces changements de registre affectaient les chemins de code qui sont venus plus tard!
Cela aurait pu être catastrophique - n'importe lequel de ces registres aurait pu être utilisé dans un code ultérieur avec l'hypothèse qu'il contenait une valeur précédente qui a maintenant été écrasée! Et je suis le genre de personne que les gens connaissent pour une réflexion méticuleuse sur le code et comme un pédant et un bâton pour être toujours consciencieux de la sécurité informatique.
Que faire si j'appelais une fonction où les arguments débordaient des registres sur la pile (comme c'est très courant sur, par exemple, x86)? Que se passe-t-il s'il y avait en fait plusieurs instructions conditionnelles dans le jeu d'instructions qui ont précédé le saut conditionnel (comme cela est courant sur, par exemple, les anciennes versions ARM)? état incohérent après avoir effectué ce changement le plus simple!
Donc, mon rappel: Le fait de manipuler manuellement les binaires est littéralement dépouillant chaque la sécurité entre vous et ce que la machine et le système d'exploitation permettront. Littéralement toutes les avancées que nous avons faites dans nos outils pour détecter automatiquement les erreurs de nos programmes, disparues .
Alors, comment pouvons-nous résoudre ce problème plus correctement? Continuer à lire.
Pour efficacement / logiquement "supprimer" plus d'une instruction, vous pouvez remplacez la première instruction que vous souhaitez "supprimer" par un saut inconditionnel à la première instruction à la fin des instructions "supprimées". Pour ce binaire ARMv8, qui ressemblait à ceci:
f2c: 14000007 b f48
f30: b0000002 adrp x2, 1000
f34: 91128442 add x2, x2, #0x4a1
f38: 320003e0 orr w0, wzr, #0x1
f3c: 2a1f03e1 mov w1, wzr
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
Fondamentalement, vous "tuez" le code (le transformez en "code mort"). Sidenote: Vous pouvez faire quelque chose de similaire avec des chaînes littérales incorporées dans le binaire: tant que vous voulez le remplacer par une chaîne plus petite, vous pouvez presque toujours vous en sortir en écrasant la chaîne (y compris l'octet nul de fin s'il s'agit d'un "C- chaîne ") et, si nécessaire, écraser la taille codée en dur de la chaîne dans le code machine qui l'utilise.
Vous pouvez également remplacer toutes les instructions indésirables par aucune opération. En d'autres termes, nous pouvons transformer le code indésirable en ce qu'on appelle un "traîneau sans opération":
f2c: d503201f nop
f30: d503201f nop
f34: d503201f nop
f38: d503201f nop
f3c: d503201f nop
f40: d503201f nop
f44: d503201f nop
f48: f94013f7 ldr x23, [sp, #32]
Je m'attendrais à ce que cela ne fasse que gaspiller les cycles de CPU par rapport à leur saut, mais c'est plus simple et donc plus sûr contre les erreurs , car vous n'avez pas à comprendre manuellement comment coder l'instruction de saut, y compris la détermination du décalage/adresse à utiliser - vous n'avez pas à penser autant pour un traîneau sans op.
Pour être clair, l'erreur est facile: j'ai foiré deux (2) fois lors du codage manuel de cette instruction de branchement inconditionnel. Et ce n'est pas toujours de notre faute: la première fois, c'était parce que la documentation que j'avais était obsolète/incorrecte et disait qu'un bit était ignoré dans l'encodage, alors qu'il ne l'était pas, alors je l'ai mis à zéro lors de mon premier essai.
Vous pourriez théoriquement utiliser cette technique pour ajouter des instructions machine aussi, mais c'est plus complexe, et je n'ai jamais eu à le faire, donc je n'ai pas d'exemple travaillé pour le moment.
Du point de vue du code machine, c'est très simple: choisissez une instruction à l'endroit où vous souhaitez ajouter du code, et convertissez-la en une instruction de saut vers le nouveau code que vous devez ajouter (n'oubliez pas d'ajouter la ou les instructions que vous souhaitez ainsi remplacé dans le nouveau code, sauf si vous n'en aviez pas besoin pour votre logique ajoutée, et pour revenir à l'instruction à laquelle vous souhaitez revenir à la fin de l'ajout). Fondamentalement, vous "épissez" le nouveau code.
Mais vous devez trouver un endroit pour mettre réellement ce nouveau code, et c'est la partie difficile.
Si vous êtes vraiment chanceux, vous pouvez simplement ajouter le nouveau code machine à la fin du fichier, et cela "fonctionnera": le nouveau code sera chargé avec le reste dans les mêmes instructions machine attendues, dans votre espace d'adressage qui tombe dans une page mémoire correctement marquée exécutable.
Dans mon expérience hexdump -R
ignore non seulement la colonne la plus à droite mais aussi la colonne la plus à gauche - vous pouvez donc littéralement simplement mettre zéro adresse pour toutes les lignes ajoutées manuellement et cela fonctionnera.
Si vous avez moins de chance, après avoir ajouté le code, vous devrez réellement ajuster certaines valeurs d'en-tête dans le même fichier: si le chargeur de votre système d'exploitation s'attend à ce que le binaire contienne des métadonnées décrivant la taille de la section exécutable (pour des raisons historiques souvent appelé la section "texte"), vous devrez trouver et ajuster cela. Autrefois, les binaires n'étaient que du code machine brut - de nos jours, le code machine est enveloppé dans un tas de métadonnées (par exemple ELF sur Linux et quelques autres).
Si vous êtes encore un peu chanceux, vous pourriez avoir un endroit "mort" dans le fichier qui est correctement chargé dans le cadre du binaire avec les mêmes décalages relatifs que le reste du code qui est déjà dans le fichier (et que un point mort peut s'adapter à votre code et est correctement aligné si votre CPU nécessite l'alignement de Word pour les instructions du CPU). Ensuite, vous pouvez l'écraser.
Si vous n'avez vraiment pas de chance, vous ne pouvez pas simplement ajouter du code et il n'y a pas d'espace mort que vous pouvez remplir avec votre code machine. À ce stade, vous devez fondamentalement être intimement familier avec le format exécutable et espérer que vous pouvez trouver quelque chose dans ces contraintes qui est humainement possible de retirer manuellement dans un délai raisonnable et avec une chance raisonnable de ne pas le gâcher. .
Mon "ci assembler disassembler" est le seul système que je connaisse qui est conçu autour du principe que quel que soit le démontage, il doit se réassembler à l'octet pour l'octet même binaire.
https://github.com/albertvanderhorst/ciasdis
Il existe deux exemples d'exécutables elf avec leur démontage et remontage. Il a été initialement conçu pour pouvoir modifier un système de démarrage, composé de code, de code interprété, de données et de caractères graphiques, avec des subtilités telles que la transition du mode réel au mode protégé. (Il a réussi.) Les exemples démontrent également l'extraction de texte à partir des exécutables, qui est ensuite utilisé pour les étiquettes. Le paquet debian est destiné à Intel Pentium, mais des plugins sont disponibles pour Dec Alpha, 6809, 8086 etc.
La qualité du démontage dépend de l'effort que vous y mettez. Par exemple, si vous ne fournissez même pas les informations selon lesquelles il s'agit d'un fichier elf, le désassemblage se compose d'un seul octet et le réassemblage est trivial. Dans les exemples, j'utilise un script qui extrait des étiquettes et crée un programme de rétro-ingénierie vraiment utilisable et modifiable. Vous pouvez insérer ou supprimer quelque chose et les étiquettes symboliques générées automatiquement seront recalculées.
Aucune hypothèse n'est faite sur le blob binaire, mais bien sûr, un démontage Intel est de peu d'utilité pour un binaire Dec Alpha.
miasme
https://github.com/cea-sec/miasm
Cela semble être la solution concrète la plus prometteuse. Selon la description du projet, la bibliothèque peut:
- Ouverture/modification/génération de PE/ELF 32/64 LE/BE avec Elfesteem
- Assemblage/Désassemblage X86/ARM/MIPS/SH4/MSP430
Il devrait donc essentiellement:
Je ne pense pas que cela génère une représentation textuelle de désassemblage, vous devrez probablement parcourir les structures de données Python.
TODO trouve un exemple minimal de la façon de faire tout cela en utilisant la bibliothèque. Un bon point de départ semble être exemple/disasm/full.py , qui analyse un fichier ELF donné. La structure clé de niveau supérieur est Container
, qui lit le fichier ELF avec Container.from_stream
. A FAIRE comment le remonter ensuite? Cet article semble le faire: http://www.miasm.re/blog/2016/03/24/re150_rebuild.html
Cette question demande s'il existe d'autres bibliothèques de ce type: https://reverseengineering.stackexchange.com/questions/1843/what-are-the-available-libraries-to-statiquement-modify-elf-executables =
Questions connexes:
Je pense que ce problème n'est pas automatisable
Je pense que le problème général n'est pas entièrement automatisable, et la solution générale est fondamentalement équivalente à "comment désosser" un binaire.
Afin d'insérer ou de supprimer des octets de manière significative, nous devons nous assurer que tous les sauts possibles continuent de sauter aux mêmes emplacements.
En termes formels, nous devons extraire le graphique de flux de contrôle du binaire.
Cependant, avec des branches indirectes par exemple, https://en.wikipedia.org/wiki/Indirect_branch , il n'est pas facile de déterminer ce graphique, voir aussi: Calcul de la destination de saut indirect
Une autre chose qui pourrait vous intéresser:
Si vous êtes intéressé, consultez: Pin, Valgrind (ou projets faisant cela: NaCl - Client natif de Google, peut-être QEmu.)
Vous pouvez exécuter l'exécutable sous la supervision de ptrace (en d'autres termes, un débogueur comme gdb) et de cette façon, contrôler l'exécution au fur et à mesure, sans modifier le fichier réel. Bien sûr, nécessite les compétences d'édition habituelles comme trouver où se trouvent les instructions particulières que vous souhaitez influencer dans l'exécutable.