domenica 29 settembre 2024

Crittografia e WCF per passare una password ( od una qualsiasi altra stringa (xml,json, etc.etc.) ) da un applicazione ad un' altra in ,relativa, sicurezza

 Il codice che segue è da considerarsi in alpha e da non utilizzare in un ambiente di produzione , qui potete trovare il  "progetto" https://sourceforge.net/projects/sourcecodesafer/

L'ipotetico scenario è quello in cui vogliamo lasciar testare la nostra applicazione .Net Windows Forms ma vogliamo evitare che sia possibile decompilarla in fase di test ,

per ottenere questo risultato crittografiamo sul nostro pc l'eseguibile da proteggere con una password (chiamiamola P , si tratta di una password simmetrica(https://it.wikipedia.org/wiki/Crittografia_simmetrica) ) ,quando ci spostiamo sulla macchina di test decrittografiamo l'eseguibile con la stessa password e lo carichiamo ed eseguiamo grazie alla reflection ,

sulla macchina di test portiamo quindi l'eseguibile crittografato , se  ci venisse carpita la password P sulla macchina di test si potrebbe decrittografare l'eseguibile e risalire al codice sorgente ,per evitare che ciò accada dobbiamo evitare che possibili keylogger intercettino lo stream della tastiera o/e che uno screen recorder registri la nostra attività sulla tastiera virtuale dell'applicazione , per risolvere questa situazione potremmo utilizzare una seconda applicazione che in remoto fornisca la password P tramite un servizio WCF ( questa è ovviamente solo una delle soluzioni possibili ). Chiamando questo servizio forniamo una password asimmetrica pubblica ( https://it.wikipedia.org/wiki/Crittografia_asimmetrica ) , quest'ultima password viene utilizzata per crittografare la password P , l'array della password così crittografata viene restituito al chiamante il quale ha la chiave privata e può risalire alla password P in chiaro .

Questo "progetto" è costituito da 3 sotto-progetti :

1) un eseguibile Ethical_Hacking.exe che è l'applicazione che vogliamo non sia decompilata in fase di test

2) un applicazione WindowsFormsProtector che si occupa di :

   a) crittografare Ethical_Hacking.exe

   b) decrittografare Ethical_Hacking.exe

   c) creare una coppia di chiavi asimmetriche e chiamare il servizio WCF di  PasswordSupplier passando la chiave pubblica ed ottenendo da PasswordSupplier  la password P crittografata e decrittografarla con la chiave privata quando è ritornata dal metodo GetPassword della classe Service di PasswordSupplier      

   d) caricare in memoria ed eseguire Ethical_Hacking.exe partendo da un array di bytes ( ovvero Ethical_Hacking.exe decrittografato )

 3)  l'applicazione PasswordSupplier che si occupa di :

   a) scegliere l'indirizzo a cui sarà reso disponibile il servizio WCF

   b) consentire di inserire la password ,dovrà essere la stessa inserita al punto 2)a)

   c) rendere disponibile (Start) il servizio all'indirizzo scelto in 3)a)

   d) fermare (Stop) il servizio avviato in 3)c)

   e) fornire l'implementazione di GetPassword  prevista dall'interfaccia IService


Segue il dettaglio dei singoli punti :

1) Ethical_Hacking.exe è un eseguibile Windows Forms si tratta di un form che ne chiama un altro,la sua funzione è solo quella di essere un eseguibile .net chiamabile tramite reflection 

2) vediamo il codice relativo al punto 2)a)

    private void button_encrypt_Click(object sender, EventArgs e)
        {
            try { 
            byte[] encrypted = null;
            string file = this.textBox1.Text;
            string pwd = this.textBox2.Text;
            if (!file.Equals(""))
            {
                    if (pwd.Length > 7)
                    {
                        FileStream fs = new FileStream(this.textBox1.Text, FileMode.Open);
                        BinaryReader br = new BinaryReader(fs);
                        byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
                        fs.Close();
                        br.Close();
                        string EncryptionKey = pwd;
                        byte[] clearBytes = bin;
                        Aes encryptor = Aes.Create();
                        Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[]
                        { 0x20,0x6e, 0x20, 0x61, 0x6e, 0x20, 0x4d, 0x65,0x20, 0x61, 0x64, 0x76,0x65});
                        encryptor.Key = pdb.GetBytes(32);
                        encryptor.IV = pdb.GetBytes(16);
                        MemoryStream ms = new MemoryStream();
                        CryptoStream cs = new CryptoStream(ms, encryptor.CreateEncryptor(),
                              CryptoStreamMode.Write);
                        cs.Write(clearBytes, 0, clearBytes.Length);
                        cs.Close();
                        encrypted = ms.ToArray();
                        FileStream fs1 = new FileStream(this.folderName + "\\encryptedExe.encrypt", FileMode.Create);
                        BinaryWriter br1 = new BinaryWriter(fs1);
                        br1.Write(encrypted);
                        fs1.Close();
                        br1.Close();
                    } else
                    {
                        MessageBox.Show("inserire una password di almeno 8 caratteri");
                    }       
            }          
            else {
                    MessageBox.Show("inserire il nome di un file");      
                }
            } catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }

       }        


-segue il codice relativo al punto 2)b) 

                string EncryptionKey = this.textBox2.Text;
                string dir = Directory.GetCurrentDirectory();
                FileStream fs = new FileStream(this.EncryptedFile, FileMode.Open);               
                BinaryReader br = new BinaryReader(fs);
                byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
                byte[] exe = null;
                fs.Close();
                br.Close();
                using (Aes encryptor = Aes.Create())
                {
                    Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[]
                         {0x20, 0x6e, 0x20, 0x61, 0x6e, 0x20, 0x4d, 0x65,0x20, 0x61, 0x64, 0x76,0x65 });
                    encryptor.Key = pdb.GetBytes(32);
                    encryptor.IV = pdb.GetBytes(16);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(),
                                                   CryptoStreamMode.Write))
                        {
                            cs.Write(bin, 0, bin.Length);
                            cs.Close();
                        }
                        exe = ms.ToArray();
                    }
                }
    

l'array exe consiste dell'eseguibile decrittografato 

-passiamo al punto 2)c)

    private void GetPasswordFromRemoteAddress(object sender, EventArgs e)
        {
            try
            {
                /*https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.rsacryptoserviceprovider?view=net-7.0*/
                RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
                string publicKey = rsa.ToXmlString(false); // false to get the public key   
                string address = this.textBox5.Text;                
                ChannelFactory<IService> cf = new ChannelFactory<IService>(new WebHttpBinding(), address);
                //WebHttpBehavior abilita il modello di programmazione Web per un servizio Windows Communication Foundation (WCF).
                cf.Endpoint.Behaviors.Add(new WebHttpBehavior());
                //crea il canale di comunicazione
                IService channel = cf.CreateChannel();
                //viene invocato il metodo GetPassword sul servizio il cui indirizzo è address                
                byte[] encryptedData = channel.GetPassword(publicKey);
                //viene valorizzato decryptedData con l'array di bytes ritornati dal metodo RSADecrypt
                byte[] decryptedData = RSADecrypt(encryptedData, rsa.ExportParameters(true), true);
                UnicodeEncoding convertToString = new UnicodeEncoding();
                string str = "";
                str = convertToString.GetString(decryptedData);
                this.textBox2.Text = str;                              
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message.ToString());
            }
        }

      segue il metodo RSADecrypt che utilizza la chiave privata:

      static public byte[] RSADecrypt(byte[] DataToDecrypt, RSAParameters RSAKeyInfo, bool DoOAEPPadding)
        {
            try
            {
                byte[] decryptedData;
                //Create a new instance of RSACryptoServiceProvider.
                using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider())
                {
                    //Import the RSA Key information. This needs
                    //to include the private key information.
                    RSA.ImportParameters(RSAKeyInfo);
                    //Decrypt the passed byte array and specify OAEP padding.  
                    //OAEP padding is only available on Microsoft Windows XP or
                    //later.  
                    decryptedData = RSA.Decrypt(DataToDecrypt, DoOAEPPadding);
                }
                return decryptedData;
            }
            //Catch and display a CryptographicException  
            //using a message box
            catch (CryptographicException e)
            {                
                MessageBox.Show(e.Message.ToString());
                return null;
            }
        }

-punto 2)d)

private void button_decrypt_and_run_Click(object sender, EventArgs e)
        {
            try
            {
                string EncryptionKey = this.textBox2.Text;
                string dir = Directory.GetCurrentDirectory();
                FileStream fs = new FileStream(this.EncryptedFile, FileMode.Open);               
                BinaryReader br = new BinaryReader(fs);
                byte[] bin = br.ReadBytes(Convert.ToInt32(fs.Length));
                byte[] exe = null;
                fs.Close();
                br.Close();
                using (Aes encryptor = Aes.Create())
                {
                    Rfc2898DeriveBytes pdb = new Rfc2898DeriveBytes(EncryptionKey, new byte[]
                         {0x20, 0x6e, 0x20, 0x61, 0x6e, 0x20, 0x4d, 0x65,0x20, 0x61, 0x64, 0x76,0x65 });
                    encryptor.Key = pdb.GetBytes(32);
                    encryptor.IV = pdb.GetBytes(16);
                    using (MemoryStream ms = new MemoryStream())
                    {
                        using (CryptoStream cs = new CryptoStream(ms, encryptor.CreateDecryptor(),
                                                   CryptoStreamMode.Write))
                        {
                            cs.Write(bin, 0, bin.Length);
                            cs.Close();
                        }
                        exe = ms.ToArray();
                    }
                }
               //https://learn.microsoft.com/en-us/dotnet/api/system.reflection.assembly.load?view=net-8.0
                Assembly assembly1 = Assembly.Load(exe);
                var programType1 = assembly1.GetTypes().FirstOrDefault(c => c.Name == "Program");
                MethodInfo method1=programType1.GetMethod("Start", BindingFlags.Public | BindingFlags.Static);
                method1.Invoke(null, new object[] { });                
            }
            catch(Exception ex)
            {
                string msg= " controllare anche che la password per la decriptazione sia corretta";
                MessageBox.Show(ex.Message.ToString() + " " + msg);                
            }
        }

per utilizzare il codice soprastante bisogna modificare il codice di Ethical_Hacking.exe aggiungendo il metodo Start :     
    
static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            //Start();
        }
                
        public static void Start()  
        {
            Form1 f = new Form1();
            f.ShowDialog();           
        }
    }
    
il metodo chiama il primo Form dell'applicazione (Form1) .

3)  
  a) si tratta di un textbox dove possiamo inserire un indirizzo http a cui sarà possibile trovare il servizio WCF 
 b)  come sopra si tratta di un textbox in cui inserire la password che abbiamo utilizzato al punto 2)a) 
 c) al click del button1 (Start) eseguiamo il gestore dell'evento click
         private void button1_Click(object sender, EventArgs e)
        {
                try
                {
                  string uri = this.textBox1.Text;                    
                  string s = "";
                  s = this.textBox2.Text;                    
                  this.password = s;
                  //WebServiceHost è una classe derivata da ServiceHost che integra il modello di programmazione REST
                  //di Windows Communication Foundation (WCF).
                  //https://learn.microsoft.com/it-it/dotnet/api/system.servicemodel.web.webservicehost?view=netframework-4.8
                  host = new WebServiceHost(typeof(Service), new Uri(uri));                       
                  host.Open();               
                } catch(Exception ex)
                {
                   MessageBox.Show(ex.Message.ToString());
                }
        } 

d) al click del button2 (Stop) 
         private void button2_Click(object sender, EventArgs e)
        {
            try { 
                host.Close();
            }catch(Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

e) segue la classe service che implementa IService 
public class Service : IService
    {        
        public byte[] GetPassword(string publicKey)
        {
             try {                
                byte[] encryptedData = null;
                Form1 f = (Form1)Application.OpenForms["Form1"];
                string text = f.password;
                UnicodeEncoding byteConverter = new UnicodeEncoding();
                byte[] dataToEncrypt = byteConverter.GetBytes(text);

                //load the encryptedData variable with the return of the RSACryptoServiceProvider encrypt method
                using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
                {
                    // load public key from publicKey parameter ,publicKey is a valid xml    
                    rsa.FromXmlString(publicKey);
                    // Encrypt the data and store it in the encyptedData bytes array                       
                    encryptedData = rsa.Encrypt(dataToEncrypt, true);
                }
                return encryptedData;
            } catch(Exception ex)
            {
                MessageBox.Show(ex.Message.ToString());
            } 
            return null;
        }       
    }
 
sia WindowsFormsProtector che PasswordSupplier possiedono ed utilizzano la seguente interfaccia :

//https://learn.microsoft.com/en-us/dotnet/framework/wcf/designing-service-contracts
    //https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.servicecontractattribute?view=dotnet-plat-ext-7.0
    [ServiceContract]
    public interface IService
    {
        //https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.operationcontractattribute?view=dotnet-plat-ext-7.0
        [OperationContract]
        //https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.web.webgetattribute?view=netframework-4.8.1
        [WebGet]
        byte[] GetPassword(string s);       

    }

Per suggerimenti ,critiche o segnalazione errori potete inviare un email a gianmarco.castagna@gmail.com

domenica 15 settembre 2024

Scegliere password complesse ma facili da "indovinare" ? Come evitarlo..

 La lunghezza di una password non ne implica sempre la sicurezza , la password "Passw0rd!" è composta da 9 caratteri  di cui uno maiuscolo ,un numero ed un carattere speciale quindi formalmente è una password forte, in realtà non è così nella pratica se le credenziali sono sottoposte ad un attacco a dizionario usando le password più comuni ( https://it.wikipedia.org/wiki/Attacco_a_dizionario )  . Probabilmente un dizionario usato nell'attacco potrebbe contenere anche la password di cui sopra perchè è molto comune , per mitigare questo tipo di problemi possiamo utilizzare delle liste che contengono le password più utilizzate per verificare in fase di registrazione dell'utente che la password scelta dall'utente non sia tra queste . A quest'indirizzo   https://github.com/danielmiessler/SecLists/tree/master/Passwords/Common-Credentials potete trovare delle liste di password comuni (1000000 di password e c'è anche 'Passw0rd!' , :-) ) .

Ovviamente un attacco dizionario non è comunque facile da portare a segno se abbiamo configurato la nostra applicazione adeguatamente, ad esempio in asp.net core possiamo settare così la nostra applicazione :

builder.Services.AddDefaultIdentity<IdentityUser>
    (options =>
    {
        options.SignIn.RequireConfirmedAccount = true;
        options.Password.RequireDigit = true;
        options.Password.RequiredLength = 8;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequireLowercase = true;
        options.Lockout.MaxFailedAccessAttempts = 3;
        options.Lockout.AllowedForNewUsers = true;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
    });

impostare il lockout con la valorizzazione dei tentativi falliti dopo i quali l'account è locked ed il periodo per cui esso sarà lockout è quindi relativamente semplice .

E' disponibile qui  https://info.microsoft.com/rs/157-GQE-382/images/EN-CNTNT-eBook-MicrosoftPasswordGuidance.pdf  un documento di Microsoft  che  è  una "Microsoft Password Guidance" , in questo paper è presente anche un link al sito di "Schneier on Security" ( https://www.schneier.com/ ) ,in particolare al post https://www.schneier.com/blog/archives/2014/03/choosing_secure_1.html , chi fosse interessato a tematiche di sicurezza e crittografia potrà trovare interessante la newsletter mensile del sito di Schneier

Per verificare se la password inserita dall'utente appartiene a quelle da ritenere troppo comuni per prima cosa carichiamo le "password comuni" in una tabella 'Passwords' creata nel db "GestioneUtenti" ,un pò di "pseudocodice" :

 private void button1_Click(object sender, EventArgs e)
        {
            IEnumerable<string> lines = File.ReadLines(@"C:\Users\gianm\Desktop\10-million-password-list-top-1000000.txt");
            long l = lines.LongCount<string>(); //999998

            SqlConnection conn = new SqlConnection("Data Source=(localdb)\\mssqllocaldb;Initial Catalog=GestioneUtenti;Integrated Security=True");
            conn.Open();

            foreach (string line in lines) {
                SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
                par.Value = line;

                SqlCommand cmd = new SqlCommand("insert into Passwords(password) values (" + par + ")",conn);
                cmd.Parameters.Add(par);
                cmd.ExecuteNonQuery();
                par = null;
                cmd = null;
            }
            conn.Close();
        }

il codice impiegherà da qualche decina di secondi ad alcuni minuti per caricare le passwords nella tabella 'Passwords' 

segue un metodo per verificare se la password inserita dall'utente esiste nella tabella Passwords:

 private bool CheckPasswordExists(string s)
        {
            bool b = false;
            SqlConnection conn = new SqlConnection("Data Source=(localdb)\\mssqllocaldb;Initial Catalog=GestioneUtenti;Integrated Security=True");
            conn.Open();
            SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
            par.Value = s;
            SqlCommand cmd = new SqlCommand();
            cmd.Parameters.Add(par);
            cmd.Connection = conn;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "select * from Passwords where Password like " + par;
            SqlDataReader reader = cmd.ExecuteReader();
            bool read = reader.Read();
            b = read;
            reader.Close();
            conn.Close();       
            return b;
        }

se il metodo torna true significa che la password esiste nella lista delle password comuni e quindi bisogna indicare all'utente di scegliere un' altra password ,
se il metodo torna false la password non è tra quelle della nostra lista e possiamo lasciar procedere l'utente nella registrazione .

Ora vedremo come utilizzare ulteriori funzionalità proprie di Asp .Net Core per verificare che la password immessa dall'utente non sia troppo comune :

nel file Program.cs ,di un progetto AspNetCore, possiamo aggiungere alla configurazione un AddPasswordValidator con cui costruire una nostra logica di validazione della password ( https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.identitybuilder.addpasswordvalidator?view=aspnetcore-7.0 ) :

builder.Services.AddDefaultIdentity<IdentityUser>
            (options =>
            {
                options.SignIn.RequireConfirmedAccount = true;
                options.Password.RequireDigit = true;
                options.Password.RequiredLength = 8;
                options.Password.RequireNonAlphanumeric = true;
                options.Password.RequireUppercase = true;
                options.Password.RequireLowercase = true;
                options.Lockout.MaxFailedAccessAttempts = 3;
                options.Lockout.AllowedForNewUsers = true;
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
            }) .AddPasswordValidator<TestPasswordValidator<IdentityUser>>()               
               .AddEntityFrameworkStores<AppDbContext>();

creiamo una classe TestPasswordValidator :

public class TestPasswordValidator<TUser> : IPasswordValidator<TUser> where TUser : class
    {
        
public TestPasswordValidator()
{
}
    public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password)
    {
        SearchIntoDb s = new SearchIntoDb();
        bool res = s.CheckPasswordExists(password);        
        IdentityResult result;
        if (res)
        {
            result = IdentityResult.Failed(new IdentityError { Description = "Password troppo facile da indovinare" });
        } else {
            result = IdentityResult.Success;
        }
        s = null;
        return Task.FromResult(result);
    }
}

la classe deve implementare l'interfaccia IPasswordValidatorhttps://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.ipasswordvalidator-1?view=aspnetcore-7.0 ) e quindi essa deve avere un' implementazione del metodo ValidateAsync previsto dall'interfaccia , sarà questo metodo a ritornare un IdentityResult . Creando l'IdentityError possiamo valorizzare la Description dell'errore , nel caso la password non sia presente nell'elenco la variabile "res" varrà false ed il metodo ritornerà un IdentityResult.Success.

Il metodo ValidateAsync utilizza una classe SearchIntoDb ed un metodo CheckPasswordExists (già riportato sopra ) per effettuare la select nel db al fine di verificare se la password appartiene alla tabella Passwords oppure no.

public class SearchIntoDb
{

    private string connectionString = null;    
    public SearchIntoDb()
    {
        var config = new ConfigurationBuilder()
                .SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
                .AddJsonFile("appsettings.json").Build();
        string t=config.GetSection("ConnectionStrings").GetSection("UtentiConnection").Value;
        connectionString = t;
    }
   
    public bool CheckPasswordExists(string password)
    {
        bool t = false;        
        SqlConnection conn = new SqlConnection(connectionString);
        conn.Open();
        SqlParameter par = new SqlParameter("@password", System.Data.SqlDbType.Text);
        par.Value = password;
        SqlCommand cmd = new SqlCommand();
        cmd.Parameters.Add(par);
        cmd.Connection = conn;
        cmd.CommandType = System.Data.CommandType.Text;
        cmd.CommandText = "select * from Passwords where Password like " + par;
        SqlDataReader reader = cmd.ExecuteReader();
        bool b = reader.Read();
        t = b;
        reader.Close();
        conn.Close();       
        return t;
    }

}

proviamo il codice in un applicazione asp .net core , inseriamo la famigerata password "Passw0rd!" 

                              

ed otteniamo quanto ci aspettavamo :



    


Il post ha solo scopo di esempio e la lista di password utilizzata non rappresenta un elenco esaustivo delle password "comuni" .


Parallel Linq "troppo" veloce ?!?

 PLINQ (parallel linq)  permette la gestione di operazioni su insiemi di oggetti su più core  con un notevole vantaggio nei tempi di esecuzi...