📜 ⬆️ ⬇️

Protection of games and mobile applications from hacking for dummies (Unity, C #, Mono)

Hello everyone again! We reached out to write a cool article on a very important topic for game developers. So, let's talk about protecting your precious games and applications that you cut on Unity in the hope of earning a loaf of bread from hacking by evil students. Why schoolchildren? Because reliable 100% protection a priori can not be. And whoever wants to, will still hack. The only question is how much time and energy he spends on it. And as the security guards love to joke - nobody canceled thermorectal cryptanalysis.

So, in the article I will try to tell as much as possible about 3 aspects (and of course, I will propose an implementation):

image

1. Preparation


First you need to learn how to convert game data (types, classes) into strings. Worth learning JSON or XML serialization. I do not recommend starting with XML, because There will be problems with iOS. It is better to learn JSON, here is the link wiki.unity3d.com/index.php/SimpleJSON . Unfortunately, this is the topic of a separate article and I will not dwell on it. If you are too lazy to figure it out, you can manually sculpt the string using separators. For example:
')
var profile = "name=player;money=999;level=80"; 

You also need to be able to convert strings to byte arrays and back. Everything is simple:

 var bytes = Encoding.Default.GetBytes(profile); profile = Encoding.Default.GetString(bytes); 

Further, the line can be veiled by applying the base64 transformation to it. I note in particular that base64 is not encryption, it does not have an encryption key and all that. base64 converts your string to a new string, consisting only of ASCII characters. Visually see how this happens, you can link base64.ru . I'll just give the implementation code:

 using System; using System.Text; namespace Assets.Scripts.Common { public static class Base64 { public static string Encode(string plainText) { var plainTextBytes = Encoding.UTF8.GetBytes(plainText); return Convert.ToBase64String(plainTextBytes); } public static string Decode(string base64EncodedData) { var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); return Encoding.UTF8.GetString(base64EncodedBytes); } } } 

Also note that base64 works quickly and is comparable in speed with the addition operation. You can perform such conversions even in the Update loop.

2. Game data protection (saves)


So, now we can convert game data into a string. Now you need to think where to save them. First of all, it comes to mind to save saves to files in Application.persistentDataPath. There are two disadvantages to this method:

The second and most correct way is to save to PlayerPrefs. Example below:

 const string key = "profile"; var profile = "name=player;money=999;level=80"; PlayerPrefs.SetString(key, profile); PlayerPrefs.Save(); if (PlayerPrefs.HasKey(key)) { profile = PlayerPrefs.GetString(key); } 

Oh yes, baby, super! Now we need to encrypt our save. Here you can quickly perform the base64 conversion, this will protect the save from editing through most hacking programs. But in hardcore it's time to screw the normal encryption. Go to work, take AES and encrypt. Copy the AES.cs file and don’t wonder how it works:

 using System; using System.IO; using System.Security.Cryptography; using System.Text; namespace Assets.Scripts.Common { /// <summary> /// AES (Advanced Encryption Standard) implementation with 128-bit key (default) /// - 128-bit AES is approved by NIST, but not the 256-bit AES /// - 256-bit AES is slower than the 128-bit AES (by about 40%) /// - Use it for secure data protection /// - Do NOT use it for data protection in RAM (in most common scenarios) /// </summary> public static class AES { public static int KeyLength = 128; private const string SaltKey = "ShMG8hLyZ7k~Ge5@"; private const string VIKey = "~6YUi0Sv5@|{aOZO"; // TODO: Generate random VI each encryption and store it with encrypted value public static string Encrypt(byte[] value, string password) { var keyBytes = new Rfc2898DeriveBytes(password, Encoding.UTF8.GetBytes(SaltKey)).GetBytes(KeyLength / 8); var symmetricKey = new RijndaelManaged { Mode = CipherMode.CBC, Padding = PaddingMode.Zeros }; var encryptor = symmetricKey.CreateEncryptor(keyBytes, Encoding.UTF8.GetBytes(VIKey)); using (var memoryStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(value, 0, value.Length); cryptoStream.FlushFinalBlock(); cryptoStream.Close(); memoryStream.Close(); return Convert.ToBase64String(memoryStream.ToArray()); } } } public static string Encrypt(string value, string password) { return Encrypt(Encoding.UTF8.GetBytes(value), password); } public static string Decrypt(string value, string password) { var cipherTextBytes = Convert.FromBase64String(value); var keyBytes = new Rfc2898DeriveBytes(password, Encoding.UTF8.GetBytes(SaltKey)).GetBytes(KeyLength / 8); var symmetricKey = new RijndaelManaged { Mode = CipherMode.CBC, Padding = PaddingMode.None }; var decryptor = symmetricKey.CreateDecryptor(keyBytes, Encoding.UTF8.GetBytes(VIKey)); using (var memoryStream = new MemoryStream(cipherTextBytes)) { using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { var plainTextBytes = new byte[cipherTextBytes.Length]; var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); memoryStream.Close(); cryptoStream.Close(); return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount).TrimEnd("\0".ToCharArray()); } } } } } 

3. Protection of application memory


Everyone remembers such a PC program like ArtMoney? She was able to look for values ​​in RAM and in several iterations of screening allowed to become a millionaire in the game. Now there are a lot of similar programs for Android and iOS, for example the most popular one is GameKiller.

Protection from such programs is quite simple - you need to encrypt the values ​​in the application's memory. Encrypt EVERY TIME while writing and decrypt EVERY TIME while reading. And since the operation is quite frequent, it makes no sense to use heavy AES and we need a super-fast algorithm. I propose to slightly modify our base64 and implement our encryption - effective, fast, with blackjack and XOR:

 using System; using System.Text; namespace Assets.Scripts.Common { /// <summary> /// Simple and fast Base64 XOR encoding with dynamic key (generated on each app run). Use for data protection in RAM. Do NOT use for data storing outside RAM. Do NOT use for secure data encryption. /// </summary> public class B64X { public static byte[] Key = Guid.NewGuid().ToByteArray(); public static string Encode(string value) { return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(value), Key)); } public static string Decode(string value) { return Encoding.UTF8.GetString(Encode(Convert.FromBase64String(value), Key)); } public static string Encrypt(string value, string key) { return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(value), Encoding.UTF8.GetBytes(key))); } public static string Decrypt(string value, string key) { return Encoding.UTF8.GetString(Encode(Convert.FromBase64String(value), Encoding.UTF8.GetBytes(key))); } private static byte[] Encode(byte[] bytes, byte[] key) { var j = 0; for (var i = 0; i < bytes.Length; i++) { bytes[i] ^= key[j]; if (++j == key.Length) { j = 0; } } return bytes; } } } 

Now, as soon as we read and decrypted the AES profile from the save, we immediately encrypt all the values ​​with this B64X (I came up with the name myself). And decrypt each time you need to find out how much money a player has, what level he has, etc. B64X can use a key (password) for encryption, or it can use a random session session key so that we do not steam, where and how to store it.

4. Protection of in-app purchases


For many developers, this topic is not relevant and very few people implement protection. In principle, if you have a multiplayer game, then you need to think about protecting its economy. There is such a program - Freedom. It requires a root and, if in a nutshell, replaces the service of in-game purchases. In short - the player can make purchases for free.

Let us omit consideration of the mechanism for checking purchases on the developer's server, because not everyone has it. I'll tell you what Google offers in such cases.

UPD: Unity has implemented the purchase mechanism and its verification (http://docs.unity3d.com/Manual/UnityAnalyticsReceiptVerification.html), so the information below now only has a theoretical load.

When creating an application in the developer console, Google generates a key pair for the RSA algorithm — public and private key. If you do not know what it is - google asymmetric encryption. The public key can be obtained in the developer console:

image

You still use it when you implement the game store in the application.

The Google private key will never show you and will use it to digitally sign purchases. Accordingly, the private key can only encrypt the signature, and the public key can only decrypt the signature.

The protection mechanism is pretty simple - Google signs all the json-responses of the shopping server, and no one else can fake such a signature. The developer, knowing the public key, can verify the digital signature of the server responses. And if the server was fabricated using Freedom, the digital signature will be incorrect.

Let's turn to implementation. First you need to perform one unpleasant operation. You need to convert the base64 public key from the developer console to an xml key that is suitable for decrypting the signature. It seems to Swidz that it is enough just to decode it with base64. But it is not. I propose to use the online service and immediately prikopat xml-key in the application. Especially not to bother about his protection is not worth it - it's the public key. It can be fabricated, but that's another story. So, here is the service, insert your base64 key there and get the xml-key: superdry.apphb.com/tools/online-rsa-key-converter

image

The bottom field is our xml key. Save it in the game or application. And then everything is simple. Google returns the purchase to us. If you use a free plugin in your application to make purchases from OpenIAB, then this is an object of the Purchase class, it has 2 fields we need:

 Purchase purchase; var json = purchase.OriginalJson; var signature = purchase.Signature; 

Now I will give the implementation of the signature verification mechanism:

 using System; using System.Security.Cryptography; namespace Assets.Scripts.Common { public static class GooglePlayPurchaseGuard { /// <summary> /// Verify Google Play purchase. Protect you app against hack via Freedom. More info: http://mrtn.me/blog/2012/11/15/checking-google-play-signatures-on-net/ /// </summary> /// <param name="purchaseJson">Purchase JSON string</param> /// <param name="base64Signature">Purchase signature string</param> /// <param name="xmlPublicKey">XML public key. Use http://superdry.apphb.com/tools/online-rsa-key-converter to convert RSA public key from Developer Console</param> /// <returns></returns> public static bool Verify(string purchaseJson, string base64Signature, string xmlPublicKey) { using (var provider = new RSACryptoServiceProvider()) { try { provider.FromXmlString(xmlPublicKey); var signature = Convert.FromBase64String(base64Signature); var sha = new SHA1Managed(); var data = System.Text.Encoding.UTF8.GetBytes(purchaseJson); return provider.VerifyData(data, sha, signature); } catch (Exception e) { UnityEngine.Debug.Log(e); } return false; } } } } 


Well, now, when Google received the answer that the purchase was made, check its signature and show the player a fig, if the signature does not match:

 if (GooglePlayPurchaseGuard.Verify(purchase.OriginalJson, purchase.Signature, publicKeyXml)) { } else { } 

I want to note that when making a purchase, it is better to add a random payload to the request, this will protect against man-in-the-middle attacks, when you can re-push the correct server response with the correct, but the same digital signature. This is an optional argument in the OpenIAB implementation, on which most bolts put:

 public static void purchaseProduct(string sku, string developerPayload = "") 

A more detailed description of the mechanism can be found in English at the link: mrtn.me/blog/2012/11/15/checking-google-play-signatures-on-net

5. Conclusion


I hope the article was not too boring. In any case, thank you for your attention, make high-quality games and give players new experiences!
PS Recently, in the gaming industry and I want to change the scope of activities)

Source: https://habr.com/ru/post/249681/


All Articles