Unity製アプリに対するよくある不正行為とその対策

この記事は DeNA 23 新卒 Advent Calendar 2022 の14日目の記事です。

https://qiita.com/advent-calendar/2022/dena-23-shinsotsu

# 1. はじめに
こんにちは。初アドベントカレンダー参加&初Qiita執筆の佐藤([@hanaaaaaachiru](https://twitter.com/hanaaaaaachiru))です。
普段は[インディーゲーム制作](https://store.steampowered.com/app/2142130/TRPG/)を行ったり、[はなちるのマイノート](https://www.hanachiru-blog.com/)という技術ブログを運営していたりします。

https://store.steampowered.com/app/2142130/TRPG/

https://www.hanachiru-blog.com/

今回はUnity製アプリに対するよくある不正行為とその対策について紹介したいと思います。
ただ通信に関連した話(例.`URL`から推測して未公開の`AssetBundle`を取得する、不正なスコアの送信)はせず、あくまでクライアント側のみを扱います。

また他人に迷惑をかけるような利用は必ずしないようにしてください。よろしくお願いします。

# 2. 目次
[1.はじめに](#1-はじめに)
[2.目次](#2-目次)
[3.実験環境](#3-実験環境)
[4.下準備](#4-下準備)
[5.よくある不正行為](#5-よくある不正行為)
[5-1.コード解析](#5-1-コード解析)
[5-2.アセット解析](#5-2-アセット解析)
[5-3.メモリ改竄](#5-3-メモリ改竄)
[5-4.セーブデータ等のファイル改竄](#5-4-セーブデータ等のファイル改竄)
[6.おわりに](#6-おわりに)

# 3. 実験環境
Unity 2021.3.0f1
Windows10
Build Target : Standalone Windows

# 4. 下準備
実験台になってもらうプロジェクトを作成しました。

https://github.com/hanachiru/CheatSample

中身は解析用に適当に作ったものなので遊べません。

# 5. よくある不正行為
以下について取り上げます。
- コード解析
- アセット解析
- メモリ改竄
- セーブデータ等のファイル改竄

書きすぎると不正行為を助長してしまう可能性があるので、ざっくりと概要だけ取り上げます。

## 5-1 コード解析
### 概要
プログラムを解析をするツールを用いて、どんな処理が行われているか調べることができます。

結論からいうと<code>IL2CPP</code>を用いることでコード解析の難易度を上げることができるので、特に理由がない限り有効にしましょう。

> IL2CPP (Intermediate Language To C++) は Unity が開発したスクリプティングバックエンドで、さまざまなプラットフォームのプロジェクトをビルドするときに Mono の代わりに使用できます。IL2CPP を使用してプロジェクトをビルドする場合、Unity は選択したプラットフォーム用のネイティブのバイナリファイル (例えば .exe、apk、.xap など) を作成する前に、スクリプトやアセンブリからの IL コードを C++ に変換します。IL2CPP を使用すると、Unity プロジェクトのパフォーマンス、セキュリティ、プラットフォームの互換性を向上させることができます。
https://docs.unity3d.com/ja/2019.4/Manual/IL2CPP.html

### やり方
#### IL2CPPを使わない場合
<code>C#</code>の中間言語である<code>IL</code>が<code>dll</code>としてそのまま同梱されているので、それに対して解析を行います。
Unityでは<code>Assembly definitions</code>や<code>Assembly References</code>を自身で定義しなければ、開発者が書いたコードは(基本は)<code>Assembly-CSharp.dll</code>に含まれています。

例えば<code>ILSpy</code>というツールを使うことで逆コンパイルをすることができます。

> ILSpy is the open-source .NET assembly browser and decompiler.
https://github.com/icsharpcode/ILSpy#ilspy----

<code>StandaloneWindows</code>向けにビルドした場合は、<code>○○_Data/Managed</code>の中に<code>.dll</code>が入っています。

<code>.dll</code>を<code>ILSpy</code>を使うと以下のように解析することができます。

```cs:Player.cs
// オリジナル (←このコメントは執筆時付け足したもの)
using UnityEngine;

public class Player : MonoBehaviour
{
   [SerializeField] private float xBound;
   
   private Transform _transform;

   private void Awake()
   {
      _transform = transform;
   }
   
   private void Update()
   {
      if (Input.GetKey(KeyCode.LeftArrow) && _transform.position.x > -xBound)
      {
         _transform.Translate(-0.1f, 0, 0);
      }
      if (Input.GetKey(KeyCode.RightArrow) && _transform.position.x < xBound)
      {
         _transform.Translate(0.1f, 0, 0);
      }
   }
}
```

```cs:Player.cs
// 解析したもの (←このコメントは執筆時付け足したもの)
// Player
using UnityEngine;

public class Player : MonoBehaviour
{
	[SerializeField]
	private float xBound;

	private Transform _transform;

	private void Awake()
	{
		_transform = base.transform;
	}

	private void Update()
	{
		if (Input.GetKey(KeyCode.LeftArrow) && _transform.position.x > 0f - xBound)
		{
			_transform.Translate(-0.1f, 0f, 0f);
		}
		if (Input.GetKey(KeyCode.RightArrow) && _transform.position.x < xBound)
		{
			_transform.Translate(0.1f, 0f, 0f);
		}
	}
}
```

ちなみに<code>ILSpy</code>は中身を覗くだけですが、<code>dnSpy</code>というツールでは書き換えも可能です。

> dnSpy is a debugger and .NET assembly editor. You can use it to edit and debug assemblies even if you don't have any source code available. 
https://github.com/dnSpy/dnSpy#dnspy---latest-release

#### IL2CPPを使う場合
例えば<code>Il2CppDumper</code>という<code>IL2CPP</code>でビルドされたバイナリから元の<code>Managed DLL</code>に変換するツールを用いることでコードの解析をすることができます。

> Unity il2cpp reverse engineer
> Complete DLL restore (except code), can be used to extract MonoBehaviour and MonoScript
https://github.com/Perfare/Il2CppDumper

<code>Windows Standalone</code>の場合は以下の2つのファイルを<code>Il2CppDumper</code>に読み込ませます。
- <code>GameAssembly.dll</code>
- <code>○○_Data/il2cpp_data/Metadata/global-metadata.dat</code>

![スクリーンショット 2022-12-04 173154.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/0656a41f-2b21-d5cc-566d-cc1a26a20791.png)

無事に実行ができると<code>DummyDll</code>というフォルダが生成されるので、その中身を<code>ILSpy</code>などで解析します。

```cs:Player.cs
// 解析したもの (←このコメントは執筆時付け足したもの)
// Player
using Il2CppDummyDll;
using UnityEngine;

[Token(Token = "0x2000005")]
public class Player : MonoBehaviour
{
	[Token(Token = "0x4000007")]
	[FieldOffset(Offset = "0x18")]
	[SerializeField]
	private float xBound;

	[Token(Token = "0x4000008")]
	[FieldOffset(Offset = "0x20")]
	private Transform _transform;

	[Token(Token = "0x600000A")]
	[Address(RVA = "0x196510", Offset = "0x195910", VA = "0x180196510")]
	private void Awake()
	{
	}

	[Token(Token = "0x600000B")]
	[Address(RVA = "0x196540", Offset = "0x195940", VA = "0x180196540")]
	private void Update()
	{
	}

	[Token(Token = "0x600000C")]
	[Address(RVA = "0x196260", Offset = "0x195660", VA = "0x180196260")]
	public Player()
	{
	}
}
```

<code>Address</code>属性の値から、バイナリ（<code>GameAssembly.dll</code>）の該当箇所を探し出します。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3332373033342f30633432393430382d353731652d343836362d313365632d3035663430643436383839322e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/bea90ff6-9d09-6fb9-4784-0a3d8f688707.png)


私の知識量ではこの先どう編集すればよいのか分からないのでなにも言えませんが、<code>CPU</code>の命令セットを理解していれば編集できそうです。（勉強します...）

### 対策
コードの難読化を行うことが有効だと思います。

例えば<code>ConfuserEx2</code>という難読化ツールを用いることで、<code>.dll</code>(<code>.exe</code>)に対してコードの難読化を行うことができます。

> ConfuserEx 2 is a free and open-source protector for .NET applications.
https://github.com/mkaring/ConfuserEx/wiki/Introduction

<code>ConfuserEx2</code>は様々な難読化(<code>protection</code>)を設定することができるのですが、例えば以下の<code>protection</code>を設定した場合はコードが以下のように変換されます。
- <code>Anti IL Dasm Protection</code>(<code>anti ildasm</code>)
- <code>Control Flow Protection</code>(<code>ctrl flow</code>)
- <code>Protection Hardening</code>(<code>harden</code>)


```cs:Player.cs
// オリジナル (←このコメントは執筆時付け足したもの)
using UnityEngine;

public class Player : MonoBehaviour
{
   [SerializeField] private float xBound;
   
   private Transform _transform;

   private void Awake()
   {
      _transform = transform;
   }
   
   private void Update()
   {
      if (Input.GetKey(KeyCode.LeftArrow) && _transform.position.x > -xBound)
      {
         _transform.Translate(-0.1f, 0, 0);
      }
      if (Input.GetKey(KeyCode.RightArrow) && _transform.position.x < xBound)
      {
         _transform.Translate(0.1f, 0, 0);
      }
   }
}
```

```cs:Player.cs
// 難読化したもの (←このコメントは執筆時付け足したもの)
// Player
using UnityEngine;

public class Player : MonoBehaviour
{
	[SerializeField]
	private float xBound;

	private Transform _transform;

	private void Awake()
	{
		_transform = base.transform;
	}

	private void Update()
	{
		if (Input.GetKey(KeyCode.LeftArrow))
		{
			goto IL_000f;
		}
		goto IL_0102;
		IL_0102:
		int num;
		int num2;
		if (!Input.GetKey(KeyCode.RightArrow))
		{
			num = -1390479301;
			num2 = num;
		}
		else
		{
			num = -1172218411;
			num2 = num;
		}
		goto IL_0014;
		IL_0014:
		while (true)
		{
			uint num3;
			switch ((num3 = (uint)num ^ 0xAAD3DB21u) % 7u)
			{
			case 3u:
				break;
			default:
				return;
			case 5u:
			{
				int num6;
				int num7;
				if (_transform.position.x <= 0f - xBound)
				{
					num6 = -1214332492;
					num7 = num6;
				}
				else
				{
					num6 = -1936459645;
					num7 = num6;
				}
				num = num6 ^ ((int)num3 * -488439093);
				continue;
			}
			case 1u:
				_transform.Translate(0.1f, 0f, 0f);
				num = (int)(num3 * 2036419465) ^ -1946491989;
				continue;
			case 0u:
				_transform.Translate(-0.1f, 0f, 0f);
				num = ((int)num3 * -1231275453) ^ -798382778;
				continue;
			case 6u:
			{
				int num4;
				int num5;
				if (_transform.position.x >= xBound)
				{
					num4 = 771771467;
					num5 = num4;
				}
				else
				{
					num4 = 245490753;
					num5 = num4;
				}
				num = num4 ^ (int)(num3 * 1098800460);
				continue;
			}
			case 4u:
				goto IL_0102;
			case 2u:
				return;
			}
			break;
		}
		goto IL_000f;
		IL_000f:
		num = -1743280129;
		goto IL_0014;
	}
}
```

このコードは<code>IL2CPP</code>を利用せずにビルドをし、難読化した後に<code>ILSpy</code>を用いてコード解析したものです。

補足ですがプラットフォーム依存の<code>protection</code>があったり、一緒に難読化しない他アセンブリが存在する場合はメソッド名を変換してはいけない等いくつか注意点があります。
https://github.com/mkaring/ConfuserEx/wiki/Protections

またUnityの難読化アセットである[Obfuscator](https://assetstore.unity.com/packages/tools/utilities/obfuscator-48919?aid=1100l4SPn)を利用するのも有効かつ簡単な手法でしょう。

## 5-2 アセット解析
### 概要
<code>AssetStudio</code>を筆頭とする<code>Assets</code>・<code>AssetBundle</code>を解析するツールが出回っています。

> AssetStudio is a tool for exploring, extracting and exporting assets and assetbundles.
https://github.com/Perfare/AssetStudio

<code>GitHub</code>の<code>readme</code>にも記載されていますが、以下のファイルをサポートしています。
- Texture2D : convert to png, tga, jpeg, bmp
- Sprite : crop Texture2D to png, tga, jpeg, bmp
- AudioClip : mp3, ogg, wav, m4a, fsb. support convert FSB file to WAV(PCM)
- Font : ttf, otf
- Mesh : obj
- TextAsset
- Shader
- MovieTexture
- VideoClip
- MonoBehaviour : json
- Animator : export to FBX file with bound AnimationClip

また<code>AssetBundle</code>が<code>LZ4</code>や<code>LZMA</code>に圧縮されていたとしても、問題なく解析することができます。

### やり方
プロジェクト単位(フォルダを指定するとフォルダ内部のファイルを走査)もしくは解析したいファイルを<code>AssetStudio</code>に読み込ませます。

ちなみにローカルから<code>AssetBundle</code>を読み込む実装をしている場合は、大抵<code>AssetBundle</code>が<code>StreamingAssets</code>の中に含まれているので、<code>○○_Data/StreamingAssets</code>の中にあります。

サーバーからロードしてローカルに保存しておく場合は<code>C://User/[UserName]/AppData/LocalLow/[CompanyName]/[ProductName]</code>（<code>Application.persistentDataPath</code>）が多いと思います。

![スクリーンショット 2022-12-04 021010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/83339386-aa51-dc0f-6fb8-04ea54d217f2.png)

### 対策
ファイルを暗号化することが有効です。
<code>AssetBundle</code>に着目するなら以下の2種類の方法が考えられます。
- 暗号化したファイルを<code>AssetBundle</code>に含める
- <code>AssetBundle</code>自体を暗号化する

こちらに関しては先人の方々が色々と知恵を絞られて実装を考えられています。
[AssetBundleの暗号化について図でまとめてみた](https://www.slideshare.net/dena_tech/unity-20182019denaunity-dena-techcon-2019)
[Unity 2018-2019を見据えたDeNAのUnity開発のこれから DeNA TechCon 2019](https://www.slideshare.net/dena_tech/unity-20182019denaunity-dena-techcon-2019)
私もあまり詳しいわけではないので、詳細は割愛させてください。(勉強します...)

例えば[AES](https://docs.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.aes?view=net-6.0)を用いて<code>AssetBundle</code>自体を暗号化してみると<code>AssetStudio</code>では解析できないことが確認できます。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3332373033342f31623563616330392d333938622d363064612d616333352d3635663630336262373431642e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/4f87b67f-f32a-3dac-ddc7-97d3df9237b2.png)

![スクリーンショット 2022-12-04 020057.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/9e1b30ad-6736-67a6-6168-957407f6e74e.png)

また<code>AssetBundle</code>に限らずとも、普通の<code>Project</code>内の<code>Asset</code>に対しても暗号化処理を挟む手法を先人の方達が色々と考えられています。
[Unity製アプリにおいてアセットを暗号化する手法](https://technote.qualiarts.jp/article/32)

こちらのサイバーエージェントの記事が分かりやすく、<code>ScriptedImporter</code>を利用したり、<code>BuildPlayerWindow.RegisterBuildPlayerHandler</code>(<code>IPreprocessBuildWithReport</code>だと<code>AssetBundle</code>ビルドができないので使っていない模様)を使って暗号化処理を挟むあたりはなるほどなぁという感じです。

https://docs.unity3d.com/ja/2022.2/Manual/ScriptedImporters.html

https://docs.unity3d.com/ja/2018.4/ScriptReference/BuildPlayerWindow.RegisterBuildPlayerHandler.html

## 5-3 メモリ改竄
### 概要
<code>Cheat Engine</code>を筆頭とするメモリ改竄を行うツールが出回っています。

https://github.com/cheat-engine/cheat-engine

> Cheat Engine is a tool designed to help you with modifying single player games without internet connection so you can make them harder or easier depending on your preference(e.g: Find that 100hp is too easy, try playing a game with a max of 1 HP)
https://cheatengine.org/aboutce.php

### やり方
メモリに格納されている値から、改竄したい値が格納されているアドレスを探し出します。

例. <code>Score</code>を<code>22</code>から<code>23</code>へと変化させたときに、同じような値の変化をしているメモリのアドレスを調べる。
![スクリーンショット 2022-12-04 164549.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/d9e73c95-f308-fb1c-6e9a-6021dbd291e9.png)

### 対策
メモリ上にそのままのデータを置かないようにする事が有効です。
よくある例としては<code>XOR</code>した値をメモリに書き込んでおき、利用する際に再度<code>XOR</code>するというものです。

``` 
// XORの性質
平文 ^ 鍵 = 暗号文
暗号文 ^ 鍵 = 平文 
```

お試しでメモリ上に<code>XOR</code>を利用した値を置くような自作<code>int</code>を作ってみました。

```cs:CustomInt.cs
[Serializable]
public struct CustomInt : IFormattable, IEquatable<CustomInt>, IComparable<CustomInt>, IComparable<int>, IComparable
{
    private static readonly Random Random = new Random();
    
    [SerializeField] private int key;
    [SerializeField] private int fakeValue;
    
    public CustomInt(int value) : this(GenerateKey(), value){}

    public CustomInt(int key, int value)
    {
        this.key = key;
        fakeValue = Encrypt(key, value);
    }

    public static int Encrypt(int key, int value)
        => key ^ value;

    public static int Decrypt(int key, int value)
        => key ^ value;

    public static int GenerateKey()
        => Random.Next();

    private static CustomInt Add(CustomInt input, int x)
    {
        var value = Decrypt(input.key, input.fakeValue);
        value += x;
        input.fakeValue = Encrypt(input.key, value);
        return input;
    }
    
    public static implicit operator CustomInt(int value)
        => new CustomInt(GenerateKey(), value);

    public static implicit operator int(CustomInt value)
        => Decrypt(value.key, value.fakeValue);

    public static CustomInt operator ++(CustomInt input)
        => Add(input, 1);

    public static CustomInt operator --(CustomInt input)
        => Add(input, -1);
    
    public override bool Equals(object obj)
        => obj is CustomInt customInt && Equals(customInt);

    public override int GetHashCode()
        => Decrypt(key, fakeValue).GetHashCode();

    public bool Equals(CustomInt other)
        => other != null && key == other.key && fakeValue == other.fakeValue;

    public int CompareTo(CustomInt other)
        => Decrypt(key, fakeValue).CompareTo(Decrypt(other.key, other.fakeValue));

    public int CompareTo(int other)
        => Decrypt(key, fakeValue).CompareTo(other);

    public int CompareTo(object obj)
        => Decrypt(key, fakeValue).CompareTo(obj);

    public override string ToString()
        => Decrypt(key, fakeValue).ToString();
    
    public string ToString(string format, IFormatProvider formatProvider)
        => Decrypt(key, fakeValue).ToString(format, formatProvider);
}
```

また[Anti Cheat Toolkit](https://assetstore.unity.com/packages/tools/utilities/anti-cheat-toolkit-2021-202695?aid=1100l4SPn)というアセットにも同様の機能が備わっています。(<code>Anti Cheat Toolkit</code>も<code>XOR</code>している)

> 📌 共通の機能
> メモリ内の変数を保護します。
https://assetstore.unity.com/packages/tools/utilities/anti-cheat-toolkit-2021-202695?locale=ja-JP

## 5-4 セーブデータ等のファイル改竄
### 概要
Unityでセーブを行う際は大抵以下のパスにファイルが保存されています。(<code>Standalone Windows</code>の場合)

- <code>C://User/[UserName]/AppData/LocalLow/[CompanyName]/[ProductName]</code>（<code>Application.persistentDataPath</code>）

<code>PlayerPrefs</code>を利用している場合はかなり特殊で、<code>HKCU/Software/[company name]/[product name]</code>という場所に入っています。
### やり方
#### PlayerPrefsを利用していない場合
<code>C://User/[UserName]/AppData/LocalLow/[CompanyName]/[ProductName]</code>（<code>Application.persistentDataPath</code>）内に配置されているファイルを編集します。

![スクリーンショット 2022-12-04 162632.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/a1e5c634-5c54-acb5-dc99-a0f959aebd15.png)

ここは実装する人によって様々な手法を取っているので割愛します。

#### PlayerPrefsを利用している場合
**レジストリエディター**を利用して値の編集を行います。

<code>Windows10</code>の場合はタスクバーの検索ボックスに<code>regedit</code>と入力することでレジストリエディターを開けます。

![スクリーンショット 2022-12-04 031806.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/26b7c02a-1a41-3ab0-20c4-57ede0c43369.png)

<code>HKEY_CURRENT_USER/SOFTWARE/[company name]/[product name]</code>を選択すると、データの一覧が表示されるはずです。

``` cs:Log.cs
// PlayerPrefsを用いてセーブをする
PlayerPrefs.SetInt("LogCount", count);
```

![スクリーンショット 2022-12-04 025841.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/327034/666030a0-7847-e6fb-56be-a63d6284407e.png)

画像の場合では後ろに<code>_h1852915458</code>のような文字列が追加されていましたが、簡単に特定・編集することができました。

### 対策
<code>PlayerPrefs</code>を利用する・しないに関わらず暗号化したデータを格納することが有効です。

セーブ関連で有名なUnityのアセットとしては[Easy Save](https://assetstore.unity.com/packages/tools/utilities/easy-save-the-complete-save-data-serializer-system-768?aid=1100l4SPn)が挙げられます。(<code>AES</code>による暗号化)

> Easy Save supports AES encryption with an 128-bit key, which can be enabled in the default settings or by using an ES3Settings object as a parameter.
https://docs.moodkie.com/easy-save-3/es3-guides/es3-encryption-compression/

後[Anti Cheat Toolkit](https://assetstore.unity.com/packages/tools/utilities/anti-cheat-toolkit-2021-202695?aid=1100l4SPn)というアセットには<code>PlayerPrefs</code>を暗号化する機能があるみたいですね。

> This is an Obscured analogue of the PlayerPrefs class.
> Saves data in encrypted state, optionally locking it to the current device.
Automatically encrypts PlayerPrefs on first read (auto migration), has tampering detection and more.
https://codestage.net/uas_files/actk/api/class_code_stage_1_1_anti_cheat_1_1_storage_1_1_obscured_prefs.html

# 6. さいごに
マサカリ大歓迎です。

また普段から週２目安で技術ブログの記事を投稿しているので、よろしければこちらも是非。

https://www.hanachiru-blog.com/