Code dynamique
Le chiffrement du code permet de chiffrer des méthodes précises d’une application, qui peuvent ensuite être substituées en fonction des fonctionnalités auxquelles l’utilisateur a accès. Par exemple, si une application propose des fonctionnalités de base et des fonctionnalités avancées, les méthodes chiffrées des fonctionnalités avancées peuvent être stockées séparément de l’exécutable principal et chargées à l’exécution uniquement si l’utilisateur possède une licence qui lui y donne accès. Cette sélection de code dynamique permet aux développeurs de créer une version de démonstration ou d’essai de leur application, aux fonctionnalités limitées, et de déverrouiller des fonctionnalités supplémentaires en fournissant une clé de licence aux utilisateurs. Avec le chiffrement du code, les développeurs protègent leur propriété intellectuelle tout en proposant des formules de licence souples à leurs utilisateurs.
Le code dynamique fourni par le chiffrement du code permet de basculer à l’exécution entre différentes versions d’une méthode en fonction de conditions externes. Par exemple, une application peut proposer un ensemble limité de fonctionnalités en mode démonstration et activer des fonctionnalités plus avancées en présence d’un fichier de licence valide. Pour cela, le code des versions limitée et avancée des fonctionnalités est chiffré, et un mécanisme de sélection de code dynamique bascule de l’une à l’autre selon que le fichier de licence est présent ou non.
Application de démonstration
Dans la manière classique d’implémenter une application de démonstration, le code commercial est déjà présent dans la version de démonstration, et le fichier de licence autorise l’exécution des fonctionnalités avancées. Cette approche présente toutefois un inconvénient majeur : le code qui s’exécute lorsque le fichier de licence est présent n’est pas protégé, et un attaquant pourrait contourner la validation de licence pour déverrouiller les fonctionnalités sous licence.
Avec la sélection de code dynamique, en revanche, le code qui déverrouille les fonctionnalités avancées est chiffré et ne peut en aucun cas être exécuté sans le fichier de licence, qui contient la clé de chiffrement nécessaire pour déchiffrer et exécuter le code. La protection contre la rétro-ingénierie et l’accès non autorisé aux fonctionnalités avancées est ainsi meilleure. Le fichier de licence sert de clé unique pour déverrouiller des sections de code précises, ce qui rend difficile pour un attaquant de contourner le mécanisme de licence et d’accéder aux fonctionnalités avancées.
Considérez le projet d’exemple suivant, disponible sur GitHub. Vous pouvez obtenir le code source de l’exemple avec Git en exécutant la commande suivante en ligne de commande :
git clone https://github.com/babelfornet/dynamic-code-example.gitL’exemple montre comment utiliser le code dynamique pour modifier le comportement de l’application selon la présence de certains fichiers.
[Obfuscation(Feature = "msil encryption:id=feature1;source=demo;internal=true;password=1234", Exclude = false)]
public void Feature1Demo()
{
Console.WriteLine("Feature1 Demo");
Feature2Demo();
}
[Obfuscation(Feature = "msil encryption:id=feature2;source=demo;internal=true;password=1234", Exclude = false)]
public static void Feature2Demo()
{
Console.WriteLine("Feature2 Demo");
}Ce sont deux méthodes de démonstration marquées pour être remplacées à l’exécution par d’autres versions grâce à la sélection de code dynamique. Les méthodes Feature1Demo() et Feature2Demo() sont chiffrées avec la fonctionnalité de chiffrement du code, avec respectivement les identifiants « feature1 » et « feature2 ». La source du code chiffré est « demo », et la propriété internal est définie sur true pour conserver le code chiffré dans l’assembly. Le mot de passe de chiffrement est « 1234 ».
Lorsqu’une application s’exécute sans fichier de licence valide, elle est considérée comme étant en mode démonstration. Dans ce cas, l’assembly appelle les deux méthodes ci-dessus, marquées pour être remplacées à l’exécution par le code dynamique. Ces méthodes implémentent les fonctionnalités de base de l’application, qui peuvent être limitées par rapport à la version complète sous licence.
static void Main(string[] args)
{
var app = new App();
app.Feature1Demo();
}Considérez maintenant les deux méthodes suivantes, qui remplaceront les méthodes de démonstration lorsque l’application est sous licence. Notez que la source est « retail » : si un fichier de licence est trouvé, la méthode d’identifiant « feature1 », Feature1Retail(), remplace Feature1Demo(), et celle d’identifiant « feature2 », Feature2Retail(), remplace Feature2Demo().
[Obfuscation(Feature = "msil encryption:id=feature1;source=retail;internal=true;password=4567", Exclude = false)]
public static void Feature1Retail()
{
Console.WriteLine("Feature1 Retail");
Feature2Retail();
}
[Obfuscation(Feature = "msil encryption:id=feature2;source=retail;internal=true;password=4567", Exclude = false)]
public static void Feature2Retail()
{
Console.WriteLine("Feature2 Retail");
}Ces deux méthodes ne sont pas appelées à l’exécution par l’assembly, qui fonctionne en mode démonstration, et elles sont chiffrées avec un mot de passe différent de celui des méthodes de démonstration. Ce mot de passe n’est pas accessible au code de l’application, car Babel supprime cette information pendant l’obfuscation, après avoir chiffré les méthodes.
Il faut un moyen de basculer dynamiquement entre « demo » et « retail » avec le chiffrement du code. Comme les méthodes ci-dessus ont été chiffrées avec un mot de passe, il faut définir le point d’entrée du chiffrement du code de Babel qui récupère le mot de passe à l’exécution.
[Obfuscation(Feature = "msil encryption get password", Exclude = false)]
internal static string GetPassword(string source)
{
Console.WriteLine($"> Get {source} password");
if (source == "demo")
return "1234";
string pwd = File.ReadAllText(source + ".txt");
Console.WriteLine($">> {source} password: {pwd}");
return pwd;
}La méthode GetPassword() est utilisée par la BVM (Bytecode Virtual Machine) pour obtenir le mot de passe nécessaire au déchiffrement d’une méthode chiffrée lorsque celle-ci est appelée pendant l’exécution du programme.
Si le nom de la source est « demo », la méthode renvoie le mot de passe codé en dur « 1234 », celui qui a servi à chiffrer les méthodes « demo » Feature1Demo() et Feature2Demo(). Sinon, elle lit le mot de passe dans un fichier texte portant le nom de la source et l’extension « .txt », puis le renvoie à l’appelant. Notez que le mot de passe de démonstration est stocké dans le code, car l’application peut toujours s’exécuter en mode démonstration et il n’est pas nécessaire de masquer ce mot de passe.
Le basculement entre les sources « demo » et « retail » est effectué par la méthode GetSourceStream(), que la BVM appelle lorsqu’une des méthodes chiffrées est appelée.
[Obfuscation(Feature = "msil encryption get stream", Exclude = false)]
internal static object GetSourceStream(string source)
{
Console.WriteLine($"> Get {source} stream");
if (source == "demo")
{
string[] sources = { "retail", "special" };
foreach (var src in sources)
{
if (File.Exists($"{src}.txt"))
{
Console.WriteLine($">> Switch to {src}");
return src;
}
}
return null;
}
// External sources
if (File.Exists(source + ".eil"))
{
Console.WriteLine($">>> Read {source} stream");
return File.OpenRead(source + ".eil");
}
return null;
}Cette méthode joue un rôle essentiel dans la sélection dynamique de la source de méthodes chiffrées appropriée en fonction de conditions externes, comme la présence d’un fichier de licence. En effet, au démarrage, l’application appelle les méthodes de démonstration, qui peuvent être déchiffrées avec le mot de passe renvoyé par la méthode GetPassword().
Dans le cadre de la sélection de code dynamique, la BVM (Binary Virtual Machine) exécute le code chiffré et bascule entre les différentes versions d’une méthode en fonction de conditions externes. Avant d’exécuter une méthode « demo », la BVM appelle la méthode GetSourceStream() en lui passant « demo » en paramètre. Comme les méthodes « demo » sont internes à l’assembly, il n’est pas nécessaire de renvoyer un flux pour la source « demo ». En revanche, si le fichier « retail.txt » est trouvé, il faut basculer vers le code de la source « retail » ; pour indiquer à la BVM d’exécuter la source « retail », la méthode GetSourceStream() renvoie donc la chaîne « retail ».
Lorsque la BVM reçoit la chaîne « retail » de GetSourceStream(), elle sait qu’elle doit exécuter le code « retail ». Elle appelle alors GetPassword() avec l’argument « retail ». GetPassword() lit le mot de passe de la version « retail » dans le fichier retail.txt et le renvoie à la BVM, qui s’en sert pour déchiffrer le code « retail ». Une fois déchiffré, le code « retail » est exécuté à la place du code « demo ». L’application peut ainsi basculer à l’exécution entre différentes versions du code chiffré, selon qu’un fichier de licence est présent ou non, sans qu’il soit nécessaire de recompiler ou de redéployer le code.
Le code dynamique permet de définir plusieurs sources internes et externes. Vous pouvez par exemple déclarer une autre source externe, « special », chargée à la place du code « retail » lorsque le fichier special.txt, qui contient le mot de passe de déchiffrement du code, est présent.
[Obfuscation(Feature = "msil encryption:id=feature1;source=special;internal=false;password=0000", Exclude = false)]
public static void Feature1Special()
{
Console.WriteLine("Feature1 Special");
Feature2Special();
}
[Obfuscation(Feature = "msil encryption:id=feature2;source=special;internal=false;password=0000", Exclude = false)]
public static void Feature2Special()
{
Console.WriteLine("Feature2 Special");
}Ce code montre deux méthodes, Feature1Special() et Feature2Special(), marquées avec des attributs Obfuscation dont la propriété « internal » est définie sur « false ». Le code obfusqué de ces méthodes est donc généré dans un fichier externe portant l’extension « .eil » et n’est pas inclus dans l’assembly lui-même. Le fichier « .eil » externe contenant le code chiffré peut être chargé et déchiffré à l’exécution avec le mot de passe indiqué dans les attributs Obfuscation.
Pour exécuter l’exemple fourni, générez d’abord la solution en configuration Debug. Ouvrez ensuite le projet obfuscate.babel dans Babel Desktop et exécutez-le. Comme le projet ne définit pas de chemin de sortie, Babel Desktop écrit l’assembly obfusqué dans un dossier BabelOut à côté de l’assembly d’entrée. Copiez ensuite les fichiers DynamicCode.exe, DynamicCode.runtimeconfig.json et DynamicCode.deps.json dans le dossier BabelOut. Enfin, depuis une fenêtre PowerShell, exécutez DynamicCode.exe dans le dossier BabelOut ; la sortie suivante doit s’afficher :
> Get demo stream
> Get demo password
Feature1 Demo
Feature2 DemoPour tester la source « retail », copiez le fichier retail.txt dans le dossier BabelOut et exécutez DynamicCode.exe. La sortie doit être la suivante :
> Get demo stream
>> Switch to retail
> Get retail password
>> retail password: 4567
Feature1 Retail
Feature2 RetailPour tester la source « special », copiez le fichier special.txt dans le dossier BabelOut et exécutez DynamicCode.exe. La sortie doit être la suivante :
> Get demo stream
>> Switch to special
> Get special stream
>>> Read special stream
> Get special password
>> special password: 0000
Feature1 Special
Feature2 Special