re-game について
re-gameは、Unity、Unreal、Luaスクリプト、グラフィックシェーダーを対象とするゲーム解析スキルです。エンジンの識別、スクリプトやオブジェクト情報の抽出、メモリー検索、関数解析、シェーダー調査を扱い、アンチチート分析では隔離環境を求めています。
主な機能
- 01UnityとUnrealの成果物を識別
- 02LuaとLuaJITのバイトコードを解析
- 03CEによる値とポインターの検索
- 04対象関数の特定と変更
- 05SPIR-V、DXBC、DXILの調査
活用シーン
- 01ゲーム内の値やポインターを特定
- 02Unity IL2CPPのメソッド情報を復元
- 03Luaスクリプトとバイトコードを調査
- 04シェーダー資源と描画パラメーターを追跡
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dslsdzc/ rev-skills --skill re-gameインストール後、Claude Code で/re-gameを入力して読み込まれたか確認します。
詳細
ゲームのリバースエンジニアリング(Unity / Unreal / Lua スクリプトエンジン / グラフィックシェーダー)
使用する場合 / 使用しない場合
- 使用する:ゲームロジックのリバースエンジニアリング(Unity mono/IL2CPP、Unreal);メモリ検索と変更(CE の値/ポインタースキャン);関数の特定と変更([[re-patching]]);アンチチート(EAC/BE)の検出回避(分析環境内)
- 使用しない:Unity mono のロジックを静的に読むだけの場合(直接 [[re-dotnet]] で
Assembly-CSharp.dllを逆コンパイルすればよく、本スキルは不要です) - 使用しない:モバイルゲーム([[re-mobile]] / [[re-apk]] / [[re-ios]] / [[re-frida]])
- 使用しない:完成済みの改造ツール/チートツールだけが必要な場合——本スキルは分析手順を扱うもので、公開用ツールは作成しません。オンライン対戦でのアンチチートに関するリスクは自己責任です
- 使用しない:Electron アプリの V8 バイトコード .jsc([[re-electron]] を使用してください)
ツールの準備
アンチチート機能のあるゲームの分析では、サンドボックスと隔離を標準とします([[re-analyze/platform-tips]] の最優先原則;[[re-sandbox]] が必須の事前条件です)。動的デバッグは OS に応じて使い分けます。
Cheat Engine(CE、Windows)
- インストール:公式サイト cheatengine.org からダウンロードしてインストール(
choco install cheatengineは任意) - 確認:CE を起動し、任意のプロセスにアタッチして数値スキャンを実行できることを確認します(First Scan で結果が表示されること)
Unity ツール
- Il2CppDumper(IL2CPP の復元):GitHub
Perfare/Il2CppDumperreleases(Windows exe / dotnet tool) - mono の場合:追加ツールは不要です。
Game_Data/Managed/*.dllをそのまま [[re-dotnet]] に渡します(dnSpy/ILSpy) - 確認:Il2CppDumper で対象の
global-metadata.datを処理し、dump.cs(クラス/メソッドのシグネチャ)とscript.json(関数アドレス)が生成されることを確認します
Unreal ツール(UnrealFinder の考え方)
- UnrealFinder(GitHub
povlhp/UnrealFinderまたは同種のDumper-7):GObjects/GNamesなどのグローバルシンボルを特定し、UObject のクラスツリーと関数アドレスをエクスポートします - 確認:対象の UE ゲーム exe から
GWorld/GNamesのアドレスを列挙し、オブジェクトリストをエクスポートできることを確認します
デバッガー(OS に応じて使用)
- Windows:[[re-x64dbg]](軽量なブレークポイントデバッグ)または [[re-windbg]](例外分析)
- Linux:[[re-gdb]](Wine 上で Windows ゲームを実行する場合、Wine プロセスのメモリを読み取ります。[[re-analyze/platform-tips]] の Linux 分岐にある Wine の直接読み取り方法を参照してください)
操作手順
順番に実行し、各手順の記録を保存します(エンジン識別の根拠、アドレスとオフセットの記録、dump 生成物の sha256、[[re-triage]] による証拠保全)。
-
ランタイム/エンジンの識別:
- ディレクトリの特徴:
Game_Data/Managed/Assembly-CSharp.dll→ Unity mono;Game_Data/il2cpp_data/+global-metadata.dat→ Unity IL2CPP - Unreal の特徴:
Engine/Binaries/、*.pak、<ゲーム名>-Win64-Shipping.exe;文字列からGWorld/GNamesを検索します - その他/独自エンジン → 通常の [[re-binary-core]] の手順を使用し、本スキルは使用しません
- 記録:エンジンの種類 + バージョン(後続のツール選定に必要です)
- ディレクトリの特徴:
-
Unity mono と IL2CPP のエクスポート:
- mono:
Game_Data/Managed/*.dllを直接逆コンパイルします([[re-dotnet]]、dnSpy/ILSpy で Assembly-CSharp.dll を開くと、すべての C# ロジックを取得できます。動的分析部分には本スキルは不要です) - IL2CPP(マネージド DLL がない場合):Il2CppDumper でエクスポートします:
Il2CppDumper.exe <Game.exe> <Game_Data>/il2cpp_data/Metadata/global-metadata.dat → dump.cs(类/方法/字段签名) + script.json(方法地址) + il2cpp.h(结构体)dump.csで対象のメソッド名を特定し、script.jsonから対応する関数アドレスを取得して、exe の該当アドレスをデバッグ/patch します([[re-x64dbg]] / [[re-windbg]])- 記録:dump.cs/script.json + 対象メソッドのアドレス一覧
- mono:
-
メモリ検索(CE):
- 数値スキャン:プロセスにアタッチ → 現在の値を入力して
First Scan→ ゲーム内で値を変更 →Next Scanでアドレス一覧を絞り込みます - ポインタースキャン(ASLR によりアドレスが変わる場合):特定済みのアドレスに対して
What accesses this addressを実行 → アクセス命令のオフセットを記録 →Pointer scanでポインターチェーンを生成します(.ptファイルを保存すると、再起動後も使用できます) - 多段ポインター:構造体の基底アドレスは
Game.exe+0x2A3F4C → +0x10 → +0x28のような形式です。オフセットはエンジンオブジェクトの構造によって決まります(Unity では MonoBehaviour のフィールド順、Unreal では UPROPERTY のオフセットを確認します) - 変更の検証:メモリ編集または CE Lua スクリプトで値を固定し、ゲーム内の動作の変化を観察します
- 記録:アドレス式(モジュール+オフセット)、ポインターチェーンのファイル、スキャンパラメーター
- 数値スキャン:プロセスにアタッチ → 現在の値を入力して
-
関数の特定と変更:
- コードとメモリの対応付け:重要なアドレスに対して
Find out what writes to this addressを実行 → ブレークポイントで書き込みコードの位置を特定 → モジュール内オフセットを記録 → 逆コンパイラー([[re-ghidra]] / [[re-binaryninja]])で該当アドレスの関数を分析します - または
dump.cs/script.jsonから IL2CPP 関数アドレスを直接取得 → 逆コンパイル → 変更します([[re-patching]]:ジャンプ/引数/戻り値を変更) - 変更後のテスト:サンドボックス内で実行して機能が有効になったことを確認し([[re-sandbox]])、変更前後の動作の違いを記録します
- コードとメモリの対応付け:重要なアドレスに対して
-
アンチチートへの対処(検出回避、サンドボックス内):
- 注意事項:EAC/BattleEye はデバッガーの存在、メモリ書き換えの痕跡、CE/チートツールの痕跡、複数起動/仮想化の特徴を検出します。分析環境は実際のオンライン環境から完全に隔離します(専用 VM + スナップショット、[[re-sandbox]] + [[re-analyze/platform-tips]] の最優先原則)
- 方法:まず検出箇所を静的に分析し([[re-anti-analysis]] のアンチデバッグ手法)、検出 API(NtQuerySystemInformation など)を hook/回避します。変更は最小限にし(使用後は元に戻します)、痕跡が長時間残らないようにします
- 終了時:分析後にスナップショットを復元します。オンライン対戦での使用はしません
ゲームスクリプトエンジン(Lua / LuaJIT)
ゲームロジックはスクリプト層(キャラクターの動作、スキルの数値、レベル設定など)に実装されていることがよくあります。分析対象がエンジンの C++ 側からスクリプトのバイトコードに移る場合は、まずスクリプト VM と正確なバージョンを特定し、そのバージョンに応じたバイトコードの解析手順に従います。
バイトコードの識別(header シグネチャ/バージョン)
- 標準 Lua バイトコードのヘッダー:
0x1B 4C 75 61(\x1bLua)+ バージョンバイト——5.0=0x50、5.1=0x51、5.2=0x52、5.3=0x53、5.4=0x54;5.3 以降のヘッダーにはフォーマット/チェック用データと int、size_t、命令、lua_Integer、lua_Number の各幅フィールドが含まれ、末尾にはチェック値があります - LuaJIT バイトコードのヘッダー:
0x1B 4C 4A(\x1bLJ)+ バージョンバイト——2.0=0x01、2.1=0x02。その後にエンディアンと各種型の幅を示すフラグが続きます - メモリ上での識別:マジックナンバーをスキャンする([[re-memdump]])、またはインタープリターのバージョン文字列(
Lua 5.x/LuaJIT 2.x)やエラーメッセージ(attempt to call a nil valueなど)の参照からインタープリターを特定します - 記録:VM の種類 + 正確なバージョン + エンディアン/幅のパラメーター(解析手順を決定します)
命令の構造
- Lua 5.1~5.3:32 ビット命令 = opcode(6) + A(8) + B(9) + C(9);Bx は B|C の連結(18 ビット)、sBx は符号付きです
- Lua 5.4:opcode が 7 ビットに拡張され、A は 8 ビット、B/C は 8 ビットに縮小し、新たに k フラグビットが加わります。Bx は 17 ビットです——5.3 以前の解析テーブルは再利用できません
- LuaJIT:32 ビット命令には 2 種類の形式があります——ABC 形式 opcode(8) + A(8) + C(8) + B(8)(メモリ上のリトルエンディアンのバイト順は OP/A/C/B)、AD 形式 A(8) + D(16);Bx フィールドはなく、D フィールドが定数インデックスとジャンプオフセットを担います(ジャンプオフセットはバイアス付きで、D − 0x8000)
- 解析方法:バージョンに応じた命令レイアウトテーブルを使用して、32 ビット語をフィールドに分解します。レイアウトテーブルのバージョンが間違っている場合、ジャンプ先やレジスターインデックスが範囲外になります
- ジャンプオフセットの単位は命令インデックス(バイトではありません)です。基準はバージョンによって多少異なるため、解析時にバージョンを確認してください
定数テーブル/upvalue テーブルの特定
- 関数プロトタイプ(Proto)のシリアライズ順序は固定です:source 文字列 → 行番号/引数/vararg/maxstacksize → コード領域 → 定数テーブル → upvalue テーブル → ネストした Proto → debug 情報。文字列は長さプレフィックス付きでエンコードされ、順に読み進めます
- 定数テーブルの価値:スクリプト内の数値/文字列(スキルの数値、ステージ設定、検証用キー)は直接読み取れるため、完全な意味を復元するよりも素早く調べられます
- メモリ側:ロード/実行 API で Proto のレイアウトを取得するか、既知の定数値の特徴(書式文字列、特定の数値)を使ってヒープ内を検索します
逆アセンブルの考え方(luac 系ツール)
- 「ヘッダー → 命令 → 定数/upvalue → ネストした Proto」の順に段階的に解析し、擬似アセンブリ(
GETGLOBAL/CALL系)を出力します - 既存のパーサーが対象バージョンに対応していない場合は、上記のフィールド順に従って独自のパーサーを作成します(規模は小さめです)。核心となるのは、正しいバージョンの命令レイアウト表です
- 活用方法:定数テーブル内の文字列/数値をクロスリファレンスし、対象関数を探します。命令オペランドや定数値を変更して改変します([[re-patching]] の考え方)
LuaJIT バイトコードの違い
- LuaJIT バイトコードは標準の Lua 5.1 バイトコードと互換性がありません(言語レベルでは互換性がありますが、命令セットが異なります)。LuaJIT 2.0 と 2.1 の間にも互換性はありません(opcode 番号/意味が変更されています)
- バイトコードはランタイムに依存します:エンディアン、ポインター幅、型の幅がすべてファイルヘッダーに記録されるため、異なるプラットフォームでダンプしたファイルを別の環境に移すと解析に失敗します
- 対策:
\x1bLJ+ バージョンバイトを厳密に確認して LuaJIT の解析経路を選びます。変更はソース/バイトコードのレベルで行い、バージョンをまたいだ移植は避けます
Cocos 系スクリプトリソースの抽出(.jsc / .luac の難読化バリアント)
- Cocos2d-x Lua の場合:
.luacは標準の Lua/LuaJIT バイトコードです。よくある難読化バリアントはヘッダーの切り詰め(\x1bLuaマジックの削除/変更)と、ファイル全体への 1 バイト XOR です。暗号化バリアント(XXTEA 方式。暗号文にxxteaの開始マーカーが含まれる場合は、バイトコードの解析経路に進む前に復号します) - Cocos Creator の場合:
.jscは JS エンジンのバイトコードです。同様に、ヘッダーの切り詰め/XOR バリアントがよく見られます - 対策(識別してから復号):既知のマジックを crib として復元します——マスク = マジックの対応バイト XOR ファイル先頭のバイト。マジックを復元してからバージョンに応じて解析します。切り詰められている場合は、先にヘッダーバイトを補います。この方法は [[re-fw-extract]] の XOR の章と同じ考え方です(ブロック先頭のマスク、ブロック単位の周期変換の考え方をそのまま再利用できます)
スクリプト関数の hook([[re-frida]]/メモリ側)
- ロード側:
luaL_loadbuffer/luaL_loadbufferx/luaL_loadfile/lua_load——hook で元のバイトコード/ソースを取得します。スクリプトのロジックの変更(バイトコードの置き換え、定数の変更)に適しています - 実行側:
lua_pcall/lua_pcallk/lua_call——hook で引数/戻り値を取得します。実行時の動作の変更に適しています。LuaJIT には、C API のラッパーを経由しないアセンブリの呼び出し経路もあります - メモリ側:hook を使わずに、ロード済みバイトコードを直接 patch します(定数値/ジャンプの変更)。またはリソースファイルを変更してロード経路を置き換えます
- シンボルがない場合:バージョン文字列/エラーメッセージの参照を使ってインタープリター関数のアドレスを特定します
よくある落とし穴
- Lua バージョン間でバイトコードに互換性がない:症状——ダンプしたバイトコードを別のツール/ランタイムで解析すると文字化けする、または失敗する。原因——Lua 5.1~5.4 と LuaJIT では命令レイアウト、ヘッダーフィールド、検証データがすべて異なり、同じバージョンでもエンディアン/幅に違いがあります。5.4 では opcode のビット数まで変更されています。対策——まずマジック + バージョンバイトでバージョンとエンディアン/幅を正確に判定してから、対応する解析経路を選びます。バージョンをまたぐ場合は、同じバージョンのソース/インタープリターを探して再コンパイルして比較し、無理に解析しないでください
- .jsc/.luac バリアントでマジックが切り詰められている:症状——ファイルヘッダーが
\x1bLua/\x1bLJではなく、バイトコードとして直接解析すると失敗します。原因——難読化バリアントではマジックが切り詰められるか変更されています(ヘッダーの切り詰め、ファイル全体への 1 バイト XOR)。対策——識別してから復号します:既知のマジックを crib としてマスク(マスク = マジックのバイト XOR ファイル内の対応位置のバイト)を復元するか、切り詰められたヘッダーを補い、マジックを復元してからバージョンに応じて解析します。XXTEA 系の暗号化は、暗号化の特徴に基づいて復号してからバイトコードの解析経路に進みます - hook ポイントの選択:lua_pcall だけが入口ではない:症状——
lua_pcallだけを hook すると、一部のスクリプト動作が実行されない、または重複して実行されます。ロード系 API を hook すると対象スクリプトを取得できません。原因——インタープリターには複数の呼び出し面があります(lua_call/lua_pcallk/ロード系 API はそれぞれ別の面です)。LuaJIT には C API を経由しないアセンブリの呼び出し経路もあります。対策——まず目的を決めます:スクリプトのロジックを変更する場合はロード側を hook してバイトコードを取得し、実行時の動作を変更する場合は実行側を hook するかメモリを patch します。hook できない場合は、バージョン文字列/エラーメッセージの参照を使ってインタープリター内部の呼び出し箇所を特定します - ジャンプ先の換算ミス:症状——逆アセンブルしたジャンプ先アドレスが不自然、または範囲外になります。原因——Lua バイトコードのジャンプオフセットは命令インデックスを単位とし(バイト単位ではありません)、Bx のビット幅と基準位置はバージョンによって異なります(5.1~5.3 では現在の命令インデックス + 1 + sBx)。対策——バージョンに応じてビット幅と基準位置を確認し、換算後にジャンプ先がコード領域内にあることを確認します
グラフィックスと Shader
グラフィックス分野でのリバースエンジニアリングの主な目的は、視覚効果(色/パラメーター/カリング)の変更、リソースのバインド箇所と定数の更新箇所の特定、レンダリングパイプラインのロジックの復元です。入口は、ファイル側(shader リソース)とメモリ側(ランタイムのロード箇所)に分かれます。
SPIR-V の逆アセンブル(spirv-dis 系の考え方)
- マジックは 0x07230203(ファイルのバイト順では 03 02 23 07)です。word ストリームの構造は、ヘッダー(マジック/バージョン/生成器/bound/schema)+ 命令列(各命令:word 数 + opcode + オペランド)です
- 逆アセンブルの考え方:word 境界ごとに命令ストリームを分割し、OpFunction/OpEntryPoint/OpConstant/OpType*/OpDecorate などの意味を復元します
- 特定のポイント:OpEntryPoint で入口(stage の種類 + 入口関数)を特定します。OpDecorate の DescriptorSet/Binding/Location でリソースバインドと頂点属性を特定します。OpConstant で定数の初期値を特定します(直接変更可能です)
- 汎用化:バイナリ IR の解析手法(マジック → 構造ヘッダー → 命令ストリーム → 意味テーブル)は、複数の GPU IR で共通して使えます。形式が変わっても、マジックと命令テーブルを変えるだけです
DXIL / DXBC の逆アセンブル(dxc 系の考え方)
- DXBC:DX10/11 のコンテナ形式です("DXBC" マジック + chunk テーブル)。シェーダーコードは SHDR/SHEX chunk にあり、入力/出力シグネチャとリソースリフレクションは ISGN/OSGN/RDEF などの chunk にあります
- DXIL:DX12 SM6+ のバイトコードです。外側は引き続き DXBC コンテナですが、内側の "DXIL" サブオブジェクトは LLVM bitcode 形式です——解析経路は DXBC とまったく異なります
- 逆アセンブルの考え方:まずコンテナ/サブオブジェクトの種類(DXBC chunk か DXIL か)を判定し、それぞれの形式に応じて解析します。SM バージョン(命令セットを決定します)と cbuffer リフレクション(名前/メンバーオフセット)に注目します
- メモリ側:"DXBC"/"DXIL" マジックをスキャンしてコンパイル済みシェーダーを特定する([[re-memdump]])か、作成系 API を hook してバイト列を取得します
GLSL の復元(spirv-cross 系の考え方、汎用化)
- 復元経路:IR → ターゲット言語(GLSL/HLSL/MSL)。型の再構築(OpType* → 言語の型)→ 意味の復元(OpDecorate → layout/修飾子)→ 式の再構築、という流れです
- 復元は不可逆です:最適化後のループ/switch/構造の再構築は不完全です。復元結果は閲覧や分析に使うもので、必ずしも元どおりに再コンパイルできるとは限りません
- 汎用化:IR→ソース復元器は「型 → 宣言 → 関数本体」の 3 パスで構成し、ターゲット言語が変わる場合は構文出力レイヤーだけを変えます
レンダリングの入口と定数テーブルの特定
- ファイル側:shader リソース(.spv/.dxbc/.hlsl/.cso またはパッケージ内リソース)を先に展開し([[re-fw-extract]])、形式に応じて逆アセンブルします。バイナリ内のロード API 文字列参照を使ってロード箇所を特定します(glShaderSource/glCompileShader、CreateVertexShader/CreatePixelShader/CreateComputeShader、vkCreateShaderModule 系)
- メモリ側:ロード系 API を hook して元のバイト列(ソースまたはコンパイル済みバイトコード)を取得するか、マジックをスキャンしてダンプします([[re-memdump]])
- 定数テーブルの特定:CPU 側の更新箇所——glUniform* 系(単一値の uniform は location インデックスで指定)、VSSetConstantBuffers/UpdateSubresource 系、Vulkan の vkCmdPushConstants/descriptor 更新系。GPU 側のレイアウト——cbuffer は構造体メンバーのオフセット + 16 バイトアラインメントでパックされます(float4 配列の間には padding が入ります)。単一値の uniform にはこの規則はありません
- パラメーター変更の入口:定数の更新箇所で実行時に値を変更する、バイトコード内の定数初期値を静的に patch する、または定数テーブルのデータを変更します
常见坑
- DXIL vs DXBC 判断错(SM6 vs SM5 路径不同):现象——按 DXBC 的 SHDR/SHEX 解析失败,或按 DXIL 路径反汇编乱码;原因——SM5.x 及以下是 DXBC 字节码,SM6+ 是 DXIL(LLVM bitcode 风格),解析路径完全不同,且 DXIL 外层容器同样是 "DXBC" 魔数,只看容器名会误判;对策——先确认 SM 版本(反射/RDEF 或编译配置),SM6+ 在容器内找 "DXIL" 子对象走 DXIL 路径,SM5- 走 DXBC chunk 路径,两条路径互不通用
- SPIR-V 无函数名(按入口点/资源绑定定位):现象——反汇编结果全是 OpFunction/OpLabel 编号,找不到目标逻辑;原因——发布版通常剥离 OpName/OpMemberName 调试信息;对策——按 OpEntryPoint 定入口(stage 类型 + 函数引用),按 OpDecorate 的 Binding/Set/Location 定资源,按 OpConstant 已知值特征(矩阵初值/阈值常量)定位目标片段
- 着色器常量表 vs 统一缓冲区混淆:现象——CPU 侧找到的 uniform 更新点和 GPU 侧反汇编的常量布局对不上;原因——同段逻辑可能走两套路径: 单值 uniform(按 location 序号直传)与统一/常量缓冲区(结构体成员偏移 + 16 字节对齐打包,含 padding);对策——先由反汇编/反射确认路径(SPIR-V 看 OpDecorate Uniform/Block,DXBC 看 RDEF 的 cbuffer 成员表),单值按 location 号、cbuffer 按成员偏移换算,两套打包规则不混用
跨域联合
- [[re-binary-core]]:游戏 exe 静态分析底座([[re-format-pe]] 等)
- [[re-dotnet]]:Unity mono 托管逻辑反编译(本技能静态侧的主要工具)
- [[re-patching]]:函数修改落地(跳转/字节级 patch)
- [[re-anti-analysis]]:反作弊检测对抗(反调试思路复用)
- [[re-sandbox]]:反作弊游戏分析强制隔离([[re-analyze/platform-tips]] 最高原则)
- [[re-mobile]]:移动端游戏(Unity/Unreal 移动版)转 [[re-apk]] / [[re-ios]] / [[re-frida]]
- [[re-cracking]]:单机游戏授权/激活校验绕过
- [[re-console]]:现代主机(Switch/PS4/PS5)与复古平台(ROM 头/卡带格式/Cheat 码)游戏逆向
常见坑与陷阱
- IL2CPP 无托管 DLL 还去找 DLL:现象——
Game_Data/Managed/下只有少量/无 DLL,反编译找不到目标逻辑;原因——IL2CPP 把 C# 编成原生二进制,托管 DLL 不存在;对策——直接用 Il2CppDumper(版本须支持目标 Unity 版本,太旧解析global-metadata.dat会失败),从 dump.cs 走函数地址 - 指针链扫描繁琐且易失效:现象——数值扫描出的地址每次启动都变,直接改无效;原因——游戏使用动态内存分配/ASLR,基址每次不同;对策——CE
Pointer scan保存.pt指针链,或以模块基址+偏移(Game.exe+0x...)定位,启动后重算基址再偏移;多层偏移逐层确认 - 反作弊(EAC/BE)强检测:现象——attach 被拒、调试器被检测、账号被封;原因——反作弊驱动级监控调试器与内存痕迹;对策——分析环境完全隔离(专用 VM/离线,[[re-sandbox]] + [[re-analyze/platform-tips]] 最高原则),检测点静态分析后绕过([[re-anti-analysis]]),绝不在在线环境挂调试器
- 游戏更新频繁补丁失效:现象——更新后原地址全失效、patch 不生效;原因——每次更新重编译,地址/偏移变化;对策——patch 记录用「模块+偏移+字节模式」而非绝对地址,更新后用新旧 exe diff 定位变化点,或按字符串/函数名(IL2CPP dump)重新定位
- 反作弊检测到虚拟化/多开:现象——VM 里游戏直接退出或行为异常;原因——部分反作弊检测 VBS/VMP 特征与虚拟化环境;对策——按反作弊已知规避清单调整 VM 配置(隐藏 hypervisor 特征选项),仍不行就静态分析为主,动态部分转到无反作弊的旧版本/离线模式
- UE5 Dx12 绘制 hook(虚表替换):现象——Dx11 的 Present hook(抄 ImGui)在 UE5 Dx12 上失效,Present 断下后堆栈里找不到主模块代码;原因——UE5 自带单独编译的
D3D12Core.dll(Release 版叫 D3D12Core.dll、Debug 版叫 d3d12SDKLayers.dll,不用系统目录 dll,特征码在系统 dll 里搜不到),且开启 GPU 加速/超分辨率时 Present 上层堆栈被其他 dll 占据;对策——用开源 UE5 源码自己编译一份 D3D12Core.dll 拖 IDA 做参照(ExecuteCommandLists在 CommandQueue 虚表 index=10);Present 断下后直接定位主模块下的返回地址(超分辨率按 dll 名识别,换模式换 dll 名思路不变);虚表替换点:mov rax,[rcx]调用处取 rcx(对象指针)替换虚表 - 游戏内 5 字节 hook 检测(反 hook):现象——定位到的目标函数头部已被改(经典 5 字节 hook 桩),且游戏自身调用能过、外部 hook 调用失败;原因——游戏/引擎自带调用来源检测(查堆栈返回),非自身调用会还原头部 5 字节;对策——不 hook 被保护函数本体,改在调用它的位置(堆栈返回处)hook 取对象指针(rcx/r12 等)替换虚表达成同等效果;识别特征:函数头 5 字节被改 + 函数内部有还原头部逻辑(FName::ToString/StaticFindObject 等关键函数多处同样处理)
- IL2CPP 元数据还原:现象——dump 工具报版本不支持/输出不全;原因——Unity 版本改 metadata 格式;对策——升级 dump 工具(Il2CppDumper 或 Il2CppInspectorRedux),必须用同一次 dump 的产物对(script.json 与 so 匹配,换 IDA 清缓存)
- 加密的 global-metadata.dat:现象——元数据文件是密文;原因——反作弊/自定义加密;对策——找初始化解密函数(il2cpp_init 周围),Frida 在 mmap/read 后 dump 内存中已解密的元数据再喂给 dump 工具
- IL2CPP 方法 hook:现象——Frida 裸 hook 报错;原因——IL2CPP 方法非标准 Java/ObjC,需按 metadata 算偏移;对策——用 frida-il2cpp-bridge 库,不硬写 Interceptor.attach
- patch 后闪退:现象——静态修改后启动崩溃;原因——文件 hash 校验/anti-tamper;对策——hook 优先(不改文件);必须静态 patch 时同步处理校验逻辑;重打包后删 META-INF 重新签名(apksigner) (来源:reverse-skill field-journal,MIT)
- IL2CPP 双文件结构配对(metadata 骨架 + 二进制血肉):现象——只有
global-metadata.dat或只拿二进制,dump 结果缺方法实现/字段偏移/类型图,两份文件版本错位时解析错乱;原因——IL2CPP 产物分两半:metadata(magic 0xFAB11BAF)只存声明型骨架(类型/方法/字段定义、参数默认值、字符串字面量、泛型容器、接口偏移、vtable 方法索引),二进制(GameAssembly.dll / libil2cpp.so 等)存血肉(Il2CppType 类型图、字段偏移表、方法指针表、泛型实例表),类型引用跨两侧互链,任一缺失或版本错位都无法完整还原;且信息归属随版本漂移(v27 移除 metadata usage、新版本又把 method specs 等迁回 metadata——具体迁移版本以实测为准,别按记忆中的版本号写死);对策——两份文件同版本配对使用;信息归属按版本分叉处理;二进制侧解析只依赖「虚拟地址→文件偏移映射 + 指针大小」两个原语,PE/ELF/Mach-O/WASM/NSO 统一抽象 (来源:Cpp2IL(SamboyCoding),MIT) - metadata 版本判定要靠 version 号 + Unity 版本联合裁决:现象——dump 工具对同一 version 号在不同游戏上时好时坏,结构解析错位(字段读歪、索引越界);原因——头部 version 字段是粗粒度信号,同一版本号对应多代结构(24 细分为 24.1~24.5、27 分 27.1/27.2 等),必须用 Unity 版本号二次裁决才能正确读结构;不同产品线还可能同号不同义(换皮陷阱),需按版本走分支;Unity 6 起(v38+)类型/方法/字段索引不再固定 4 字节,而是 8/16/32 位动态宽度,宽度从 section 的 Size/Count 反推,类型索引宽度甚至要从二进制侧 interfaceOffsets 结构推导(类型表不在 metadata 里);对策——version 字段 + Unity 版本联合裁决,按「引入变更的 Unity 版本」建版本映射表并记录每代变更点(字段增删、位宽变化、结构移动),hotfix 版本号有时是关键分界;索引变宽后先反推元素宽度再读表,不写死 4 字节 (来源:Cpp2IL(SamboyCoding),MIT)
- 注册结构定位:计数模式搜索 + 候选仲裁:现象——在二进制里搜 MetadataRegistration/CodeRegistration 结构,假命中一堆,按第一个解析全是垃圾;原因——这两个注册结构(MetadataRegistration:类型/字段偏移/metadata usage 的指针与计数表;CodeRegistration:方法指针表、泛型方法指针表、codegen module 指针表)是 dump 的第一步,没有名字可依,只能以 metadata 已知计数(方法数/类型定义数)为字节模式全文件搜索,假命中极多;对策——命中处按结构体解析后用验证规则集排除假命中:count 字段不超上限(方法数/类型数十万量级)、指针字段必须能映射回文件内、关键计数与 metadata 侧交叉一致(fieldOffsetsCount == 类型定义数、numTypes 不小于类型定义数);上限阈值随样本库持续更新(真实样本把某计数推高到数十万量级);v24.2+ 可走 xref 引用链(mscorlib 字符串 → 指向它的 codegen module 指针 → 模块列表 → 回退到 CodeRegistration 基址);全部失败时降级人工输入地址 (来源:Cpp2IL(SamboyCoding),MIT)
- metadata 被篡改/加密的静态对抗:现象——metadata 解析失败(magic 不符、section 数对不上、布局乱),只想到运行时 dump 一条路;原因——反作弊/自定义保护可能加密或打乱 metadata 头部;对策——先定 Unity 版本再解析,版本来源有优先级与互验(globalgamemanagers 优先 → data.unity3d 包文件 → 主程序版本,出现可疑值自动换备用来源重试);加密密钥用「已知明文特征」反推而非爆破——头部 offset/length 字段高字节必为 0(文件小于 32MB)、字符串区以两个 0 字节开头、头部字段是顺序字节流(XOR 后相邻字节差固定),约束收敛后逐段校验确认;布局被打乱时把头部所有候选 offset/length 当池子 DFS 拼接重建 section 链(偏移与当前位置差在合理区间、长度使位置推进且不越界、对齐约束);解析失败可挂 fixup 回调(原始字节 + Unity 版本 → 返回修复后字节重读),这是对抗加密/混淆 metadata 的挂载点 (来源:Cpp2IL(SamboyCoding),MIT)
- AOT 泛型实例不完整 + 共享实现:现象——反编译里某些泛型方法没有实现或地址是占位值,不同泛型调用点还原出同一实现、类型对不上;原因——AOT 只生成游戏实际用到的泛型实例,未用实例指针为占位值;值类型参数与引用类型参数按尺寸分组共享实现(List<某引用类型> 与 List 共享同一实现指针,List 单独实现);编译器去重/共享方法体使「地址→方法」多对一;对策——从调用地址反查 ConcreteGenericImplementationsByAddress 列出该地址对应的全部泛型实例,这是还原泛型调用的关键通道;共享体还原用保守仲裁(仅当恰好一个非静态候选的声明类型匹配接收者类型才定案,宁可不解不猜);共享泛型体调用时额外隐藏传 MethodInfo* 尾参,还原时注意别把隐藏参数当普通参数 (来源:Cpp2IL(SamboyCoding),MIT)