Leia este post em Português
Attention à l’écart
S'il y a une chose que j'ai appris dans ma carrière informatique, c'est que tôt ou tard (le plus souvent la première), une connaissance plus approfondie des différentes technologies abstraites par les bibliothèques / frameworks sera nécessaire pour les utiliser efficacement et / ou résoudre des problèmes (dans le contexte de cet article, Mono.Cecil abstrait divers aspects CIL).
La semaine dernière, alors que je travaillais sur une fonctionnalité en Cecilifier, j'ai commencé à obtenir des assemblys invalides sans aucune modification du code Cecilfier lui-même. Après quelques enquêtes j'ai compris le motif: en changeant le code de les classes de testes il y a des instructions de branchement que était plus de 128 bytes de son cibles.
Dans IL il y a deux formats des instructions de branchement : les branches courtes i et les branches longues ii. En bref, le format court utilise un octet (de -128 à 127) pour le décalage tandis que les autres utilisent 4 bytes (une plage beaucoup plus large) et Cecilifier était produit des instructions sur le format courtes indistinctement de le décalage.
Plus concrètement, le programme suivant ajoute naïvement une série d'instructions nop dans le bloc if menant au décalage de la branche (jusqu'à la fin du if) à déborder:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.IO; | |
using System.Linq; | |
using Mono.Cecil; | |
using Mono.Cecil.Cil; | |
using Mono.Cecil.Rocks; | |
namespace CecilSandbox | |
{ | |
class Program | |
{ | |
static void Main(string[] args) | |
{ | |
if (args.Length == 0) | |
{ | |
var programName = Path.GetFileName(Environment.GetCommandLineArgs()[0]); | |
Console.WriteLine($"run: '{programName} modify <num>' to save a modified version of the assembly where <num> is the number of NOP instructions to inject and then\n"); | |
Console.WriteLine($"run: '{programName} run' to execute the method with the modified body."); | |
return; | |
} | |
var nopCount = 0; | |
if (args.Length == 2 && args[0] == "modify" && !int.TryParse(args[1], out nopCount)) | |
{ | |
Console.WriteLine($"'{args[1]}' is not a valid # of nops to inject."); | |
return; | |
} | |
else if(args.Length == 1 && args[0] == "run") | |
{ | |
Console.WriteLine("Trying to run the modified method..."); | |
Foo(1); | |
return; | |
} | |
else if (nopCount == 0) | |
{ | |
Console.WriteLine("Invalid command line args"); | |
return; | |
} | |
using (var a = AssemblyDefinition.ReadAssembly(typeof(Program).Assembly.Location)) | |
{ | |
var t = a.MainModule.Types.Single(tc => tc.Name == "Program"); | |
var m = t.Methods.Single(mc => mc.Name == "Foo"); | |
foreach (var i in m.Body.Instructions) | |
{ | |
Console.WriteLine(i); | |
} | |
if (nopCount == -1) | |
return; | |
var branch = m.Body.Instructions.First(i => i.OpCode.OperandType == OperandType.ShortInlineBrTarget || i.OpCode.OperandType == OperandType.InlineBrTarget); | |
var il = m.Body.GetILProcessor(); | |
for(int i = 0; i < nopCount; i++) | |
il.InsertAfter(branch, Instruction.Create(OpCodes.Nop)); | |
var targetAssemblyPath = Path.Combine(Path.GetTempPath(), "Output.exe"); | |
a.Write(targetAssemblyPath); | |
Console.WriteLine($"Modified assembly saved to '{targetAssemblyPath}'. Run it with '{targetAssemblyPath} run'"); | |
} | |
} | |
static void Foo(int i) | |
{ | |
if (i > 0) | |
{ | |
Console.WriteLine("" + i); | |
Console.WriteLine("" + i++); | |
Console.WriteLine("" + i++); | |
Console.WriteLine("" + i++); | |
} | |
Console.WriteLine("The End"); | |
} | |
} | |
} |
Heureusement Mono.Cecil offre un moyen relativement simple de s'assurer que tous les branchements ont le décalage correct grâce à la méthode SimplifyMacros() (définie dans Mono.Cecil.Rocks.MethodBodyRocks) qui visite toutes les instructions de la méthode remplaçant celles qui utilisent le format court des opcodes avec le format long respectif; cela signifie que les instructions de branchement telles que br.s, beq.s, etc. sont remplacées par leurs équivalents longs br, beq, etc de le même manière que les opcodes ldarg.x sont remplacées par darg x. Puisque ces instructions utilisent des décalages de 4 octets, il est très improbable que les cibles se trouvent en dehors de la plage valide.
Après avoir fait des modifications dans la méthode, vous pouvez appeler OptimizeMacros() pour s'assurer que toutes les instructions utilisent le format le plus efficiente possible en remplaçant des instructions qui utilisent le format long par les équivalents en format court quand c’est possible.
En connaissant ces méthodes nous pouvons changer nos programmes comme suit :
Maintenant l'assembly produit n'est plus invalide !
Finalement, rappelle toi que dans le but de produire les petites assemblys ,en général, les compilateurs émettent des instructions les plus petites possible (ou, les instructions en format court). C’est bien mais ça signifie aussi qu’en changeant une assembly (avec Mono.Cecil) c'est possible qu’une instruction de branchement devienne invalide quand on ajoute quelques instructions.
Amuse-toi.
#monocecil #cecilifier
No comments:
Post a Comment