Приветствую всех, кто читает эту статью.
Как-то так сложилось, что на хабре практически нет упоминаний про замечательную штуку под названием Frida. Самое толковое из них заключается в паре строк кода и общем описании(HabraFrida, из которой, собственно, я и узнал про существование этой штуковины, за что отдельное спасибо автору).
Если вкратце, то Frida занимается тем, что инжектит JS-движок от Гугла(V8) в таргетный процесс(при отсутствии защиты, конечно же), причем встроенный js-код умеет работать с памятью, перехватывать вызовы функций, делать эти самые вызовы и заниматься прочими непотребствами.
Если честно, с реверсом я знаком крайне посредственно и, в-основном, из MMORPG Runes of Magic, с которой я и начал учиться кодить и с которой связанна немалая часть моих текущих познаний в программировании. Собственно, до сих пор время от времени развлекаюсь написанием всяких разностей под нее(дырявая, кстати говоря, игрушка… Каких только шикарных багов в ней не находили, начиная от рисовки предметов и заканчивая sql-inject'ом). Вот для нее я и написал немножко тестового кода на Frida, позволяющего делать… разное.
Почему node.js? Прост. В конце-концов, это же хаб ненормального программирования)
Самой сложной частью для меня оказалась сборка node-frida. Почему? Да было лень ставить 2013 студию. Не повторяйте эту ошибку — под 2015 вряд ли получится собрать. Если кому понадобится, могу позже выложить собранный аддон под win x64.
Итак, приступим, собственно, к коду. Сам проект — это обычный проект ноды, стартовый скрипт у меня называется app.js, с него и начнем.
В этом куске кода происходит следующее: считываем название процесса из аргументов(node app.js Client.exe->process.argv[2]->Client.exe) и указываем список js-файлов, которые будут встроены в таргетный процесс. Use strict нужен был ровно в 1 месте для использования class из ES6(самый простой способ, который нашел для реализации нужного функционала).
Далее вызывается подключение к целевому процессу, встраивание в него итоговой строки, составленной из содержимого js-файлов и инициализируется итоговый скрипт.
Пока вроде просто, не так ли?
Кстати, код здесь представлен не весь — только выжимка сути, т.к. код оригинального проекта довольно сильно изгажен всякими-разными тестовыми кусками.
Но хватит лирики, пойдем дальше.
В самом клиенте используется куча разных функций, вызывающих функции, вызывающих функции, вызывающих функции… убивающих мозг бесполезностью. Я использовал 2 функции, 1 для передачи, 2 для приема(до шифрования трафика).
В целом и общем, функция передачи выглядит как-то так:
SendToLocal(packetsize, (void*)packedata);
где packetdata — некая структура, в общем виде выглядящая как-то так:
что, в общем-то(забегая наперед), приводится примерно к такому виду в js:
но об этом позже.
Итак, посмотрим на пример перехвата функции.
В общем-то, все тривиально просто: 0x6694E0 — адрес перехватываемой функции, onEnter — эвент при вызове функции(еще есть onLeave, подробнее смотрите JS API), args — аргументы перехватывамой функции.
В моем случае помним, что перехватывается функция SendToLocal((int)size,(void*)data), а значит, в args[2] лежит указатель на data(ptr преобразовывает его в NativePointer(довольно удобную обертку для работы с указателями). Этот кусок кода эквивалентен чему-то вроде такого в плюсах(если не ошибаюсь, с плюсами у меня как-то не очень сложилось):
Если посмотрим чуть выше, на Move_CtoL_PlayerMoveObject, то увидим, что val==Move_CtoL_PlayerMoveObject.command. Я использую это дальше для получения имени пакета по его command id(var packetname=GamePGCommandEnum.enumName(val), где GamePGCommandEnum — самопальная функция по переносу C++ enum в js, а GamePGCommandEnum.enumName — получение названия enum'а по его id).
Прелесть этого вот Interceptor.attach в том, что он вызывается до вызова оригинальной функции, а значит, никто не мешает изменить аргументы или даже перезаписать память передаваемых данных по полученному указателю). К примеру, используем что-нибудь в духе args[1]=10101010101, то поведение функции может крайне озадачить(начиная от краша клиента и заканчивая игнорированием пакета, т.к. изменили размер передаваемых данных).
Перейдем к MakeStruct — функции, нужной для десериализации бинарных данных в подобие структуры.
Функции toString/fromString — хэлперы для работы с char array. Остальное, по факту, обертки на считыванием/записью из frida для более удобной работы. Используется это как-то так:
заставит персонажа прикольно телемпаться при беге(координата x будет увеличиваться на 100 относительно реальной, а сервер будет недоумевать по поводу произошедшего).
Ну и на закуску — пример вызова функции.
Я использую обертку над NativeFunction из Frida.
Немножко поподробнее: class — сахарок для js-прототипов, в данном случае позволяющий легко получить функцию по имени без извращений с eval'ом или что-там-еще-может-прийти-в-голову-в-3-часа-ночи.
_call вызывается как-то так:
в данном случае эквивалентно вызову в чате /gm? give 0x31994(гм-команда для выдачи вещи по ее ID).
_setpos — еще один биндинг к реальной полу-гм функции клиента, позволяющей изменять координаты персонажа в клиенте и на сервере, принимая в качестве аргумента строку с 3 координатами. Зарядив что-нибудь в духе
получаем персонажа в состоянии delirium tremens(его неслабо колбасит, кстати).
В общем и целом, на данный момент я использовал frida больше для развлечения и разминки мозгов, однако, при должном подходе, она может превратиться в весьма достойный инструмент для исследования различного рода процессов.
Спасибо за внимание, надеюсь, вам понравилось.
И напоследок, материалы:
Сайт Frida
Frida JS API
Frida-node
Class в ECMAScript 6
Конь
Как-то так сложилось, что на хабре практически нет упоминаний про замечательную штуку под названием Frida. Самое толковое из них заключается в паре строк кода и общем описании(HabraFrida, из которой, собственно, я и узнал про существование этой штуковины, за что отдельное спасибо автору).
Если вкратце, то Frida занимается тем, что инжектит JS-движок от Гугла(V8) в таргетный процесс(при отсутствии защиты, конечно же), причем встроенный js-код умеет работать с памятью, перехватывать вызовы функций, делать эти самые вызовы и заниматься прочими непотребствами.
Если честно, с реверсом я знаком крайне посредственно и, в-основном, из MMORPG Runes of Magic, с которой я и начал учиться кодить и с которой связанна немалая часть моих текущих познаний в программировании. Собственно, до сих пор время от времени развлекаюсь написанием всяких разностей под нее(дырявая, кстати говоря, игрушка… Каких только шикарных багов в ней не находили, начиная от рисовки предметов и заканчивая sql-inject'ом). Вот для нее я и написал немножко тестового кода на Frida, позволяющего делать… разное.
Почему node.js? Прост. В конце-концов, это же хаб ненормального программирования)
Самой сложной частью для меня оказалась сборка node-frida. Почему? Да было лень ставить 2013 студию. Не повторяйте эту ошибку — под 2015 вряд ли получится собрать. Если кому понадобится, могу позже выложить собранный аддон под win x64.
Итак, приступим, собственно, к коду. Сам проект — это обычный проект ноды, стартовый скрипт у меня называется app.js, с него и начнем.
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); });
В этом куске кода происходит следующее: считываем название процесса из аргументов(node app.js Client.exe->process.argv[2]->Client.exe) и указываем список js-файлов, которые будут встроены в таргетный процесс. Use strict нужен был ровно в 1 месте для использования class из ES6(самый простой способ, который нашел для реализации нужного функционала).
Далее вызывается подключение к целевому процессу, встраивание в него итоговой строки, составленной из содержимого js-файлов и инициализируется итоговый скрипт.
Пока вроде просто, не так ли?
Кстати, код здесь представлен не весь — только выжимка сути, т.к. код оригинального проекта довольно сильно изгажен всякими-разными тестовыми кусками.
Но хватит лирики, пойдем дальше.
В самом клиенте используется куча разных функций, вызывающих функции, вызывающих функции, вызывающих функции… убивающих мозг бесполезностью. Я использовал 2 функции, 1 для передачи, 2 для приема(до шифрования трафика).
В целом и общем, функция передачи выглядит как-то так:
SendToLocal(packetsize, (void*)packedata);
где packetdata — некая структура, в общем виде выглядящая как-то так:
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; } };
что, в общем-то(забегая наперед), приводится примерно к такому виду в 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} });
но об этом позже.
Итак, посмотрим на пример перехвата функции.
Interceptor.attach(ptr("0x6694E0"), { onEnter: function(args) { var dataptr = ptr(args[2]); var val = Memory.readU32(dataptr); } }
В общем-то, все тривиально просто: 0x6694E0 — адрес перехватываемой функции, onEnter — эвент при вызове функции(еще есть onLeave, подробнее смотрите JS API), args — аргументы перехватывамой функции.
В моем случае помним, что перехватывается функция SendToLocal((int)size,(void*)data), а значит, в args[2] лежит указатель на data(ptr преобразовывает его в NativePointer(довольно удобную обертку для работы с указателями). Этот кусок кода эквивалентен чему-то вроде такого в плюсах(если не ошибаюсь, с плюсами у меня как-то не очень сложилось):
void SendToLocal( int Size , void* Data ){ int *dataptr=(int*)Data; int val = dataptr[0]; }
Если посмотрим чуть выше, на Move_CtoL_PlayerMoveObject, то увидим, что val==Move_CtoL_PlayerMoveObject.command. Я использую это дальше для получения имени пакета по его command id(var packetname=GamePGCommandEnum.enumName(val), где GamePGCommandEnum — самопальная функция по переносу C++ enum в js, а GamePGCommandEnum.enumName — получение названия enum'а по его id).
Прелесть этого вот Interceptor.attach в том, что он вызывается до вызова оригинальной функции, а значит, никто не мешает изменить аргументы или даже перезаписать память передаваемых данных по полученному указателю). К примеру, используем что-нибудь в духе args[1]=10101010101, то поведение функции может крайне озадачить(начиная от краша клиента и заканчивая игнорированием пакета, т.к. изменили размер передаваемых данных).
Перейдем к MakeStruct — функции, нужной для десериализации бинарных данных в подобие структуры.
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; };
Функции toString/fromString — хэлперы для работы с char array. Остальное, по факту, обертки на считыванием/записью из frida для более удобной работы. Используется это как-то так:
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(); } } });
заставит персонажа прикольно телемпаться при беге(координата x будет увеличиваться на 100 относительно реальной, а сервер будет недоумевать по поводу произошедшего).
Ну и на закуску — пример вызова функции.
Я использую обертку над NativeFunction из 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); }
Немножко поподробнее: class — сахарок для js-прототипов, в данном случае позволяющий легко получить функцию по имени без извращений с eval'ом или что-там-еще-может-прийти-в-голову-в-3-часа-ночи.
_call вызывается как-то так:
var cmd="give 0x31194"; _call('_gmcommand', { contentsize: cmd.length, content: cmd });
в данном случае эквивалентно вызову в чате /gm? give 0x31994(гм-команда для выдачи вещи по ее ID).
_setpos — еще один биндинг к реальной полу-гм функции клиента, позволяющей изменять координаты персонажа в клиенте и на сервере, принимая в качестве аргумента строку с 3 координатами. Зарядив что-нибудь в духе
var memalloc = Memory.alloc(128); var q=-4050.7; setInterval(function(){ Memory.writeUtf8String(memalloc, q+++", 244.5, -8251.9"); _setpos(memalloc); }, 100);
получаем персонажа в состоянии delirium tremens(его неслабо колбасит, кстати).
В общем и целом, на данный момент я использовал frida больше для развлечения и разминки мозгов, однако, при должном подходе, она может превратиться в весьма достойный инструмент для исследования различного рода процессов.
Спасибо за внимание, надеюсь, вам понравилось.
И напоследок, материалы:
Сайт Frida
Frida JS API
Frida-node
Class в ECMAScript 6
Конь