UEのBlueprintバイナリ改ざんをC++で検知する


# はじめに

筆者はゲーム開発者のただの高校生です。間違い等ありましたら申し訳ございません。

追記

C++側も改ざんできるのは知っています(IDA ProやGhidra等)。
言い出したらきりがないですし結局いたちごっこですし、どんなに暗号化や難読化をしようとクライアント側にデータがある時点でいつかは必ず割れます。
この記事は一種のエンターテイメントとしてご覧ください(ガチの実用性を求めないでください)


## Blueprintが案外簡単に改ざんできた

皆さんは普段Blueprintを使用し開発をしていると思いますが、実はこのBlueprint非常に簡単にバイナリ改ざんができるものとなっています。まずパッケージ化した際に全てのuassetはチャンク分けしていなければ

WindowsNoEditor→"projectname"→Content→Paks→WindowsNoEditor.pak

に入ります。ここで問題なのがBlueprintはコンパイル時にBytecodeとなりVM上で実行されるのですが、Bytecode自体がそのままここに入っています。(暗号化できないのが一つ問題)
BlueprintVM自体の説明はErenさんが書かれている記事が一つ参考になります。(許可をもらえたので掲載します)

https://intaxwashere.github.io/blueprint-performance/

そしてこのpak自体はエンジンにデフォルトでついているUnrealPak.exeで解凍できます。
一応暗号化機能があるのですが、秒でAESKeyが割れてしまうため事実上機能していないです。

![Desktop Screenshot 2023.12.07 - 18.25.39.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/0440f8a2-309c-ef49-e6e0-faa46dda9111.png)

## バイナリ改ざんでできること

全てのパラメーターを変更することができます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/UE4?src=hash&amp;ref_src=twsrc%5Etfw">#UE4</a> のBlueprintのバイナリデータを.pakが暗号化されていることろから改ざんできました<br>(キャラクタームーブメントコンポーネントのZVelocityを600→3000、AirControlを0.2→0.6に動画ではしています)<br>体験版を出すときはフラグ一つで製品版と切り替えるのではなくちゃんとコード消した方がいい <a href="https://t.co/5trITv3gFV">pic.twitter.com/5trITv3gFV</a></p>&mdash; aoharu/gamedev (@aoharuisgod) <a href="https://twitter.com/aoharuisgod/status/1733754100347785578?ref_src=twsrc%5Etfw">December 10, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

またその気になれば処理の変更を行い、チート状態のpakを作成できます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">UnrealEngineのBlueprintを使用した場合非常に簡単にバイナリ改ざんができるとわかりました。<br>今回はいわゆる無限弾薬チートを実装してみました。 <a href="https://t.co/KcsGUnwA3s">pic.twitter.com/KcsGUnwA3s</a></p>&mdash; aoharu/gamedev (@aoharuisgod) <a href="https://twitter.com/aoharuisgod/status/1735299156389036191?ref_src=twsrc%5Etfw">December 14, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そのためBlueprintで改ざん検知を実施してもそれ自体を停止するパッチを簡単に充てられる為無意味です。
これについて先ほどの方(BP2CPPというBPをC++に変換するプラグインを開発しているBytecodeガチ勢の方)に質問させていただいたのですが、
>Blueprintsのバイトコードは、異なる値に解釈されるuint8/バイト値の配列に過ぎず、エンジン内の他のシステムとの依存関係がたくさんあります。何かに触れると、エンジンの半分が完全に壊れてしまう :D

>バイトコードは安全ではなく、ゲームが実行されているときにリバースエンジニアが容易に逆アセンブルや修正を行うことができます。また、バイトコードを暗号化したり難読化することも難しいです。より強固な保護を実装するためには、複雑なC++の概念を深く理解し、Unreal Engine 5のソースコードを変更する必要があります。しかし、それでも完全な保護を提供することはできません。
フォートナイトのような大規模予算のゲームは、このような問題を防ぐために高価なサードパーティツールを使用しています。また、彼らはブループリントよりもC++側で機密データとロジックを保持しています。

という話でした。

## それでも...Blueprintが使いたい！

ここで出てくるのがC++です。C++もリバースエンジニアリングできるのですが、Bytecodeと比べて遥かに難しい為不正行為の難易度を上げられるのは事実だと思います。又BPに比べて自由度が高い為独自の暗号化を施すなどのこともできると思います。(Blueprintも難読化等はできると思います)(保証はできません)

## .pakのハッシュ値を取る

https://github.com/BlueMountainsIO/ue-plugin-sha256

こちらのPluginを使いpakのハッシュ値を事前にとっておき、C++で作成したゲームインスタンスで起動時にハッシュ値を取り、ファイルが無い場合又は違う場合にゲームを停止します。
まず普通にPluginsフォルダーを作り入れ、有効化します。
次にBuild.csに依存関係を記入します
```Build.cs
using UnrealBuildTool;

public class BinaryHack : ModuleRules
{
    public BinaryHack(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HashSHA256" });

        PublicIncludePaths.AddRange(new string[] { });

        PrivateDependencyModuleNames.AddRange(new string[] { "HashSHA256" });

        // Uncomment if you are using Slate UI
        // PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

        // Uncomment if you are using online features
        // PrivateDependencyModuleNames.Add("OnlineSubsystem");

        // To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
    }
}
```

新規C++から

![Desktop Screenshot 2023.12.15 - 21.36.05.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/d11032fd-cb87-595d-4f13-e6316df0c2d8.png)

GameInstanceクラスを作成します

```MyGameInstance.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "SHA256Hash.h"
#include "Kismet/KismetSystemLibrary.h" 
#include "MyGameInstance.generated.h"

UCLASS()
class YOURPROJECT_API UMyGameInstance : public UGameInstance
{
    GENERATED_BODY()

public:
    UMyGameInstance();
    virtual void Init() override;

private:
    void CheckPakFileHash();
    void TerminateGame();

    FString ExpectedHash = TEXT("ハッシュ値をここに入力");
};
```
```MyGameInstance.cpp

#include "MyGameInstance.h"
#include "Engine/Engine.h"
#include "Misc/Paths.h"
#include "HAL/PlatformFileManager.h"

UMyGameInstance::UMyGameInstance()
{
    
}

void UMyGameInstance::Init()
{
    Super::Init();
    CheckPakFileHash();
}

void UMyGameInstance::CheckPakFileHash()
{
    FString PakFilePath = FPaths::Combine(FPaths::RootDir(), TEXT("BinaryHack/Content/Paks/パック名.pak"));
    IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

    // ファイルが存在するかチェック
    if (PlatformFile.FileExists(*PakFilePath))
    {
        FSHA256Hash PakFileHash;
        if (PakFileHash.FromFile(PakFilePath)) // ファイルからハッシュ値を計算
        {
            FString HashString = PakFileHash.GetHash();
            if (HashString != ExpectedHash)
            {
                TerminateGame(); // ハッシュ値が一致しない場合、ゲームを閉じる
            }
        }
    }
    else
    {
        TerminateGame(); // ファイルが存在しない場合もゲームを閉じる
    }
}

void UMyGameInstance::TerminateGame()
{
    FGenericPlatformMisc::RequestExit(false); // ゲームを閉じる
}
```

次に保護対象のチャンク分けをします。まずプロジェクト設定からチャンクの生成を有効化します。


![Desktop Screenshot 2023.12.15 - 21.44.36.76.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/a548952d-2322-99dd-a81e-88e360ca7dfa.png)


次にエディターの環境設定から
チャンクIDの割り当てを許可します。

![Desktop Screenshot 2023.12.15 - 21.47.29.65.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/02505602-106f-fe07-f602-a7621bfa3aa6.png)

するとBPの上で右クリックをするとアセットアクション→チャンクに割り当てが出ます

![Desktop Screenshot 2023.12.15 - 21.48.55.74.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/1d891a9d-7d9d-1568-946e-d45bfa5662fd.png)

任意のチャンクIDを割り当てます

![Desktop Screenshot 2023.12.15 - 21.49.02.64.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/b5c09324-72c3-4156-4b76-caea97e1fc29.png)

そうするとパッケージ化した際pakが分割されます

![Desktop Screenshot 2023.12.15 - 21.52.38.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/22beee43-4515-4574-75ad-c677fe0a8faf.png)

そうしたら一回BPでハッシュ値を取ってメモしてください。おすすめはBPでゲームインスタンスを作り取る感じです。(ゲームインスタンスを切り替えるだけでいいので)

![Desktop Screenshot 2023.12.15 - 21.44.16.71.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2953041/98799a33-6423-e47d-9d1b-44aea7bbf4fa.png)

:::note warn
BPのハッシュ値はコンパイルボタンを押すたびに変わります。気をつけてください
:::

最後にメモしたハッシュ値をヘッダーファイルに記入してインスタンスを指定すればオッケーです。

## 最後に
銀の弾はクライアント側に処理を置かない以外ないです。