Create un patch maker in C#

Data

by "Quake2"

 

30/09/2004

UIC's Home Page

Published by Quequero

"She walks in beauty, like the night Of cloudless climes and starry skies" Lord Byron

Ok ok sto meditando vendetta per chi usa la MIA sezione ;p. Grazie mille quake :)

"Here I drink alone and remember
A graven life, the stain of her memory
In this cup, love's poison
For love is the poison of life
Tip the cup, feed the fire,
And forget about useless hope. . ." Agalloch - A Desolation Song

....

Home page: http://pmode.cjb.net
E-mail: [email protected]
Nick: Quake2^AM, UIN: 51184823, AzzurraNet: #gameprog-ita, #crack-it, #asm, #pmode, #c#, #programmazione, #beos

....

Difficoltà

( )NewBies (x)Intermedio ( )Avanzato ( )Master

 
 

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# :) 

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 :)

Ess

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


Non mi assumo nessuna responsabilità in caso il codice qui riportato arrechi danni permamenti al vostro compilatore, ovvero se inizia a bestemmiare, prendervi in giro, rifituarsi di compilare i vostri file, modificarvi il codice per dispetto, crearvi i bug per farvi impazzire, non è colpa mia. Ricordate che i compilatori non vanno maltrattati, ma vanno trattati con rispetto, non fategli compilare milioni di righe di codice al giorno, imparate a compilarvi i vostri programmi a mano, è ora di smetterla con questo sfruttamento.
Ricordate che il codice non va copiato o rubato, ma va scritto, quindi che lo leggete a fare sto tutorial? :)

Noi programmiamo al solo scopo di perdere tempo e peggiorare la nostra vista.