UNPKG

gattacker

Version:
588 lines (469 loc) 23.3 kB
require('env2')('config.env'); var debug = require('debug')('advertise'); var bleno = require('./lib/bleno'); var fs = require('fs'); var util = require('util'); var utils = require('./lib/utils') var hookFunctions = require('./hookFunctions/pos.js'); var path=require('path'); var events = require('events'); var getopt = require('node-getopt'); var options = new getopt([ ['a' , 'advertisement=FILE' , 'advertisement json file'], ['s' , 'services=FILE' , 'services json file'], ['S' , 'static' , 'static - do not connect to ws-slave/target device'], ['f' , 'funmode' , 'have fun!'], ['' , 'jk' , 'see http://xkcd.com/1692'], ['h' , 'help' , 'display this help'], ]); options.setHelp("Usage: node advertise -a <FILE> [ -s <FILE> ] [-S] \n[[OPTIONS]]" ) opt=options.parseSystem(); if ( !opt.options.advertisement) { console.info(options.getHelp()); process.exit(0); } if (opt.options.help) { options.showHelp() process.exit(0) } if (opt.options.funmode) { console.log('>>>>>>>>>>>>>>>>> MAY THE FUN BE WITH YOU! <<<<<<<<<<<<<<<<<<'.rainbow.inverse) } var devicesPath=process.env.DEVICES_PATH; var dumpPath=process.env.DUMP_PATH; var myAddress = ''; var mitmservices; var servicesLookup = []; //keep hooks and notify subsciptions var subscriptions=[]; var hookTable=[]; //list of active notify subscriptions var subscriptions = []; if (opt.options.static) { staticRun = true; //local eventEmitter to get events from hook functions var wsclient = new events.EventEmitter(); wsclient.write = function (peripheralId, serviceUuid, uuid) { console.log('static run write not defined in hooks ' + getServiceNames(serviceUuid, uuid)); }; wsclient.read = function (peripheralId, serviceUuid, uuid) { console.log('static run read not defined in hooks '+ getServiceNames(serviceUuid, uuid)); }; wsclient.notify = function (peripheralId, serviceUuid, uuid) { console.log('static run subscribe '+ getServiceNames(serviceUuid, uuid)); }; wsclient.write(); } else { staticRun = false; var wsclient = require('./lib/ws-client'); } baseAdvFile=path.basename(opt.options.advertisement); var peripheralId = baseAdvFile.substring(0, baseAdvFile.indexOf("_")); console.log("peripheralid: " + peripheralId) if (opt.options.advertisement.indexOf('.adv.json') > -1 ) { advertisementFile=opt.options.advertisement; } else { advertisementFile=opt.options.advertisement + '.adv.json'; } console.log('advertisement file: ' + advertisementFile) var advertisement = JSON.parse(fs.readFileSync(advertisementFile, 'utf8')); var eir = new Buffer(advertisement.eir,'hex'); var scanResponse = advertisement.scanResponse ? new Buffer(advertisement.scanResponse, 'hex') : ''; console.log("EIR: " + eir.toString('hex')); console.log("scanResponse: " + scanResponse.toString('hex')); if (opt.options.services) { if (opt.options.services.indexOf('.srv.json') > -1 ) { servicesFile=opt.options.services; } else { servicesFile=opt.options.services + '.srv.json'; } } else { servicesFile = devicesPath+ '/'+peripheralId+'.srv.json'; } var services = JSON.parse(fs.readFileSync(servicesFile, 'utf8')); setServices(services, function(services){ mitmservices=services; }) if (!staticRun) { //wait for the ws connection wsclient.on('ws_open', function(){ wsclient.getMac(function(address){ myAddress = address; console.log('Noble MAC address : ' + myAddress); }) wsclient.initialize(peripheralId, services, true, function(){ console.log('initialized !'); startAdvertising(); }) }); } else { startAdvertising(); } function startAdvertising(){ if (bleno.state != 'poweredOn') { console.log('waiting for interface to initialize...'); bleno.once('stateChange', function(state){ if (state === 'poweredOn') { bleno.startAdvertisingWithEIRData(eir,scanResponse); } else { console.log('Interface down! Exiting...'); process.exit(1); } }) } else { console.log('Static - start advertising'); bleno.startAdvertisingWithEIRData(eir,scanResponse); } } //return name of service and characteristic (as defined in input json file) function getServiceNames(serviceUuid, uuid) { if (servicesLookup[serviceUuid]) { var serviceName = servicesLookup[serviceUuid].name; var characteristicName = servicesLookup[serviceUuid].characteristics[uuid].name; } var info = serviceUuid; if (serviceName) { info += ' (' + serviceName + ')' }; info +=' -> ' + uuid; if (characteristicName) { info += ' (' + characteristicName +' )' } return info; } function getDateTime() { var date = new Date(); var hour = date.getHours(); hour = (hour < 10 ? "0" : "") + hour; var min = date.getMinutes(); min = (min < 10 ? "0" : "") + min; var sec = date.getSeconds(); sec = (sec < 10 ? "0" : "") + sec; var msec = date.getMilliseconds(); msec = (msec < 100 ? "0" : "") + msec; var year = date.getFullYear(); var month = date.getMonth() + 1; month = (month < 10 ? "0" : "") + month; var day = date.getDate(); day = (day < 10 ? "0" : "") + day; return year + "." + month + "." + day + " " + hour + ":" + min + ":" + sec + '.' + msec; } //dump transmission log to file function dumpLog(type, peripheralId, serviceUuid, uuid, data ){ var dumpFile=dumpPath + '/' + peripheralId + '.log'; if (servicesLookup[serviceUuid]) { var serviceName = servicesLookup[serviceUuid].name; var characteristicName = servicesLookup[serviceUuid].characteristics[uuid].name; } var toSave = getDateTime() + ' | ' + type + ' | ' + serviceUuid; if (serviceName) { toSave += ' (' + serviceName + ')'; }; toSave += ' | ' + uuid; if (characteristicName) { toSave += ' (' + characteristicName + ')'; }; toSave += ' | ' + data.toString('hex') + ' (' + utils.hex2a(data.toString('hex'))+ ')\n'; fs.appendFile(dumpFile, toSave, function(err) { if(err) { return console.log(err); } }) } bleno.on('stateChange', function(state) { console.log('BLENO - on -> stateChange: ' + state); if (state === 'poweredOn') { // console.log('poweredOn'); } else { console.log('Interface down! Reset/power it up again...') bleno.stopAdvertising(); } }); bleno.on('advertisingStart', function(error) { console.log('on -> advertisingStart: ' + (error ? 'error ' + error : 'success')); if (error) { console.log("Adv error ".red,error); } else { bleno.setServices( mitmservices, function(error){ console.log('setServices: ' + (error ? 'error ' + error : 'success')); console.log(' <<<<<<<<<<<<<<<< INITIALIZED >>>>>>>>>>>>>>>>>>>> '.magenta.inverse) }); } }); bleno.on('accept', function(clientAddress) { console.log('Client connected: ' + clientAddress); if (clientAddress === myAddress) { console.log('SELF CONNECT!'); bleno.disconnect(); } else { if (!staticRun) { //notify the ws-slave of victim's connection wsclient.clientConnection(clientAddress, true); } } bleno.updateRssi(); }); //update the ws-slave on client disconnect bleno.on('disconnect', function(clientAddress){ console.log('Client disconnected: ' + clientAddress); if (!staticRun) { wsclient.clientConnection(clientAddress,false); } }) wsclient.on('disconnect', function(peripheralId){ console.log(' target device disconnected'); }) wsclient.on('connect', function(peripheralId){ console.log(' target device connected'); }) //change the advertisement wsclient.on('advchange', function(newEir, newScanResponse){ bleno.stopAdvertising(function(){ //wait 2 seconds after stopAdvertising, otherwise the HCI will often fail with "Command Disallowed" setTimeout(function() { console.log('Advertisement change: waited 2s for device, should work now...'); console.log(' old ' + eir.toString('hex') + ' ('+ utils.hex2a(eir) +') '+ ' : ' + scanResponse.toString('hex') + ' ('+ utils.hex2a(scanResponse) +')'); console.log(' new ' + newEir.toString('hex') + ' ('+ utils.hex2a(newEir) +') '+ ' : ' + newScanResponse.toString('hex') + ' ('+ utils.hex2a(newScanResponse) +') '); bleno.startAdvertisingWithEIRData(newEir,newScanResponse); eir=newEir; scanResponse=newScanResponse; }, 2000); }); }) //listen to all notifications, invoke appropriate subscription callbacks wsclient.on('notification', function(peripheralId, serviceUuid, uuid, data) { if (servicesLookup[serviceUuid]) { var serviceName = servicesLookup[serviceUuid].name; var characteristicName = servicesLookup[serviceUuid].characteristics[uuid].name; } console.log('<< Notify: '.green + getServiceNames(serviceUuid,uuid) + ' : ' + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex')) + ')'); // console.log('forwarding...'); dumpLog('> N',peripheralId, serviceUuid, uuid, data); if (hookTable[serviceUuid]) { var hook = hookTable[serviceUuid][uuid]; } if (hook) { if (hook.staticNotifyValue) { var data = hook.staticValue; console.log('<< Notify static val: '.green + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex'))+ ')'); //return the static value callback(result, new Buffer(data,'hex')); } else if (hook.dynamicNotify) { hookFunctions[hook.dynamicNotify](peripheralId, serviceUuid, uuid, 'notify', data , wsclient, function(err, modifiedData){ if (modifiedData) { console.log('<< Notify DATA hook : '.yellow + modifiedData.toString('hex').yellow.inverse + ' (' + utils.hex2a(modifiedData.toString('hex'))+ ')'); if (subscriptions[serviceUuid] && subscriptions[serviceUuid][uuid]) { subscriptions[serviceUuid][uuid](modifiedData); } } else { console.log('<< Notify DATA hook: '.yellow + 'intercept, not forwarding'.yellow); } }) } else { //there are hooks, but not for notifications, invoke directly subscriptions[serviceUuid][uuid](data); } } else { // no hook, just send directly if (!subscriptions[serviceUuid]) { //the ws-slave received notification from previous connection // console.log('Notification from previous connection, but services not set yet, not forwarding'); } else { //invoke the subscription callback subscriptions[serviceUuid][uuid](data); } } }); function setServices(services, callback){ var PrimaryService = bleno.PrimaryService; var Characteristic = bleno.Characteristic; var BlenoDescriptor = bleno.Descriptor; var mitmcharacteristics=[]; var mitmservices = []; for (sindex = 0; sindex < services.length; ++sindex) { serviceToCopy = services[sindex]; servicesLookup[serviceToCopy.uuid]= { characteristics: [] }; debug("Setting up service: " + serviceToCopy.uuid); mitmcharacteristics=[]; for (cindex = 0; cindex<serviceToCopy.characteristics.length; ++cindex) { characteristicToCopy = serviceToCopy.characteristics[cindex]; servicesLookup[serviceToCopy.uuid].characteristics[characteristicToCopy.uuid] = { name: characteristicToCopy.name, properties: characteristicToCopy.properties // more info not needed yet }; debug(" Setting up Characteristic: " + characteristicToCopy.uuid + ' Value: ' + characteristicToCopy.value + ' ('+utils.hex2a(characteristicToCopy.value)+')'); debug(" StartHandle: " + characteristicToCopy.startHandle); debug(" ValueHandle: " + characteristicToCopy.valueHandle); debug(" Properties: " + characteristicToCopy.properties); // console.log(" Secure: " + characteristicToCopy.secure); if (characteristicToCopy.descriptors) { debug(" Descriptors: " + util.inspect(characteristicToCopy.descriptors, {showHidden: false, depth: null})); } if (characteristicToCopy.hooks) { if (!hookTable[serviceToCopy.uuid]) { hookTable[serviceToCopy.uuid]=[]; } if (!hookTable[serviceToCopy.uuid][characteristicToCopy.uuid]) { hookTable[serviceToCopy.uuid][characteristicToCopy.uuid] = characteristicToCopy.hooks; } debug(" hooks: " + util.inspect(characteristicToCopy.hooks, {showHidden: false, depth: null})); } var mitmcharacteristic = new Characteristic({ uuid: characteristicToCopy.uuid, serviceUuid: serviceToCopy.uuid, name: characteristicToCopy.name, startHandle: characteristicToCopy.startHandle, valueHandle: characteristicToCopy.valueHandle, properties: characteristicToCopy.properties, // targeted mobile application will not verify this anyway ;) // secure: [ ... ], // enable security for properties, can be a combination of 'read', 'write', 'writeWithoutResponse', 'notify', 'indicate' descriptors: characteristicToCopy.descriptors, onReadRequest: function(offset, callback) { var peripheralId = this.peripheralId; var serviceUuid = this.serviceUuid; var uuid = this.uuid; var info = getServiceNames(serviceUuid, uuid); debug('<< Read req : '.green + this.serviceUuid +' -> ' + this.uuid + ' offset: ' + offset) //we assume the original device read success //todo? - forward possible error to client var result=this.RESULT_SUCCESS; if (hookTable[serviceUuid]) { var hook = hookTable[serviceUuid][uuid]; } if (hook) { var hook = hookTable[serviceUuid][uuid]; if (hook.staticValue) { var data = hook.staticValue; console.log('<< Read static val '.green + info +' : ' + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex'))+ ')'); //return the static value callback(result, new Buffer(data,'hex')); } else if (hook.staticRead) { hookFunctions[hook.staticRead](peripheralId, serviceUuid, uuid, 'read', null , wsclient, function(data){ callback(result, data); console.log('<< Read static hook '.green + info +' : ' + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex'))+ ')'); }) } else if (hook.dynamicRead) { wsclient.read(peripheralId, serviceUuid,uuid, function(data) { console.log('<< Read for dynamic hook '.green + info + ' : ' + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex'))+ ')'); hookFunctions[hook.dynamicRead](peripheralId, serviceUuid, uuid, 'read', data, wsclient, function(err, modifiedData){ //if err... callback(result, modifiedData); }) }) } } else { // no hook, just read directly from device wsclient.read(peripheralId, serviceUuid, uuid, function(data) { if (data) { console.log('<< Read: '.green + info + ' : ' + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex'))+ ')'); dumpLog('> R',peripheralId, serviceUuid, uuid, data); callback(result, data); } else { //we did not receive the data (e.g. it was authenticated read and we have no bond with device) console.log('<< Read DATA error '.red + info); //should we inform the client or maybe deceive him? callback(this.RESULT_FAILURE, null); } }) } }, onWriteRequest: function(data, offset, withoutResponse, callback) { var peripheralId = this.peripheralId; var serviceUuid = this.serviceUuid; var uuid = this.uuid; console.log('>> Write: '.blue + getServiceNames(serviceUuid,uuid) + ' : '+ data.toString('hex').blue.inverse + ' (' + utils.hex2a(data.toString('hex'))+')'); if (withoutResponse) { dumpLog('< W',peripheralId, serviceUuid, uuid, data); } else { dumpLog('< C',peripheralId, serviceUuid, uuid, data); } if (hookTable[serviceUuid]) { var hook = hookTable[serviceUuid][uuid]; } if (hook) { if (hook.staticWrite) { //send the write data to static hook function, do not forward the write to device hookFunctions[hook.staticWrite](peripheralId, serviceUuid, uuid, 'write', data, wsclient, function(error, data){ console.log('>> Write static hook: '.green + data.toString('hex').green.inverse + ' (' + utils.hex2a(data.toString('hex'))+ ')'); callback(0x00); }) } else if (hook.dynamicWrite) { // invoke dynamic function, then forward returned data to device hookFunctions[hook.dynamicWrite](peripheralId, serviceUuid, uuid, 'write', data, wsclient, function(err, modifiedData){ //todo if err... if (modifiedData) { // if the dynamicWrite function returns null, do not forward anything to device wsclient.write(peripheralId, serviceUuid,uuid, modifiedData, withoutResponse, function(error) { if (error){ console.log('------ Write error: '.red); throw(error); } callback(0x00); }); } else { console.log(' hook function did not return modified data'); callback(0x00); } }) } else { //other hooks, but not for write wsclient.write(peripheralId, serviceUuid, uuid, data, withoutResponse, function(error) { if (error){ console.log('------ Write error: '.red); throw(error); } callback(0x00); }); } } else { // no hook, just write directly to device wsclient.write(peripheralId, serviceUuid, uuid, data, withoutResponse, function(error) { if (error){ console.log('------ Write error: '.red); throw(error); } callback(0x00); }); } }, onSubscribe: function(maxValueSize, updateValueCallback) { // optional notify/indicate subscribe handler, function(maxValueSize, updateValueCallback) { ...} var peripheralId = this.peripheralId; var serviceUuid = this.serviceUuid; var uuid = this.uuid; if (servicesLookup[serviceUuid]) { var serviceName = servicesLookup[serviceUuid].name; var characteristicName = servicesLookup[serviceUuid].characteristics[uuid].name; } var info = '>> Subscribe: '.blue + serviceUuid; if (serviceName) { info += ' (' + serviceName + ')' }; info +=' -> ' + uuid; if (characteristicName) { info += ' (' + characteristicName +' )' } console.log(info); if (! subscriptions[this.serviceUuid]) { subscriptions[this.serviceUuid]=[]; } if (! subscriptions[this.serviceUuid][this.uuid]) { subscriptions[this.serviceUuid][this.uuid]=updateValueCallback; } if (!staticRun) { } //send the subscription request to device wsclient.notify(peripheralId, serviceUuid, uuid, true, function(err, service, characteristic, state) { if (err) { console.log('--------- SUBSCRIBE ERROR'.red); } else { console.log(' ' + service + ':' + characteristic + ' confirmed subscription state: ' + state); } }); }, onUnsubscribe: function() { // optional notify/indicate unsubscribe handler, function() { ...} // console.log('========== onUnsubscribe'); }, onNotify: function() {// optional notify sent handler, function() { ...} // console.log('========== onNotify'); }, onIndicate: function(updateValueCallback) { // optional indicate confirmation received handler, function() { ...} // console.log('========== onIndicate'); } }); mitmcharacteristic.peripheralId = peripheralId; mitmcharacteristics.push(mitmcharacteristic); } var primaryService = new PrimaryService({ uuid: serviceToCopy.uuid, name: serviceToCopy.name, startHandle: serviceToCopy.startHandle, endHandle: serviceToCopy.endHandle, type: serviceToCopy.type, characteristics: mitmcharacteristics }); mitmservices.push(primaryService); servicesLookup[serviceToCopy.uuid].name = serviceToCopy.name; servicesLookup[serviceToCopy.uuid].type = serviceToCopy.type; } callback(mitmservices); }