11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unreal Engine (UE)Advent Calendar 2023

Day 16

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

Last updated at Posted at 2023-12-15

はじめに

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

追記

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

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

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

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

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

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

Desktop Screenshot 2023.12.07 - 18.25.39.15.png

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

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

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

そのためBlueprintで改ざん検知を実施してもそれ自体を停止するパッチを簡単に充てられる為無意味です。
これについて先ほどの方(BP2CPPというBPをC++に変換するプラグインを開発しているBytecodeガチ勢の方)に質問させていただいたのですが、

Blueprintsのバイトコードは、異なる値に解釈されるuint8/バイト値の配列に過ぎず、エンジン内の他のシステムとの依存関係がたくさんあります。何かに触れると、エンジンの半分が完全に壊れてしまう :D

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

という話でした。

それでも...Blueprintが使いたい!

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

.pakのハッシュ値を取る

こちらの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

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

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

Desktop Screenshot 2023.12.15 - 21.47.29.65.png

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

Desktop Screenshot 2023.12.15 - 21.48.55.74.png

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

Desktop Screenshot 2023.12.15 - 21.49.02.64.png

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

Desktop Screenshot 2023.12.15 - 21.52.38.13.png

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

Desktop Screenshot 2023.12.15 - 21.44.16.71.png

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

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

最後に

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

11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?