Unity3Dユニバーサルダンプ/インジェクター(Mono、Android)

画像






ご挨拶!



少し前に、私はアンドロイド用のゲームの研究に興味を持つようになりました。 結局のところ、非常に多くの開発者がUnity3Dを使用しています(おそらく、私が興味を持っていたゲームの50〜60%は、おそらくこのエンジンに基づいています)。 私はすぐに予約します-私はハッキングの専門家ではありませんし、C ++ / asmも知りません(このトピックに少し精通していますが)。グラビア銃の助けを借りて便器を投げないでください。 また、少し説明します。「青に変わるまでストーリーダンジョンを盗み、さらにセミオフラインでアリーナで戦う」というスタイルのMMO /セミオンラインゲームのみを勉強しました。 Unity3Dのオフラインゲームは、単純に探索するのが退屈です。



実際、私が知る限り、Unity3Dのおもちゃは2つのテクノロジーを使用しています。MonoとIl2cppです。

この資料では、.NET dll'okを置き換えるプロセスを検討し、同じdll'okの暗号化されたバージョンでもゲームから直接ダンプします。



私は自分が使用しているもののコンテキストで技術スタックを説明するので、windows / node.js向けに開発しています。



したがって、次のものが必要です。



1.ルーティングされたアンドロイド(frida-serverはルートなしでは起動しません)

2. Android SDK(より正確にはadb

3.フリーダ。



それは何で、なぜここを読む必要があるのですか- フリーダ

Android向けガイドの例-Androidガイド



frida-nodefrida-load 、およびfrida-serverが必要になりました(どのアーカイブが必要かはわかりませんが、アーキテクチャによって異なります。

frida-server-10.6.19-android-x86.xz)。



実際には、アーカイブからファイルを抽出し、ファイル名を短くして(たとえば、serv)、adb pushまたはpenを使用してどこかにプッシュします。



塗りつぶし:



-servなどのファイルの名前を変更します

-デバイスに記入:

adb push serv / data / local / tmp / serv



打ち上げ:



-adbシェル

-su

-/データ/ローカル/ tmp / serv



4.コードの横にcsharpフォルダーを作成します。 はい、私はとても怠け者なので、このフォルダの存在を確認するために2行のコードを追加します(これを明確にするためにより多くの文字が必要であるという事実も考慮に入れます)。



5.実際には、コード。

前述のfrida-nodeをインストールし、app.jsとunity_bootstrap.jsの2つのファイルを作成します。



ファイルコード:



app.js



const frida = require('frida'); const load = require('frida-load'); const fs = require('fs'); const spawn = require('child_process').spawn; const spawnAwait = (file)=>new Promise((resolve, reject)=>{ const child = spawn('adb', ['push', 'csharp/'+file, "/sdcard/"+file]); child.on('close', (code) => { console.log(`child process exited with code ${code}`); resolve(); }); }); const waitBuild = (file)=>new Promise((resolve, reject)=>{ const child = spawn('build.bat', []); child.on('close', (code) => { console.log(`child process exited with code ${code}`); resolve(); }); }); let appName=process.argv[2]; if(!appName){ appName="COM.ANDROID.SOMETHING"; } let session, script; const hexToBytes=(hex)=>{ let newLine=0; for (var bytes = [], c = 0; c < hex.length; c += 2){ bytes.push(hex.substr(c, 2)); newLine+=2; if(newLine>=40){ bytes.push("\n"); newLine=0; } } return bytes.join(" "); } // /data/local/tmp/serv (async () => { fs.writeFileSync("session_log.txt", "Starting session\n", ()=>{}); const device = await frida.getUsbDevice(); let pid = await device.spawn([appName]); session = await device.attach(pid); const source = await load(require.resolve('./unity_bootstrap.js')); script = await session.createScript(source); script.events.listen('message', (message,b) => { if (pid && message.type === 'send' && message.payload && message.payload.event === 'ready'){ device.resume(pid); console.log("Resume"); } else { if(!message.payload){ console.log(message); return; } if (message.payload.event == "dump") { fs.appendFile("csharp/"+message.payload.name, b, ()=>{}); } } }); await script.load(); let injectedLibs=['Assembly-CSharp.dll'/* , 'UnityEngine.dll' */]; injectedLibs=injectedLibs.filter(x=>fs.existsSync("csharp/"+x)); if(!injectedLibs.length){ script.post({type: 'loadData', count: 0}); } await Promise.all(injectedLibs.map(x=>spawnAwait(x))); injectedLibs.forEach(x=>script.post({type: 'loadData', count: injectedLibs.length, payload: x}, fs.readFileSync("csharp/"+x))); process.on('exit', function (){}); console.log("Done"); })();
      
      





unity_bootstrap.js



 var dllData={} var globalCaller; function onMessage(message, data) { if(message.type=="loadData"&&message.count>0){ dllData[message.payload]=data; console.log(message.payload, dllData, Object.keys(dllData).length); send({ event: "waiting" }) if(Object.keys(dllData).length==message.count) send({ event: "ready" }); else send({ event: "waiting" }) } if(message.type=="loadData"&&message.count==0){ send({ event: "ready" }); } recv(onMessage); } recv(onMessage); var awaitForCondition = function (callback) { var int = setInterval(function () { var addr = Module.findExportByName(null, "mono_get_root_domain"); if (addr) { clearInterval(int); callback(); return; } }, 0); } function _s(str){ return Memory.allocUtf8String(str); } function hookSet(){ var mono_assembly_get_image=new NativeFunction(Module.findExportByName(null, "mono_assembly_get_image"), 'pointer', ['pointer']); var mono_image_open_full=new NativeFunction(Module.findExportByName(null, "mono_image_open_full"), 'pointer', ["pointer", "pointer", "int"]); var imgLoads={}; for(var i in dllData){ var img=mono_image_open_full(_s("/sdcard/"+i), NULL, 1); imgLoads[i]=img; } var addr = Module.findExportByName(null, "mono_assembly_load_from_full"); Interceptor.attach(addr, { onEnter: function (args) { var name=Memory.readUtf8String(ptr(args[1])); console.log(name); var parts=name.split('/'); if(parts.length<2){ parts=name.split(','); } var dllName=parts[parts.length-1]; this.dllName=dllName; if(dllData[dllName]){ var img=imgLoads[dllName]; args[0]=img; args[1]=_s("/sdcard/"+dllName); console.log("Replaced"); } }, onLeave: function(retval){ if(this.dllName=='Assembly-CSharp.dll'){ console.log(retval, this.dllName); } //DUMP DLL if(!dllData[this.dllName]){ var image=mono_assembly_get_image(retval); var dataPtr=ptr(Memory.readInt(image.add(8))); var dataLength=Memory.readInt(image.add(12)); var result=Memory.readByteArray(dataPtr, dataLength); send({ event: 'dump', name: this.dllName }, result); } } }); } awaitForCondition(hookSet);
      
      





コードをさらに詳しく考えてみましょう(ところで、コードは理想とはほど遠いことは承知していますが、まだなめる必要はありません)。



App.jsはローダーとして機能します。 標準起動はノードアプリPACKAGE_IDです(COM.ANDROID.SOMETHINGを置き換えることでソースで解析できます)。



いくつかの追加機能を除いて、ほとんどの場合、マニュアルから通常のフリーダダウンロードがあります:



 await Promise.all(injectedLibs.map(x=>spawnAwait(x)));
      
      





そして



 injectedLibs.forEach(x=>script.post({type: 'loadData', count: injectedLibs.length, payload: x}, fs.readFileSync("csharp/"+x)));
      
      





実際、ここでは2つの方法が組み合わされています。



一般的に、バイトの配列を転送することから始めましたが、1つのおもちゃでメモリからライブラリをロードできないという状況に遭遇したため、最終的にはバイト配列とファイルをロードしますが、この例ではファイルのみを使用します。



waitBuildは、dllのアセンブリを簡素化するヘルパー関数です。 この例では使用されていないため、無視できます。



つまり、app.jsが起動し、frida-serverがjsエンジンをターゲットプロセスに挿入し、app.jsがunity_bootstrap.jsソースコードを送信し、組み込みエンジンがコードを実行します。



app.jsは、埋め込みが必要なライブラリを読み取り、それをunity_bootstrap.jsに送信し、ダウンロードが完了するのを待って、メインプロセスの実行を続けます。



次に、メインコード自体(unity_bootstrap.js)を検討します。



awaitForCondition関数は、モノのロードを待機します。 なぜなら メインコードの実行前にコードを埋め込みます。コードの実行時には、必要な機能はまだありません。



その後、実際にすべてのコードを実行します。 モノAPIをここで読むことができます 。使用例はこちらです。 開発中であっても、 この記事は役に立ちました。



実際に、以下を実行します:mono_assembly_load_from_fullを介してライブラリのロードをインターセプトし、ロードされたライブラリのパスを読み取り、必要に応じてそれを独自のものに置き換えます(mono_image_open_fullを使用して、Androidファイルシステムからbinarを読み取ります)。



秘trickは次のとおりです。実際、MonoImageにロードされたバイナリコードを置き換えています。



コードのさらに下の方に、dll'okのダンプを担当する部分があります(コメント// dump dllを参照)。

関数が実行されるのを待ってから、戻り値を読み取り、app.jsに送り返します。これにより、dllがcsharpフォルダーにダンプされます。



実際には、アプリケーションを起動した後、フォームの行が表示されるまで待つ必要があります

/data/app/OUR_AWESOME_GAME.APK/assets/bin/Data/Managed/System.dll、これはインターセプトが機能し、成功したことを意味します。



1回ダウンロードすると、ラズベリーを台無しにしないように、ライブラリをダンプするコードをコメントアウトできます。 正直に言うと、私はこれをプログラムで実行するコードを書くのが面倒でした。



すべてを正しく行い、運がよければ、必要なすべてのライブラリをcsharpフォルダーに入れます。 現時点では、unity3dで約20個のおもちゃを調査しました。このコードは予約済み(1個のおもちゃでは人為的な遅延を追加する必要があり、2個ではメモリの代わりにファイルシステムからコードをロードする必要がありました)は、Monoを使用するすべての人に有効でした。



PS調べたすべてのおもちゃの中で、私は本当に深刻な脆弱性を1つだけ見つけました(ほとんど1つに多くの時間を費やしていませんでした):この種のおもちゃの多くでは、ソロダンジョンがオフラインで計算されますが、このゲームではこのドロップのみサーバーに行き、そこに保存されます。 その結果、ダンジョンのドロップを完全に置き換えて、私のバージョンのsqliteデータベースをアップロードした後、20のVIP、ダイヤモンドの束、あらゆる種類のジャンク、禁止、サポートレポート、バグを開発者に転送する約束、およびその後の修正を取得しました。 彼らは感謝さえ言った、それはよかった。)。



luaを使用してコロナで作成された別の1つのおもちゃでは、ダンジョンのゴールドの数を置き換えることが判明しましたが、サーバーには常に5kが与えられているため、サーバーに何らかの制限があります。 そして、クライアントで計算されるすべての小さなことは、必要に応じて変更できます。



PPS誰かが興味を持っている場合、原則としてdnSpyでコードを編集する(非常にクールなこと)、ライブラリを埋め込む、ログをWebサーバーなどに送信するミニガイドを書くことができます。



あなたの注意と建設的な批判への希望に感謝します!



All Articles