obfuscation en couches: une taxonomie des techniques d’obfuscation logicielle pour la sécurité en couches

Cette section examine les techniques d’obfuscation pour des éléments de code spécifiques. Cette couche couvre la plupart des publications dans la zone d’obscurcissement des logiciels. Comme le montre la Fig. 4, selon les éléments cibles d’une technique d’obscurcissement, nous divisons cette catégorie en cinq sous-catégories: mises en page obscurcissantes, contrôles obscurcissants, fonctions obscurcissantes de données et classes obscurcissantes.

Fig. 4
 figure4

Les techniques d’obscurcissement de la couche d’élément de code

Mise en page obscurcissante

Mise en page L’obscurcissement de la mise en page brouille la mise en page des codes ou des instructions tout en conservant la syntaxe d’origine intacte. Cette section traite de quatre stratégies d’obscurcissement de la mise en page : classificateurs dénués de sens, suppression des symboles redondants, séparation des codes associés et des codes indésirables.

Identifiants sans signification

Cette approche est également connue sous le nom d’obscurcissement lexical qui transforme les identifiants significatifs en identifiants sans signification. Pour la plupart des langages de programmation, l’adoption de règles de nommage significatives et uniformes (par exemple, la notation hongroise (Simonyi 1999)) est une bonne pratique de programmation. Bien que ces noms soient spécifiés dans les codes sources, certains resteront dans le logiciel publié par défaut. Par exemple, les noms des variables globales et des fonctions en C / C++ sont conservés dans des binaires, et tous les noms de Java sont réservés dans des bytecodes. Parce que de tels noms significatifs peuvent faciliter l’analyse contradictoire des programmes, nous devrions les brouiller. Pour rendre les identifiants obscurcis plus confus, Chan et Yang (2004) ont proposé d’employer délibérément les mêmes noms pour des objets de différents types ou dans différents domaines. De telles approches ont été adoptées par ProGuard (2016) en tant que schéma d’obscurcissement par défaut pour les programmes Java.

Suppression des symboles redondants

Cette stratégie supprime les informations symboliques redondantes des logiciels publiés, telles que les informations de débogage pour la plupart des propgrammes (Low 1998). En outre, il existe d’autres symboles redondants pour des formats particuliers de programmes. Par exemple, les fichiers ELF contiennent des tables de symboles qui enregistrent les paires d’identifiants et d’adresses. Lors de l’adoption d’options de compilation par défaut pour compiler des programmes C /C++, telles que l’utilisation de LLVM (Lattner et Adve 2004), les binaires générés contiennent de telles tables de symboles. Pour supprimer ces informations redondantes, les développeurs peuvent utiliser l’outil strip de Linux. Un autre exemple avec des informations redondantes est les codes smali Android. Par défaut, les codes smali générés contiennent des informations démarrées par.ligne et.source, qui peut être supprimée à des fins d’obscurcissement (Dalla Preda et Maggi 2017).

Séparer les codes liés

Un programme est plus facile à lire si ses codes liés logiquement sont également physiquement proches (Collberg et al. 1997). Par conséquent, la séparation des codes ou des instructions connexes peut augmenter les difficultés de lecture. Il s’applique à la fois aux codes sources (par exemple, variables de réorganisation (Low 1998)) et aux codes d’assemblage (par exemple, instructions de réorganisation (Wroblewski 2002)). En pratique, l’utilisation de sauts inconditionnels pour réécrire un programme est une approche populaire pour y parvenir. Par exemple, les développeurs peuvent mélanger les codes d’assemblage, puis utiliser goto pour reconstruire le flux de contrôle d’origine (You and Yim 2010). Cette approche est populaire pour les codes d’assemblage et les bytecodes Java avec la disponibilité des instructions goto (Dalla Preda et Maggi 2017).

Codes indésirables

Cette stratégie ajoute des instructions indésirables qui ne sont pas fonctionnelles. Pour les binaires, nous pouvons ajouter des instructions de non-opération (NOP ou 0x00) (Dalla Preda et Maggi 2017; Marcelli et al. 2018). En outre, nous pouvons également ajouter des méthodes indésirables, telles que l’ajout de méthodes défuntes dans les codes smali Android (Dalla Preda et Maggi 2017). Les codes indésirables peuvent généralement modifier les signatures des codes, et donc échapper à la reconnaissance de formes statiques.

Comme l’obscurcissement de la mise en page n’altère pas la syntaxe du code d’origine, il est moins sujet aux problèmes de compatibilité ou aux bogues. Par conséquent, ces techniques sont les plus préférées dans la pratique. De plus, les techniques d’identifiants vides de sens et de suppression des symboles redondants peuvent réduire la taille des programmes, ce qui les rend encore plus attrayants (ProGuard 2016). Cependant, la puissance de l’obscurcissement de la mise en page est limitée. Il a une résilience prometteuse aux attaques de désobfuscation car certaines transformations sont à sens unique, qui ne peuvent pas être inversées. Cependant, certaines informations de mise en page peuvent difficilement être modifiées, telles que les identifiants de méthode du SDK Java. Ces informations résiduelles sont essentielles pour que les adversaires puissent récupérer les informations obscurcies. Par exemple, Bichsel et al. (2016) ont essayé de désobfuser les applications obscurcies par ProGuard, et ils ont réussi à récupérer environ 80% des noms.

Contrôles d’obscurcissement

Ce type de techniques d’obscurcissement transforme les contrôles des codes pour augmenter la complexité du programme. Cela peut être réalisé via de faux flux de contrôle, des flux de contrôle probabilistes, des contrôles basés sur le répartiteur et des contrôles implicites.

Flux de contrôle faux

Les flux de contrôle faux font référence aux flux de contrôle qui sont délibérément ajoutés à un programme mais ne seront jamais exécutés. Cela peut augmenter la complexité d’un programme, par ex., dans la complexité de McCabe (McCabe 1976) ou les métriques de Harrison (Harrison et Magel 1981). Par exemple, la complexité de McCabe (McCabe 1976) est calculée comme le nombre d’arêtes sur un graphique de flux de contrôle moins le nombre de nœuds, puis plus deux fois les composants connectés. Pour augmenter la complexité de McCabe, nous pouvons soit introduire de nouvelles arêtes, soit ajouter à la fois de nouvelles arêtes et de nouveaux nœuds à un composant connecté.

Pour garantir l’inaccessibilité des flux de contrôle bidon, Collberg et al. (1997) ont suggéré d’employer des prédicats opaques. Ils ont défini opaque predict comme le prédicat dont le résultat est connu pendant le temps d’obscurcissement mais est difficile à déduire par une analyse de programme statique. En général, un prédicat opaque peut être constamment vrai (PT), constamment faux (PF) ou dépendant du contexte (P?). Il existe trois méthodes pour créer des prédicats opaques: les schémas numériques, les schémas de programmation et les schémas contextuels.

Schémas numériques

Les schémas numériques composent des prédicats opaques avec des expressions mathématiques. Par exemple, 7×2-1≠y2 est constamment vrai pour tous les entiers x et y. Nous pouvons directement utiliser de tels prédicats opaques pour introduire de faux flux de contrôle. La figure 5a illustre un exemple, dans lequel le prédicat opaque garantit que le flux de contrôle bidon (c’est-à-dire la branche else) ne sera pas exécuté. Cependant, les attaquants auraient plus de chances de les détecter si nous employons fréquemment les mêmes prédicats opaques dans un programme obscurci. Arboit (2002) a donc proposé de générer automatiquement une famille de prédicats opaques de ce type, de sorte qu’un obscurcisseur puisse choisir un prédicat opaque unique à chaque fois.

Fig. 5
 figure5

Obscurcissement du flux de contrôle avec des prédicats opaques

Une autre approche mathématique avec une sécurité plus élevée consiste à utiliser des fonctions cryptographiques, telles que la fonction de hachage \(\mathcal{H}\) (Sharif et al. 2008), et le chiffrement homomorphe (Zhu et Thomborson 2005). Par exemple, nous pouvons remplacer un prédicat x == c par \(\mathcal{H}(x) == c_{hash}\) pour masquer la solution de x pour cette équation. Notez qu’une telle approche est généralement utilisée par les logiciels malveillants pour échapper à l’analyse dynamique des programmes. Nous pouvons également utiliser des fonctions cryptographiques pour crypter des équations qui ne peuvent pas être satisfaites. Cependant, de tels prédicats opaques entraînent beaucoup de frais généraux.

Pour composer des constantes opaques résistantes à l’analyse statique, Moser et al. (2007) ont suggéré d’utiliser des problèmes 3-SAT, qui sont NP-durs. Cela est possible car on peut avoir des algorithmes efficaces pour composer de tels problèmes difficiles (Selman et al. 1996). Par exemple, Tiella et Ceccato (2017) ont démontré comment composer de tels prédicats opaques avec des problèmes de k-clique.

Pour composer des constantes opaques résistantes à l’analyse dynamique, Wang et al. (2011) ont proposé de composer des prédicats opaques avec une forme de conjectures non résolues qui se bouclent plusieurs fois. Parce que les boucles sont difficiles pour l’analyse dynamique, l’approche dans la nature devrait être résistante à l’analyse dynamique. Des exemples de telles conjectures incluent la conjecture de Collatz, la conjecture 5x + 1, la conjecture de Matthews. La figure 5b montre comment utiliser la conjecture de Collatz pour introduire de faux flux de contrôle. Quelle que soit la façon dont nous initialisons x, le programme se termine par x = 1 et originalCodes() peut toujours être exécuté.

Schémas de programmation

Parce que l’analyse de programme contradictoire est une menace majeure pour les prédicats opaques, nous pouvons utiliser des problèmes d’analyse de programme difficiles pour composer des prédicats opaques. Collberg et coll. suggéré deux problèmes classiques, l’analyse des pointeurs et les programmes concurrents.

En général, l’analyse des pointeurs consiste à déterminer si deux pointeurs peuvent ou peuvent pointer vers la même adresse. Certains problèmes d’analyse de pointeur peuvent être NP-durs pour l’analyse statique ou même indécidables (Landi et Ryder 1991). Un autre avantage est que les opérations de pointeur sont très efficaces lors de l’exécution. Par conséquent, les développeurs peuvent composer des prévisions opaques résilientes et efficaces avec des problèmes d’analyse de pointeurs bien conçus, tels que le maintien de pointeurs vers certains objets avec des structures de données dynamiques (Collberg et al. 1998a).

Les programmes simultanés ou les programmes parallèles sont un autre problème difficile. En général, une région parallèle de n instructions a n ! différentes manières d’exécution. L’exécution n’est pas seulement déterminée par le programme, mais également par l’état d’exécution d’un ordinateur hôte. Collberg et coll. (1998a) ont proposé d’utiliser des programmes simultanés pour améliorer l’approche basée sur les pointeurs en mettant à jour simultanément les pointeurs. Majumdar et Thomborson (2006) ont proposé d’utiliser des programmes parallèles distribués pour composer des prédicats opaques.

En outre, certaines approches composent des prédicats opaques avec des astuces de programmation, telles que l’exploitation des mécanismes de gestion des exceptions. Par exemple, Dolz et Parra (2008) ont proposé d’utiliser le mécanisme try-catch pour composer des prédicats opaques pour .Net et Java. Les événements d’exception incluent la division par zéro, le pointeur nul, l’index hors de portée ou même des exceptions matérielles particulières (Chen et al. 2009). La sémantique du programme d’origine peut être obtenue via des schémas de gestion des exceptions sur mesure. Cependant, de tels prédicats opaques n’ont aucune base de sécurité et sont vulnérables aux attaques avancées faites à la main.

Schémas contextuels

Des schémas contextuels peuvent être utilisés pour composer des prédicats opaques de variantes (c’est-à-dire {P?}). Les prédicats doivent posséder des propriétés déterministes telles qu’ils peuvent être utilisés pour obscurcir les programmes. Par exemple, Drape et et al. (2009) ont proposé de composer de tels prédicats opaques qui sont invariants sous une contrainte contextuelle, par exemple, le prédicat opaque x mod3 == 1 est constamment vrai si x mod3:1?x++ : x = x +3. Palsberg et coll. (2000) ont proposé des prédicats opaques dynamiques, qui comprennent une séquence de prédicats corrélés. Le résultat d’évaluation de chaque prédicat peut varier à chaque cycle. Cependant, tant que les prédicats sont corrélés, le comportement du programme est déterministe. La figure 5c illustre un exemple de prédicats opaques dynamiques. Peu importe comment nous initialisons * p et * q, le programme est équivalent à y = x + 3, x = y + 3.

La résistance des flux de contrôle fictifs dépend principalement de la sécurité des prédicats opaques. Une propriété de sécurité idéale pour les prédicats opaques est qu’ils nécessitent un temps exponentiel dans le pire des cas pour se briser, mais uniquement un temps polynomial pour se construire. Notez que certains prédicats opaques sont conçus avec de tels problèmes de sécurité, mais peuvent être implémentés avec des failles. Par exemple, les problèmes 3-SAT proposés par Ogiso et al. (2003) sont basés sur des paramètres de problème triviaux qui peuvent être facilement simplifiés. Si de tels prédicats opaques sont correctement implémentés, ils promettent d’être résilients.

Flux de contrôle probabilistes

Les flux de contrôle faux peuvent causer des problèmes à l’analyse de programme statique. Cependant, ils sont vulnérables à l’analyse dynamique du programme car les flux de contrôle fictifs sont inactifs. L’idée des flux de contrôle probabilistes adopte une stratégie différente pour faire face à la menace (Pawlowski et al. 2016). Il introduit des réplications de flux de contrôle avec la même sémantique mais une syntaxe différente. Lors de la réception de la même entrée plusieurs fois, le programme peut se comporter différemment pour des temps d’exécution différents. La technique est également utile pour lutter contre les attaques par canal latéral (Crane et al. 2015).

Notez que la stratégie des flux de contrôle probabilistes est similaire aux flux de contrôle fictifs avec des prédicats opaques contextuels. Mais ils sont de nature différente car les prédicats opaques contextuels introduisent des chemins morts, bien qu’ils n’introduisent pas de codes indésirables.

Contrôles basés sur le répartiteur

Un contrôle basé sur le répartiteur détermine les prochains blocs de codes à exécuter pendant l’exécution. De tels contrôles sont essentiels pour l’obscurcissement des flux de contrôle car ils peuvent masquer les flux de contrôle d’origine contre l’analyse de programme statique.

Une approche d’obfuscation basée sur le répartiteur est l’aplatissement du flux de contrôle, qui transforme les codes de profondeur en codes peu profonds plus complexes. Wang et coll. (2000) ont tout d’abord proposé l’approche. La figure 6 montre un exemple de leur papier qui transforme une boucle while en une autre forme avec switch-case. Pour réaliser une telle transformation, la première étape consiste à transformer le code en une représentation équivalente avec des instructions if-then-goto comme le montre la Fig. 6; ensuite, ils modifient les instructions goto avec des instructions de cas de commutation comme le montre la Fig. 6. De cette façon, la sémantique du programme d’origine est réalisée implicitement en contrôlant le flux de données de la variable de commutation. L’ordre d’exécution des blocs de code étant déterminé dynamiquement par la variable, on ne peut pas connaître les flux de contrôle sans exécuter le programme. Cappaert et Preneel (2010) ont formalisé l’aplatissement du flux de contrôle en utilisant un nœud de répartiteur (par exemple, un commutateur) qui contrôle le bloc de code suivant à exécuter ; après l’exécution d’un bloc, le contrôle est transféré au nœud de répartiteur. En outre, il existe plusieurs améliorations à l’aplatissement du flux de code. Par exemple, pour améliorer la résistance à l’analyse de programme statique sur la variable de commutation, Wang et al. (2001) ont proposé d’introduire des problèmes d’analyse des pointeurs. Pour compliquer davantage le programme, Chow et al. (2001) ont proposé d’ajouter de faux blocs de code.

Fig. 6
 figure6

Approche d’aplatissement du flux de contrôle proposée par Wang et al. (2000)

László et Kiss (2009) ont proposé un mécanisme d’aplatissement du flux de contrôle pour gérer une syntaxe C++ spécifique, telle que try-catch, while-do, continue. Le mécanisme est basé sur un arbre syntaxique abstrait et utilise un modèle de mise en page fixe. Pour que chaque bloc de code obfusque, il construit une instruction while dans la boucle externe et un composé de boîtier de commutation à l’intérieur de la boucle. Le composé de cas de commutation implémente la sémantique du programme d’origine, et la variable de commutation est également utilisée pour terminer la boucle externe. Cappaert et Preneel (2010) ont constaté que les mécanismes pouvaient être vulnérables à l’analyse locale, c’est-à-dire que la variable de commutation était immédiatement assignée de sorte que les adversaires puissent déduire le bloc suivant à exécuter en ne regardant qu’un bloc actuel. Ils ont proposé une approche renforcée avec plusieurs astuces, telles que l’utilisation d’une affectation de référence (par exemple, swVar = swVar + 1) au lieu d’une affectation directe (par exemple, swVar = 3), le remplacement de l’affectation via if-else par une expression d’affectation uniforme et l’utilisation de fonctions unidirectionnelles dans le calcul du successeur d’un bloc de base.

Outre l’aplatissement du flux de contrôle, il existe plusieurs autres enquêtes d’obscurcissement basées sur le répartiteur (p. ex. (Linn et Debray, 2003; Ge et al. 2005; Zhang et coll. 2010; Schrittwieser et Katzenbeisser 2011)). Linn et Debray (2003) ont proposé d’obscurcir les binaires avec des fonctions de branche qui guident l’exécution en fonction des informations de pile. De même, Zhang et coll. (2010) ont proposé d’utiliser des fonctions de branche pour obscurcir les programmes orientés objet, qui définissent un style d’appel de méthode unifié avec un pool d’objets. Pour renforcer la sécurité de ces mécanismes, Ge et al. (2005) ont proposé de masquer les informations de contrôle dans un autre processus autonome et d’utiliser des communications inter-processus. Schrittwieser et Katzenbeisser (2011) ont proposé d’utiliser des blocs de code diversifiés qui implémentent la même sémantique.

L’obscurcissement basé sur le répartiteur résiste à l’analyse statique car il masque le graphique de flux de contrôle d’un logiciel. Cependant, il est vulnérable à l’analyse dynamique des programmes ou aux approches hybrides. Par exemple, Udupa et al. (2005) ont proposé une approche hybride pour révéler les flux de contrôle cachés avec une analyse statique et une analyse dynamique.

Contrôles implicites

Cette stratégie convertit les instructions de contrôle explicites en instructions implicites. Cela peut empêcher les rétro-ingénieurs d’aborder les flux de contrôle corrects. Par exemple, nous pouvons remplacer les instructions de contrôle des codes d’assemblage (par exemple, jmp et jne) par une combinaison de mov et d’autres instructions qui implémentent la même sémantique de contrôle (Balachandran et Emmanuel 2011).

Notez que toutes les approches d’obscurcissement du flux de contrôle existantes se concentrent sur la transformation au niveau syntaxique, tandis que la protection au niveau sémantique a rarement été discutée. Bien qu’ils puissent démontrer une certaine résilience aux attaques, leur efficacité d’obscurcissement concernant la protection sémantique reste incertaine.

Données obscurcissantes

Les techniques actuelles d’obscurcissement des données se concentrent sur les types de données courants, tels que les entiers, les chaînes et les tableaux. Nous pouvons transformer des données via le fractionnement, la fusion, la procéduralisation, l’encodage, etc.

Fractionnement/ fusion de données

Le fractionnement de données répartit les informations d’une variable en plusieurs nouvelles variables. Par exemple, une variable booléenne peut être divisée en deux variables booléennes, et effectuer des opérations logiques sur elles peut obtenir la valeur d’origine.

La fusion de données, en revanche, agrège plusieurs variables en une seule variable. Collberg et coll. (1998b) ont démontré un exemple qui fusionne deux entiers de 32 bits en un entier de 64 bits. Ertaul et Venkatesh (2005) ont proposé une autre méthode qui regroupe plusieurs variables dans un espace avec des logarithmes discrets.

Procéduralisation des données

La procéduralisation des données remplace les données statiques par des appels de procédure. Collberg et coll. (1998b) ont proposé de remplacer les chaînes par une fonction qui peut produire toutes les chaînes en spécifiant des valeurs de paramètres paticulaires. Drape et al. (2004) ont proposé de coder des données numériques avec deux fonctions inverses f et g. Pour affecter une valeur v à une variable i, on l’affecte à une variable injectée j comme j = f(v). Pour utiliser i, nous invoquons g(j) à la place.

Codage des données

Codage des données code les données avec des fonctions mathématiques ou des chiffrements. Ertaul et Venkatesh (2005) ont proposé de coder des chaînes avec des chiffrements affines (par exemple, le chiffrement de Caser) et d’utiliser des logarithmes discrets pour emballer des mots. Fukushima et al. (2008) ont proposé de coder les nombres clairs avec des opérations ou exclusives, puis de déchiffrer le résultat du calcul avant la sortie. Kovacheva (2013) a proposé de chiffrer des chaînes avec le chiffrement RC4, puis de les déchiffrer pendant l’exécution.

Transformation de tableau

Le tableau est l’une des structures de données les plus couramment utilisées. Pour obscurcir les tableaux, Collberg et al. (1998b) ont discuté de plusieurs transformations, telles que la division d’un tableau en plusieurs sous-réseaux, la fusion de plusieurs tableaux en un seul tableau, le pliage d’un tableau pour augmenter sa dimension ou l’aplatissement d’un tableau pour réduire la dimension. Ertaul et Venkatesh (2005) ont suggéré de transformer les indices de tableau avec des fonctions composites. Zhu et coll. (2006); Zhu (2007) a proposé d’utiliser un chiffrement homomorphe pour la transformation de tableau, y compris le changement d’index, le pliage et la flatterie. Par exemple, nous pouvons mélanger les éléments d’un tableau avec i∗m mod n, où i est l’indice d’origine, n est la taille du tableau d’origine, et m et n sont relativement premiers.

Méthodes d’obscurcissement

Méthode inline/outline

Une méthode est une procédure indépendante qui peut être appelée par d’autres instructions du programme. La méthode inline remplace l’appel procédural d’origine par le corps de la fonction lui-même. Le contour de la méthode fonctionne de la manière opposée qui extrait une séquence d’instructions et résume une méthode. Ce sont de bonnes entreprises qui peuvent obscurcir l’abstraction originale des procédures (Collberg et al. 1997).

Clone de méthode

Si une méthode est fortement invoquée, nous pouvons créer des réplications de la méthode et en appeler une au hasard. Pour confondre l’interprétation contradictoire, chaque version de la réplication doit être unique d’une manière ou d’une autre, par exemple en adoptant différentes transformations d’obscurcissement (Collberg et al. 1997) ou des signatures différentes (Ertaul et Venkatesh 2004).

Méthode d’agrégation / diffusion

L’idée est similaire à l’obscurcissement des données. Nous pouvons agréger des méthodes non pertinentes en une seule méthode ou disperser une méthode en plusieurs méthodes (Collberg et al. 1997; Faible 1998).

Méthode proxy

Cette approche crée des méthodes proxy pour confondre l’ingénierie inverse. Par exemple, nous pouvons créer les mandataires en tant que méthodes statiques publiques avec des identifiants randomisés. Il peut y avoir plusieurs proxys distincts pour la même méthode (Dalla Preda et Maggi 2017). L’approche est extrêmement utile lorsque les signatures de méthode ne peuvent pas être modifiées (Protsenko et Muller 2013).

Classes obscurcissantes

Les classes obscurcissantes partagent des idées similaires avec les méthodes obscurcissantes, telles que le fractionnement et le clone (Collberg et al. 1998b). Cependant, comme la classe n’existe que dans les langages de programmation orientés objet, tels que JAVA et .NET, nous en discutons comme une catégorie unique. Nous présentons ci-dessous les principales stratégies pour obscurcir les classes.

Suppression de modificateurs

Les programmes orientés objet contiennent des modificateurs (par ex., public, privé) pour restreindre l’accès aux classes et aux membres des classes. L’abandon des modificateurs supprime ces restrictions et rend tous les membres publics (Protsenko et Muller 2013). Cette approche peut faciliter la mise en œuvre d’autres méthodes d’obscurcissement de classe.

Classe de fractionnement / coalescence

L’idée de coalescence / fractionnement est d’obscurcir l’intention des développeurs lors de la conception des classes (Sosonkin et al. 2003). Lors de la coalescence de classes, nous pouvons transférer des variables locales ou des groupes d’instructions locales vers une autre classe (Fukushima et al. 2003).

Aplatissement de la hiérarchie des classes

L’interface est un outil puissant pour les programmes orientés objet. Similaire à la méthode proxy, nous pouvons créer des proxy pour les classes avec des interfaces (Sosonkin et al. 2003). Cependant, un moyen plus efficace consiste à rompre la relation d’héritage d’origine entre les classes avec des interfaces. En laissant chaque nœud d’un sous-arbre de la hiérarchie des classes implémenter la même interface, nous pouvons aplatir la hiérarchie (Foket et al. 2012).

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.