セーブデータの改竄を防止するプラグイン

累積比率パレートマン
記事: 33
登録日時: 2022年1月12日(水) 01:12

セーブデータの改竄を防止するプラグイン

投稿記事by 累積比率パレートマン » 2023年2月21日(火) 15:52

エンジニアの皆様

お疲れ様です。

セーブデータの値を改ざんして

パラメーターやアイテムを不正に獲得するユーザーが後を絶たず困っております。
(私の作品では、アイテム収集を主なゲーム性としているため、大変迷惑しています)

こうしたゴト行為を防ぐプラグインを探しましたが、不思議と見つからなかったため

見よう見まねで作ってみようと考えました。

仕様は、以下のようなものです。

・セーブデータ作成時に、もう一つのセーブデータを保持しておき、ロード時にその差分でチェックを行う。
・バックアップ側のファイルは暗号化し、チェック時にのみ内部で復号化させる。

以下がコードになります。

コード: 全て選択

(function() {
   
    // セーブデータのファイル名とバックアップファイル名
    var saveFileName = 'file1.rpgsave';
    var backupFileName = 'file1_backup.rpgsave';
   
    // バックアップファイルのパス
    var backupFilePath = 'save/' + backupFileName;
   
    // バックアップファイルの暗号化キー
    var encryptionKey = 'password';
   
    // セーブデータの書き込み時にバックアップを作成する
    var DataManager_makeSavefileInfo = DataManager.makeSavefileInfo;
    DataManager.makeSavefileInfo = function() {
        var info = DataManager_makeSavefileInfo.call(this);
        if (info) {
            // バックアップファイルを作成する
            this.backupSaveFile();
        }
        return info;
    };
   
    // バックアップファイルを作成する関数
    DataManager.backupSaveFile = function() {
        // セーブデータファイルを読み込む
        var saveFileData = StorageManager.load(saveFileName);
        if (saveFileData) {
            // バックアップファイルを作成する
            var backupData = LZString.compressToBase64(saveFileData);
            var encryptedBackupData = CryptoJS.AES.encrypt(backupData, encryptionKey).toString();
            StorageManager.save(backupFileName, encryptedBackupData);
        }
    };
   
    // セーブデータの読み込み時に改ざんを検出する
    var DataManager_loadGame = DataManager.loadGame;
    DataManager.loadGame = function(savefileId) {
        if (this.isSavefileValid(savefileId)) {
            // セーブデータファイルを読み込む
            var saveFileData = StorageManager.load(saveFileName);
            if (saveFileData) {
                // バックアップファイルを読み込む
                var encryptedBackupData = StorageManager.load(backupFilePath);
                if (encryptedBackupData) {
                    var decryptedBackupData = CryptoJS.AES.decrypt(encryptedBackupData, encryptionKey).toString(CryptoJS.enc.Utf8);
                    var backupData = LZString.decompressFromBase64(decryptedBackupData);
                    // セーブデータとバックアップデータを比較する
                    if (saveFileData !== backupData) {
                        // 改ざんが検出された場合はエラーを表示してゲームを終了する
                        throw new Error('このセーブデータは改ざんされた可能性があります。ロードできません。');
                    }
                }
            }
            DataManager_loadGame.call(this, savefileId);
        }
    };
   
})();


導入しましたが、セーブ時、デバッグコンソールに「Save data too big!」というエラーが出て

バックアップが作成されませんでした。

生成するファイルサイズの上限が超過しているものと思われますが

セーブデータ自体のサイズはとても小さいため、別の理由かと思われます。

他にどんな原因が考えられますでしょうか?

有識者の方のお知恵を拝借できましたら幸いです。

よろしくお願いいたします。

アバター
Plasma Dark
記事: 676
登録日時: 2020年2月08日(土) 02:29
連絡を取る:

Re: セーブデータの改竄を防止するプラグイン

投稿記事by Plasma Dark » 2023年2月22日(水) 09:06

不思議と見つからなかったため


費用対効果の問題ですね。プラグインそのものを見られてしまえば、チェックをすり抜ける術もわかってしまうので、あんまりそのためのプラグインを用意しても意味がないのです。

復号化させる


細かいことで恐縮ですが、復号化するという言葉は誤りです。平文化する、あるいは復号すると言います。

Save data too big!


これはエラーではなく警告です。そして、提示されたプラグインをOFFにした状態でセーブしても同じ警告が出るものと思われます。
セーブデータの容量が一定以上の場合に警告が出るのみで、セーブ処理は止まりません。

バックアップが作成されませんでした。


何か別のエラーが出ていませんか。例えば、crypt-jsを利用するためのモジュール読み込みをしていないとか。


余談ですが、globalinfoの改ざんを防ぐだけではメインのセーブデータは改ざんを防げないような気がします。
また、セーブデータまるごと複製を別ファイルに吐き出すよりも、適当な(改ざん防止したいデータに依存する)ハッシュをセーブデータに埋め込んで照合するほうが実装もシンプルだし、パフォーマンス面でユーザーの体験を損なわずに済むんではないかと思います。
累積比率パレートマン
記事: 33
登録日時: 2022年1月12日(水) 01:12

Re: セーブデータの改竄を防止するプラグイン

投稿記事by 累積比率パレートマン » 2023年2月22日(水) 15:44

Plasma Dark様


適格なご指摘と素晴らしい代替案を賜り、誠にありがとうございます。

なるほど!ハッシュを埋め込んで照合すればスマートになります。

改竄を防止したいデータと言えば、アイテム・武器・防具・アクターのパラメーターなので

以下のようなコードを作成してみました。

尚、CryptoJSは使わないようにしています。

コード: 全て選択

/*:
 * @plugindesc データ改ざんを検知するプラグインです。
 * @author 累積比率パレートマン
 *
 * @param Items
 * @desc ハッシュ計算に含めるアイテムのIDをカンマ区切りで指定してください。
 * @default
 *
 * @param Weapons
 * @desc ハッシュ計算に含める武器のIDをカンマ区切りで指定してください。
 * @default
 *
 * @param Armors
 * @desc ハッシュ計算に含める防具のIDをカンマ区切りで指定してください。
 * @default
 *
 * @param Actors
 * @desc ハッシュ計算に含めるアクターのIDをカンマ区切りで指定してください。
 * @default
 *
 * @param SecretKey
 * @desc ハッシュ計算に用いる秘密鍵を指定してください。
 * @default my_secret_key
 *
 * @param HashLength
 * @desc ハッシュ値の長さを指定してください。値が大きいほど衝突の確率は低くなりますが、計算時間が増加します。
 * @default 32
 *
 * @param HashAlgorithm
 * @desc ハッシュ関数のアルゴリズムを指定してください。"SHA256"または"SHA3-256"が使用可能です。
 * @default SHA256
 *
 * @param EnableDebugMode
 * @desc デバッグモードを有効にする場合はtrueにしてください。デバッグモードではハッシュ値や計算に使用されるデータをログ出力します。
 * @default false
 *
 * @param DebugModePrefix
 * @desc デバッグモードのログ出力に使用するプレフィックスを指定してください。
 * @default [Data Integrity Checker]
 *
 * @param DebugModeColor
 * @desc デバッグモードのログ出力に使用する色を指定してください。
 * @default #00FFFF
 *
 * @help
 * プラグインコマンドはありません。
 *
 */

(function() {

var parameters = PluginManager.parameters('DataIntegrityChecker');
var items = parameters['Items'].split(',');
var weapons = parameters['Weapons'].split(',');
var armors = parameters['Armors'].split(',');
var actors = parameters['Actors'].split(',');
var secretKey = parameters['SecretKey'];
var hashLength = parseInt(parameters['HashLength']);
var hashAlgorithm = parameters['HashAlgorithm']);
var enableDebugMode = parameters['EnableDebugMode'] === 'true';
var debugModePrefix = parameters['DebugModePrefix'];
var debugModeColor = parameters['DebugModeColor'];

var _DataManager_makeSavefileInfo = DataManager.makeSavefileInfo;
DataManager.makeSavefileInfo = function() {
    var info = _DataManager_makeSavefileInfo.call(this);
    var hash = generateHash();
    info.dataIntegrityHash = hash;
    return info;
};

var _DataManager_loadGame = DataManager.loadGame;
DataManager.loadGame = function(savefileId) {
    var success = _DataManager_loadGame.call(this, savefileId);
    if (success) {
        var save = this.loadAllSavefileContents()[savefileId - 1];
        var hash = generateHash(save);
        var expectedHash = save.dataIntegrityHash;
        if (hash !== expectedHash) {
            success = false;
            alert('セーブデータが改ざんされているため、読み込めません。');
        }
    }
    return success;
};

function generateHash(save) {
    if (!save) {
        var data = {
            items: getItems(),
            weapons: getWeapons(),
            armors: getArmors(),
            actors: getActors()
        };
        save = JSON.stringify(data);
    }
    var raw = secretKey + save;
    var hash = hashAlgorithm ? sha3_256(raw) : sha256(raw);
    hash = hash.substr(0, hashLength);
    if (enableDebugMode) {
        console.log(debugModePrefix + 'Hash: %c' + hash, 'color: ' + debugModeColor);
        console.log(debugModePrefix + 'Raw Data: %c' + raw, 'color: ' + debugModeColor);
    }
    return hash;
}

function getItems() {
    return $gameParty.allItems().filter(function(item) {
        return items.includes(item.id.toString());
    }).map(function(item) {
        return {
            id: item.id,
            quantity: $gameParty.numItems(item)
        };
    });
}

function getWeapons() {
    return $gameParty.weapons().filter(function(weapon) {
        return weapons.includes(weapon.id.toString());
    }).map(function(weapon) {
        return {
            id: weapon.id,
            quantity: 1
        };
    });
}

function getArmors() {
    return $gameParty.armors().filter(function(armor) {
        return armors.includes(armor.id.toString());
    }).map(function(armor) {
        return {
            id: armor.id,
            quantity: 1
        };
    });
}

function getActors() {
    return $gameActors.filter(function(actor) {
        return actors.includes(actor.actorId().toString());
    }).map(function(actor) {
        return {
            id: actor.actorId(),
            params: actor.paramPlus()
        };
    });
}

})();


保存はうまくいきましたが、読込時に以下のエラーが発生しました。

TypeError: this.isSavefileValid is not a function

DataManager.isSavefileValidが定義されていないといったことだと思うので

プラグイン内で再定義しましたが、同じエラーとなりました。

コード: 全て選択

DataManager.isSavefileValid = function(savefileId) {
    if (this.isThisGameFile(savefileId)) {
        var save = this.loadGame(savefileId);
        if (this.isJsonSaveFile(save)) {
            var json = JsonEx.parse(save);
            var integrityHash = json.dataIntegrityHash;
            var hash = generateHash(save);
            return hash === integrityHash;
        } else {
            return false;
        }
    } else {
        return true;
    }
};


何か抜本的に方法を間違えていますでしょうか?

ご指導を賜れましたら幸いです。
アバター
Plasma Dark
記事: 676
登録日時: 2020年2月08日(土) 02:29
連絡を取る:

Re: セーブデータの改竄を防止するプラグイン

投稿記事by Plasma Dark » 2023年2月23日(木) 03:43

概ねロジックは合っているように見えますが、コアスクリプトに存在しない関数を呼び出そうとしていますね。
何を参考にして書かれたのでしょう。

“MV:質問” へ戻る