UNPKG

btlejuice

Version:

Bluetooth Low-Energy spoofing and MitM framework

657 lines (546 loc) 16.9 kB
/** * Main interceptor module. * * This is BtleJuice UI main controller, handling intercepted requests and forwarding them. **/ var Interceptor = function(){ this.MODE_CONFIG = 'config'; this.MODE_FORWARD = 'forward'; this.MODE_INTERACTIVE = 'interactive'; this.STATE_IDLING = 0; this.STATE_EDITING = 1; this.config = { status: 'disconnected', proxy: '', deviceId: '', devices: [], profile:null, }; /* Current state. */ this.state = this.STATE_IDLING; /* Options. */ this.shouldReconnect = true; this.keepHandles = true; /* Registered hooks. */ this.hooks = {}; /* Pending requests. */ this.pendingRequests = []; /* Forward everything by default. */ this.mode = this.MODE_FORWARD; /* Main socket. */ this.socket = io(); this.socket.removeAllListeners(); /* Transactions controller. */ this.transactions = null; /* Event manager. */ this.listeners = {}; this.socket.on('app.status', function(status){ console.log('got status notif:' + status); this.onStatusChange(status); }.bind(this)); /* Install handlers. */ this.setup(); /* Ensure proxy is not connected to another device. */ this.disconnect(); }; /** * setup() * * Installs event handlers. **/ Interceptor.prototype.setup = function() { /* Forward writes. */ this.socket.on('proxy_write', function(s, c, d, o, w) { this.onProxyWrite(s, c, d, o, w); }.bind(this)); /* Forward reads. */ this.socket.on('proxy_read', function(s,c) { this.onProxyRead(s,c); }.bind(this)); this.socket.on('proxy_notify', function(s, c, enable){ this.onProxyNotify(s,c,enable); }.bind(this)); this.socket.on('data', function(s,c,d){ this.onNotification(s,c,d); }.bind(this)); this.socket.on('app.target', function(target){ this.onTargetChange(target); }.bind(this)); this.socket.on('app.proxy', function(proxy){ this.onProxyChange(proxy); }.bind(this)); this.socket.on('target.profile', function(profile){ this.onProfileChange(profile); }.bind(this)); this.socket.on('app.connect', function(client){ this.onClientConnect(); }.bind(this)); this.socket.on('app.disconnect', function(client){ this.onClientDisconnect(); }.bind(this)); this.socket.on('device.disconnect', function(target){ this.onRemoteDeviceDisconnected(target); }.bind(this)); } Interceptor.prototype.clear = function() { this.socket.removeAllListeners(); }; /** * transaction() * * Keep track of a transaction (read, write, notify). **/ Interceptor.prototype.transaction = function(action, service, characteristic, data, disableRefresh) { if (this.transactions == null) this.transactions = angular.element(document.getElementById('transactions')).scope(); this.transactions.addTransaction({ op: action, service: service, characteristic: characteristic, data: buffer2hexII(data), dataHexii: buffer2hexII(data), dataHex: buffer2hex(data) }, disableRefresh); }; Interceptor.prototype.setHook = function(service, characteristic, callback) { var key = service+':'+characteristic; this.hooks[key] = callback; }; Interceptor.prototype.isHooked = function(service, characteristic) { var key = service+':'+characteristic; return (key in this.hooks); }; Interceptor.prototype.removeHook = function(service, characteristic) { var key = service+':'+characteristic; if (key in this.hooks) delete this.hooks[key]; }; /** * proxyWriteResponse() * * Send write response to the proxy. **/ Interceptor.prototype.proxyWriteResponse = function(service, characteristic, error) { this.socket.emit('proxy_write_resp', service, characteristic, error); }; /** * proxyReadResponse() * * Send read response to the proxy. **/ Interceptor.prototype.proxyReadResponse = function(service, characteristic, data, disableRefresh) { this.transaction('read', service, characteristic, data, disableRefresh); this.socket.emit('proxy_read_resp', service, characteristic, data); }; /** * deviceWrite() * * Send the device a write request. **/ Interceptor.prototype.deviceWrite = function(service, characteristic, data, offset, withoutResponse, disableRefresh) { /* Register our callback to send the response to the engine. */ this.socket.once('ble_write_resp', function(s,c,e){ this.socket.emit('proxy_write_resp', s,c,e); }.bind(this)); /* Ask the proxy to perform the BLE write. */ this.socket.emit('ble_write', service, characteristic, data, offset, withoutResponse); /* Add a transaction. */ this.transaction('write', service, characteristic, data, disableRefresh); }; /** * onProxyWrite() * * Event handler called when the proxy asks for a write request. **/ Interceptor.prototype.onProxyWrite = function(service, characteristic, data, offset, withoutResponse) { /* Forward mode ? */ if (this.mode == this.MODE_FORWARD) { /* Should the request be edited before processing ? */ var key = service+':'+characteristic; console.log(key); if (key in this.hooks) { if (this.hooks[key] == null) { console.log('>> manual hook found for '+key); if (this.state == this.STATE_EDITING) { this.pendingRequests.push({ 'action': 'write', 'service': service, 'characteristic': characteristic, 'data': data, 'offset': offset, 'withoutResponse': withoutResponse }); } else { /* Mark as editing. */ this.state = this.STATE_EDITING; /* Open our edit box. */ this.emit('hooks.write', service, characteristic, data, offset, withoutResponse); } } else { /* Apply our modifying function. */ data = this.hooks[key](data); /* Send write to device. */ this.deviceWrite(service, characteristic, data, offset, withoutResponse); } } else { console.log('>> forwarding write'); this.deviceWrite(service, characteristic, data, offset, withoutResponse); } } else { if (this.state == this.STATE_EDITING) { this.pendingRequests.push({ 'action': 'write', 'service': service, 'characteristic': characteristic, 'data': data, 'offset': offset, 'withoutResponse': withoutResponse }); } else { /* Mark as editing. */ this.state = this.STATE_EDITING; /* Open our edit box. */ this.emit('hooks.write', service, characteristic, data, offset, withoutResponse); } } } /** * deviceRead() * * Send a read request to the target device. **/ Interceptor.prototype.deviceRead = function(service, characteristic, callback) { this.socket.once('ble_read_resp', function(s,c,data){ /* Call the modifying callback. */ callback(s, c, data); }); this.socket.emit('ble_read', service, characteristic); }; /** * onProxyRead() * * Event handler called when the proxy asks for a read request. **/ Interceptor.prototype.onProxyRead = function(service, characteristic) { /* Forward mode ? */ if (this.mode == this.MODE_FORWARD) { console.log('>> forwarding read'); this.deviceRead(service, characteristic, function(service, characteristic, data){ /* Should we edit the data ? */ var key = service+':'+characteristic; console.log(key); if (key in this.hooks) { if (this.hooks[key] == null) { if (this.state == this.STATE_EDITING)  { this.pendingRequests.push({ 'action': 'read', 'service': service, 'characteristic': characteristic, 'data': data, }); } else { /* Mark as editing. */ this.state = this.STATE_EDITING; /* Ask the user to modify and forward. */ this.emit('hooks.read', service, characteristic, data); } } else { data = this.hooks[key](data); this.proxyReadResponse(service, characteristic, data); } } else { /* Send response to our fake device. */ this.proxyReadResponse(service, characteristic, data); } }.bind(this)); } else { /* Send read to device. */ this.deviceRead(service, characteristic, function(service, characteristic, data){ if (this.state == this.STATE_EDITING)  { this.pendingRequests.push({ 'action': 'read', 'service': service, 'characteristic': characteristic, 'data': data, }); } else { /* Mark as editing. */ this.state = this.STATE_EDITING; /* Ask the user to modify and forward. */ this.emit('hooks.read', service, characteristic, data); } }.bind(this)); } }; /** * deviceNotify() * * Send a notify request to the target device. **/ Interceptor.prototype.deviceNotify = function(service, characteristic, enabled) { this.socket.once('ble_notify_resp', function(s,c,e){ console.log('got notify_resp'); /* Call the modifying callback. */ this.socket.emit('proxy_notify_resp', s, c, e); }.bind(this)); this.socket.emit('ble_notify', service, characteristic, enabled); }; /** * onProxyNotify() * * Event handler called when the proxy registers for a notification. **/ Interceptor.prototype.onProxyNotify = function(service, characteristic, enabled) { console.log('>> forwarding notify'); this.deviceNotify(service, characteristic, enabled); }; /** * proxyNotifyData() * * Send a data notification to connected devices. **/ Interceptor.prototype.proxyNotifyData = function(service, characteristic, data, disableRefresh) { this.transaction('notification', service, characteristic, data, disableRefresh); this.socket.emit('proxy_data', service, characteristic, data); }; /** * onNotification() * * Event handler called when the device sends a notification. **/ Interceptor.prototype.onNotification = function(service, characteristic, data) { console.log('>> got notification data'); /* Forward mode ? */ if (this.mode == this.MODE_FORWARD) { /* Should we edit the data ? */ var key = service+':'+characteristic; console.log(key); if (key in this.hooks) { if (this.hooks[key] == null) { if (this.state == this.STATE_EDITING)  { this.pendingRequests.push({ 'action': 'notify', 'service': service, 'characteristic': characteristic, 'data': data, }); } else { /* Mark as editing. */ this.state = this.STATE_EDITING; /* Ask the user to modify and forward. */ this.emit('hooks.notify', service, characteristic, data); } } else { data = this.hooks[key](data); this.proxyNotifyData(service, characteristic, data); } } else { /* No hook, forward data. */ console.log('>> forwarding data notification'); this.proxyNotifyData(service, characteristic, data); } } else { if (this.state == this.STATE_EDITING)  { this.pendingRequests.push({ 'action': 'notify', 'service': service, 'characteristic': characteristic, 'data': data, }); } else { /* Mark as editing. */ this.state = this.STATE_EDITING; /* Ask the user to modify and forward. */ this.emit('hooks.notify', service, characteristic, data); } } }; /** * processNextRequest() * **/ Interceptor.prototype.processNextRequest = function() { if (this.pendingRequests.length > 0) { if (this.mode == this.MODE_INTERACTIVE) { /* Send event to ask for modification. */ var nextReq = this.pendingRequests.pop(); /* Mark as editing. */ this.state = this.STATE_EDITING; switch(nextReq.action) { case 'read': this.emit('hooks.read', nextReq.service, nextReq.characteristic, nextReq.data, true ); break; case 'write': this.emit( 'hooks.write', nextReq.service, nextReq.characteristic, nextReq.data, nextReq.offset, nextReqwithoutResponse, true ); break; case 'notify': this.emit( 'hooks.notify', nextReq.service, nextReq.characteristic, nextReq.data, true ); break } } else { for (var req_idx in this.pendingRequests) { var req = this.pendingRequests[req_idx]; if (req.action == 'write') { this.deviceWrite( req.service, req.characteristic, req.data, req.offset, req.withoutResponse ); } else if (req.action == 'read') { this.proxyReadResponse(req.service, req.characteristic, req.data); } else if (req.action == 'notify') { this.proxyNotifyData(req.service, req.characteristic, req.data); } } /* No more pending requests. */ this.pendingRequests = []; /* Mark as editing. */ this.state = this.STATE_EDITING; } } else { this.state = this.STATE_IDLING; } }; /** * setMode() * * Set the interceptor mode. **/ Interceptor.prototype.setMode = function(mode) { this.mode = mode; }; /** * getMode() * * Get the interceptor mode. **/ Interceptor.prototype.getMode = function() { return this.mode; }; Interceptor.prototype.isInteractive = function() { return (this.mode == this.MODE_INTERACTIVE); } /** * scanDevices() * * Set the interceptor in config mode and lists devices. **/ Interceptor.prototype.listDevices = function(callback) { this.clear(); this.socket.on('peripheral', function(p, name, rssi){ console.log('got peripheral'); callback(p, name, rssi); }); this.socket.emit('scan_devices'); }; /** * selectTarget() * * Select a given device as a target. **/ Interceptor.prototype.selectTarget = function(target, callback) { this.clear(); this.socket.on('app.status', function(status){ console.log('got status notif:' + status); this.onStatusChange(status); }.bind(this)); this.socket.once('profile', function(profile){ this.onProfileChange(profile); }.bind(this)); /* We wait until the proxy is ready. */ this.socket.once('ready', function(){ /* Notify we're connected to the selected target. */ callback(true); /* Setup the interceptor. */ this.setup(); }.bind(this)); /* Asks the proxy to select the correct target with correct options. */ this.socket.emit('target', target, this.keepHandles); }; Interceptor.prototype.disconnect = function() { this.socket.emit('stop'); this.emit('app.status', 'disconnected'); }; Interceptor.prototype.getConfig = function(){ return this.config; }; Interceptor.prototype.getProfile = function() { return this.config.profile; }; Interceptor.prototype.onProfileChange = function(profile) { /* Save profile. */ this.config.profile = profile; /* Notify listerners. */ this.emit('target.profile', profile); }; Interceptor.prototype.onStatusChange = function(status) { /* Save status. */ this.config.status = status; /* Notify listeners. */ this.emit('status.change', status); }; Interceptor.prototype.onTargetChange = function(target) { /* Save status. */ this.config.target = target; /* Notify listeners. */ this.emit('target.change', target); }; Interceptor.prototype.onProxyChange = function(proxy) { /* Save status. */ this.config.proxy = proxy; /* Notify listeners. */ this.emit('proxy.change', proxy); }; Interceptor.prototype.on = function() { return this.config; }; Interceptor.prototype.onClientConnect = function(clientAddress) { this.transaction('event','connect', clientAddress); }; Interceptor.prototype.onClientDisconnect = function(clientAddress) { this.transaction('event','disconnect', clientAddress); }; Interceptor.prototype.onRemoteDeviceDisconnected = function(target) { /* Client has been disconnected for sure :) */ this.transaction('event','disconnect', null); /* Based on settings, asks the proxy to connect again. */ if (this.shouldReconnect) { this.selectTarget(target,function(){ console.log('Target reselected !'); }); } }; Interceptor.prototype.on = function(queueName, callback) { if (!(queueName in this.listeners)) this.listeners[queueName] = []; this.listeners[queueName].push(callback); }; Interceptor.prototype.emit = function() { var queueName = arguments[0]; for (var listener in this.listeners[queueName]) { var callback = this.listeners[queueName][listener]; callback.apply(callback, Array.from(arguments).splice(1,arguments.length)); } }; var interceptor = new Interceptor();