Annonce

Réduire
Aucune annonce.

Obfusquez efficacement votre code source.

Réduire
X
 
  • Filtre
  • Heure
  • Afficher
Tout nettoyer
nouveaux messages

  • Tutoriel Obfusquez efficacement votre code source.




    L'obfuscation



    Bonsoir à tous.

    L'optique de ce tutoriel est de définir et expliquer de manière pratique l'ensemble des méthodes d'obfuscation du code source, c'est à dire comment altérer celui-ci avant l'étape de compilation afin de rendre la lisibilité après décompilation la plus ardue possible, et surtout de complexifier le travail du désobfuscateur.



    I. Introduction : les finalités de l'obfuscation

    Tout développeur partageant ses applications recherche avant tout la sécurité et la protection de celles-ci. En effet, depuis ces dernières années la tendance à l'analyse systématique des applications a vu naitre une guerre directe entre coding et reverse-engineering (RE). Le RE désigne l'ensemble des procédures techniques visant à décompiler les applications dans le but d'en retrouver le code source, ou, à tout le moins, les suites d'instructions globales permettant d'en reconstituer l'architecture initiale.

    Pour s'en protéger, il existe une multitude de méthodes permettant de protéger relativement efficacement le code source des applications distribuées. Ainsi, l'on distingue la protection restrictive (application développée pour une architecture hyper-spécifique), le chiffrement ou crypting (chiffrer le code source via des clés générées par algorithmes), l'exécution de code distant (scinder l'application en deux parties dont une distante) qui fera l'objet d'un autre turoriel, et l'obfuscation que nous traiterons dans ce tutoriel.





    II. Techniques d'obfuscation

    Nous allons donc étudier les différentes procédures classiques existantes permettant l'obuscation du code source. Ces procédures possèdent chacune un coefficient d'efficacité différent, c'est pour cela qu'elles sont souvent hybridées afin de rechercher une obfuscation maximale.


    A. La suppression des commentaires
    C'est la base de l'apprentissage du développeur : commenter précisément son code. Cependant, lors de la décompilation, ces commentaires apparaissent bien évidemment en clair et donnent de précieuses informations quant au code. La première étape consiste donc à enlever tous les commentaires du code source.


    Avant suppression des commentaires :
    Code:
    #include <stdio.h> // intégration de la bibliothèque standard
    #include <math.h> // intégration de la bibliothèque mathématiques
    
    int main(int argc char *argv ) // fonction principale
    {
    int a, b=10, c[10]; // déclaration des entiers a b, et du tableau c
    
    for (a=0;a<10;a++) // boucle sur a qui s'incrémente pendant 10 tours
    {
    c[a]=b-a; // remplissage des cases du tableau via le résultat de la soustraction b-a
    }
    return 0; // la fonction retourne 0, tout s'est bien passé
    }

    Après suppression des commentaires :
    Code:
    #include <stdio.h>
    #include <math.h>
    
    int main(int argc char *argv )
    {
    int a, b=10, c[10];
    
    for (a=0;a<10;a++) 
    {
    c[a]=b-a; 
    }
    return 0; 
    }



    B. Suppression de la structure formelle
    La structure formelle, ou style, désigne l'ensemble des règles de développement que respectent les programmeurs pour plus de clarté : indentation, nombre de caractère par ligne, nombre de caractère par colonne... Elle assure une lisibilité et une compréhension optimales du code source. Supprimer ces règles permettent de rendre la lecture plus complexe à la décompilation.


    Avant suppression du style :
    Code:
    #include <stdio.h>
    #include <math.h>
    
    int main(int argc char *argv )
    {
    int a=0;
    int b=10;
    int c[10];
    
      for (a=0;a<10;a++) 
       {
        c[a]=b-a; 
       }
    return 0; 
    }

    Après suppression du style :
    Code:
    #include <stdio.h>
    #include <math.h>
    int main(int argc char *argv ){
    int a, b=10, c[10];
    for (a=0;a<10;a++) {c[a]=b-a;}
    return 0;}


    C. Suppression des tests de debugging
    Afin de pouvoir mener des analyses de performances efficaces, le développement nécessite l'intégration d'instructions de debugging après chaque fonctions « sensibles » dans le but de traquer le plus rapidement possible les erreurs, les bugs, et de fournir à l'utilisateur un rapport explicatif sur les interruptions non-prévues. Ces tests fournissent des informations précieuses lors de la décompilation, notamment en permettant de remonter à l'agencement cohérent des fonctions natives. Les supprimer augmente la difficulté de reconstruction du code source.


    Avant suppression des tests de debugging :
    Code:
    #include <stdio.h>
    #include <winsock2.h>
    
    int main(int argc char *argv )
    {
          int OnStart=WSAStartup(MAKEWORD(2,2),&initialisation_win32);
          if (OnStart!=0)
                printf("\nUnable to initialize WSAStartup : %d %d",erreur,WSAGetLastError());
          else
          printf("\nWSA → initialized !");
    
          SOCKET thisSocket=socket(AF_INET,SOCK_STREAM,0);
          if (thisSocket==INVALID_SOCKET)
                printf("\nUnable to initialize socket : %d",WSAGetLastError());
          else
                printf("\nSocket → initialized !");
    }
    return 0; 
    }

    Après suppression des tests de debugging :
    Code:
    #include <stdio.h>
    #include <winsock2.h>
    
    int main(int argc char *argv )
    {
          int OnStart=WSAStartup(MAKEWORD(2,2),&initialisation_win32);
    
          SOCKET thisSocket=socket(AF_INET,SOCK_STREAM,0);
    }
    return 0; 
    }



    D. Renommage des variables
    Partie primordiale de l'apprentissage de tout développeur : donner des noms clairs et précis aux variables en rapport avec leur finalité. Le renommage des variables, ou refactoring, a pour but de modifier le nom de toutes les variables afin que lors de la décompilation, le lecteur ne puisse pas s'y retrouver. Il existe trois méthodes de refactoring : le reading violation, l'overload et le random processing.


    Avant refactoring :
    Code:
    int calculerAddition(int premierTerme, int deuxiemeTerme)
    {
    static int resultat = 0;
    resultat = premierTerme+deuxiemeTerme;
    return resultat;
    }

    Après reading violation :
    Code:
    int #xxx(int  ~#yyy, int @#zzz)
    {
    static int ~www = 0;
    ~www = ~#[email protected]#zzz;
    return ~www;
    }

    Après overload :
    Code:
    int x(int  xx, int xxx)
    {
    static int xxxx = 0;
    xxxx = xx+xxx;
    return xxxx;
    }

    Après random processing :
    Code:
    int U98vxZw65hfRtBB2gom(int  wIhu74hdfJd09k, int 9Yxc3qzQdaf98jkj)
    {
    static int o7hNuyGDR52ewA = 0;
    o7hNuyGDR52ewA =wIhu74hdfJd09k+9Yxc3qzQdaf98jkj;
    return o7hNuyGDR52ewA;
    }



    E. Transformation superglobale
    Dans un code source bien construit, chaque variable possède une fonction précise et est donc déclarée localement, c'est à dire dans le bloc où elle est utilisée. Afin de perturber la construction du schéma d'intégration du desobfuscateur, l'idée est ici d'élever une majorité de variables locales en superglobales dans le but de complexifier le schéma d'assimilation du desobfuscateur.


    Avant transformation superglobale :
    Code:
    #include <stdio.h>
    #include <string.h>
    
    int calculerFluxBinaire(int byte, int facteur)
    {
    int maximum = 500;
    unsigned int pile = maximum;
    int upPile = pile+byte;
    int downPile = pile-byte;
    int quadraticMoy = (upPile+downPile)*facteur;
    return quadraticMoy;
    }
    
    void ordonnerTableau()
    {
    int periph[2][8];
    int iPeriph = 0, jPeriph = 0;
    for(iPeriph=0;iPeriph<2;iPeriph++)
    {
    for (jPPeriph=0;jPeriph<8;jPeriph++)
    {
    periph[iPeriph][jPeriph] = (iPeriph+jPeriph)-iPeriph;
    }
    }
    }

    Après transformation superglobale :
    Code:
    #include <stdio.h>
    #include <string.h>
    
    int maximum, pile, upPile, periph[0][0], downPile, quadraticMoy;
    #define MINT 2
    #define MAXT 8
    #define MAX 500
    
    int calculerFluxBinaire(int byte, int facteur)
    {
    maximum = MAX;
    pile = maximum;
    upPile = pile+byte;
    downPile = pile-byte;
    quadraticMoy = (upPile+downPile)*facteur;
    return quadraticMoy;
    }
    
    void ordonnerTableau()
    {
    periph[MINT][MAXT];
    for(upPile=0;upPile<MINT;upPile++)
    {
    for (quadraticMoy=0;quadraticMoy<MAXT;quadraticMoy++)
    {
    periph[upPile][quadraticMoy] = (upPile+quadraticMoy)-upPile;
    }
    }
    }



    F. Dead instructions
    L'intégration de dead instructions, code mort ou encore code silencieux, permet d'augmenter la taille du code, sans réduire la vitesse d'exécution de l'application, mais en gênant très fortement le schéma d'analyse du desobfuscateur, ce qui provoque le plus souvent des erreurs de logique de sa part lors du rendu final. Le code silencieux désigne l'ajout de code complètement inutile au sein des instructions valides.


    Avant ajout de code silencieux :
    Code:
    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
    char entryText[50];
    char textButton[]="Validate";
    int buffer=strlen(textButton);
    scanf("%s",&entryText);
    entryText[strlen(entryText);
    strcat(textButton,entryText);
    }

    Après ajout de code silencieux :
    Code:
    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
    char entryText[50];
    entryText[50-5];
    unsigned static int coeff=5;
    char textButton[]="Validate";
    entryText[45+coeff];
    int buffer=strlen(textButton);
    buffer+strlen(textButton);
    scanf("%s",&entryText);
    buffer=coeff-2;
    entryText[strlen(entryText)];
    strcat(textButton,entryText);
    }



    G. Chiffrement des caractères
    Le chiffrement des chaines de caractères, afin qu'elles n'apparaissent pas en clair, permet d'alourdir le schéma d'intégration du desobfuscateur qui souvent fini par tout simplement sauter l'étape de rendu de ces chaines.


    Avant chiffrement :
    Code:
    #include <stdio.h>
    
    int main()
    {
    char textBox[]="Entrez votre login";
    char textInfoBulle[]="Champ de login";
    char textCancelButton[]="Annuler";
    
    int rectX, rectY, rectZ;
    recupererCoord();
    
    if(rectX>5&&rectX<80)
    activateButton();
    
    return 0;
    }

    Après chiffrement :
    Code:
    #include <stdio.h>
    
    int main()
    {
    char textBox[]="äÒÏɬ×Ä";
    char textInfoBulle[]="ß¡ÂÓÒÍØÖ";
    char textCancelButton[]="ÙÆÈÕÈØäÒÏ";
    
    int rectX, rectY, rectZ;
    recupererCoord();
    
    if(rectX>5&&rectX<80)
    activateButton();
    
    return 0;
    }



    H. Boucles noyées
    La noyade de boucles consiste à intégrer des étapes supplémentaires sans effet dans la boucle de contrôle principale de la condition afin d'obliger le desobfuscateur à mobiliser sa puissance de calcul dans la fabrication du schéma conditionnel.


    Avant noyade :
    Code:
    #include <stdio.h>
    
    int main()
    {
    int k,l;
    
    for(k=0;k<50;k++)
    {
    for(l=0;<49;l++)
    {
    indexTab[k][l]=ajouterValeur(k-1,l-1);
    }
    }
    return 0;
    }

    Après noyade :
    Code:
    #include <stdio.h>
    
    int main()
    {
    int k,l,K,L;
    
    for(K=50;K>1;K--)
    {
    for(k=0;k<50;k++)
    {
    for(L=50;L>1;L--)
    {
    for(l=0;l<L;l++)
    indexTab[k][l]=ajouterValeur(k-1,l-1);
    }
    }
    }
    }
    return 0;
    }



    I. Clonage fonctionnel
    Le clonage fonctionnel, ou cloning statement, désigne le fait de créer plusieurs fonctions ayant le même objectif, et d'y faire ensuite appel aléatoirement dans le code. Ceci oblige le desobfuscateur à constamment revoir son schéma d'intégration et fini par créer des incohérence dans le résultat final.


    Avant clonage :
    Code:
    #include <stdio.h>
    int multiplication(int premierTerme, int deuxiemeTerme)
    {
    int resultat;
    resultat = premierTerme*deuxiemeTerme;
    return resultat;
    }
    
    int main()
    {
    multiplication(5,9);
    return 0;
    }

    Après clonage :
    Code:
    int operationParProduit(int terme1, terme2)
    {
    int produitFinal;
    produitFinal=terme2*terme1;
    return produitFinal;
    }
    
    int terme1foisterme2(int unChiffre, int autreChiffre)
    {
    int resultatObtenu;
    resultatObtenu=unChiffre*autreChiffre;
    return resultatObtenu;
    }
    
    int unTrucParUnAutreTruc(int nimporteQuoi, int certaineChose)
    {
    int solutionDuTruc;
    solutionDuTruc=certaineChose*nimporteQuoi;
    return solutionDuTruc;
    }
    
    int main()
    {
    unTrucParUnAutreTruc(7,3);
    …
    operationParProduit(43,9);
    …
    terme1foisterme2(63,11);
    
    return 0;
    }



    J. Explosion fonctionnelle
    L'explosion fonctionnelle permet d'éclater une fonction principale en plusieurs petites fonctions remplissant chacune une tâche spécifique et subordonnées à une fonction de lancement. Le fait de répartir les opérations oblige le desobfuscateur à établir un schéma de liaison entre les différents appels et complexifie ainsi son rendu final.


    Avant explosion :
    Code:
    #include <stdio.h>
    
    void keylogger()
    {
    char keysText[]={'A','B','C',D','E','F'};
    char keysNum[]={1,2,3,4,5,6,7,8,9,0};
    char keysSymb[]={'?','#','~','!','@'};
    
    char mail[]="[email protected]";
    char login[]="admin";
    char mdp[]="root";
    
    FILE* log=fopen("w","log.txt");
    while(1){
    if (toucheDown=='A')
    fprintf(log,'A');
    …
    }

    Après explosion :
    Code:
    #include <stdio.h>
    
    void definirTouches()
    {
    char keysText[]={'A','B','C',D','E','F'};
    char keysNum[]={1,2,3,4,5,6,7,8,9,0};
    char keysSymb[]={'?','#','~','!','@'};
    }
    
    void definirID()
    {
    char mail[]="[email protected]";
    char login[]="admin";
    char mdp[]="root";
    }
    
    void enregistrerTouches()
    {
    FILE* log=fopen("w","log.txt");
    while(1){
    if (toucheDown=='A')
    fprintf(log,'A');
    …
    }
    
    void keylogger()
    {
    definirTouches();
    definirID();
    enregistrerTouches();
    }



    K. Parallélisme fonctionnel
    La linéarité fonctionnelle, c'est à dire le séquencement logique des instructions, est un facteur de déroulement stable pour l'application et architecture donc de façon cohérente les différents appels fonctionnels. Le fait de paralléliser le séquencement, c'est à dire de répartir les appels au sein de plusieurs fonctions, augmente considérablement la fabrication de schéma d'intégration du désobfuscateur.


    Avant parallélisme :
    Code:
    void keylogger()
    {
    char keysText[]={'A','B','C',D','E','F'};
    char keysNum[]={1,2,3,4,5,6,7,8,9,0};
    char keysSymb[]={'?','#','~','!','@'};
    
    char mail[]="[email protected]";
    char login[]="admin";
    char mdp[]="root";
    
    FILE* log=fopen("w","log.txt");
    while(1){
    if (toucheDown=='A')
    fprintf(log,'A');
    …
    }

    Après parallélisme :
    Code:
    #include <stdio.h>
    
    void A_keylogger_1()
    {
    char keysText[]={'A','B','C',D','E','F'};
    char keysNum[]={1,2,3,4,5,6,7,8,9,0};
    char keysSymb[]={'?','#','~','!','@'};
    
    char mail[]="[email protected]";
    char login[]="admin";
    char mdp[]="root";
    }
    
    void A_keylogger_2()
    {
    FILE* log=fopen("w","log.txt");
    while(1){
    if (toucheDown=='A')
    fprintf(log,'A');
    …
    }


    Voilà pour les principales méthodes d'obfuscation. Il en existe des dizaines d'autres comme le transtypage, le polymorphisme, l'héritage explosé ect ect.
    Le prochaine cours portera sur l'appel de code distant comme protection du code source.



    Ex-membre Hackademiciens.

  • #2
    Salut madhat, j'ai hâte de lire celui sur le cryptage Surtout de savoir ce que tu va utiliser comme fonctions dans la lib mth.

    Et l'appel de code m'intrigue également.

    Merci pour ce récapitulatif à propos de l'obfuscation
    sigpic

    Cyprium Download Link

    Plus j'étudie plus j'me rends compte que je n'sais rien.

    †|

    Commentaire


    • #3
      Obfusquez efficacement votre code source.

      Trés intéressant. Alors tu dirais que la majorité obfusquent leur code ou pas ? Parceque ca demande quand même un effort supplémentaire

      Commentaire


      • #4
        Statistiquement c'est difficile à établir. Je dirai que les développeurs utilisant ces techniques sont par contre de plus en plus nombreux oui. Il existe des obfuscateurs automatiques, mais leurs algorithmes sont connus, et ils sont finalement bien moins efficaces qu'une obfuscation manuelle.
        Ex-membre Hackademiciens.

        Commentaire


        • #5
          De rajouter un détail : l'obfuscation n'est pas réservé qu'aux langages compilés. Par exemple, il n'est pas rare de trouver du code obfusquer dans le source d'une page web, généralement à l'aide d'obfuscateurs en JS (ou cryptés).

          Le plus intéressant est de le crypter par un algo perso ou de le faire que pour une architecture spécifique ; mais ces deux cas entraînent des désagréments de lecture flagrants. Il faut que cela reste à la fois simple, mais efficace.

          D'ailleurs je pense que tôt ou tard madhat développera un peu sur les limites rencontrées car la confrontation entre dév et reversers est tellement intense que chacun use d'ingéniosité mais finit toujours par rencontrer des limites : l'autre. Bien que le combat soit sans fin, ça demande de plus en plus de compétences de chaque côté, ça devient donc de facto moins accessibles aux petits nouveaux qui testeraient le RE. D'un autre côté, y'a de + en + de gens fascinés par le RE, donc c'est loin d'être fini. Mais c'est intéressant de voir les limites que cela pose.

          @atreide: obfusquer son source ne demande pas grand temps. Obfusquer son source de façon inédite entraînant le développement d'une fonction d'obfuscation nouvelle et un minimum complexe demande forcément + de temps. Mais cet obfuscateur, une fois fait, tu peux simplement modifier rapidement quelques variables de ton algorithme de traitement pour modifier l'output conséquemment. Donc, bon...
          sigpic

          Cyprium Download Link

          Plus j'étudie plus j'me rends compte que je n'sais rien.

          †|

          Commentaire

          Chargement...
          X