SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

入門! チートの解剖学 ~セキュアプログラミングでゲームのチートは防げるのか? ~

チートには高度な技術が必要なのか ~Unityアプリ改変の例

 すでにビルドされてバイナリファイルになっているプログラムを改変する、ネットワークを流れる通信内容やコンピューター内のメモリの値を書き換える、といったチートで使われる攻撃方法は何やら難しい技術のようにも見えます。たしかに、チートを行う攻撃者が高い技術力を持っていると思われる事案もあり、ゲーム側が備える高度な保護処理が突破されてしまうような場合もあります。しかしながら実際に多いのは簡単に行えてしまうチートであり、簡単だからこそ誰にでもチートができてしまい被害が大きくなるというのがよくあるケースです。

 チートが高度な技術のように見えるのは、脆弱性情報データベースで公開されるような脆弱性を突いた攻撃と同じようなものに見えるからではないかと思われます。バッファーオーバーフローを突く攻撃として謎のバイト列をサーバーに送りつけると任意のコードを実行できてしまうといった話は、どういう仕組みでそうなってしまうのかの理解も難しいですし、仕組みが理解できても「どうしてそんな攻撃を思いつけるのだろうか」と思われるものばかりです。チートの仕組みは一見するとこの種の脆弱性を突いた高度な攻撃に似ていて、メモリを改変する話はバッファーオーバーフローを突いてのメモリの上書きに、通信を改変する話はサーバーに送りつける謎のバイト列に、プログラムを改変する話は脆弱性を突いての任意コードの実行に、それぞれ似ているようにも見えます。実際には両者はまったく異なるもので、チートの多くは「まあ、そうやって攻撃するだろうね」と思われるような内容です。

 ここでは、この「チートはそう難しいことではない」という感覚を疑似体験するべく、Unityを使って作成したアプリのプログラムを改変してみることにします。文章量の都合等もあるので極度に簡略化したモデルにはなっていますが、プログラムを改変してチートを行うのにそれほど高度な技術が必要なわけではないことは感じられるかと思います。

アプリの内容

 以下のようなC#スクリプトを作成し、適当なゲームオブジェクトのコンポーネントにすることで実行させるだけのアプリです。今回は、これをAndroidアプリにビルドしたものに対してプログラム改変を行ってみます。

using UnityEngine;

public class MinimalGame : MonoBehaviour
{
    void Start() {
        Debug.Log("score = " + Random.Range(0, 10));
    }
}

 アプリを起動すると「score = N」のようなログ出力が行われる様子がadb logcatで確認できます。ここでNは0から9までのいずれかの値を取る乱数です。この「score =」として出力される値を、99のような異常に大きな値にするというプログラム改変を行うことにします。

.dllファイルの逆アセンブル

 UnityでAndroidアプリを作成した場合、C#で書いたプログラム内容は.apkファイル内の「assets/bin/Data/Managed/Assembly-CSharp.dll」というファイルに含まれるようになります。これはバイナリファイルですが、.NET Frameworkに含まれるIldasm.exeやMonoに含まれるmonodis(1)といったツールによって逆アセンブルすることで、以下のようなテキストファイルに変換できます。

.assembly extern UnityEngine
{
  .ver 0:0:0:0
}
.assembly extern mscorlib
{
  .ver 2:0:5:0
  .publickeytoken = (7C EC 85 D7 BE A7 79 8E ) // |.....y.
}
.assembly 'Assembly-CSharp'
{
  .custom instance void class [mscorlib]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::'.ctor'() =  (
        01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78   // ....T..WrapNonEx
        63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01       ) // ceptionThrows.

  .hash algorithm 0x00008004
  .ver  0:0:0:0
}
.module 'Assembly-CSharp.dll' // GUID = {A17528F0-B33E-437D-9112-75403AFC9DF9}

  .class public auto ansi beforefieldinit MinimalGame
      extends [UnityEngine]UnityEngine.MonoBehaviour
  {

    // method line 1
    .method public hidebysig  specialname  rtspecialname
           instance default void '.ctor' ()  cil managed
    {
        // Method begins at RVA 0x20ec
    // Code size 7 (0x7)
    .maxstack 8
    IL_0000:  ldarg.0
    IL_0001:  call instance void class [UnityEngine]UnityEngine.MonoBehaviour::'.ctor'()
    IL_0006:  ret
    } // end of method MinimalGame::.ctor

    // method line 2
    .method private hidebysig
           instance default void Start ()  cil managed
    {
        // Method begins at RVA 0x20f4
    // Code size 29 (0x1d)
    .maxstack 8
    IL_0000:  ldstr "score = "
    IL_0005:  ldc.i4.0
    IL_0006:  ldc.i4.s 0x0a
    IL_0008:  call int32 class [UnityEngine]UnityEngine.Random::Range(int32, int32)
    IL_000d:  box [mscorlib]System.Int32
    IL_0012:  call string string::Concat(object, object)
    IL_0017:  call void class [UnityEngine]UnityEngine.Debug::Log(object)
    IL_001c:  ret
    } // end of method MinimalGame::Start

  } // end of class MinimalGame

逆アセンブル結果の改変と.dllファイルへの再アセンブル

 今回体験するプログラム改変ではランダムに変化する値を改変することを目指すため、まずはAssembly-CSharp.dllの逆アセンブル結果の中でrandomのような内容がないかを検索します。結果、48行目に

    IL_0008:  call int32 class [UnityEngine]UnityEngine.Random::Range(int32, int32)

 のようにあるのが見つかり、前後を見てみると

    IL_0000:  ldstr "score = "
    IL_0005:  ldc.i4.0
    IL_0006:  ldc.i4.s 0x0a
    IL_0008:  call int32 class [UnityEngine]UnityEngine.Random::Range(int32, int32)
    IL_000d:  box [mscorlib]System.Int32
    IL_0012:  call string string::Concat(object, object)

 のようになっています。UnityEngine.Random::Range(int32, int32)が引数に指定された上限と下限の間に一様分布する乱数を返すメソッドであることと、string::Concat(object, object)が引数を連結した文字列を返すメソッドであること、およびログに出力されるのがscore = Nのような文字列であることを考えると、

    IL_0005:  ldc.i4.0
    IL_0006:  ldc.i4.s 0x0a
    IL_0008:  call int32 class [UnityEngine]UnityEngine.Random::Range(int32, int32)

 の部分を例えば

    //IL_0005:  ldc.i4.0
    IL_0006:  ldc.i4.s 99
    //IL_0008:  call int32 class [UnityEngine]UnityEngine.Random::Range(int32, int32)

 のように改変すれば常にscore = 99になるのではないかと推定されます。このとき実際にそうなるか確信が持てなくても、攻撃者は当て推量で試してみて、期待どおりの結果が得られなければまた他の場所を探してみるといった試行錯誤的な方法でチートを試みてきます。

 Ildasm.exeやmonodis(1)によって生成した逆アセンブル結果は、.NET Frameworkに含まれるIlasm.exeやMonoに含まれるilasm(1)といったツールによってアセンブルすることで、.dllファイルに変換することができます。今回の例ではこれが意図どおりの改変となることが、生成された.dllファイルで.apkファイル中のAssembly-CSharp.dllを上書きしたものをAndroid端末にインストールして実行してみることで確認できます。なお、この際には.apkファイルの電子署名を付け直す必要があります。

Unityは危ない? iOSは安全?

 ここで紹介しているプログラム改変の例について、逆アセンブル結果が元になったC#ソースファイルの内容と酷似していることに気づかれた方もいるかと思います。MinimalGameというクラス名やStartというメソッド名だけでなく、スーパークラスがUnityEngine.MonoBehaviourであることや、void Start()のメソッドシグニチャとprivateメソッドであること、デフォルトコンストラクターを除けばMinimalGameクラスには他にメンバーはないことなどが、逆アセンブル結果から容易に読み取れます。

 これはAssembly-CSharp.dllに含まれる内容が中間言語であることによるものです。中間言語はCPUによって実行される機械語ではなく、実行前にもう一度コンパイルされて機械語に変換されます。中間言語にはいろいろな種類のものがありますが、C#コードのコンパイル結果として出力される中間言語はクラスやメソッド等のメタ情報を豊富に含む極めて読みやすいものになっています。

 C#を使用するUnityではアプリの一部として中間言語コードが含まれることについて、C++などを使用してアプリに中間言語コードが含まれないようにした場合に比べて、チート被害を受けやすくなるという声を聞くことがあります。これには、確かにそうだといえる部分とあまりそうともいえない部分があります。中間言語コードは解析や改変が容易であるというのは間違いありませんが、ネイティブの実行ファイルを逆アセンブルした結果も部分部分であればまったく読めないというほど複雑なものではありません。チートが行われる際にプログラムの広い範囲にわたって処理が大きく改変されることはほとんどないため、部分的にある程度は読めるのであればチートのおそれは十分にあるといえるでしょう。

 また、ファイル改変・通信改変・メモリ改変はアプリを開発したプログラミング言語とは無関係に行われるチートですが、手法としてはこちらのほうが容易であることからプログラム改変に比べるとより多くのチート被害を発生させています。Unityアプリに対するチートが難しくないというのは正しい主張といえますが、Unityアプリでなくてもチートは難しくないというのもまた正しい主張といえるのではないでしょうか。

 同様の話として、AndroidではiOSよりもチートの被害を受けやすいというものもあります。Androidアプリの開発に用いられるJavaは、C#のように中間言語にコンパイルされること、Androidではシェルアクセスが標準でサポートされていること、AndroidではGoogle Playを通さずに.apkファイルを直接インストールできること、などがその論拠とされます。これも確かにそうであるといえる部分はありますが、大して違わないというのが実際のところです。

 なお、UnityであってもiOSアプリを作る場合には中間言語コードがアプリに含まれることはありません。これは、iOSアプリにビルドされる場合は中間言語コードから機械語コードへのコンパイルもアプリのビルド時に済まされるためです。この仕組みはIL2CPPと呼ばれるもので、2016年7月にリリースされたUnity 5.4からはAndroidアプリにビルドする場合でもこれがサポートされるようになりました。普及にはいましばらくの時間がかかるように思われますが、やがてはUnityで作られたアプリには中間言語コードが含まれるという認識もなくなっていくでしょう。

次のページ
Webアプリケーションのセキュリティとの違いはどこか

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

舛形 謙介(サイファー・テック株式会社)(マスガタ ケンスケ)

 サイファー・テック株式会社 執行役員 / ソフトウェアエンジニア。2012年の入社以来、主にAndroidプラットフォームでセキュリティ関連の商品開発・サービス業務に従事し、現在はチート対策SDK「CypherGuard AppTrusty」の開発とクラッキング耐性診断サービス「Critical ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/9638 2016/09/20 14:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー