Unity 遊戲存檔機制淺談,加密 (Encryption) 保護遊戲存檔防作弊
延續在前一篇文章中的議題。
前題,任何的加密機制只是增加破解難度,單機遊戲存檔若沒有特殊硬體的保護,幾乎都可以被破解,光是程式碼反編譯 (decompiling) 並且分析程式碼,便能知道該遊戲如何處理存檔加密,甚至能直接拿到加密用的金鑰 (key)。
關於加密 (Encryption)
關於密碼學 (cryptography) 中的可逆加密 (reversible encryption) 演算法,大致上分為兩種,對稱式加密 (symmetric encryption) 或者是非對稱式加密 (asymmetric encryption)。
對稱式加密 (symmetric encryption),使用相同一把金鑰 (key) 進行加密與解密,例如 3DES、Blowfish、或者是 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
- 5 Common Encryption Algorithms and the Unbreakables of the Future
- Chapter 11. Message Authentication and Hash Functions, Cryptography and Network Security Principles and Practice Fourth Edition by William Stallings
沒有留言: