Forum Linux.général Fonctionnement interpreteur linux

Posté par .
1
14
fév.
2012

Bonjour,

je travaille sur un projet ou je dois modifier un fichier lors de sa propre exécution (je change les opcodes pendant l'exécution du binaire).

Mon problème et que je charge des opcodes non-valides et que l'interpreteur me renvoie l'exception :

illegal instruction

J'essaie de contourner cette limitation, j'ai pensé à mettre une exception (exit), et de la modifier ensuite en une instruction ne faisant rien.

J'ai remarqué que le processeur charge en cache des groupes d'instruction, ce qui fait que je dois laisser un certaines nombres d'opcodes entre mes modifications.

Ce qui me surprend et que mon programme fonctionne avec GDB, car celui-ci interprète ligne par ligne et n'effectue que des opcodes valides, alors que l'interpréteur lui ne voit pas que les opcodes seront ensuite modifié. Mais lorsque j'essaie de le contourner en modifiant des instructions alors la il arrive à me suivre...

J'aurai besoin d'information sur l'interpréteur, principalement savoir comment il vérifie qu'un programme est valide, utilise-t-il le processeur ou un check logicielle ?

Merci d'avance pour votre aide.
Manticore

OS : Linux armadeus 2.6.29.6
support matériel : APF27

  • # je penses qu'il nous manque plein d'infos

    Posté par . Évalué à 2.

    quel langage ?
    quelle techno (compilée, interpretée) ?

    en effet, tu parles de l'execution du binaire on peut donc en deduire que c'est un langage compilé

    mais tu cherches à savoir comment fonctionne l'interpreteur, ce qui est en contradiction avec la ligne precedente.

    bref, dis nous plus precisement ce que tu fais et avec quel langage, on aura peut-etre des pistes pour t'aiguiller.

  • # pourquoi faire comme ça ?

    Posté par . Évalué à 2.

    la methode est etrange de mon point de vu... mais bon ... y'a longtemps que j'ai pas codé...

    J'aurai bien vu un chargement en memoire le binaire réel généré par le binaire executable et le lancer dans un processus fils tout connement ?? (genre donnée encapsulée dans le binaire executable)...
    un peu comme quand on codait des 4kdemos avec vidéos sur atari...on intégrait les vidéos et images dans le binaire executé sur le secteur de boot de la disquette....

    ca évite tous les problèmes d'interpretation et ton binaire reste un encrypté avec cette structure là....

  • # Interpréteur ?

    Posté par . Évalué à 4.

    alors que l'interpréteur lui ne voit pas que les opcodes seront ensuite modifié. Mais lorsque j'essaie de le contourner en modifiant des instructions alors la il arrive à me suivre...
    J'aurai besoin d'information sur l'interpréteur ?

    Tu parles de quel interpréteur ?

    Tu modifies tes opcodes de quelle façon ? Si c'est en modifiant le fichier proprement dit, alors bien que celui-ci soit mappé en mémoire par le système, cette opération n'est qu'une vue de l'esprit et code réellement exécuté par le micro-processeur ne sera modifié qu'à partir du moment où le système aura pu reprendre la main au moins une fois et en profiter pour faire la mise à jour. Et encore, seulement s'il décide de la faire et s'il n'estime pas que les deux images sont désormais distinctes entre elles.

    Ça dépend aussi de ce que tu utilises pour modifier ton fichier. Si c'est un fwrite() ou un fprintf() en C standard, alors tout ce que tu vas écrire va aller dans un tampon propre à ton processus, et l'appel système qui va effectivement modifier le fichier n'aura lieu que plus tard.

    Tout cela dépend donc de beaucoup de points différents, surtout quand on tient compte du fait que le système est en principe conçu pour que ce genre de manip' soit interdite. Donc, il ne faut pas s'attendre à ce que les choses soient faciles.

  • # complément d'informations

    Posté par . Évalué à 1.

    Désolé je suis conscient que j'étais un peu vague, j'étais trop plongé dans mon projet quand j'ai écris ce post.

    Alors dans l'ordre :
    > en effet, tu parles de l’exécution du binaire on peut donc en déduire que c'est un langage compilé

    Oui je parle d'un fichier compilé (elf), donc en langage machine (opcode). Pour moi l’interpréteur est le "logiciel" qui lance l'exécutable (commande ./). Si je fais une erreur de sémantique, peux-tu me donner les termes correctes ?

    dis nous plus précisément ce que tu fais et avec quel langage, on aura peut-être des pistes pour t'aiguiller.

    Donc en gros je récupère un fichier binaire, compilé dans un langage compilé (c, c++...), je le modifie avec un script python pour y injecter du code, en modifiant les sections, segments pour garder la structure du fichier elf. Cette partie fonctionne très bien.

    J'aurai bien vu un chargement en memoire le binaire réel généré par le binaire executable et le lancer dans un processus fils tout connement ?? (genre donnée encapsulée dans le binaire executable)...
    un peu comme quand on codait des 4kdemos avec vidéos sur atari...

    Oui, c'est vers cela que je vais me diriger, mais j'avais envie de comprendre dans les détails pourquoi ma première idée ne fonctionne pas.

    Tu modifies tes opcodes de quelle façon ?

    Je modifie directement les opcodes chargé en ram.

    Ça dépend aussi de ce que tu utilises pour modifier ton fichier. Si c'est un fwrite() ou un fprintf() en C standard, alors tout ce que tu vas écrire va aller dans un tampon propre à ton processus, et l'appel système qui va effectivement modifier le fichier n'aura lieu que plus tard

    Je modifie directement le fichier avant l'exécution.

    En résumé :
    1. Je reçois un fichier binaire compilé
    2. Je le patch (chiffrement + modification du point d'entrée, ajout de la méthode de déchiffrement)
    3. Lorsque j'exécute le fichier, il est chargé en mémoire et ma routine de déchiffrement prend le relais pour aller modifier les opcodes chargés en mémoire. (Cela fonctionne aussi)

    Tout cela dépend donc de beaucoup de points différents, surtout quand on tient compte du fait que le système est en principe conçu pour que ce genre de manip' soit interdite. Donc, il ne faut pas s'attendre à ce que les choses soient faciles.

    Justement j'essaie de me documenter sur les limitations que le système met en place ;).

    Merci de votre aide

    • [^] # Re: complément d'informations

      Posté par . Évalué à 3. Dernière modification le 15/02/12 à 14:39.

      Avant toute chose : chaque commentaire est muni d'un lien « répondre » après le texte. Il faut l'utiliser quand tu réponds à une personne, pour que les différents commentaires soient bien organisés en arbre.

      Pour le reste :

      Oui je parle d'un fichier compilé (elf), donc en langage machine (opcode). Pour moi l’interpréteur est le "logiciel" qui lance l'exécutable (commande ./). Si je fais une erreur de sémantique, peux-tu me donner les termes correctes ?

      Un « interpréteur » est un système qui lit des instructions, les unes à la suite des autres, et qui les « interprète », c'est-à-dire qui va exécuter, généralement au moment où il les lit, une action correspondante. En ce sens, le CPU interprète du code en langage machine, mais on n'utilise jamais le terme dans ce sens-là, justement pour éviter des confusions.

      Un interpréteur est donc, en pratique, un logiciel qui est déjà en cours d'exécution et donc la fonction est de lire et d'exécuter, pendant sa propre exécution, un programme transmis par une source donnée (généralement un fichier, mais ça peut être aussi l'entrée standard, ou autre). Ça va donc concerner des langages comme le Shell, les vieux BASIC, le Perl, le PHP, etc.

      C'est de la même façon qu'en musique, un interprète est un musicien qui va exécuter un morceau, qu'il lit sur une partition.

      Le système chargé de reconnaître la nature d'un fichier et, s'il est exécutable, soit de l'exécuter directement, soit de démarrer l'interpréteur approprié n'a pas, à ma connaissance, de nom académique, mais tout le monde parle de « lanceur », tout simplement.

      Je modifie directement les opcodes chargé en ram.

      Oui mais comment ? En principe, un segment de code est protégé en écriture. Tu devrais obtenir systématiquement une segfault si tu n'as pas obtenu explicitement les droits d'accès dessus, et ça ne se fait pas de façon simple.

      En résumé :
      1. Je reçois un fichier binaire compilé
      2. Je le patch (chiffrement + modification du point d'entrée, ajout de la méthode de déchiffrement)
      3. Lorsque j'exécute le fichier, il est chargé en mémoire et ma routine de déchiffrement prend le relais pour aller modifier les opcodes chargés en mémoire. (Cela fonctionne aussi)

      Que tu patches le fichier exécutable avant lancement et que tu y ajoutes une routine, OK. Par contre, à l'exécution, la routine ne devrait pas pouvoir modifier directement le code sans avoir obtenu les privilèges nécessaires. Tu es sûr que c'est bien le code que tu modifies alors, et pas un segment de données quelconque ?

      • [^] # Re: complément d'informations

        Posté par . Évalué à 0.

        Avant toute chose : chaque commentaire est muni d'un lien « répondre » après le texte. Il faut l'utiliser quand tu réponds à une personne, pour que les différents commentaires soient bien organisés en arbre.

        Désolé, j'avais pas compris, j'ai plutôt l'habitude de voir des forums en liste.

        Oui mais comment ? En principe, un segment de code est protégé en écriture. Tu devrais obtenir systématiquement une segfault si tu n'as pas obtenu explicitement les droits d'accès dessus, et ça ne se fait pas de façon simple.

        Il suffit de changer les droits du segment, cela se fait assez facilement.

        Le système chargé de reconnaître la nature d'un fichier et, s'il est exécutable, soit de l'exécuter directement, soit de démarrer l'interpréteur approprié n'a pas, à ma connaissance, de nom académique, mais tout le monde parle de « lanceur », tout simplement.

        merci je parlerai de lanceur désormais, (laucher en anglais ?). J'ai utilisé ce terme à défaut d'avoir trouvé le terme adéquat.

        Que tu patches le fichier exécutable avant lancement et que tu y ajoutes une routine, OK. Par contre, à l'exécution, la routine ne devrait pas pouvoir modifier directement le code sans avoir obtenu les privilèges nécessaires. Tu es sûr que c'est bien le code que tu modifies alors, et pas un segment de données quelconque ?

        En tout cas, lorsque j’exécute mon code au débugger (ida ou gdb), mes instructions changes et j'arrive à dévier le flux de mon programme de la façon suivante :

        entry_point -> routine_dechiffrement -> exception(exit)
        en
        entry_point -> routine_dechiffrement -> branch_old_entry_point

        • [^] # Re: complément d'informations

          Posté par (page perso) . Évalué à 2.

          As-tu regardé les projets similaires à ton besoin (si j'ai bien tout compris) d'outil de compression ELF, exemple "UPX" (http://upx.sourceforge.net/).

          Ce sont des outils qui compressent le binaire initial, y ajoutent une routine de decompression et regenerent un ELF après.
          A l'execution c'est la routine de decompression qui prend la main initialement, elle charge et decompresse le code binaire initial puis "branch" dessus.

          • [^] # Re: complément d'informations

            Posté par . Évalué à 1.

            Oui, il s'agit de packers, suite à mes échecs répétés, j'ai décidé d'en porter un pour arm:
            packer elf par nibbles

            Mais je suis toujours intéresser à comprendre pourquoi ma première idée ne fonctionne pas. Je vais cependant la laisser un peu de côté et je l'attaquerai à nouveau lorsque j'aurai pris un peu de recul.

            Merci

            • [^] # Re: complément d'informations

              Posté par . Évalué à 1.

              J'ai enfin trouvé d'où venait mon problème : l'architecture de Harvard utilisé par Arm (contrairement à Intel qui utilise du Von Neumann).

              Dans notre cas on s'intéresse surtout à la gestion du cache, avec l'architecture de Harvard, nous en avons deux distinct, l'un pour les datas l'autre pour les instructions.

              Ainsi en faisant du code auto-modifié, certaines données sont considéré comme des datas et ne sont pas mise à jour. Il est possible de forcer cette mise à jour grâce à un appel de la fonction (gcc) suivante :

              Code : Tout sélectionner
              void __clear_cache(char* beg, char* end);

  • # c'est sensé fonctionner ?

    Posté par . Évalué à 3.

    Pour aller dans le sens de ce que dit Obsidian, les instructions à exécuter se baladent un peut partout: disk, ram, cache, pipe du cpu.
    Si tu les modifient à un endroit (ram ?) qu'est ce qui les modifie ailleurs ?

    Ce que tu cherche à faire me fait penser à ce que l'on peut faire avec du bytecode java et qui est utilisé par exemple en programmation par aspects. Mais dans ce cas, tu as toute une artillerie pour y parvenir: jvm, bytecode, classloader, security manager, etc.

    Bref, c'est prévu dans la théorie de Turing, mais dans les faits, pas persuadé que cela fonctionne avec la machinerie ELF.

  • # oui

    Posté par . Évalué à 1.

    J'imagine que c'est faisable, le principale soucis est de déchiffrer avant que le processeur charge en mémoire les instructions, comme je travail sur de l'embarqué avec des processeurs ayant des caches de quelques ko, ça me semble largement faisable.

    Hormis le cache, je ne vois pas ou les instructions peuvent se cacher, en tout cas, pas sur le disque (il peut y avoir un sys.read(), mais c'est l'appel que je chiffre pas les datas).

    Bref, c'est prévu dans la théorie de Turing, mais dans les faits, pas persuadé que cela fonctionne avec la machinerie ELF.

    Oui cela doit fonctionner (en tout cas, j'ai fais plusieurs tests fonctionnels, ou je change dynamiquement une levé d'exception en branch).

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.