diff --git a/main.go b/main.go index 3e2442d1..3bd26976 100644 --- a/main.go +++ b/main.go @@ -51,6 +51,7 @@ func main() { sess.Register(modules.NewRestAPI(sess)) sess.Register(modules.NewWOL(sess)) sess.Register(modules.NewWiFiRecon(sess)) + sess.Register(modules.NewBLERecon(sess)) sess.Register(modules.NewSynScanner(sess)) if err = sess.Start(); err != nil { diff --git a/modules/ble_options_darwin.go b/modules/ble_options_darwin.go new file mode 100644 index 00000000..854f79c8 --- /dev/null +++ b/modules/ble_options_darwin.go @@ -0,0 +1,11 @@ +package modules + +import "github.com/bettercap/gatt" + +var defaultBLEClientOptions = []gatt.Option{ + gatt.MacDeviceRole(gatt.CentralManager), +} + +var defaultBLEServerOptions = []gatt.Option{ + gatt.MacDeviceRole(gatt.PeripheralManager), +} diff --git a/modules/ble_options_linux.go b/modules/ble_options_linux.go new file mode 100644 index 00000000..38dc0872 --- /dev/null +++ b/modules/ble_options_linux.go @@ -0,0 +1,21 @@ +package modules + +import ( + "github.com/bettercap/gatt" + "github.com/bettercap/gatt/linux/cmd" +) + +var defaultBLEClientOptions = []gatt.Option{ + gatt.LnxMaxConnections(1), + gatt.LnxDeviceID(-1, true), +} + +var defaultBLEServerOptions = []gatt.Option{ + gatt.LnxMaxConnections(1), + gatt.LnxDeviceID(-1, true), + gatt.LnxSetAdvertisingParameters(&cmd.LESetAdvertisingParameters{ + AdvertisingIntervalMin: 0x00f4, + AdvertisingIntervalMax: 0x00f4, + AdvertisingChannelMap: 0x7, + }), +} diff --git a/modules/ble_recon.go b/modules/ble_recon.go new file mode 100644 index 00000000..7c0a4646 --- /dev/null +++ b/modules/ble_recon.go @@ -0,0 +1,220 @@ +package modules + +import ( + "fmt" + "io/ioutil" + golog "log" + "os" + "sort" + "strings" + "time" + + "github.com/bettercap/bettercap/core" + "github.com/bettercap/bettercap/log" + "github.com/bettercap/bettercap/network" + "github.com/bettercap/bettercap/session" + + "github.com/bettercap/gatt" + + "github.com/olekukonko/tablewriter" +) + +var ( + bleAliveInterval = time.Duration(5) * time.Second + blePresentInterval = time.Duration(30) * time.Second +) + +type BLERecon struct { + session.SessionModule + gattDevice gatt.Device + quit chan bool +} + +func NewBLERecon(s *session.Session) *BLERecon { + d := &BLERecon{ + SessionModule: session.NewSessionModule("ble.recon", s), + gattDevice: nil, + quit: make(chan bool), + } + + d.AddHandler(session.NewModuleHandler("ble.recon on", "", + "Start Bluetooth Low Energy devices discovery.", + func(args []string) error { + return d.Start() + })) + + d.AddHandler(session.NewModuleHandler("ble.recon off", "", + "Stop Bluetooth Low Energy devices discovery.", + func(args []string) error { + return d.Stop() + })) + + d.AddHandler(session.NewModuleHandler("ble.show", "", + "Show discovered Bluetooth Low Energy devices.", + func(args []string) error { + return d.Show() + })) + + return d +} + +func (d BLERecon) Name() string { + return "ble.recon" +} + +func (d BLERecon) Description() string { + return "Bluetooth Low Energy devices discovery." +} + +func (d BLERecon) Author() string { + return "Simone Margaritelli " +} + +func (d *BLERecon) Configure() (err error) { + // hey Paypal GATT library, could you please just STFU?! + golog.SetOutput(ioutil.Discard) + + if d.gattDevice, err = gatt.NewDevice(defaultBLEClientOptions...); err != nil { + return err + } + return nil +} + +func (d *BLERecon) onStateChanged(dev gatt.Device, s gatt.State) { + switch s { + case gatt.StatePoweredOn: + log.Info("Starting BLE discovery ...") + dev.Scan([]gatt.UUID{}, true) + return + default: + log.Warning("Unexpected BLE state: %v", s) + } +} + +func (d *BLERecon) onPeriphDiscovered(p gatt.Peripheral, a *gatt.Advertisement, rssi int) { + d.Session.BLE.AddIfNew(p.ID(), p, a, rssi) +} + +func (d *BLERecon) pruner() { + log.Debug("Started BLE devices pruner ...") + + for d.Running() { + for _, dev := range d.Session.BLE.Devices() { + if time.Since(dev.LastSeen) > blePresentInterval { + d.Session.BLE.Remove(dev.Device.ID()) + } + } + + time.Sleep(5 * time.Second) + } +} + +func (d *BLERecon) Start() error { + if d.Running() { + return session.ErrAlreadyStarted + } else if err := d.Configure(); err != nil { + return err + } + + return d.SetRunning(true, func() { + log.Debug("Initializing BLE device ...") + + d.gattDevice.Handle(gatt.PeripheralDiscovered(d.onPeriphDiscovered)) + d.gattDevice.Init(d.onStateChanged) + + go d.pruner() + + <-d.quit + + log.Info("Stopping BLE scan ...") + + d.gattDevice.StopScanning() + }) +} + +func (d *BLERecon) getRow(dev *network.BLEDevice) []string { + address := network.NormalizeMac(dev.Device.ID()) + vendor := dev.Vendor + sinceSeen := time.Since(dev.LastSeen) + lastSeen := dev.LastSeen.Format("15:04:05") + + if sinceSeen <= bleAliveInterval { + lastSeen = core.Bold(lastSeen) + } else if sinceSeen > blePresentInterval { + lastSeen = core.Dim(lastSeen) + address = core.Dim(address) + } + + flags := make([]string, 0) + raw := uint8(0) + if len(dev.Advertisement.Flags) > 0 { + raw = uint8(dev.Advertisement.Flags[0]) + } + + bits := map[uint]string{ + 0: "LE Limited Discoverable", + 1: "LE General Discoverable", + 2: "BR/EDR", + 3: "LE + BR/EDR Controller Mode", + 4: "LE + BR/EDR Host Mode", + } + + for bit, desc := range bits { + if raw&(1< 0 { + d.showTable(columns, rows) + } + + d.Session.Refresh() + return nil +} + +func (d *BLERecon) Stop() error { + return d.SetRunning(false, func() { + d.quit <- true + }) +} diff --git a/modules/ble_recon_sort.go b/modules/ble_recon_sort.go new file mode 100644 index 00000000..04f485a0 --- /dev/null +++ b/modules/ble_recon_sort.go @@ -0,0 +1,13 @@ +package modules + +import ( + "github.com/bettercap/bettercap/network" +) + +type ByBLERSSISorter []*network.BLEDevice + +func (a ByBLERSSISorter) Len() int { return len(a) } +func (a ByBLERSSISorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByBLERSSISorter) Less(i, j int) bool { + return a[i].RSSI > a[j].RSSI +} diff --git a/modules/events_view.go b/modules/events_view.go index 1ce5847f..ce5c25ef 100644 --- a/modules/events_view.go +++ b/modules/events_view.go @@ -139,6 +139,50 @@ func (s EventsStream) viewSnifferEvent(e session.Event) { misc) } +func (s EventsStream) viewBLEEvent(e session.Event) { + if e.Tag == "ble.device.new" { + dev := e.Data.(*network.BLEDevice) + name := dev.Device.Name() + if name != "" { + name = " " + core.Bold(name) + } + vend := dev.Vendor + if vend != "" { + vend = fmt.Sprintf(" (%s)", core.Yellow(vend)) + } + + fmt.Printf("[%s] [%s] New BLE device%s detected as %s%s %s.\n", + e.Time.Format(eventTimeFormat), + core.Green(e.Tag), + name, + dev.Device.ID(), + vend, + core.Dim(fmt.Sprintf("%d dBm", dev.RSSI))) + } else if e.Tag == "ble.device.lost" { + dev := e.Data.(*network.BLEDevice) + name := dev.Device.Name() + if name != "" { + name = " " + core.Bold(name) + } + vend := dev.Vendor + if vend != "" { + vend = fmt.Sprintf(" (%s)", core.Yellow(vend)) + } + + fmt.Printf("[%s] [%s] BLE device%s %s%s lost.\n", + e.Time.Format(eventTimeFormat), + core.Green(e.Tag), + name, + dev.Device.ID(), + vend) + } else { + fmt.Printf("[%s] [%s] %v\n", + e.Time.Format(eventTimeFormat), + core.Green(e.Tag), + e.Data) + } +} + func (s EventsStream) viewSynScanEvent(e session.Event) { se := e.Data.(SynScanEvent) fmt.Printf("[%s] [%s] Found open port %d for %s\n", @@ -155,11 +199,13 @@ func (s *EventsStream) View(e session.Event, refresh bool) { s.viewEndpointEvent(e) } else if strings.HasPrefix(e.Tag, "wifi.") { s.viewWiFiEvent(e) + } else if strings.HasPrefix(e.Tag, "ble.") { + s.viewBLEEvent(e) } else if strings.HasPrefix(e.Tag, "mod.") { s.viewModuleEvent(e) } else if strings.HasPrefix(e.Tag, "net.sniff.") { s.viewSnifferEvent(e) - } else if strings.HasPrefix(e.Tag, "syn.scan") { + } else if strings.HasPrefix(e.Tag, "syn.scan.") { s.viewSynScanEvent(e) } else { fmt.Printf("[%s] [%s] %v\n", e.Time.Format(eventTimeFormat), core.Green(e.Tag), e) diff --git a/network/ble.go b/network/ble.go new file mode 100644 index 00000000..dce5a203 --- /dev/null +++ b/network/ble.go @@ -0,0 +1,97 @@ +package network + +import ( + "encoding/json" + "sync" + "time" + + "github.com/bettercap/gatt" +) + +type BLEDevNewCallback func(dev *BLEDevice) +type BLEDevLostCallback func(dev *BLEDevice) + +type BLE struct { + sync.RWMutex + devices map[string]*BLEDevice + newCb BLEDevNewCallback + lostCb BLEDevLostCallback +} + +type bleJSON struct { + Devices []*BLEDevice `json:"devices"` +} + +func NewBLE(newcb BLEDevNewCallback, lostcb BLEDevLostCallback) *BLE { + return &BLE{ + devices: make(map[string]*BLEDevice), + newCb: newcb, + lostCb: lostcb, + } +} + +func (b *BLE) MarshalJSON() ([]byte, error) { + doc := bleJSON{ + Devices: make([]*BLEDevice, 0), + } + + for _, dev := range b.Devices() { + doc.Devices = append(doc.Devices, dev) + } + + return json.Marshal(doc) +} + +func (b *BLE) Get(id string) (dev *BLEDevice, found bool) { + b.RLock() + defer b.RUnlock() + + dev, found = b.devices[id] + return +} + +func (b *BLE) AddIfNew(id string, p gatt.Peripheral, a *gatt.Advertisement, rssi int) *BLEDevice { + b.Lock() + defer b.Unlock() + + id = NormalizeMac(id) + if dev, found := b.devices[id]; found == true { + dev.LastSeen = time.Now() + dev.RSSI = rssi + dev.Advertisement = a + return dev + } + + newDev := NewBLEDevice(p, a, rssi) + b.devices[id] = newDev + + if b.newCb != nil { + b.newCb(newDev) + } + + return nil +} + +func (b *BLE) Remove(id string) { + b.Lock() + defer b.Unlock() + + id = NormalizeMac(id) + if dev, found := b.devices[id]; found == true { + delete(b.devices, id) + if b.lostCb != nil { + b.lostCb(dev) + } + } +} + +func (b *BLE) Devices() (devices []*BLEDevice) { + b.Lock() + defer b.Unlock() + + devices = make([]*BLEDevice, 0) + for _, dev := range b.devices { + devices = append(devices, dev) + } + return +} diff --git a/network/ble_device.go b/network/ble_device.go new file mode 100644 index 00000000..2398e772 --- /dev/null +++ b/network/ble_device.go @@ -0,0 +1,25 @@ +package network + +import ( + "time" + + "github.com/bettercap/gatt" +) + +type BLEDevice struct { + LastSeen time.Time + Device gatt.Peripheral + Vendor string + Advertisement *gatt.Advertisement + RSSI int +} + +func NewBLEDevice(p gatt.Peripheral, a *gatt.Advertisement, rssi int) *BLEDevice { + return &BLEDevice{ + LastSeen: time.Now(), + Device: p, + Vendor: OuiLookup(NormalizeMac(p.ID())), + Advertisement: a, + RSSI: rssi, + } +} diff --git a/network/net.go b/network/net.go index 41a9e246..1c463aec 100644 --- a/network/net.go +++ b/network/net.go @@ -39,7 +39,7 @@ func NormalizeMac(mac string) string { parts[i] = "0" + p } } - return strings.Join(parts, ":") + return strings.ToLower(strings.Join(parts, ":")) } func buildEndpointFromInterface(iface net.Interface) (*Endpoint, error) { diff --git a/session/session.go b/session/session.go index ff93bbe1..d175757b 100644 --- a/session/session.go +++ b/session/session.go @@ -40,6 +40,7 @@ type Session struct { Env *Environment `json:"env"` Lan *network.LAN `json:"lan"` WiFi *network.WiFi `json:"wifi"` + BLE *network.BLE `json:"ble"` Queue *packets.Queue `json:"packets"` Input *readline.Instance `json:"-"` StartedAt time.Time `json:"started_at"` @@ -374,6 +375,12 @@ func (s *Session) Start() error { s.Firewall = firewall.Make(s.Interface) + s.BLE = network.NewBLE(func(dev *network.BLEDevice) { + s.Events.Add("ble.device.new", dev) + }, func(dev *network.BLEDevice) { + s.Events.Add("ble.device.lost", dev) + }) + s.WiFi = network.NewWiFi(s.Interface, func(ap *network.AccessPoint) { s.Events.Add("wifi.ap.new", ap) }, func(ap *network.AccessPoint) {