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:
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:
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:
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.
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.
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:
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...
Aggiungi un commento