Comment désassembler, modifier puis réassembler un exécutable Linux?

y a-t-il une façon de faire? J'ai utilisé objdump mais cela ne produit pas de sortie d'assemblage qui sera acceptée par n'importe quel assembleur que je connais. J'aimerais pouvoir changer les instructions d'un exécutable et le tester ensuite.

45

7 réponses

Je ne pense pas qu'il y ait 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é (au format ELF, par exemple) et de produire un programme d'assemblage de sources qui compilera le même binaire (ou assez similaire). Pour comprendre les différences, comparez la sortie de GCC compilant directement vers l'assembleur ( gcc -S ) versus la sortie d'objdump sur l'exécutable ( objdump -D ).

il y a deux complications majeures auxquelles je peux penser. Tout d'abord, le code machine lui-même n'est pas une correspondance de 1 à 1 avec le code d'assemblage, à cause de choses comme les décalages de pointeurs.

par exemple, considérez le code C pour Hello world:

int main()
{
    printf("Hello, world!\n");
    return 0;
}

cela se compile avec 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. Comparer à la sortie de objdump:

80483cd:       b8 b0 84 04 08          mov    "151920920"x80484b0,%eax
80483d2:       89 04 24                mov    %eax,(%esp)
80483d5:       e8 1a ff ff ff          call   80482f4 <printf@plt>

tout D'abord, la constante .LC0 n'est plus qu'un décalage aléatoire dans la mémoire quelque part -- il serait difficile de créer un fichier source d'assemblage qui contient cette constante à la bonne place, puisque l'assembleur et le linker 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 de position indépendant), mais je crois que la référence à printf n'est en fait pas du tout encodé à l'adresse du pointeur dans ce code, mais les en-têtes ELF contiennent une table de recherche qui remplace dynamiquement son adresse à l'exécution. Par conséquent, le code démonté ne correspond pas tout à fait au code d'assemblage des sources.

en résumé, l'ensemble source a symboles tandis que le code machine compilé a adresses qui sont difficiles à inverser.

la seconde la principale complication est qu'un fichier source d'assemblage ne peut pas contenir toutes les informations présentes dans les en-têtes de fichier ELF d'origine, comme les bibliothèques auxquelles il faut relier dynamiquement, et les autres métadonnées qui y sont placées par le compilateur d'origine. Il serait difficile de reconstituer ce.

comme je l'ai dit, il est possible qu'un outil spécial puisse manipuler toutes ces informations, mais il est peu probable qu'on puisse simplement produire du code d'assemblage qui puisse être remonté à l'exécutable.

si vous êtes intéressé à modifier juste une petite section de l'exécutable, je recommande une approche beaucoup plus subtile que de recompiler toute l'application. Utilisez objdump pour obtenir le code d'assemblage de la ou des fonctions qui vous intéressent. Convertissez-le à la main en" syntaxe d'assemblage de source " (et ici, j'aimerais qu'il y ait un outil qui produit le démontage dans la même syntaxe que l'entrée), et modifiez-le comme vous le souhaitez. Quand vous avez terminé, recompilez juste ceux fonction (s) et utilisez objdump pour calculer le code machine de votre programme modifié. Ensuite, utilisez un éditeur hexadécimal pour coller manuellement le nouveau code machine sur le dessus de la partie correspondante du programme d'origine, en prenant soin de votre nouveau code est exactement le même nombre d'octets que l'ancien code (ou de tous les décalages serait erroné). Si le nouveau code est plus court, vous pouvez le remplir en utilisant les instructions NOP. Si elle est plus longue, vous pouvez être en difficulté, et pourrait avoir à créer de nouvelles fonctions et les appeler plutôt.

27
répondu mgiuca 2010-11-30 03:39:19

pour changer le code à l'intérieur d'un assemblage binaire, il y a généralement 3 façons de le faire.

  • Si c'est juste quelque chose banale comme une constante, alors il suffit de changer l'emplacement avec un éditeur hexadécimal. En supposant que vous pouvez trouver pour commencer.
  • si vous avez besoin de modifier le code, utilisez alors le LD_PRELOAD pour écraser certaines fonctions de votre programme. Cela ne fonctionne pas si la fonction n'est pas dans les tables de fonction si.
  • pirater le code à la fonction que vous voulez corriger pour être un saut direct à une fonction que vous chargez via LD_PRELOAD et puis revenir au même endroit (c'est un combi des deux ci-dessus)

bien sûr, seul le 2ème fonctionnera, si l'assemblage fait n'importe quelle sorte de contrôle d'auto-intégrité.

Edit: si ce n'est pas évident, alors jouer avec des assemblages binaires est un truc de développeur de très haut niveau, et vous aurez du mal à poser des questions sur ici, sauf si c'est vraiment des choses spécifiques que vous demandez.

7
répondu Cine 2010-11-30 04:17:42

@mgiuca a correctement abordé cette réponse d'un point de vue technique. En fait, démonter un programme exécutable en une source d'assemblage facile à recompiler n'est pas une tâche facile.

pour ajouter quelques éléments à la discussion, il y a quelques techniques/outils qui pourraient être intéressants à explorer, bien qu'ils soient techniquement complexes.

  1. Statique/Dynamique instrumentation . Cette technique cela implique d'analyser le format de l'exécutable, d'insérer/supprimer/remplacer des instructions d'assemblage spécifiques pour un but donné, de corriger toutes les références aux variables/fonctions dans l'exécutable, et d'émettre un nouvel exécutable modifié. Certains outils que je connais sont: PIN , Hijacker , PEBIL , DynamoRIO . Considérer que la configuration de tels outils à une fin différente de ce qu'ils ont été conçus pour pourrait être difficile, et nécessite la compréhension des formats exécutables et des jeux d'instructions.
  2. Complet de l'exécutable décompilation . Cette technique tente de reconstruire une source d'assemblage complète à partir d'un exécutable. Vous pouvez jeter un coup d'oeil au Online Disassembler , qui tente de faire le travail. Vous perdez de toute façon des informations sur différents modules source et éventuellement des fonctions/noms de variables.
  3. Reciblage décompilation . Cette technique tente d'extraire plus d'informations de l'exécutable, en regardant "les empreintes du compilateur (i.e., les patrons de code générés par les compilateurs connus) et d'autres trucs déterministes. Le but principal est de reconstruire le code source de niveau supérieur, comme C source, à partir d'un exécutable. Ceci est parfois capable de retrouver des informations sur les noms de fonctions/variables. Considérez que compiler des sources avec -g souvent offre de meilleurs résultats. Vous pourriez vouloir donner le Retargetable Decompiler un essai.

la majeure partie de cette somme provient des domaines de recherche sur l'évaluation et l'exécution de l'analyse de la vulnérabilité. Il s'agit de techniques complexes et, souvent, les outils ne peuvent pas être utilisés immédiatement. Néanmoins, ils fournissent une aide précieuse lors de l'essai d'ingénierie inverse de certains logiciels.

6
répondu ilpelle 2016-06-19 16:00:10

miasme

https://github.com/cea-sec/miasm

Cette solution semble la plus prometteuse. Selon la description du projet, la bibliothèque peut:

  • ouverture / modification / production PE / ELF 32 / 64 LE / BE using Elfesteem
  • assemblage / démontage X86 / ARM / MIPS / SH4 / MSP430

donc il devrait essentiellement:

  • analyser L'ELF en une représentation interne (démontage)
  • modifier ce que vous voulez
  • générer un nouveau ELF (assemblée)

Je ne pense pas qu'il génère une représentation de désassemblage textuel, vous devrez probablement marcher à travers les structures de données Python.

TODO trouver un exemple minimal de la façon de faire tout cela en utilisant la bibliothèque. Un bon point de départ semble être example/disasm/full.py , qui analyse un fichier ELF donné. La principale structure de niveau supérieur est Container , qui lit le fichier ELF avec Container.from_stream . TODO comment faire pour le remonter par la suite? Cet article semble le faire: http://www.miasm.re/blog/2016/03/24/re150_rebuild.html

cette question demande s'il y a une autre bibliothèques: https://reverseengineering.stackexchange.com/questions/1843/what-are-the-available-libraries-to-statically-modify-elf-executables

questions connexes:

je pense que ce problème n'est pas automatique

je pense que le problème n'est pas entièrement automatisable, et la solution générale est essentiellement équivalent à "comment désosser" binaire.

pour insérer ou supprimer des octets de manière significative, nous devrions nous assurer que tous les sauts possibles continuez à sauter aux mêmes endroits.

en termes formels, nous avons besoin d'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 du saut Indirect

1

autre chose qui pourrait vous intéresser:

  • instrumentation binaire-modification du code existant

si vous êtes intéressé, consultez: Pin, Valgrind (ou les projets faisant cela: NaCl - Client natif de Google, peut-être QEmu.)

0
répondu Grzegorz Wierzowiecki 2011-01-02 22:31:45

vous pouvez exécuter l'exécutable sous la supervision de ptrace (en d'autres termes, un débogueur comme gdb) et ainsi, contrôler l'exécution au fur et à mesure, sans modifier le fichier lui-même. Bien sûr, nécessite les compétences habituelles d'édition comme trouver où les instructions particulières que vous voulez influencer sont dans l'exécutable.

0
répondu user502515 2011-01-02 22:39:56

je fais ça avec hexdump et un éditeur de texte. Vous devez être vraiment à l'aise avec le code machine et le format de fichier le stocker, et flexible avec ce qui compte comme "démonter, modifier, puis remonter".

si vous pouvez vous en sortir en faisant juste 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 des instructions existantes, parce qu'alors vous auriez à ajuster manuellement n'importe quel décalage relatif effectué dans le code de machine, pour les sauts/branches/charges/magasins par rapport au compteur de programme, les deux dans hardcoded immediate valeurs et ceux calculés par registres .

vous devriez toujours être en mesure de vous en tirer sans supprimer les octets. Ajouter des octets pourrait être nécessaire pour modifications plus complexes, et devient beaucoup plus difficile.

étape 0 (préparation)

après que vous avez en fait démonté le fichier correctement avec objdump -D ou ce que vous utilisez normalement en premier pour réellement le comprendre et trouver les taches que vous devez changer, vous aurez besoin de prendre note des choses suivantes pour vous aider à localiser les octets corrects à modifier:

  1. l '"adresse" (offset from le début du fichier) des octets que vous devez changer.
  2. la valeur brute de ces octets tels qu'ils sont actuellement (l'option --show-raw-insn à objdump est vraiment utile ici).

Étape 1

Dump la représentation hexadécimale brute du fichier binaire avec hexdump -Cv .

l'Étape 2

ouvrir le fichier hexdump ed et trouver les octets à l'adresse vous êtes vous cherchez à changer.

cours de crash rapide dans hexdump -Cv sortie:

  1. la colonne la plus à gauche est l'adresse des octets (relative au début du fichier binaire lui-même, tout comme objdump fournit).
  2. la colonne de droite (entourée des 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 . position debout pour tous les octets qui ne correspondent pas à un caractère imprimable ASCII.
  3. la substance importante est entre - chaque octet comme deux chiffres hexadécimaux séparés par des espaces, 16 bytes par ligne.

attention: contrairement à objdump -D , qui vous donne l'adresse de chaque instruction et montre l'hexagone brut de l'instruction basée sur la façon dont elle est documentée comme étant encodée, hexdump -Cv décharge chaque octet exactement dans l'ordre où il apparaît dans le fichier. Ce peut être un peu déroutant car d'abord sur les machines où les octets d'instruction sont dans l'ordre opposé à cause des différences d'endianess, ce qui peut aussi être déroutant quand vous attendez un octet spécifique comme adresse spécifique.

l'Étape 3

Modifier les octets qui doivent changer - vous devez évidemment comprendre le codage d'instruction de machine brute (pas l'assemblage mnémonique) et écrire manuellement dans les octets corrects.

Note: Vous ne pas besoin de changer la représentation explicite dans la colonne la plus à droite. hexdump l'ignorera quand vous le" dé-larguerez".

Étape 4

"Un-dump" le fichier hexdump modifié en utilisant hexdump -R .

Step 5 (sanity check)

objdump votre nouveau fichier un hexdump ed et vérifiez que le démontage que vous avez modifié semble correct. diff contre le objdump de l'original.

sérieusement, ne sautez pas cette étape. Je fais une erreur plus souvent qu'autrement en éditant manuellement le code machine et c'est comme ça que je les attrape.

exemple

voici un exemple de travail réel de quand J'ai modifié un binaire ARMv8 (little endian) récemment. (Je sais, la question est taggés x86 , mais je n'ai pas un x86 exemple à portée de main, et les fondamentaux les principes sont les mêmes, juste les instructions sont différentes.)

dans ma situation, j'avais besoin de désactiver un" vous ne devriez pas faire ce "contrôle manuel: dans mon exemple binaire, dans la sortie objdump --show-raw-insn -d la ligne dont je me souciais 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 est" helpfully "quitter en sautant dans une fonction error (qui met fin à la programme.) Inacceptable. Donc nous allons transformer cette instruction en no-op. Nous recherchons donc les octets 0x97fffeeb à l'adresse/offset 0xf44 .

Voici la ligne hexdump -Cv 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 effectivement inversés (petit encodage endian dans l'architecture s'applique aux instructions machine comme à toute autre chose) et comment cela se rapporte légèrement unintuentiously à ce que octet est à ce 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 démonte à nop donc cela semble être un bon candidat pour mon no-op instruction. Je modifie la ligne dans le hexdump ed fichier 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 , démonté le nouveau binaire avec objdump --show-raw-insn -d et vérifié que le changement était correct:

     f40:   aa1503e3    mov x3, x21
     f44:   d503201f    nop
     f48:   f94013f7    ldr x23, [sp, #32]

puis j'ai lancé le binaire et j'ai obtenu le comportement que je voulais - le contrôle pertinent n'a plus causé le programme d'avorter.

Modification du Code Machine réussie.

!!! Avertissement !!!

ou Ai-je réussi? Avez-vous remarqué 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 vais élaborer:

j'ai seulement changé le dernier de l'enseignement dans l'erreur de cas sur la direction! Le saut dans la fonction qui quitte le problème. Mais comme vous pouvez le voir, le registre x3 a été modifié par le mov juste au-dessus! En fait, un total de quatre (4) registres ont été modifiés dans le cadre du préambule à appeler error , et un registre a été. Voici le code machine complet pour cette branche, à partir du saut conditionnel au-dessus du bloc if et se 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 sur l'hypothèse que l'état du programme était comme il était avant le saut conditionnel ! Mais en faisant juste le saut final à la fonction error code a no-op, j'ai créé un chemin de code où nous atteignons ce code avec l'état de programme incohérent/incorrect !

dans mon cas, en fait, semblait à ne pas causer de problèmes. J'ai donc eu de la chance. très chance: seulement après que j'ai déjà couru mon binaire modifié (qui, soit dit en passant, était un binaire de sécurité critique : il avait la capacité de setuid , setgid , et changer contexte SELinux !) est-ce que j'ai réalisé que j'avais oublié de tracer les chemins de code pour savoir si ces changements de Registre ont affecté les chemins de code qui sont venus plus tard!

qui aurait pu être catastrophique - n'importe lequel de ces registres aurait pu être utilisé plus tard dans le code 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 en tant que pédant et pointilleux toujours être conscient de la sécurité informatique.

et si j'appelais une fonction où les arguments se déversaient des registres sur la pile (comme c'est très courant sur, par exemple, x86)? Et s'il y avait en fait plusieurs instructions conditionnelles dans le jeu d'instructions qui précédaient le saut conditionnel (comme c'est courant sur, par exemple, les anciennes versions de bras)? J'aurais été encore plus témérairement état incohérent après avoir fait la plus simple-semblant changer!

donc ceci Mon Rappel de mise en garde: tourner manuellement avec des binaires est littéralement enlever chaque sécurité entre vous et ce que la machine et le système d'exploitation permettra. Littéralement tout les avances que nous avons faites dans nos outils pour attraper automatiquement des erreurs nos programmes, parti .

alors comment réparer cela plus correctement? Lire sur.

Suppression Du Code

à effectivement / logiquement "Supprimer" plus d'une instruction, vous pouvez remplacer la première instruction que vous voulez "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]

En gros, vous" tuez "le code (le transformez en"code mort"). Sidenote: vous pouvez faire quelque chose de similaire avec des chaînes littérales intégrées dans le binaire: aussi longtemps que vous voulez le remplacer par une plus petite chaîne, vous pouvez presque toujours vous en tirer avec l'écrasement de la chaîne (y compris le octet null final si c'est une "C-string") et si nécessaire l'écrasement de la taille codée dure de la chaîne dans le code machine qui l'utilise.

vous pouvez également remplacer toutes les instructions indésirables par no-ops. En d'autres termes, nous pouvons transformer le code indésirable en ce qu'on appelle un "no-op sled":

     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 ce est juste gaspiller les cycles CPU relative à sauter au-dessus d'eux, mais il est plus simple et donc plus sûr contre les erreurs , parce que vous ne devez pas comprendre manuellement comment encoder l'instruction de saut, y compris en calculant l'offset / adresse à utiliser dans elle - vous ne il faut penser autant pour un sled no-op.

pour être clair, l'erreur est facile: j'ai foiré deux (2) fois lors de l'encodage manuel de cette instruction de branche inconditionnelle. Et ce n'est pas toujours notre faute: la première fois, c'était parce que la documentation que j'avais était périmée/erronée et disait qu'un peu était ignoré dans l'encodage, alors que ce n'était pas le cas, donc je l'ai mis à zéro à mon premier essai.

Ajouter Le Code

vous pourrait théoriquement utiliser cette technique pour ajouter Instructions machine trop, mais il est plus complexe, et je n'ai jamais eu à le faire, donc je n'ai pas un exemple travaillé à ce moment.

du point de vue du code machine, c'est un peu facile: Choisissez une instruction à l'endroit où vous voulez ajouter du code, et convertissez-la EN instruction de saut vers le nouveau code que vous devez ajouter (n'oubliez pas d'ajouter enseignement(s) vous avez donc remplacé dans le nouveau code, à moins que vous n'avez pas besoin de cela pour votre plus logique, et pour revenir à l'instruction que vous voulez revenir à la fin de l'addition). En gros, vous "copiez" le nouveau code.

mais vous devez trouver un endroit pour réellement mettre ce nouveau code, et c'est la partie difficile.

si vous êtes vraiment lucky, vous pouvez juste ajouter le nouveau code machine à la fin du fichier, et cela "ne fera que fonctionner": le nouveau code sera chargé avec le reste dans les mêmes instructions machine attendues, dans votre espace d'adresse qui tombe dans une page de mémoire correctement marquée exécutable.

d'après mon expérience hexdump -R ignore non seulement la colonne la plus à droite, mais aussi la colonne la plus à gauche-donc vous pouvez littéralement juste mettre des adresses zéro pour toutes les lignes ajoutées manuellement et ça va marcher.

si vous avez moins de chance, après avoir ajouté le code que vous aurez à 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. Dans le passé, 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 d'autres).

si vous êtes encore un peu de chance, vous pourriez avoir un point mort dans le fichier qui est chargé correctement dans le binaire avec les mêmes décalages relatifs que le reste du code qui est déjà dans le fichier (et ce point mort peut correspondre à votre code et est correctement aligné si votre CPU nécessite un alignement de mots pour les instructions CPU). Ensuite, vous pouvez le remplacer.

si vous êtes vraiment malchanceux, vous ne pouvez pas juste ajouter du code et il n'y a pas d'espace mort que vous pouvez remplir avec votre code machine. À ce point, vous devez fondamentalement être intimement familiarisé avec le format exécutable et espérer que vous pouvez comprendre quelque chose dans ces contraintes qui est humainement faisable de tirer au loin manuellement dans une quantité raisonnable pour le temps et avec une chance raisonnable de ne pas le gâcher.

0
répondu mtraceur 2018-10-01 09:25:05