Home .NET Unity3D Universal Dumper/Injector (Mono,Android)

Unity3D Universal Dumper/Injector (Mono,Android)

by admin

Unity3D Universal Dumper/Injector (Mono,Android)
Not so long ago I got into researching games for android.As it turned out, quite a few developers use Unity3D (probably about 50-60% of the games I was interested in are based on this engine).Right away I should clarify – I’m not a cracking expert and even barely know C++/asm (despite my slight familiarity with this topic), so please don’t throw toilet bowls at me with gravimanunks.Also a little clarification – I practically researched only MMO/semi-online games of the "steal a story dungeon till I’m blue in the face and then fight in the arena with other players, semi-offline)style. Offline Unity3D games are simply boring to explore.
Actually, as far as I know toys for Unity3D use 2 technologies: Mono and Il2cpp.
Within this tutorial I want to look at how to spoof .NET dll’s and dump even encrypted versions of these dll’s directly from the game.
I’m developing under windows/node.js, so I’ll describe the technology stack in the context of what I use myself.
So, we’re going to need :
1. Rooted android (frida-serverwill not start without root)
2. Android SDK(more precisely, adb )
3. Frida.
What it is and why it is needed can be read here Frida
An example of a guide for android — Android guide
What we need right now is frida-node , frida-load and frida-server (I’m not sure which archive you need, it depends on the architecture.
Actually, extract the file from the archive somewhere, rename it something shorter (for example, serv) and shove it somewhere through adb push or by hand.
Pouring :
-Rename the file, for example, to serv
-Fill in the device :
adb push serv /data/local/tmp/serv
Launch :
-adb shell
4. Next to the code, create a folder csharp. Yes, I’m lazy enough to add 2 lines of code to check the existence of this folder (even though it took more characters to explain it).
5. The actual code is.
Install the aforementioned frida-node, create 2 files – app.jsand unity_bootstrap.js.
File Code :

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");})();


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"});elsesend({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 DLLif(!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);

Let’s take a closer look at the code (by the way, I know it’s not perfect, but I don’t need to tidy it up yet).
App.js acts as a loader. Startup standard – node app PACKAGE_ID (can be charcoded in the source, replacing COM.ANDROID.SOMETHING).
For the most part, here is the usual frida download from their manual, except for some extra features :

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


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

Actually, there is a combination of 2 ways.
Generally, I started by passing an array of bytes, but in one of the games I ran into a situation where loading the library from memory did not work, so in the end I load an array of bytes and a file, but in the example I use only the file.
waitBuild is a helper function to make building your dll easier. It is not used in this example, so you can ignore it.
In short, it works like this: app.js runs, frida-server injects the js engine into the target process, app.js forwards the unity_bootstrap.js source code, the embedded engine executes the code.
app.js reads the libraries that need to be embedded, then forwards them to unity_bootstrap.js, waits until it finishes loading, and continues execution of the main process.
Now let’s look at the core code itself (unity_bootstrap.js).
The awaitForCondition function is responsible for waiting for mono to load. Since we’re embedding the code before the execution of the main code, the functions we’re looking for aren’t there yet when our code is executed.
After that you get the code which is the reason of all this. You can read the API for mono here , an example of how to use here Also when developing the this article
We actually do the following: we intercept the library loading via mono_assembly_load_from_full, then we read the path of the loaded library and, if necessary, replace it with our own (with 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 into the MonoImage.
Farther down the code you can see the piece responsible for dll dump (see comment //dump dll).
It waits for the function to execute, then reads the return value and sends it back to app.js, which dumps the dll into the csharp folder.
Actually, after starting the application, you should wait for lines like
/data/app/OUR_AWESOME_GAME.APK/assets/bin/Data/Managed/System.dll, it means that interception worked and it’s working.
After 1 download you can comment out the code responsible for dumping the libraries, so it does not spoil the Raspberry. To be honest, I was just too lazy to write code that does this programmatically.
If you’ve done everything correctly and you’re lucky, you’ll have all the libraries you need in the csharp folder. So far I’ve tested about 20 unity3d games, this code worked with some reservations (in 1 game I had to add artificial delays, in 2 I had to load code from file system instead of memory) in all the ones using Mono.
P.S. Of all the investigated games, I found a really serious vulnerability only in 1 (admittedly, almost none of the games I spent a large amount of time): in many toys such plan solo dunge are calculated off-line, but only in this game, this drop goes to the server and there and is stored. As a result, it was able to completely replace the drop in dunge, uploading their version sqlite’s database, after which I got at once 20 VIP, a bunch of diamonds, sundry junk, ban, report to sapport, promise to refer the bug developers and the subsequent fix. Even said thank you, it was nice).
More 1 toy, written in Corona using lua managed to switch the amount of gold for dunge, but they have some kind of restriction on the server, so it was given all the time statically by 5k. And so – all sorts of little things that the client is calculated, however, it just can change as you like.
P.P.S. If anyone is interested, I can write a mini guide on editing code in dnSpy (very cool stuff), building my library, sending logs to my web-server and other fun and not so fun stuff.
Thank you for your attention and hope for constructive criticism!

You may also like