Frida-node или немножко странного кода

    Приветствую всех, кто читает эту статью.
    Как-то так сложилось, что на хабре практически нет упоминаний про замечательную штуку под названием 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
    Конь
    Метки:
    • +12
    • 12,9k
    • 8
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 8
    • +5
      Очень подходящие теги.
      • –8
        Javascript добрался до системного уровня?! ЗА ЧТО? Операционную систему на js/node/v8 еще никто не пишет?
          • +4
            Сочувствую.
            • 0
              Всё, что может быть написано на JavaScript — будет написано

              Зачем — другой вопрос. Просто у молодёжи много сил и энтузиазма, а JS популярен и учат его чуть ли не со школы, вот и получаем тезис выше.
              • 0
                Угу, когда есть интерес и вдохновение, экспериментирую с разными языками. Пока лидируют C# и js.
                Да и, если на то пошло — если задачу можно реализовать на языке без большого геммороя — то чем плох этот язык для этой задачи? Можно кодить на системном уровне(причем с простым и читальбельным синтаксисом… если не считать callback-in-callback) — почему бы и нет? Да и нода поддерживает нативные аддоны, не хватает функционала — дописываем на плюсах)
            • 0
              Да, буквально на днях открыл ее для себя (в применении к андроиду). Очень крутая штука. Есть недоработки, но в целом очень удобно.
              • 0
                О, под андроид — это интересно. Есть желание и возможность написать хотя бы небольшую статью с обзором?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.