📜 ⬆️ ⬇️

Frida-node or a little bit strange code

Greetings to all who read this article.
Somehow it happened that in Habré there is almost no mention of the wonderful thing called Frida. The most sensible of them is a couple of lines of code and a general description ( HabraFrida , from which, in fact, I learned about the existence of this thing, for which a special thank you to the author).
In short, Frida is committed to injecting the JS engine from Google (V8) into the targeted process (in the absence of protection, of course), and the built-in js-code can work with memory, intercept function calls, make these same calls and do other obscene.
To be honest with the reverse, I am familiar with very mediocre and, mostly, from the MMORPG Runes of Magic, with which I began to learn how to code and with which a considerable part of my current knowledge in programming is connected. Actually, I still have fun from time to time writing all sorts of differences for it (by the way, a toy is full of holes ... I didn’t find any smart bugs in it, starting from sketching objects and ending with sql-inject). Here for her, I wrote a little test code on Frida, allowing you to do ... different.
Why node.js? Simple In the end, the hub is abnormal programming)

The most difficult part for me was the assembly of node-frida . Why? Yes, it was too lazy to put 2013 studio. Do not repeat this error - for 2015 it is unlikely to be able to collect. If anyone needs it, I can later post the assembled add-on for win x64.

So let's get down to the code itself. The project itself is a normal project node, the startup script is called app.js, and we will start with it.

var processName = process.argv[2]; var script="'use strict'; "; [ './views/js/frida/romdata.js', './views/js/frida/romlib.js', './views/js/frida/rompackets.js', './views/js/frida/rom.js' ].forEach(function(elem){ script+=fs.readFileSync(elem, 'utf8'); }); frida.attach(processName).then(function (session) { return session.createScript(script); }).then(function (script) { script.load().then(function () { console.log("script loaded"); }); }).catch(function (err) { console.error(err); }); 

')
In this piece of code, the following happens: we read the process name from the arguments (node ​​app.js Client.exe-> process.argv [2] -> Client.exe) and specify the list of js-files that will be embedded in the targeted process. Use strict was needed in exactly 1 place to use the class from ES6 (the easiest way that I found to implement the necessary functionality).
Next, the connection to the target process is called, the result string is built into it, composed of the contents of the js-files, and the final script is initialized.

While it seems easy, right?
By the way, the code here is not all - only the essence of the essence, since The code of the original project is rather heavily filled with all sorts of different test pieces.
But enough of the lyrics, let's go further.

The client itself uses a bunch of different functions, calling functions, calling functions, calling functions ... killing the brain by uselessness. I used 2 functions, 1 for transmission, 2 for reception (before encrypting traffic).
In general and in general, the transfer function looks something like this:
SendToLocal (packetsize, (void *) packed data);
where packetdata is a kind of structure that looks like this in general:

 struct PG_Move_CtoL_PlayerMoveObject { GamePGCommandEnum Command; int GItemID; RolePosStruct Pos; ClientMoveTypeENUM Type; float vX; float vY; float vZ; int AttachObjID; PG_Move_CtoL_PlayerMoveObject() { Command = EM_PG_Move_CtoL_PlayerMoveObject; } }; 


which, in general (running ahead), is approximately as follows in js:

 var Move_CtoL_PlayerMoveObject = new MakeStruct(Memory, memalloc, { command: {type: 'int', value: 24}, gitemid: {type: 'int', value: 2045}, x: {type: 'float', value: 1000.0}, y: {type: 'float', value: 1000.0}, z: {type: 'float', value: 1000.0}, dir: {type: 'float', value: 154}, type: {type: 'int', value: 0}, vX: {type: 'float', value: 0.0}, vY: {type: 'float', value: 0.0}, vZ: {type: 'float', value: 0.0}, attachObjID: {type: 'int', value: 0} }); 


but more on that later.

So, let's look at an example of intercepting a function

  Interceptor.attach(ptr("0x6694E0"), { onEnter: function(args) { var dataptr = ptr(args[2]); var val = Memory.readU32(dataptr); } } 


In general, everything is trivially simple: 0x6694E0 is the address of the function being intercepted, onEnter is an event when a function is called (there is still onLeave, see the JS API for details), args are arguments of the intercepted function.
In my case, remember that the SendToLocal ((int) size, (void *) data) function is intercepted, which means that in args [2] there is a pointer to data (ptr converts it to NativePointer (a pretty convenient wrapper for working with pointers). This piece of code is equivalent to something like this in the pluses (if I'm not mistaken, I somehow didn’t have a lot of pluses):

 void SendToLocal( int Size , void* Data ){ int *dataptr=(int*)Data; int val = dataptr[0]; } 


If we look a little higher, on Move_CtoL_PlayerMoveObject, then we will see that val == Move_CtoL_PlayerMoveObject.command. I use it further to get the package name by its command id (var packetname = GamePGCommandEnum.enumName (val), where GamePGCommandEnum is the self-made function of transferring C ++ enum to js, ​​and GamePGCommandEnum.enumName - getting the name of enum by its id).

The beauty of this Interceptor.attach is that it is called before the original function is called, which means that no one bothers to change the arguments or even overwrite the memory of the transmitted data by the received pointer). For example, we use something in the spirit of args [1] = 10101010101, then the behavior of the function can be extremely puzzling (starting from crashing the client and ending with ignoring the packet, because it changed the size of the transmitted data).

Let us turn to the MakeStruct function, which is necessary for deserializing binary data into a similar structure.

 function toString(buf, length) { var binary=new Uint8Array(buf); var result=new Uint8Array(length); for(var i=0; i<result.length; i++) result[i]=binary[i]; //console.log(length, result); return String.fromCharCode.apply(null, result); } function fromString(str, length) { var buf = new ArrayBuffer(length); // 2 bytes for each char var bufView = new Uint8Array(buf); for (var i=0, strLen=length; i<strLen; i++) { if(i<str.length) bufView[i] = str.charCodeAt(i); else bufView[i] = 0; } return buf; } var MakeStruct = function(Memory, ptr, options){ var _this=this; _this.offset = 0; _this.struct={}; _this.types={ byte: function(name, size){ _this.struct[name] = { type: 'byte', value: Memory.readS8(ptr.add(_this.offset)), size: 1, offset: _this.offset }; _this.offset+=1; }, short: function(name, size){ _this.struct[name] = { type: 'short', value: Memory.readS16(ptr.add(_this.offset)), size: 2, offset: _this.offset }; _this.offset+=2; }, int: function(name, size){ _this.struct[name] = { type: 'int', value: Memory.readS32(ptr.add(_this.offset)), size: 4, offset: _this.offset }; _this.offset+=4; }, float: function(name, size){ _this.struct[name] = { type: 'float', value: Memory.readFloat(ptr.add(_this.offset)), size: 4, offset: _this.offset }; _this.offset+=4; }, string: function(name, size, typesize){ //console.log(size, typesize); var mem=Memory.readByteArray(ptr.add(_this.offset), typesize); _this.struct[name] = { type: 'string', value: toString(mem, size), size: typesize, offset: _this.offset }; _this.offset+=typesize; }, bytes: function(name, size){ var mem=Memory.readByteArray(ptr.add(_this.offset), size); _this.struct[name] = { type: 'bytes', value: mem, size: size, offset: _this.offset }; _this.offset+=size; } }; _this.update = function(){ for(var key in _this.struct){ if(_this.struct.hasOwnProperty(key)){ var item=_this.struct[key]; switch(item.type){ case 'byte': Memory.writeS8(ptr.add(item.offset), item.value); break; case 'short': Memory.writeS16(ptr.add(item.offset), item.value); break; case 'int': Memory.writeS32(ptr.add(item.offset), item.value); break; case 'float': Memory.writeFloat(ptr.add(item.offset), item.value); break; case 'bytes': Memory.writeByteArray(ptr.add(item.offset), item.value); break; case 'string': Memory.writeByteArray(ptr.add(item.offset), fromString(item.value, item.size)); break; } } } } _this.sizeof = function(){ var s=0; for(var key in _this.struct){ if(_this.struct.hasOwnProperty(key) && _this.struct[key].size){ s+=_this.struct[key].size*1; } } return s; } _this.print = function(){ for(var key in _this.struct){ if(_this.struct.hasOwnProperty(key) && _this.struct[key].size){ console.log(key, _this.struct[key].value); } } console.log(''); } _this.struct['update']=_this.update; _this.struct['sizeof']=_this.sizeof; _this.struct['print']=_this.print; _this.struct['ptr']=ptr; for(var key in options){ if(options.hasOwnProperty(key)){ var elem=options[key]; _this.types[elem.type](key, typeof elem.size==="number" || typeof elem.size==="undefined"?elem.size:_this.struct[elem.size].value, elem.typesize); if(elem.value) _this.struct[key].value=elem.value; } } return _this.struct; }; 


The toString / fromString functions are helpers for working with the char array. The rest, in fact, wrappers for reading / writing from frida for more convenient work. It is used like this:

 Interceptor.attach(ptr("0x60CCC0"), { onEnter: function(args) { var dataptr = ptr(args[1]); var val = Memory.readS16(dataptr); var packetname=GamePGCommandEnum.enumName(val); if(packetname=="EM_PG_Move_CtoL_PlayerMoveObject"){ var Move_CtoL_PlayerMoveObject = new MakeStruct(Memory, dataptr, { command: {type: 'int'}, gitemid: {type: 'int'}, x: {type: 'float'}, y: {type: 'float'}, z: {type: 'float'}, dir: {type: 'float'}, type: {type: 'int'}, vX: {type: 'float'}, vY: {type: 'float'}, vZ: {type: 'float'}, attachObjID: {type: 'int'} }); Move_CtoL_PlayerMoveObject.x.value+=100; Move_CtoL_PlayerMoveObject.update(); Move_CtoL_PlayerMoveObject.print(); } } }); 


will make the character cool teleamp when running (the x coordinate will increase by 100 relative to the real, and the server will be perplexed about what happened).

Well, for a snack - an example of a function call.
I use a wrapper over NativeFunction from Frida.

 var _sendToLocal=new NativeFunction(ptr("0x60CCC0"), 'void', ['int', 'pointer']); var _setpos=new NativeFunction(ptr("0x79AE70"), 'void', ['pointer']); function _Send(obj){ _sendToLocal(obj.sizeof(),obj.ptr); } class Structs{ _gmcommand(ptr){ var memalloc = ptr||Memory.alloc(4096); var v = ""; var Talk_CtoL_GMCommand = new MakeStruct(Memory, memalloc, { command: {type: 'int', value: 154}, gitemid: {type: 'int', value: 0}, contentsize: {type: 'int', value: v.length}, content: {type: 'string', typesize: 512, size: 'contentsize', value: v}, }); return Talk_CtoL_GMCommand; } _moveTest(ptr){ var memalloc = ptr||Memory.alloc(4096); var Move_CtoL_PlayerMoveObject = new MakeStruct(Memory, memalloc, { command: {type: 'int', value: 24}, gitemid: {type: 'int', value: 2045}, x: {type: 'float', value: 1000.0}, y: {type: 'float', value: 1000.0}, z: {type: 'float', value: 1000.0}, dir: {type: 'float', value: 154}, type: {type: 'int', value: 0}, vX: {type: 'float', value: 0.0}, vY: {type: 'float', value: 0.0}, vZ: {type: 'float', value: 0.0}, attachObjID: {type: 'int', value: 0} }); return Move_CtoL_PlayerMoveObject; } }; function _call(name, args, ptr){ var struct = new Structs(); var s=struct[name](ptr); for(var i in args){ s[i].value=args[i]; } s.update(); _Send(s); } 


A little more in detail: class - sugar for js-prototypes, in this case allows you to easily get a function by name without perversions with eval or something-else-can-come-in-head-at-3-hours-nights.
_call is called something like this:

  var cmd="give 0x31194"; _call('_gmcommand', { contentsize: cmd.length, content: cmd }); 


in this case is equivalent to a call to chat / gm? give 0x31994 (um-team for issuing things by its ID).

_setpos is another binding to the real semi-um client function that allows you to change the character coordinates in the client and on the server, taking as an argument a string with 3 coordinates. Charging something in the spirit

  var memalloc = Memory.alloc(128); var q=-4050.7; setInterval(function(){ Memory.writeUtf8String(memalloc, q+++", 244.5, -8251.9"); _setpos(memalloc); }, 100); 


we get the character in a state of delirium tremens (his sausage is not weak, by the way).

In general, at the moment I used frida more for entertaining and warming up the brain, however, with the right approach, it can turn into a very worthy tool for researching various kinds of processes.
Thank you for your attention, I hope you enjoyed it.

And finally, materials:

Frida website
Frida JS API
Frida-node
Class in ECMAScript 6
Horse

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


All Articles