アーキテクチャ
RenMori はビルド時ツールとネイティブのランタイムコアで構成されます。ビルド済みの Ren'Py 8.x(Python 3)の配布用ビルドを入力として受け取り、ゲームのソースを一切変更せずに保護済みの配布用ビルドを生成します。
ネイティブランチャーは 4 つの層のうちの 1 つにすぎません。実際の保護を担うのは、残る 3 つの層(アセット、スクリプト、エンジン)です。
全体の第一原則:パフォーマンスを優先する
ランタイムのパフォーマンスと衝突する保護手段は、削除するか弱めます。その逆、つまりパフォーマンスを犠牲にすることはありません。具体的には次のとおりです。
- デフォルトでは重量級のパッカーや VM プロテクターを使用しない(VMProtect、Themida など):起動を遅くし、Proton/Wine(Steam Deck と Linux)の下で動作しなくなり、ウイルス対策ソフトの誤検知を招き、macOS の公証を妨げるため
- ホットパスではアンチデバッグや自己検査を行わない:完全性の検査は起動時に 1 回だけ実行する
- 対称暗号かつハードウェアアクセラレーションに対応した暗号のみを使用する(AES、AES-NI):復号はネイティブ(Rust または C)で実装し、Python のループでは決して行わない
- 保護の主軸は多態化が担う:多態化はランタイムのコストがゼロのビルド時の変換であり、戦略は「実行時に防御する」ことではなく「一括では破れない」ことにある
- 受け入れ基準:保護済みビルドは、フレーム時間、シーンの切り替え、CG と動画の再生において保護されていないビルドと区別がつかないこと。ある手段がこれを満たせない場合、取り除くのはその手段であり、パフォーマンスではない
4 つの層
1. アセット(フォントを含むすべてのファイル)
game/以下のすべてのファイル(画像、音声、動画、フォント、.rpyc、tl/、JSON など)を独自の暗号化コンテナに格納し、RPA アーカイブ形式を完全に置き換える。各ファイルはそれぞれ固有の鍵と完全性タグを持つ- 実行時にはネイティブモジュールが
renpy.loaderのファイルオープンとインデックスのエントリポイントを引き継ぎ、復号済みでストリーミング可能なファイルオブジェクトを返す。フォント(FreeType)、動画(ffmpeg)、音声はいずれもローダーを経由して読み込まれるため、単一のチョークポイントですべてをカバーできる- 実装上の課題:すべてのリソース種別の読み込み経路を監査して
renpy.loaderを経由していることを確認し、経由しない経路があればフックを追加する
- 実装上の課題:すべてのリソース種別の読み込み経路を監査して
- オフセット指定によるストリーミングかつランダムアクセス可能な復号を行い、動画や音声を丸ごとメモリ上で復号することはない。そのため、コンテナ形式はオフセットでアドレス指定できる必要がある
- 復号結果は Ren'Py の既存のキャッシュ(テクスチャ、音声)を再利用するため、同じ画像が 2 回復号されることはない
2. スクリプト(ソースの難読化)
- 出力は標準の
.rpycではない:独自の AST シリアライズを用い、クラス名のマッピング、フィールドの順序、構造のレイアウトをビルドごとにランダム化するため、偽のrenpyモジュールに依存する pickle ベースのデコンパイラには、結び付けられる安定した対象が存在しない - Ren'Py スクリプトに埋め込まれた Python(
init python、$)はビルド時にコンパイルして暗号化し、実行時には実行のためにのみ復号する。デバッグ用のメタデータ(ソースファイル名、行番号)は任意で除去できる - 台詞と文字列は暗号化して格納し、使用時にのみ復号する
3. エンジン
renpy/のエンジンコードと Python 標準ライブラリをパックして暗号化し、改ざん対策として起動時に完全性の検査を行う- ホワイトリスト方式の読み込み:ビルド時に署名されたスクリプトとモジュールのみを読み込み、ゲームディレクトリに置かれた紛れ込みの
.rpyや.rpycファイルは拒否する。これによりun.rpyc型の注入式デコンパイラを阻止する - 配布用ビルドでは、開発者コンソール、開発者モード、データをエクスポートする CLI サブコマンド(compile、lint、台詞のエクスポートなど)を無効化する
4. ネイティブランチャー(シェル)
- Ren'Py 標準のランチャーをプラットフォームごとのネイティブランチャーに置き換える。このランチャーは組み込みの Python を起動し、暗号化コンテナからエンジンを読み込み、実行時に鍵を導出するため、ディスク上に平文の鍵は残らない
- Rust または C で記述し、ランチャーと CPython 拡張モジュールの両方としてコンパイルするため、中核のロジックは Python ではなくネイティブコードに置かれる
ビルドごとの多態化(中核となる価値)
ビルドのたびにコンテナ形式のパラメータ、AST のエンコードマップ、鍵導出の経路をランダム化するため、汎用的なアンパッカーは成立しません。これはビルド時の変換であり、ランタイムのコストは一切増えません。
オープンコアの分割
分割の原則は、公開されているどの構成要素も、たとえ完全に理解されたとしても、すべての RenMori ゲームに通用する単一のアンパッカーを誰かが書けるようにしてはならないということです。
オープン部分(本リポジトリ)
- CLI の骨格、設定の解析、CI との連携、GitHub Actions のサンプル
renpy.loaderのフックフレームワークとランタイム SDK:ローダーを引き継ぎ、復号したストリームをエンジンに返す仕組みの部分。これをオープンソースとして公開すること自体が 1 つの特長であり、開発者はバックドアや外部への通信が存在しないことを確認でき、互換性の問題を自分でデバッグできる- ベースラインコンテナ形式 v1 の仕様:固定かつ公開された中程度の強度の形式(コンテナ形式を参照)
- 標準的な暗号プリミティブの使い方:AES は公開された標準であり、その使い方を公開しても何も失われない
- ドキュメント、サンプル、テスト
オープン部分は単独でも利用でき、その場合はベースラインの強度(固定された予測可能な形式)となります。
クローズド部分(プリコンパイル済みのバイナリプラグイン、決定事項)
- 多態化/変異エンジン:ビルドごとにコンテナのレイアウト、フィールドの順序、AST のクラスマッピングをどうランダム化するかを決める部分。ビルド時にのみ動作し、プレイヤーには決して配布されない。これが漏洩すると変換空間全体が明らかになり、攻撃者が汎用的なソルバーを書けるようになるため、クローズドである必要がある
- 鍵導出の難読化:鍵は最終的にはクライアント上で導出可能だが、鍵をどのように導出し隠すかが、リバースエンジニアリングのコストを引き上げる
- ネイティブの改ざん対策と鍵復元のランタイム:ソースを伴わないプリコンパイル済みバイナリとして提供する。ゲームと共に配布されるため最終的にはリバースエンジニアリング可能であり、クローズドにすることはハードルを上げるにすぎない
提供方式: オープンな CLI がビルド時にクローズドのプリコンパイル済みバイナリを呼び出します。この方式は CI と相性がよく、オフラインで動作し、無償での配布にも適しています。両者は安定した、文書化されたプロセスインターフェースを介して通信します(stdin/stdout を用いる CLI 呼び出し、またはローカル IPC。入力はビルド成果物のパスと設定、出力は保護済みの成果物とマニフェスト)。インターフェースは公開され、実装は公開されません。バイナリはプラットフォームごとに 1 つです。オープン部分とクローズド部分のインターフェースを参照してください。
任意の機能として、ランタイムが公式のクローズドバイナリによって署名されたビルドフィンガープリントのみを受け入れるようにでき、独自に書かれた弱い多態化層が公式になりすますことを防げます。
リバースエンジニアリング対策:誠実なモデル
前提として、プレイヤーに配布され、ローカルで復号しなければならないバイナリを破られないようにすることはできません。これがホワイトボックスの問題です。目標は、コストを引き上げ、解析結果を他に流用できないようにし、方式をローテーションして古い解析結果を陳腐化させることです。成功とは、汎用的なアンパッカーが存在せず、ゲームごとに専門家による個別の取り組みが必要になることを指します。詳しい論拠は脅威モデルにあります。
2 つのクローズドな成果物は、異なる脅威モデルに直面します。
- ビルド時のバイナリ(多態化と鍵導出):開発用マシンや CI マシン上でのみ動作し、プレイヤーには決して配布されないため、保護ははるかに容易
- ランタイムコア(配布されるすべてのゲームに含まれる復号と改ざん対策のモジュール):攻撃者のマシン上で動作し、すべてを復号できなければならないため、完全にホワイトボックスの状態にある
ランタイムコアの堅牢化策を、効果の大きい順に示します(いずれもパフォーマンス優先の原則の範囲内です)。
- すべてネイティブで実装し、Python は使用しない:鍵のロジックを含む
.pyや.pycがあれば、それだけで実装が露呈する - 多態化によって解析結果を流用できなくする(最も重要):1 つのゲームを完全にリバースエンジニアリングしても、得られるのはそのゲームのそのバージョン専用のツールにすぎない
- 鍵は決して静的な定数にしない:鍵は実行時に複数の要素(コードセグメントの自己ハッシュ、分散した定数、計算で求める値)から導出されるため、1 回のメモリダンプではそのまま使える鍵は得られない。真のホワイトボックス暗号は難しく、学術的な構成も破られてきたため、この点について過大な約束はしない
- 配布用ビルドではシンボルを除去し、デバッグ情報を含めない
- ネイティブの難読化:obfuscator-LLVM(クロスプラットフォーム)による制御フローの平坦化、不透明述語、命令置換。コンパイル時のみに行い、ランタイムのパフォーマンスを損なうものは一切含めない
- 自己完全性を復号に結び付ける:バイナリやエンジンにパッチが当てられると、鍵導出は明確なエラーではなく無意味な値を生成するため、パッチの問題の切り分けが難しくなる(起動時のみ)
- アンチデバッグ:最小限とし、デフォルトでは無効。いたちごっこであり、誤検知がウイルス対策ソフトを反応させ、パフォーマンスも消費するため
- チャンク単位の遅延復号と即時解放:CG や動画の全体を平文のままメモリ上に保持することは決してない。これはメモリからの抽出のコストを引き上げるが、抽出を防ぐものではない
互換性とパフォーマンスに関する結論(パフォーマンス優先の原則によりすでに決定済み)は、ネイティブコード、軽量な obfuscator-LLVM、シンボルの除去、多態化です。重量級の商用パッカーは Windows 専用かつデフォルトで無効のオプションとして残し、知覚できるパフォーマンスへの影響がないことが計測で示された場合にのみ有効にします。
プラットフォームとの統合
Steam
- Steam オーバーレイによる注入を許可する:オーバーレイはゲームのプロセスに注入されるため、積極的な改ざん対策や注入対策はこれを阻止し、ストアとの統合を壊してしまう。パフォーマンス優先の原則によって改ざん対策はすでに最小限に抑えられており、この注入は明示的に許可する
- Steam DRM による二重のラップを行わない:Steam DRM は実行ファイルをラップして書き換えるため、起動時の自己検査と衝突したり、自己検査を作動させたりするおそれがある。そのため互換性をテストするか、開発者に Steam DRM を使用しないよう勧める。Steam DRM はいずれにせよ保護として弱い
- 実績と Steam クラウドのセーブデータは Ren'Py の Python API を経由し、この API には手を加えない。
libsteam_apiとsteam_appid.txtは引き続き読み込み可能で、暗号化コンテナは静的なファイルであるため、Steam のファイル検証とアップデートも通常どおり機能する
リリースの流れについては Steam へのリリースを参照してください。
macOS の署名と公証(開発者が公証を行わない前提)
- 設計上の制約:復号するのはデータのみであり、復号したコードを生成したり実行したりすることは決してない。そのため Hardened Runtime のエンタイトルメントは不要で、保護のために開発者が有料の署名を強いられることもない。署名のない配布用ビルドは、署名のない一般的な Ren'Py の macOS 版ゲームと同じように動作する(Gatekeeper に対して右クリックから「開く」で起動)
- Apple Silicon では実行に少なくとも ad-hoc 署名が必要(
codesign -s -、無料で開発者アカウントも不要):ビルドツールは、生成したランチャーと dylib にデフォルトで ad-hoc 署名を行う - 開発者が Developer ID を持ち公証を希望する場合でも、バイナリは再署名が可能で Hardened Runtime とも互換性があり、その手順は文書化される
ライセンス
- すべて(フレームワークとツール、クローズドプラグイン、ゲームと共に配布されるランタイム)が RenMori License(LICENSE(英語)、中国語版:LICENSE.zh-CN.md)の下で提供される。商用作品を含むあらゆる Ren'Py ゲームの保護に無料で使用でき、ランタイムはリリースするゲームに組み込んだ形で無償で再配布できるため、開発者は保護済みのゲームを合法的に販売できる
- 禁止事項:RenMori 本体やその改変版の販売、および RenMori を用いた有料の暗号化サービスの運営。転売禁止の条項は寛容型ライセンスでは表現できないため、これは OSI 承認のオープンソースライセンスではなく、ソース公開型(source-available)のライセンスである
- コントリビューションには CLA への同意が必要。これにより、寄稿されたコードを単一の一貫したライセンスの下でフレームワークとクローズドプラグインの両方に使用できる
- サードパーティの告知は NOTICE(英語)に記載。ffmpeg(LGPL/GPL)がコンプライアンス上の主なリスクであり、ほかに AES ライブラリと Ren'Py 自体のライセンスが関係する
- セキュリティに関する免責:破られない保護は存在しないことを明記し、ゲームが解析された場合の責任を限定する(免責事項を参照)
- 輸出と暗号に関する告知:強力な暗号を含むソフトウェアは輸出規制の対象となる場合がある
- プライバシーに関する声明:テレメトリーは収集しない(または収集する内容を誠実に列挙する)ことで、ローダーフックが外部と通信しているのではないかという疑念を払拭する
マイルストーン
- M0:本設計ドキュメント
- M1:アセット層の概念実証(Windows):暗号化コンテナとローダーの引き継ぎ。画像、音声、動画、フォントが読み込まれ、
unrpa、rpatool、RenPack、RenpyExがいずれも失敗し、CG と動画を多用したサンプルで保護あり・なしのパフォーマンスベンチマークに合格(M1 の成果:アセット層を参照) - M2:スクリプト層とエンジン層:独自の AST 形式、署名付きホワイトリストによる読み込み、リリース時のロックダウン、注入されたスクリプトの拒否(スクリプト層を参照)
- M3:単一コマンドの CLI と CI:標準的な Ren'Py のビルド成果物を入力とし、設定ファイル、終了コード、ビルドマニフェスト、GitHub Actions のサンプルを提供(CI との連携を参照)
- M4:ビルドごとの多態化、macOS(自動の ad-hoc 署名、任意の公証)と Linux/Steam Deck のランチャー、Steam オーバーレイ、Steamworks、Proton/Steam Deck の検証(M4 を参照)
検証基準
- 機能:保護済みのサンプルゲームが 3 つのプラットフォームすべてで動作してクリアまで進められ、すべてのアセットとすべてのシナリオ内容が正しいこと
- 保護:
unrpa、rpatool、RenPack、RenpyEx、unrpyc(--try-harderとun.rpycによる注入を含む)がいずれも失敗し、フォントを含め、いかなる時点でもディスク上に平文のファイルが存在しないこと - パフォーマンス:CG と動画を多用したサンプルが、保護されていないビルドに対して受け入れ基準を満たすこと
- 使いやすさと CI:クリーンな CI ランナー上で 1 つのコマンドで実行でき、終了コードが正しく、サンプルのワークフローが成功すること