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

  • Tutorial
Hello again to everyone! We got around 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 saw on Unity in the hope of earning a loaf of bread from breaking by evil schoolchildren. Why schoolchildren? Because a priori 100% reliable protection cannot be. And whoever wants to break it anyway. The only question is how much time and effort he will spend on it. And as security people like to joke, nobody canceled thermorectal cryptanalysis.

So, in the article I will try to tell you as much as possible about 3 aspects (and of course, I will offer an implementation):
  • application data protection (saves)
  • application memory protection
  • in-game purchase protection (Google Play)

image

1. Preparation


First you need to learn how to convert game data (types, classes) into strings. It is worth exploring JSON or XML serialization. I do not recommend starting with XML, as I’m having problems with iOS. Better to learn JSON, here is the wiki.unity3d.com/index.php/SimpleJSON link . Unfortunately, this is the topic of a separate article and I will not dwell on this. If lazily sorted out - you can old-fashioned sculpt a string manually using separators. For instance:

var profile = "name=player;money=999;level=80";

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

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

Then you can veil the string by applying base64 conversion to it. I emphasize that base64 is not an encryption, it does not have an encryption key and all that. base64 converts your string to a new string consisting of only ASCII characters. Visually see how this happens, you can link base64.ru . I will 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);
        }
    }
}

I 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. Protection of game data (saves)


So, now we are able to convert game data into a string. Now we need to think about where to save them. The first thing that comes to mind is saving saves to files in Application.persistentDataPath. The disadvantages of this method are two:
  • Application.persistentDataPath may change when the application is updated (for example, the application will move to the SD card). Accordingly, the save file will not be found, and the user will lose all progress
  • This will not work in web player and windows phone

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 base64 conversion, this will protect the save from editing through most programs for hacking. But for hardcore it's time to fasten normal encryption. Right to the point, take AES and encrypt. We copy the AES.cs file and do not wonder how it works:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Assets.Scripts.Common
{
    /// 
    /// 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)
    /// 
    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. Application memory protection


Does everyone remember a PC program like ArtMoney? She knew how to look for values ​​in RAM and for several iterations of screening allowed to become a millionaire in the game. Now for Android and iOS there are a lot of similar programs, for example, the most popular - GameKiller.

Protecting yourself from such programs is quite simple - you need to encrypt the values ​​in the application 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 superfast algorithm. I suggest modifying our base64 a bit and implementing my encryption - efficient, fast, with blackjack and XOR:

using System;
using System.Text;
namespace Assets.Scripts.Common
{
    /// 
    /// 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.
    /// 
    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 decode the AES-th profile from the save, we immediately encrypt all the values ​​with this B64X (I invented the name myself). And we decode every time you need to find out how much money a player has, what level he has, etc. The B64X can use the key (password) for encryption, or it can use a random session key so that we don’t worry about where and how to store it.

4. Protection of in-game purchases


For many developers, this topic is not relevant and 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, in a nutshell, replaces the in-game shopping service. In short - a player can make purchases for free.

We 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 implemented the purchase mechanism and their verification (http://docs.unity3d.com/Manual/UnityAnalyticsReceiptVerification.html), so the information below now has only a theoretical load.

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

image

you still use it when implementing the game store in the application.

Google’s 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 quite simple - Google signs all the json responses of the purchase server, and no one else can forge 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, then the digital signature will be incorrect.

Let's move on 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. Evidently, it seems simple enough to decode its base64. But this is not so. I suggest using the online service and immediately dig in the xml key in the application. It’s not worth bothering especially about its protection - it's the same public key. It can be fabricated, but that's another story. So, here's the service, insert your base64 key there and get the xml key: superdry.apphb.com/tools/online-rsa-key-converter

image

In the bottom field is our xml key. We save it in the game or application. And then everything is simple. Google returns the purchase to us. If you use the free plug-in for the implementation of OpenIAB purchases in the application, 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’ll give an implementation of the signature verification mechanism:

using System;
using System.Security.Cryptography;
namespace Assets.Scripts.Common
{
    public static class GooglePlayPurchaseGuard
    {
        /// 
        /// 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/
        /// 
        /// Purchase JSON string
        /// Purchase signature string
        /// XML public key. Use http://superdry.apphb.com/tools/online-rsa-key-converter to convert RSA public key from Developer Console
        /// 
        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 and now, when the answer came from Google that the purchase was completed, we verify 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 repeatedly cram the correct server response with the correct, but the same digital signature. This is an optional argument in the OpenIAB implementation, on which most put a bolt:

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 drawn to the gaming industry and I want to change the scope of activity)

Also popular now: