Come ben sapete, tutti gli assembly compilati per la piattaforma .NET non contengono codice nativo, ma un bytecode univocamente associabile alle istruzioni di un linguaggio intermedio, IL. Accanto, anzi prima, del bytecode, un assembly .NET contiene moltissimi metadati: i nomi di tutte le entità usate (tipi, metodi, parametri, eventi, tutto), i valori delle costanti letterali, le relazioni di ereditarietà e/o implementazione tra tipi e interfacce, gli assemby esterni importati e molto altro ancora. Insomma, essi sono una vera miniera d'oro per qualsiasi utente armato di sufficiente conoscenza del framework. Sono talmente ricchi di informazioni che è possibile, con un piccolo margine di errore, ricostruire completamente tutto il codice sorgente che è stato scritto dai suoi creatori, ed è possibile ottenerlo non solo nel linguaggio originale ma in uno qualsiasi dei linguaggi .NET. A questo proposito, vi suggerisco di scaricare .NET Reflector, di Lutz Roeder, che fa proprio tutto ciò che ho descritto fin'ora. Vi sarà anche utile CFF Explorer, un programma in grado di analizzare un assembly .NET in tutte le sue parti, isolando dati e tabelle.

 

Offuscazione

Dato che è possibile risalire al sorgente partendo dal bytecode - e non v'è modo di impedirlo - l'unica cosa che potete fare per rendere un po' più sicure le vostre applicazioni consiste nel rendere la vita più difficile a un ipotetico cracker, almeno rallentandolo. Il primo rifugio di chi scrive in .NET è l'offuscazione. Questa tecnica consente di rendere il codice "oscuro", appunto, a chi lo decompila. Come avevo precedentemente detto, infatti, un assembly contiene come metadati tutti i nomi e gli identificatori assegnati al programmatore alle varie entità: potete facilmente verificarlo cercando nello stream #Strings all'interno dell'assembly con un hex editor o con CFF Explorer. Basta cercare la sequenza "<Module>", dopo la quale seguono molte stringhe (terminate da 0x00) che indicano i vari nomi. Mediante un offuscatore, potete cambiare i nomi e gli identificatori in modo da renderli incomprensibili: il codice, nella sua totalità, lavorerà sempre allo stesso modo, poiché il flusso di esecuzione e le istruzioni sono sempre le stesse, ma disassemblandolo si rivelerà incomprensibili all'occhio umano. Prendiamo come esempio il seguente codice, molto chiaro:

 

Imports System.Security.Cryptography
Imports System.Text.RegularExpressions

Module Module1

    Public Function IsCodeValid(ByVal Code As String) As Boolean
        Return (Code = "ciaffo101")
    End Function

    Public Function GenerateCode(ByVal Salt As Byte(), ByVal Iter As Int32, ByVal Length As Int32) As String
        Static Rnd As New Random(Date.Now.Millisecond)

        Dim RndBytes(Length - 1) As Byte
        Dim Deriver As Rfc2898DeriveBytes
        Rnd.NextBytes(RndBytes)
        Deriver = New Rfc2898DeriveBytes(RndBytes, Salt, Iter)

        Return Deriver.GetBytes(Length).Aggregate(Of String)("", Function(S, B) S & B.ToString("X2"))
    End Function

    Sub Main()
        Dim Code As String

        Console.WriteLine("Inserire il codice di accesso: ")
        Code = Console.ReadLine()

        If IsCodeValid(Code) Then
            Dim Salt() As Byte
            Dim Iterations, Length As Int32
            Dim HexSalt As String

            Console.Clear()
            Console.WriteLine("Benvenuto nel programma!")
            Console.WriteLine("Inserire un salt valido come sequenza esadecimale (min. 8 bytes): ")
            Console.Write(" >  ")
            HexSalt = Console.ReadLine()
            HexSalt = (New Regex("(\W|[g-z])", RegexOptions.IgnoreCase)).Replace(HexSalt, "")
            Console.WriteLine(" >  {0} [Salt validato]", HexSalt)

            ReDim Salt(Math.Max(HexSalt.Length \ 2 - 1, 7))
            For I As Int32 = 0 To Salt.Length - 1
                Salt(I) = Int32.Parse(HexSalt.Substring(I * 2, 2), Globalization.NumberStyles.AllowHexSpecifier)
            Next

            Console.WriteLine("Inserire il numero di iterazioni: ")
            Console.Write(" >  ")
            Iterations = CType(Console.ReadLine(), Int32)

            Console.WriteLine("Inserire la lunghezza del codice (byte): ")
            Console.Write(" >  ")
            Length = CType(Console.ReadLine(), Int32)

            Console.Clear()
            Console.WriteLine("Premere un tasto per produrre un nuovo codice seriale.")
            Console.WriteLine("Premetere e per uscire.")
            Do
                Console.WriteLine(" > {0}", GenerateCode(Salt, Iterations, Length))
            Loop While (Console.ReadKey().KeyChar <> "e")
        Else
            Console.WriteLine("Codice errato!")
            Console.ReadKey()
        End If
    End Sub

End Module

Dopo averlo compilato, possiamo aprirlo con un Hex Editor, e controllare lo stream #Strings: AssemblyID.JPG Tutti i nomi usati sono lì, con l'unica particolare differenza del tag $STATIC$ per i metodi, che, essendo in un modulo, sono implicitamente static, e per le variabili esplicitamente dichiarate tali. Dopodiché possiamo disassemblare il programma con .NET Reflector e ammirare il nostro codice:

reflector.JPG

Un offuscatore cambia tutti gli identificatori dell'assembly con sequenze alfanumeriche valide ma incoerenti, in modo da rendere la comprensione del codice molto più difficile. Ecco un esempio: reflector2.JPG

L'immagine sopra mostra un'offuscazione molto superficiale perchè al momento in cui scrivo non dispongo di un offuscatore, e ho dovuto modificare i nomi "a mano". Era possibile cambiare anche il nome dell'assembly, del modulo, di Main, di My, e di tutte le classi definite in My. Inoltre, il codice proposto era molto semplice: pensate ad avere un applicativo con decine di classi dal nome incomprensibile, in cui non si riesce nemmeno a capire da dove iniziare a leggere.

 

Formattazione delle costanti stringa

Nonostante il codice sia offuscato, è tuttavia possibile cercare di dedurre le funzioni di una certa parte dai messaggi che il programma visualizza: nel nostro caso, dai Console.WriteLine. Mediante questi, il sorgente viene reso più chiaro non solo per il programatore (e di conseguenza, per l'utente), ma anche per chi vuole violare la sicurezza dell'applicativo. Per evitare che le stringhe siano direttamente accessibili, è possibile convertirle in un formato meno intellegibile, come base64 oppure come semplice array di byte. Ecco un esempio:

 

Imports System.Security.Cryptography
Imports System.Text.RegularExpressions
Module Module1
    Public Function hsdaglqop12(ByVal posowpw8920l As String) As Boolean
        Return (posowpw8920l = "ciaffo101")
    End Function
    Public Function qwdasdvcd(ByVal sdfsv5y As Byte(), ByVal qwieu As Int32, ByVal hyujaqlqo1092 As Int32) As String
        Static uu65rgnl As New Random(Date.Now.Millisecond)
        Dim yut4w3svmn(hyujaqlqo1092 - 1) As Byte
        Dim o0o0piku65rf As Rfc2898DeriveBytes
        uu65rgnl.NextBytes(yut4w3svmn)
        o0o0piku65rf = New Rfc2898DeriveBytes(yut4w3svmn, sdfsv5y, qwieu)
        Return o0o0piku65rf.GetBytes(hyujaqlqo1092).Aggregate(Of String)("", Function(S, B) S & B.ToString("X2"))
    End Function
    Sub Main()
        Dim opppiht5r4wqqsw As String
        Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {73, 110, 115, 101, 114, 105, 114, 101, 32, 105, 108, 32, 99, 111, 100, 105, 99, 101, 32, 100, 105, 32, 97, 99, 99, 101, 115, 115, 111, 58}))
        opppiht5r4wqqsw = Console.ReadLine()
        If hsdaglqop12(opppiht5r4wqqsw) Then
            Dim gfhjsadfjgasf() As Byte
            Dim qerqwre22wr, ujhiwuefyi7yrf As Int32
            Dim ew As String
            Console.Clear()
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {66, 101, 110, 118, 101, 110, 117, 116, 111, 32, 110, 101, 108, 32, 112, 114, 111, 103, 114, 97, 109, 109, 97, 33}))
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {73, 110, 115, 101, 114, 105, 114, 101, 32, 117, 110, 32, 115, 97, 108, 116, 32, 118, 97, 108, 105, 100, 111, 32, 99, 111, 109, 101, 32, 115, 101, 113, 117, 101, 110, 122, 97, 32, 101, 115, 97, 100, 101, 99, 105, 109, 97, 108, 101, 32, 40, 109, 105, 110, 46, 32, 56, 32, 98, 121, 116, 101, 115, 41, 58}))
            Console.Write(Text.UTF8Encoding.UTF8.GetString(New Byte() {32, 62, 32, 32}))
            ew = Console.ReadLine()
            ew = (New Regex("(\W|[g-z])", RegexOptions.IgnoreCase)).Replace(ew, "")
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {32, 62, 32, 32, 123, 48, 125, 32, 91, 83, 97, 108, 116, 32, 118, 97, 108, 105, 100, 97, 116, 111, 93}), ew)
            ReDim gfhjsadfjgasf(Math.Max(ew.Length \ 2 - 1, 7))
            For I As Int32 = 0 To gfhjsadfjgasf.Length - 1
                gfhjsadfjgasf(I) = Int32.Parse(ew.Substring(I * 2, 2), Globalization.NumberStyles.AllowHexSpecifier)
            Next
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {73, 110, 115, 101, 114, 105, 114, 101, 32, 105, 108, 32, 110, 117, 109, 101, 114, 111, 32, 100, 105, 32, 105, 116, 101, 114, 97, 122, 105, 111, 110, 105}))
            Console.Write(Text.UTF8Encoding.UTF8.GetString(New Byte() {32, 62, 32, 32}))
            qerqwre22wr = CType(Console.ReadLine(), Int32)
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {73, 110, 115, 101, 114, 105, 114, 101, 32, 108, 97, 32, 108, 117, 110, 103, 104, 101, 122, 122, 97, 32, 100, 101, 108, 32, 99, 111, 100, 105, 99, 101, 32, 40, 98, 121, 116, 101, 41, 58}))
            Console.Write(Text.UTF8Encoding.UTF8.GetString(New Byte() {32, 62, 32, 32}))
            ujhiwuefyi7yrf = CType(Console.ReadLine(), Int32)
            Console.Clear()
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {80, 114, 101, 109, 101, 114, 101, 32, 117, 110, 32, 116, 97, 115, 116, 111, 32, 112, 101, 114, 32, 112, 114, 111, 100, 117, 114, 114, 101, 32, 117, 110, 32, 110, 117, 111, 118, 111, 32, 99, 111, 100, 105, 99, 101, 32, 115, 101, 114, 105, 97, 108, 101, 46}))
            Console.WriteLine(Text.UTF8Encoding.UTF8.GetString(New Byte() {80, 114, 101, 109, 101, 114, 101, 32, 101, 32, 112, 101, 114, 32, 117, 115, 99, 105, 114, 101, 46}))
            Do
                Console.WriteLine(" > {0}", qwdasdvcd(gfhjsadfjgasf, qerqwre22wr, ujhiwuefyi7yrf))
            Loop While (Console.ReadKey().KeyChar <> "e")
        Else
            Console.ReadKey()
        End If
    End Sub
End Module

Il codice di sopra è la versione offuscata e con le stringhe non in chiaro del sorgente proposto all'inizio dell'articolo. Ovviamente è anche possibile criptare le sequenze di byte delle stringhe, ma bisognerebbe includere lo stesso algoritmo che le decripta nel sorgente stesso.

Wrapping dei membri pubblici

I primi due approcci illustrati sono utili per i nomi e gli identificatori che decidiamo noi, ma il codice è composto di molti altri pezzi che sfuggono al nostro controllo. Ad esempio, non possiamo oscurare i nomi dei membri delle classi esposte negli assembly pubblici, ossia quelli residenti nella GAC, che costituiscono la base della piattaforma. Per scrivere sulla console è necessario scrivere Console.WriteLine, e questo identificatore non può essere modificato... oppure sì? Uno dei possibili espedienti consiste nel creare metodi wrapper con identificatori incomprensibili per metodi pubblici, come in questo esempio:

 

Public Sub jaqijuu7d8q9i(ByVal lkjaksia8o As String)
    Console.WriteLine(lkjaksia8o)
End Sub

Dopodiché si sparpagliano le definizioni dei wrapper in giro per il sorgente, o in un modulo separato. Questo rende la consultazione ancora più faticosa, ma non la impedisce comunque totalmente. Ecco come apparirebbe il codice di Main con questo ulteriore passaggio: reflector3.JPG

 

Conclusioni

In definitiva, nessun codice è davvero sicuro in .NET. Ogni metodo adottabile aggiunge solo un'altra iterazione a quelle che un malintenzionato sarebbe costretto ad eseguire in condizioni normali, ma non ne limita comunque le possibilità. Inoltre, ogni "mezzuccio" utilizzato per ottenere questo fine può avere ripercussioni sulle prestazioni del codice: le molteplici chiamate a funzione (logicamente inutili) e le conversioni sul momento contribuiscono ad appesantire l'assembly finale con del bytecode ingombrante e inutile. Sembra che il .NET, nonostante i suoi natali in un ambiente poco "espansivo", si basi molto più soll'open source di quanto non crediamo...