關於 web service, unity, blogger 等軟體工程筆記

Unity 遊戲存檔機制淺談,加密 (Encryption) 保護遊戲存檔防作弊

Edit icon 沒有留言
遊戲存檔的加解密機制

延續在前一篇文章中的議題。

前題,任何的加密機制只是增加破解難度,單機遊戲存檔若沒有特殊硬體的保護,幾乎都可以被破解,光是程式碼反編譯 (decompiling) 並且分析程式碼,便能知道該遊戲如何處理存檔加密,甚至能直接拿到加密用的金鑰 (key)。

注意:使用 Unity 2018.1.1f1 測試,Scripting Runtime Version 採用 .Net 4.x Equvalent,而非預設的 .Net 3.5 Equvalent

關於加密 (Encryption)

關於密碼學 (cryptography) 中的可逆加密 (reversible encryption) 演算法,大致上分為兩種,對稱式加密 (symmetric encryption) 或者是非對稱式加密 (asymmetric encryption)

對稱式加密 (symmetric encryption),使用相同一把金鑰 (key) 進行加密與解密,例如 3DESBlowfish、或者是 AES

非對稱式加密 (asymmetric encryption),或稱公開金鑰加密 (public-key encryption),則是有兩把金鑰 (public key & private key),用其中一把加密則僅能用另一把解密,處理速度通常較對稱式加密還要慢,著名演算法是 RSA

若考慮單機遊戲存檔的加密方式,使用處理速度較快對稱式加密即可,但不管是哪種加密方式,在單機環境透過反編譯程式碼或者是掃描記憶體等方式,只要金鑰被對方取得就被破解了。

若要追求進一步資料保護,那勢必得有額外的伺服器 (server) 或是特殊硬體來輔助,並利用非對稱式加密方式或是混合方式來增強破解難度。

例如將存檔資料送到伺服器,利用非對稱式加密的特性,利用伺服器保護的 private key 進行加密,而 public key 則讓本機端使用解密。當然這樣的方式還存在許多漏洞,例如如果被繞過伺服器等議題。

設計單機遊戲存檔加密機制,使得很難被破解是一個不簡單的問題,但有需要花費這麼多工在加密保護,而不是專心在遊戲開發上,儘快上線測試市場反應?這是一個取捨,至少已知不受歡迎沒有價值的遊戲,通常不會有人想花時間去破解。

對稱式加密程式碼範例

以下僅筆記 3DES 以及 AES 在 C# 使用中的範例,依賴 .Net framework 已有的函式庫實作,若是要使用 Blowfish 則需要再找找看有沒有可用的 open source。

using System.IO;
using System.Text;
using System.Security.Cryptography;

public static class TripleDESUtility
{
   static TripleDES tripleDES = TripleDESCryptoServiceProvider.Create();

   private static System.Tuple<byte[], byte[]> GenerateKeyIV(string secret)
   {
      using(var md5 = new MD5CryptoServiceProvider())
      {
         var salt = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
         using (var gen = new Rfc2898DeriveBytes(secret, salt))
         {
            var key = gen.GetBytes(tripleDES.KeySize / 8);
            var iv = gen.GetBytes(tripleDES.BlockSize / 8);  // Initialization vector
            return new System.Tuple<byte[], byte[]>(key, iv);
         }
      }
   }

   public static byte[] Encrypt(byte[] src, string secret)
   {
      var v = GenerateKeyIV(secret);
      var key = v.Item1;
      var iv = v.Item2;
      return Encrypt(src, key, iv);
   }

   public static byte[] Encrypt(byte[] src, byte[] key, byte[] iv)
   {
      using (var output = new MemoryStream())
      {
         var encryptor = tripleDES.CreateEncryptor(key, iv);
         using (var cs = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
         {
            cs.Write(src, 0, src.Length);
         }
         return output.ToArray();
      }
   }

   public static byte[] Decrypt(byte[] src, string secret)
   {
      var v = GenerateKeyIV(secret);
      var key = v.Item1;
      var iv = v.Item2;
      return Decrypt(src, key, iv);
   }

   public static byte[] Decrypt(byte[] src, byte[] key, byte[] iv)
   {
      using (var output = new MemoryStream())
      {
         using (var input = new MemoryStream(src))
         {
            var decryptor = tripleDES.CreateDecryptor(key, iv);
            using (var cs = new CryptoStream(input, decryptor, CryptoStreamMode.Read))
            {
               cs.CopyTo(output);
            }
            return output.ToArray();
         }
      }
   }
}
using System.IO;
using System.Text;
using System.Security.Cryptography;

public static class AESUtility
{
   static Aes aes = AesCryptoServiceProvider.Create();

   private static System.Tuple<byte[], byte[]> GenerateKeyIV(string secret)
   {
      using(var md5 = new MD5CryptoServiceProvider())
      {
         var salt = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
         using (var gen = new Rfc2898DeriveBytes(secret, salt))
         {
            var key = gen.GetBytes(aes.KeySize / 8);
            var iv = gen.GetBytes(aes.BlockSize / 8);
            return new System.Tuple<byte[], byte[]>(key, iv);
         }
      }
   }

   public static byte[] Encrypt(byte[] src, string secret)
   {
      var v = GenerateKeyIV(secret);
      var key = v.Item1;
      var iv = v.Item2;
      return Encrypt(src, key, iv);
   }

   public static byte[] Encrypt(byte[] src, byte[] key, byte[] iv)
   {
      using (var output = new MemoryStream())
      {
         var encryptor = aes.CreateEncryptor(key, iv);
         using (var cs = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
         {
            cs.Write(src, 0, src.Length);
         }
         return output.ToArray();
      }
   }

   public static byte[] Decrypt(byte[] src, string secret)
   {
      var v = GenerateKeyIV(secret);
      var key = v.Item1;
      var iv = v.Item2;
      return Decrypt(src, key, iv);
   }

   public static byte[] Decrypt(byte[] src, byte[] key, byte[] iv)
   {
      using (var output = new MemoryStream())
      {
         using (var input = new MemoryStream(src))
         {
            var decryptor = aes.CreateDecryptor(key, iv);
            using (var cs = new CryptoStream(input, decryptor, CryptoStreamMode.Read))
            {
               cs.CopyTo(output);
            }
            return output.ToArray();
         }
      }
   }
}

加入資料驗證 (Content Authentication)

資料認證的一環,如何確保解密後的資料是否真的為原始資料,而沒有經過竄改?以遊戲存檔來說,也許可以在反序列化 (deserialization) 階段發現其資料錯誤,但較好的方式還是在解密後就進行資料驗證。

可以利用雜湊函數 (one-way hash function) 來進行資料驗證,加密階段除了遊戲存檔資料外,也將該資料經過雜湊函數運算後的雜湊值一併加密,解密後將該遊戲資料經過相同的雜湊函數,運算出的雜湊值與解密後的雜湊值比較,若不同則表示該資料有被竄改,如下圖所示。

加入資料驗證的加密流程

加入資料驗證的加密流程

因此修改上述的 AES 範例,採用 SHA-256 雜湊函數:

using System.IO;
using System.Text;
using System.Security.Cryptography;
using System.Linq;

public static class MyUtility
{
   static Aes aes = AesCryptoServiceProvider.Create();
   static SHA256 sha = SHA256.Create();

   private static System.Tuple<byte[], byte[]> GenerateKeyIV(string secret)
   {
      using(var md5 = new MD5CryptoServiceProvider())
      {
         var salt = md5.ComputeHash(Encoding.UTF8.GetBytes(secret));
         using (var gen = new Rfc2898DeriveBytes(secret, salt))
         {
            var key = gen.GetBytes(aes.KeySize / 8);
            var iv = gen.GetBytes(aes.BlockSize / 8);
            return new System.Tuple<byte[], byte[]>(key, iv);
         }
      }
   }

   public static byte[] Encrypt(byte[] src, string secret)
   {
      var v = GenerateKeyIV(secret);
      var key = v.Item1;
      var iv = v.Item2;
      return Encrypt(src, key, iv);
   }

   public static byte[] Encrypt(byte[] src, byte[] key, byte[] iv)
   {
      using (var output = new MemoryStream())
      {
         var encryptor = aes.CreateEncryptor(key, iv);
         using (var cs = new CryptoStream(output, encryptor, CryptoStreamMode.Write))
         {
            cs.Write(src, 0, src.Length);
            
            var hash = sha.ComputeHash(src);
            cs.Write(hash, 0, hash.Length);
         }
         return output.ToArray();
      }
   }

   public static byte[] Decrypt(byte[] src, string secret)
   {
      var v = GenerateKeyIV(secret);
      var key = v.Item1;
      var iv = v.Item2;
      return Decrypt(src, key, iv);
   }

   public static byte[] Decrypt(byte[] src, byte[] key, byte[] iv)
   {
      using (var output = new MemoryStream())
      {
         using (var input = new MemoryStream(src))
         {
            var decryptor = aes.CreateDecryptor(key, iv);
            using (var cs = new CryptoStream(input, decryptor, CryptoStreamMode.Read))
            {
               cs.CopyTo(output);
            }

            var hash = new byte[sha.HashSize / 8];
            var raws = new byte[output.Length - hash.Length];
            output.Seek(0, SeekOrigin.Begin);
            output.Read(raws, 0, raws.Length);
            output.Read(hash, 0, hash.Length);

            var chash = sha.ComputeHash(raws);
            if (!hash.SequenceEqual(chash))
            {
                throw new System.Exception("Invalid content hash");
            }

            return raws;
         }
      }
   }
}

此資料驗證方式,若加密方式以及金鑰都被破解,那麼驗證本身也將會徒勞無功……。

Reference

沒有留言: