UNPKG

btlejuice

Version:

Bluetooth Low-Energy spoofing and MitM framework

408 lines (330 loc) 10.7 kB
/** * BtleJuice Core module * * The App class takes a device profile in input and create a pure NodeJS * clone uppon which high-level behaviors may be implemented. **/ var async = require('async'); var events = require('events'); var util = require('util'); var colors = require('colors'); const winston = require('winston'); var io = require('socket.io-client'); var logging = require('./logging'); const path = require('path'); const express = require('express'); const http = require('http'); const exec = require('child_process').exec; /** * Core app. **/ var App = function(proxyUrl, enableWebServer, webServerPort, iface) { events.EventEmitter.call(this); /* Save status. */ this.status = 'disconnected'; this.target = null; this.profile = null; this.server = null; /* Web server options. */ this.webEnabled = (enableWebServer === true); if (webServerPort != null) this.webServerPort = webServerPort; else this.webServerPort = 8080; /* Save proxy URL */ this.proxyUrl = proxyUrl; this.connectProxy(); /* Logger */ this.logger = new (winston.Logger)({ transports: [ new (winston.transports.Console)({ level: 'debug', colorize: true, timestamp: true, prettyPrint: true }) ] }); winston.addColors({ info: 'green', warning: 'orange', error: 'red', debug: 'purple' }); /* Disable the bluetooth service if it is enabled. */ this.disableBluetoothService(iface); }; util.inherits(App, events.EventEmitter); App.prototype.createServer = function() { var app = express(); var server = http.Server(app); /* Check server */ if (this.server != null) { this.server.close(); } this.server = require('socket.io')(server); this.clients = []; /* Basic webserver. */ if (this.webEnabled) { app.set('view engine','ejs'); app.set('views', __dirname + '/views'); app.use(express.static(__dirname + '/resources')) .get('/', function(req, res){ res.render('index.ejs'); }); } server.listen(this.webServerPort); /* Handles client connection. */ this.server.sockets.on('connection', function(socket){ this.onClientConnection(socket); }.bind(this)); }; App.prototype.onClientConnection = function(client) { /** * Forward scan_devices to proxy. **/ /* Save client. */ this.clients.push(client); /* Notify client of our current config. */ this.setProxy(this.proxyUrl); this.setStatus(this.status); this.setTarget(this.target); this.setProfile(this.profile); /* Disconnection handler. */ client.on('disconnect', (function(t, c){ return function(){ c.removeAllListeners(); var index = t.clients.indexOf(c); if (index > -1) t.clients.splice(index); }; })(this, client)); client.on('scan_devices', function(){ /* Install device discovery handler. */ this.proxy.removeAllListeners('discover'); this.proxy.on('discover', function(p, name, rssi){ client.emit('peripheral', p, name, rssi); }.bind(this)); /* Ask for devices discovery. */ this.proxy.emit('scan_devices'); }.bind(this)); /** * Forward status to proxy. **/ client.on('status', function(){ this.proxy.emit('status'); }.bind(this)); /** * Handles target connection. **/ client.on('target', function(target, keepHandles){ this.setStatus('connecting'); this.setTarget(target); this.send('app.status', 'connecting'); /* Set the keepHandles option internally. */ this.keepHandles = keepHandles; /* Forward to target, create mock on callback. */ this.proxy.emit('target', target); }.bind(this)); /** * Handles stop. **/ client.on('stop', function(){ this.setStatus('disconnected'); /* Forward to target, create mock on callback. */ if (this.fake != null) { this.fake.stop(); this.fake = null; } this.proxy.emit('stop'); client.emit('app.status', 'disconnected'); }.bind(this)); /** * Forward profile data to app. **/ this.proxy.removeAllListeners('profile'); this.proxy.on('profile', function(p){ this.profile = p; client.emit('profile', p); this.createFakeDevice(p); }.bind(this)); this.proxy.on('stopped', function(){ this.setStatus('disconnected'); /* Forward to target, create mock on callback. */ if (this.fake != null) { this.fake.stop(); this.fake = null; } client.emit('app.status', 'disconnected'); }.bind(this)); /** * Forward ready message to app. **/ this.proxy.removeAllListeners('ready'); this.proxy.on('ready', function(){ this.status = 'connected'; this.logger.info('proxy set up and ready to use =)'); client.emit('ready'); client.emit('app.status', 'connected'); client.emit('app.target', this.target); }.bind(this)); this.proxy.on('ble_data', function(service, characteristic, data){ this.logger.debug('Data notification for service %s and charac. %s (ble_data)', service, characteristic); client.emit('data', service, characteristic, data); }.bind(this)); /** * Forward write message to proxy. **/ client.on('ble_write', function(service, characteristic, data){ this.proxy.once('ble_write_resp', function(s,c, error){ client.emit('ble_write_resp', service, characteristic, error); }.bind(this)); this.proxy.emit('ble_write', service, characteristic, data, false) }.bind(this)); /** * Forward read message to proxy **/ client.on('ble_read', function(service, characteristic){ this.proxy.once('ble_read_resp', function(s, c, data){ client.emit('ble_read_resp', service, characteristic, data); }.bind(this)); this.proxy.emit('ble_read', service, characteristic, 0); }.bind(this)); /** * Forward notify message to proxy **/ client.on('ble_notify', function(service, characteristic, enabled){ this.proxy.once('ble_notify_resp', function(){ client.emit('ble_notify_resp', service, characteristic); }.bind(this)); this.proxy.emit('ble_notify', service, characteristic, enabled); }.bind(this)); /* client.on('data', function(service, characteristic, data){ this.logger.debug('Forward data notification for service %s and charac. %s (data)', s, c); this.fake.emit('data', service, characteristic, data); });*/ client.on('proxy_notify_resp', function(s,c, error) { this.fake.emit('notify_resp', s, c, error); }.bind(this)); client.on('proxy_data', function(s,c,d){ this.logger.debug('Forward data notification for service %s and charac. %s (proxy_data)', s, c); this.fake.emit('data', s,c,d); }.bind(this)); client.on('proxy_read_resp', function(s, c, data) { this.fake.emit('read_resp', s, c, data); }.bind(this)); client.on('proxy_write_resp', function(s, c, error) { this.fake.emit('write_resp', s, c, error); }.bind(this)); }; /** * Send message to clients. **/ App.prototype.send = function(){ for (var client in this.clients) { this.clients[client].emit.apply( this.clients[client], arguments ); } }; /** * connectProxy() * * Connect the mock to a given proxy in order to relay services and * characteristics. */ App.prototype.connectProxy = function(){ /* Connect to the proxy. */ this.proxy = io(this.proxyUrl); this.proxy.on('connect', function(){ this.logger.info('successfully connected to proxy'); /* Create local server. */ this.createServer(); }.bind(this)); /* Error message if connection fails. */ this.proxy.on('connect_error', function(){ this.logger.error('cannot connect to proxy.'); process.exit(-1); }.bind(this)); /* Error message if remote device disconnects. */ this.proxy.on('device.disconnect', function(target){ this.logger.warn('remote device has disconnected.'); this.onRemoteDeviceDisconnected(target); }.bind(this)); }; App.prototype.createFakeDevice = function(profile) { const fake = require('./fake'); this.fake = new fake(profile, this.keepHandles); /* Listen for events on this fake device, and notify the web interface. */ this.fake.on('write', function(service, characteristic, data, offset, withoutResponse){ /* Notify our client we got a write request. */ this.send('proxy_write', service, characteristic, data, offset, withoutResponse); }.bind(this)); /* Listen for events on this fake device, and notify the web interface. */ this.fake.on('read', function(service, characteristic, offset){ /* Notify our client we got a write request. */ this.send('proxy_read', service, characteristic, offset); }.bind(this)); this.fake.on('notify', function(service, characteristic, enable){ this.send('proxy_notify', service, characteristic, enable); }.bind(this)); this.fake.on('connect', function(client) { this.send('app.connect', client); }.bind(this)); this.fake.on('disconnect', function(client) { this.send('app.disconnect', client); }.bind(this)); }; App.prototype.setStatus = function(status) { /* Save status. */ this.status = status; /* Notify status change to clients. */ this.send('app.status', status); }; App.prototype.setTarget = function(target) { this.send('app.target', target); this.target = target; }; App.prototype.setProxy = function(proxy) { this.send('app.proxy', proxy); }; App.prototype.setProfile = function(profile) { this.send('target.profile', profile); }; App.prototype.disableBluetoothService = function(iface) { /* First, check if the service exists and is running. */ exec('service bluetooth status', function(error, stdout, stderr){ if (stdout.indexOf(': active') >= 0) { /* Service is active, shut it down. */ this.logger.info('Stopping BT service ...'); exec('service bluetooth stop', function(error, stdout, stderr){ setTimeout(function(){ /* Re-enable our HCI interface. */ if (iface == null) iface = 0; this.logger.info('Making sure interface hci%d is up ...', iface); exec(util.format('hciconfig hci%d up', iface)); }.bind(this), 2000); }.bind(this)); } }.bind(this)); }; App.prototype.onRemoteDeviceDisconnected = function(target) { this.setStatus('disconnected'); /* Forward to target, create mock on callback. */ if (this.fake != null) { this.fake.stop(); this.fake = null; } this.proxy.emit('stop'); this.send('app.status', 'disconnected'); /* Notify client. */ this.send('device.disconnect', target); }; if (!module.parent) { var b = new App('http://127.0.0.1:8000', true, 8080); } else { module.exports = App; }