Zoom Icon

Patch Maker in C Sharp

From UIC Archive

Create un patch maker in C#

Contents


Patch Maker in C Sharp
Author: Quake2
Email: [email protected]
Website: http://pmode.cjb.net
Date: 30/09/2004 (dd/mm/yyyy)
Level: Some skills are required
Language: Italian Flag Italian.gif
Comments:



Introduzione

Quanti tutorial avete visto che vi insegnano a fare un patch maker in asm? Migliaia vero? Vi siete mai chiesti perché voi dovete impazzire a generare un eseguibile, facendo spazio per aggiungere le informazioni per la patch e cacchi vari, quando usando tool più moderni e più potenti potete fare la stessa cosa in molto meno tempo? Se la risposta è si, andate avanti a leggere, se è no andate avanti lo stesso sennò mi arrabbio :) Comunque, in questo tutorial vedremo come creare un patch maker multi piattaforma (si avete letto bene :)) in C# :)

Ristrutturato il 12/04/2007 da ValerioSoft


Tools usati

Leggetevi il precedente tutorial su IL :) . Comunque vi serve o il .NET Framework o Mono, come al solito Platform.NET non è supportato ne mai lo sarà nei miei tutorial :)


URL o FTP del programma

Non me va de scrive, leggetevi il precedente tutorial :)



Notizie sul programma

Beh scriveremo un programma che ci genererà un eseguibile per patchare i nostri bei file :)


Essay

Bene come promesso, eccoci di nuovo con un tutorial su C# e in più in generale su IL. Questa volta impararemo come creare un ogetto in IL, gestire le eccezioni, come utilizzare le etichette (label) e qualche altra cosetta. Siccome secondo me per imparare qualcosa bisogna trovargli un applicazione pratica, per imparare le cose nuove su IL, ci faremo un programma che tornerà molto utile nella quotidiana attività di cracking, ovvero un patch maker. Che cos'è un patch maker? Semplice, è un programmino che prende in input due file, uno è il file originale, l'altro è il file modificato, e restituisce un terzo file, che non fa altro che apportare le modifiche al file originale, in questo modo invece di distribuire un eseguibile da qualche mb, basta distribuirne uno da pochi kb. Direte voi, dov'è la difficoltà? Beh è nel generare l'eseguibile, che se lo facciamo con tecniche standard, ovvero creiamo normali file PE per windows, dovremmo tenere conto dello spazio necessario per scrivere le modifiche e altre cosette più o meno complicate, che rendono il lavoro molto lungo e faticoso. Cosa centra il c# in tutto ciò? Ma come, non vedete il collegamento con System.Reflection (e System.Reflection.Emit)? No? Semplice, grazie alla potenza di questo namespace, noi creeremo un programma che genererà un eseguibile (la nostra patch) in modo dinamico e senza impazzire su offset, istruzioni, e roba varia, proprio come abbiamo fatto nel tutorial precedente, solo che stavolta farà qualcosa di utile :) Come al solito, non perdiamoci in lunghi discorsi, e vediamo subito il codice del patch maker (chiamato per l'occasione Sharp Patcher, vediamo chi indovina a cosa mi sono ispirato per il nome :), vi do un indizio: togliete una lettera al cuore, mandate le risposte via email, per regalo non riceverete niente :)) :


using System; using System.IO; using System.Reflection; using System.Reflection.Emit;

namespace SharpPatcher {

   class Patcher
   {
       private bool m_bValid = false;
       private string m_sFile1;
       private string m_sFile2;
       private FileStream m_File1;
       private FileStream m_File2;
       private System.Collections.Hashtable m_OffsetByteTable;
       Patcher(string File1, string File2)
       {
           m_sFile1 = File1;
           m_sFile2 = File2;
           if(!File.Exists(File1) || !File.Exists(File2))
               Console.Out.WriteLine("Could not open files.");
           else
           {
               try
               {
                   m_File1 = new FileStream(File1, FileMode.Open);
                   m_File2 = new FileStream(File2, FileMode.Open);
               }
               catch(System.IO.FileNotFoundException e)
               {
                   Console.WriteLine("Could not open {0}", new object[] {(object)e.FileName});
                   return;
               }
               if(m_File1.Length != m_File2.Length)
                   Console.WriteLine("Files must be of the same size!");
               else
               {
                   m_OffsetByteTable = new System.Collections.Hashtable();
                   m_bValid = true;
                   Console.WriteLine("Input file successfully opened.");
               }
           }
       }
      
       public bool Valid
       {
           get
           {
               return m_bValid;
           }
       }
       public bool GetDifferentBytes()
       {
           long fileSize = m_File1.Length;
           byte b1 = 0;
           byte b2 = 0;
           long step = (long)(fileSize / 100);
           bool found = false;
           Console.WriteLine("Reading different bytes...");
           m_File1.Lock(0, m_File1.Length);
           m_File2.Lock(0, m_File2.Length);
           for(long i = 0; i < fileSize; i++)
           {
               b1 = (byte)m_File1.ReadByte();
               b2 = (byte)m_File2.ReadByte();
               if(b1 != b2)
               {
                   found = true;
                   m_OffsetByteTable.Add((object)(m_File1.Position-1), (object)b2);
               }
               if((i % step) == 0)
                   Console.Write("#");
           }
           m_File2.Unlock(0, m_File2.Length);
           m_File1.Unlock(0, m_File1.Length);
           m_File2.Close();
           m_File1.Close();
           Console.WriteLine("\nDone.");
           return found;
       }
       public void PrintDifferentBytes()
       {
           foreach(long position in m_OffsetByteTable.Keys)
           {
               Console.WriteLine("{0} - {1}", new object[] {(object)position,
                                       (object)m_OffsetByteTable[position]});
           }
       }
       public bool BuildPatch(string OutputFileName, string FileToPatch)
       {
           AssemblyName an = new AssemblyName();
           an.Version = new Version(1, 0, 0, 0);
           an.Name = OutputFileName;
           AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly
                                             (an, AssemblyBuilderAccess.Save);
           ModuleBuilder mb = ab.DefineDynamicModule(OutputFileName, OutputFileName + ".exe");
           TypeBuilder tb = mb.DefineType("Patcher.Patch",TypeAttributes.Class | TypeAttributes.Public);
           ConstructorBuilder cb = tb.DefineDefaultConstructor(MethodAttributes.Public);
           //ecco la prima novità, stavolta siccome vogliamo fare un exe, dobbiamo definire una funzione
           //che abbia gli attributi public e static, il nome non è necessario che sia Main, ma l'ho
           //messo tanto per chiarezza :)
           MethodBuilder mainMethod = tb.DefineMethod("Main", MethodAttributes.Public | 
           MethodAttributes.Static,CallingConventions.Standard, typeof(void), new Type[] 
           {typeof(string[])});
          
           ILGenerator ilgen = mainMethod.GetILGenerator();
           //definiamo una variabile locale di tipo System.IO.FileStream, che sarà il nostro
           //file da patchare
           ilgen.DeclareLocal(typeof(System.IO.FileStream));
                       //ecco la seconda novità, siccome dobbiamo gestire le eccezioni, definiamo una
                       // label che servirà per segnare l'uscita dalla funzione
           Label exit = ilgen.DefineLabel();
           ilgen.EmitWriteLine("Sharp Patcher by Quake2");
           ilgen.EmitWriteLine("Opening input file...");
                       //come annunciato, gestiamo l'eccezione che verrebbe lanciata in caso il file
           //non venisse trovato
           ilgen.BeginExceptionBlock();
           ilgen.Emit(OpCodes.Ldarg_0);
           ilgen.Emit(OpCodes.Ldstr, FileToPatch);
           ilgen.Emit(OpCodes.Ldc_I4, (int)System.IO.FileMode.Open);
           //una novità importante, ovvero la creazione degli oggetti, si usa l'istruzione Newobj,
           //a cui va passato il costruttore da chiamare, che viene selezionato in base
           //al numero e al tipo dei suoi parametri, diciamo che in c# corrisponde a fare
           //System.IO.FileStream fs = new System.IO.FileStream("file.dat", System.IO.FileMode.Open) ,
           //il nome ovviamente cambia eh :)
           ilgen.Emit(OpCodes.Newobj, typeof(System.IO.FileStream).GetConstructor(new Type[2]
                {typeof(string), typeof(FileMode)}));
           ilgen.Emit(OpCodes.Stloc_0);
           //essendo un blocco try { ...; } catch(...) { ...; } definiamo l'inizio del catch appunto
           ilgen.BeginCatchBlock(typeof(System.IO.FileNotFoundException));
           ilgen.EmitWriteLine("Could not find the specified file.");
           //per uscire da un eccezione, bisogna usare l'opcode Leave, che sistema lo stack
           ilgen.Emit(OpCodes.Leave, exit);
           //siccome abbiamo finito di gestire l'eccezione, chiudiamo il blocco con EndExceptionBlock
           ilgen.EndExceptionBlock();
           foreach(long offset in m_OffsetByteTable.Keys)
           {
               ilgen.Emit(OpCodes.Ldloc_0);
               //ecco un nuovo opcode, l'opcode Ldc.i8 non fa altro che caricare un intero
               //a 64bit nello stack, l'intero glie lo passiamo noi
               //(l'offset appunto in cui vogliamo scrivere il byte)
               ilgen.Emit(OpCodes.Ldc_I8, offset);
               //un altro opcode nuovo, ldc.i4.s carica nello stack un valore a 8bit
               //e lo estende a 32, in questo modo si risparmiano tre byte nella codifica
               //dell'opcode (ldc.i4.s è la versione "short" di ldc.i4, accetta quindi interi a 8bit
               ilgen.Emit(OpCodes.Ldc_I4_S, (byte)SeekOrigin.Begin);
               ilgen.Emit(OpCodes.Callvirt, typeof(System.IO.FileStream).GetMethod("Seek"));
               //novità importante, il metodo Seek della classe System.IO.FileStream, ritorna un valore,
               //precisamente un intero a 64bit, a cui però noi non siamo interessati,
               //fatto sta che questo valore è comunque sullo stack, e noi lo stack lo dobbiamo
               //mantenere pulito, quindi se non vogliamo usare questo valore,
               //semplicemente facciamo pop, che non farà altro che eliminare l'ultima cosa
               //presente sullo stack, in modo da averlo di nuovo "pulito"
               ilgen.Emit(OpCodes.Pop);
               ilgen.Emit(OpCodes.Ldloc_0);
               ilgen.Emit(OpCodes.Ldc_I4_S, (byte)m_OffsetByteTable[offset]);
               ilgen.Emit(OpCodes.Callvirt, typeof(System.IO.FileStream).GetMethod("WriteByte"));
           }
           ilgen.Emit(OpCodes.Ldloc_0);
           ilgen.Emit(OpCodes.Callvirt, typeof(System.IO.FileStream).GetMethod("Close"));
           ilgen.EmitWriteLine("File successfully patched.");
           //questo è il punto in cui vogliamo riprenda l'esecuzione del programma in caso di eccezione,
           //quindi non facciamo altro che assegnare la label "exit" a questo punto, in modo da potervi
           //fare riferimento dal codice
           ilgen.MarkLabel(exit);
           ilgen.Emit(OpCodes.Ret);
           tb.CreateType();
           //fondamentale, un exe al contrario di una dll (che ripeto, in .NET sono la stessa
           //identica cosa), ha un entry point, ovvero il punto da cui parte l'esecuzione,
           //in questo caso la funzione Main che abbiamo appena creato
           ab.SetEntryPoint(tb.GetMethod("Main"));
           ab.Save(OutputFileName + ".exe");
           return true;
       }
       [STAThread]
       public static void Main(string[] args)
       {
           Patcher p = new Patcher(args[0], args[1]);
           if(p.Valid)
           {
               if(p.GetDifferentBytes())
               {
                   Console.WriteLine("Building patch file.");
                   if(!p.BuildPatch(args[2], "Civilization3.exe"))
                       Console.WriteLine("Could not create patch file.");
                   else
                       Console.WriteLine("Patch file created.");
               }
               else
                   Console.WriteLine("No different bytes found.");
           }
           else
               Console.WriteLine("Error");
       }
   }

}

Ok come avete visto il codice è abbastanza semplice e commentato, quindi non dovreste avere problemi a capire come funziona. In ogni caso il funzionamento è semplicissimo, prima vengono aperti entrambi i file, poi byte per byte vengono confrontati, se si trovano byte diversi, in una hash table viene inserito l'offset (come chiave) e il byte diverso a quell'offset, successivamente viene generato l'eseguibile (la nostra patch), dove nel foreach vengono generati una serie di Seek e WriteByte per ogni offset (avrei potuto usare un ciclo, ma così è più semplice, anzi, per esercizio usate un ciclo voi :)), successivamente viene chiuso il file e poi viene generato l'eseguibile. Per usare questo programma, dovete fare: sharppatcher file_originale.exe file_modificato.exe file_patch , da notare che quando mettete il nome del file da generare per la patch non dovete specificare l'estenzione, tanto viene aggiunta via codice :). Come avrete notato, questo patch maker ha molti vantaggi, tra cui quello di essere multi piattaforma, su correte a patchare gli elf :) Una piccola precisazione sul motivo per cui non ho usato interfaccie grafiche, al momento, il supporto su Mono per System.Windows.Forms è molto precario, e sinceramente non ho voluto utilizzare altri framework (tipo l'ottimo wx.NET) per appesantire il tutorial, fatto sta che voi potete comunque aggiungere un interfaccia grafica per conto vostro, io ho gettato le basi, il resto spetta a voi :)

Ok come sempre spendo due parole su un istruzione in particolare, ovvero quella per caricare sullo stack un valore immediato, Ldloc, se date un occhiata alla classe System.Reflection.Emit.OpCodes, vedrete che ce ne sono tantissime versioni, il motivo è lo stesso che vi ho spiegato nel precedente tutorial, ovvero per ottimizzare la codifica degli opcode. Sono stati previsti 9 opcodes per valori immediati da 0 a 8 e -1, e si chiamano rispettivamente: ldc.i4.0, ldc.i4.1, ldc.i4.2, ldc.i4.3, ldc.i4.4, ldc.i4.5, ldc.i4.6 ldc.i4.7, ldc.i4.8, ldc.i4.m1, la presenza di i4 indica che sono interi a 32bit, se vogliamo caricare un valore (intero a 32bit) diverso da 0-8 o -1, usiamo ldc.i4, mentre per un valore a 64bit usiamo ldc.i8, mentre per un valore a 8bit, usiamo ldc.i4.s, che come abbiamo detto prende un valore a 8bit e lo estende a 32. Per quanto riguarda i numeri reali, abbiamo ldc.r4 per caricare un float a 32bit e ldc.r8 per un float a 64bit. Ricordatevi di utilizzare queste ottimizzazioni quando potete, sono importanti per ottimizzare la decodifica da parte della vm, e a voi non costa niente farci attenzione :).

Ok un altro piccolo tutorial nel fantastico mondo di IL è finito, spero che vi sia piaciuto :) Alla prossima!

Quake2

P.S.: come sempre siete invitati a mandare maledizioni, minacce, ecc... via email, anche se gradirei di più la segnalazione di eventuali imprecisioni nel tutorial :)


Note finali

Come sempre voglio ringraziare e salutare tutte le persone che mi conoscono e quelle che non mi conoscono, e in particolare i vari frequentatori dei canali su cui sto di solito, è inutile fare un elenco, tanto chi mi conosce sa che lo saluto :)


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.