📜 ⬆️ ⬇️

Universal Dumper / Injector Unity3D (Mono, Android)

image

Greetings

Not so long ago, I became interested in the study of games under android. As it turned out, a very considerable number of developers use Unity3D (probably, 50-60 percent of the games that I was interested in are based on this engine). Immediately make a reservation - I am not an expert on hacking and I don’t even practically know C ++ / asm (in spite of a slight acquaintance with this topic), so please do not throw the toilet bowls with the help of gravhes. There is also a small clarification - I practically studied only MMO / semi-online games in the style of “cracking plot dungeon until blue in the face, and then fighting in the arena with other players, and semi-offline). Offline games on Unity3D are simply boring to explore.

Actually, as far as I know, toys for Unity3D use 2 technologies: Mono and Il2cpp.
Within this material, I want to consider the process of replacing .NET dll'ok and dump even encrypted versions of these same dll'ok directly from the game.

I am developing for windows / node.js, because the technology stack will be described in the context of what I use myself.
')
So, we will need:

1. Routed android (without the root will not start frida-server)
2. Android SDK (more precisely, adb )
3. Frida.

What it is and why you need it, you can read here - Frida .
An example guide under android - Android guide .

We now need frida-node , frida-load and frida-server (which of the archives is needed, I won’t say exactly, it depends on the architecture, I wound up
frida-server-10.6.19-android-x86.xz).

Actually, extract the file somewhere from the archive, rename it somehow shorter (for example, serv) and push it somewhere via adb push or handles.

Fill:

-Rename the file, for example, in serv
-Fill on the device:
adb push serv / data / local / tmp / serv

Run:

-adb shell
-su
- / data / local / tmp / serv

4. Next to the code create a folder csharp. Yes, I'm so lazy that I would add 2 lines of code to check the existence of this folder (even considering the fact that it took more characters to clarify this).

5. Actually, the code.
Install the above frida-node, create 2 files — app.js and unity_bootstrap.js.

File ID:

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); 

Consider the code in more detail (by the way, I know that the code is far from perfect, but there is no need to lick it yet).

App.js acts as a bootloader. Starting standard - node app PACKAGE_ID (you can zaparkodit in the source, replacing COM.ANDROID.SOMETHING).

For the most part, here is the usual download of frida from their manual, with the exception of some additional features:

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

and

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

In fact, it combines 2 ways.

In general, I started with the transmission of an array of bytes, but in one of the toys I ran into a situation when loading the library from memory did not work, therefore, as a result, I load the array of bytes and the file, but in the example I use only the file.

waitBuild is a helper function to simplify the assembly of its dll. This example is not used, so you can ignore.

If in brief, it all works like this: app.js is launched, the js-engine injects the frida-server into the target process, app.js sends the source code to the unity_bootstrap.js, the built-in engine executes the code.

app.js reads the libraries that need to be embedded, then sends them to unity_bootstrap.js, waits for the download to finish, and continues the execution of the main process.

Now consider the main code itself (unity_bootstrap.js).

The awaitForCondition function is responsible for waiting for the mono to be loaded. Since we embed the code before the execution of the main code, at the time of the execution of our code, the desired functions are not yet available.

Further, the code for the sake of which all this was started is actually processed. You can read the mono API here , an example of use is here . Even during the development of this article has helped.

Actually, we do the following: intercept library loading via mono_assembly_load_from_full, then read the path of the loaded library and, if necessary, replace it with our own (with the help of mono_image_open_full we read the binary from the android file system).

The trick is this: we are, in fact, replacing the binary code that was loaded in MonoImage.

Further along the code, you can see the piece responsible for the dump dll'ok (see the comment // dump dll).
It waits for the function to execute, then it reads the return value and sends it back to app.js, which dumps the dll to the csharp folder.

Actually, after starting the application, it is worth waiting for the appearance of the lines
/data/app/OUR_AWESOME_GAME.APK/assets/bin/Data/Managed/System.dll, this means that the interception worked and it went.

After 1 download you can comment out the code responsible for the library dump so that it does not spoil the raspberries. I, frankly, was just too lazy to write code that does it programmatically.

If you did everything right and you were lucky, you will have all the necessary libraries in the csharp folder. At the moment I researched about 20 toys on unity3d, this code with reservations (I had to add artificial delays in 1 toy, in 2 - load the code from the file system instead of memory) worked in all that used Mono.

PS Of all the toys studied, I found a really serious vulnerability only in 1 (though I didn’t spend much time on just one): in many toys of this kind, solo-dungeons are calculated offline, but this drop is only in this game goes to the server and is also there. As a result, it turned out to completely replace the drop in dungeon by loading my version of sqlite base, after which I received 20 VIPs, a bunch of diamonds, junk, a ban, a support report, a promise to transfer the bug to the developers and the subsequent fix. Even said thank you, it was nice.).

Another 1 toy, written on Corona using lua, turned out to change the number of golds per dungeon, but they had some kind of restriction on the server, because all the time it was issued static at 5k. And so - every little thing that is calculated on the client, however, it can just be changed, as your heart desires.

PPS If someone is interested, in principle, I can write a mini-guide on editing the code in dnSpy (very cool stuff), embedding my library, sending logs to my web server and other funny and not so good things.

Thank you for your attention and hope for constructive criticism!

Source: https://habr.com/ru/post/345356/


All Articles