From ec58c553704ea02fce9dcc46e869d7471f35f7a4 Mon Sep 17 00:00:00 2001 From: evilsocket Date: Tue, 27 Feb 2018 00:35:42 +0100 Subject: [PATCH] new: implemented ble.enum and refactored how tabular views work ( ref #74 ) --- core/table.go | 108 +++++++++++++++ modules/ble_options_darwin.go | 2 +- modules/ble_options_linux.go | 4 +- modules/ble_recon.go | 242 +++++++++++++++++++++++++++++++--- modules/events_view_ble.go | 9 +- modules/net_recon_show.go | 12 +- modules/wifi_recon.go | 12 +- network/ble.go | 2 +- network/ble_device.go | 2 +- 9 files changed, 341 insertions(+), 52 deletions(-) create mode 100644 core/table.go diff --git a/core/table.go b/core/table.go new file mode 100644 index 00000000..3f132d37 --- /dev/null +++ b/core/table.go @@ -0,0 +1,108 @@ +package core + +import ( + "fmt" + "io" + "regexp" + "strings" +) + +var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]") + +func viewLen(s string) int { + for _, m := range ansi.FindAllString(s, -1) { + s = strings.Replace(s, m, "", -1) + } + + return len(s) +} + +func maxLen(strings []string) int { + maxLen := 0 + for _, s := range strings { + len := viewLen(s) + if len > maxLen { + maxLen = len + } + } + return maxLen +} + +type Alignment int + +const ( + AlignLeft = Alignment(0) + AlignCenter = Alignment(1) + AlignRight = Alignment(2) +) + +func getPads(s string, maxLen int, align Alignment) (lPad int, rPad int) { + len := viewLen(s) + diff := maxLen - len + + if align == AlignLeft { + lPad = 0 + rPad = diff - lPad + 1 + } else if align == AlignCenter { + lPad = diff / 2 + rPad = diff - lPad + 1 + } else { + // TODO + } + + return +} + +func padded(s string, maxLen int, align Alignment) string { + lPad, rPad := getPads(s, maxLen, align) + return fmt.Sprintf("%s%s%s", strings.Repeat(" ", lPad), s, strings.Repeat(" ", rPad)) +} + +func AsTable(w io.Writer, columns []string, rows [][]string) { + + for i, col := range columns { + columns[i] = fmt.Sprintf(" %s ", col) + } + + for i, row := range rows { + for j, cell := range row { + rows[i][j] = fmt.Sprintf(" %s ", cell) + } + } + + colPaddings := make([]int, 0) + lineSep := "" + for colIndex, colHeader := range columns { + column := []string{colHeader} + for _, row := range rows { + column = append(column, row[colIndex]) + } + mLen := maxLen(column) + colPaddings = append(colPaddings, mLen) + lineSep += fmt.Sprintf("+%s", strings.Repeat("-", mLen+1)) + } + lineSep += "+" + + table := "" + + // header + table += fmt.Sprintf("%s\n", lineSep) + for colIndex, colHeader := range columns { + table += fmt.Sprintf("|%s", padded(colHeader, colPaddings[colIndex], AlignCenter)) + } + table += fmt.Sprintf("|\n") + table += fmt.Sprintf("%s\n", lineSep) + + // rows + for _, row := range rows { + for colIndex, cell := range row { + table += fmt.Sprintf("|%s", padded(cell, colPaddings[colIndex], AlignLeft)) + } + table += fmt.Sprintf("|\n") + } + + // footer + table += lineSep + + fmt.Fprintf(w, "\n%s\n", table) +} diff --git a/modules/ble_options_darwin.go b/modules/ble_options_darwin.go index 854f79c8..4d4b6fe4 100644 --- a/modules/ble_options_darwin.go +++ b/modules/ble_options_darwin.go @@ -1,6 +1,6 @@ package modules -import "github.com/bettercap/gatt" +import "github.com/currantlabs/gatt" var defaultBLEClientOptions = []gatt.Option{ gatt.MacDeviceRole(gatt.CentralManager), diff --git a/modules/ble_options_linux.go b/modules/ble_options_linux.go index 38dc0872..138f5657 100644 --- a/modules/ble_options_linux.go +++ b/modules/ble_options_linux.go @@ -1,8 +1,8 @@ package modules import ( - "github.com/bettercap/gatt" - "github.com/bettercap/gatt/linux/cmd" + "github.com/currantlabs/gatt" + "github.com/currantlabs/gatt/linux/cmd" ) var defaultBLEClientOptions = []gatt.Option{ diff --git a/modules/ble_recon.go b/modules/ble_recon.go index 2de824cd..c9549807 100644 --- a/modules/ble_recon.go +++ b/modules/ble_recon.go @@ -8,6 +8,7 @@ import ( golog "log" "os" "sort" + "strconv" "strings" "time" @@ -16,9 +17,7 @@ import ( "github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/session" - "github.com/bettercap/gatt" - - "github.com/olekukonko/tablewriter" + "github.com/currantlabs/gatt" ) var ( @@ -28,8 +27,11 @@ var ( type BLERecon struct { session.SessionModule - gattDevice gatt.Device - quit chan bool + gattDevice gatt.Device + currDevice *network.BLEDevice + connected bool + connTimeout time.Duration + quit chan bool } func NewBLERecon(s *session.Session) *BLERecon { @@ -37,6 +39,9 @@ func NewBLERecon(s *session.Session) *BLERecon { SessionModule: session.NewSessionModule("ble.recon", s), gattDevice: nil, quit: make(chan bool), + connTimeout: time.Duration(10) * time.Second, + currDevice: nil, + connected: false, } d.AddHandler(session.NewModuleHandler("ble.recon on", "", @@ -57,6 +62,16 @@ func NewBLERecon(s *session.Session) *BLERecon { return d.Show() })) + d.AddHandler(session.NewModuleHandler("ble.enum MAC", "ble.enum ([a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2})", + "Enumerate services and characteristics for the given BLE device.", + func(args []string) error { + if d.isEnumerating() == true { + return fmt.Errorf("An enumeration for %s is already running, please wait.", d.currDevice.Device.ID()) + } + + return d.enumAll(network.NormalizeMac(args[0])) + })) + return d } @@ -72,6 +87,10 @@ func (d BLERecon) Author() string { return "Simone Margaritelli " } +func (d *BLERecon) isEnumerating() bool { + return d.currDevice != nil +} + func (d *BLERecon) Configure() (err error) { if d.gattDevice == nil { // hey Paypal GATT library, could you please just STFU?! @@ -80,7 +99,11 @@ func (d *BLERecon) Configure() (err error) { return err } - d.gattDevice.Handle(gatt.PeripheralDiscovered(d.onPeriphDiscovered)) + d.gattDevice.Handle( + gatt.PeripheralDiscovered(d.onPeriphDiscovered), + gatt.PeripheralConnected(d.onPeriphConnected), + gatt.PeripheralDisconnected(d.onPeriphDisconnected), + ) d.gattDevice.Init(d.onStateChanged) } @@ -149,9 +172,9 @@ func (d *BLERecon) getRow(dev *network.BLEDevice) []string { address = core.Dim(address) } - isConnectable := core.Red("✕") + isConnectable := core.Red("no") if dev.Advertisement.Connectable == true { - isConnectable = core.Green("✓") + isConnectable = core.Green("yes") } return []string{ @@ -159,21 +182,11 @@ func (d *BLERecon) getRow(dev *network.BLEDevice) []string { address, dev.Device.Name(), vendor, - strings.Join(dev.Advertisement.Flags, ", "), isConnectable, lastSeen, } } -func (d *BLERecon) showTable(header []string, rows [][]string) { - fmt.Println() - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(header) - table.SetColWidth(80) - table.AppendBulk(rows) - table.Render() -} - func (d *BLERecon) Show() error { devices := d.Session.BLE.Devices() @@ -185,16 +198,205 @@ func (d *BLERecon) Show() error { } nrows := len(rows) - columns := []string{"RSSI", "Address", "Name", "Vendor", "Flags", "Connectable", "Last Seen"} + columns := []string{"RSSI", "Address", "Name", "Vendor", "Connectable", "Last Seen"} if nrows > 0 { - d.showTable(columns, rows) + core.AsTable(os.Stdout, columns, rows) } d.Session.Refresh() return nil } +func (d *BLERecon) setCurrentDevice(dev *network.BLEDevice) { + d.connected = false + d.currDevice = dev +} + +func (d *BLERecon) onPeriphDisconnected(p gatt.Peripheral, err error) { + if d.Running() { + // restore scanning + log.Info("Device disconnected, restoring BLE discovery.") + d.setCurrentDevice(nil) + d.gattDevice.Scan([]gatt.UUID{}, true) + } +} + +func (d *BLERecon) onPeriphConnected(p gatt.Peripheral, err error) { + defer func() { + log.Info("Disconnecting from %s ...", p.ID()) + p.Device().CancelConnection(p) + }() + + // timed out + if d.currDevice == nil { + log.Debug("Connected to %s but after the timeout :(", p.ID()) + return + } + + d.connected = true + + d.Session.Events.Add("ble.device.connected", d.currDevice) + + /* + if err := p.SetMTU(500); err != nil { + log.Warning("Failed to set MTU: %s", err) + } */ + + log.Info("Enumerating all the things for %s!", p.ID()) + services, err := p.DiscoverServices(nil) + if err != nil { + log.Error("Error discovering services: %s", err) + return + } + + d.showServices(p, services) +} + +func parseProperties(ch *gatt.Characteristic) (props []string, isReadable bool) { + isReadable = false + props = make([]string, 0) + mask := ch.Properties() + + if (mask & gatt.CharBroadcast) != 0 { + props = append(props, "bcast") + } + if (mask & gatt.CharRead) != 0 { + isReadable = true + props = append(props, "read") + } + if (mask&gatt.CharWriteNR) != 0 || (mask&gatt.CharWrite) != 0 { + props = append(props, core.Bold("write")) + } + if (mask & gatt.CharNotify) != 0 { + props = append(props, "notify") + } + if (mask & gatt.CharIndicate) != 0 { + props = append(props, "indicate") + } + if (mask & gatt.CharSignedWrite) != 0 { + props = append(props, core.Yellow("*write")) + } + if (mask & gatt.CharExtended) != 0 { + props = append(props, "x") + } + + return +} + +func parseRawData(raw []byte) string { + s := "" + for _, b := range raw { + if b != 00 && strconv.IsPrint(rune(b)) == false { + return fmt.Sprintf("%x", raw) + } else if b == 0 { + break + } else { + s += fmt.Sprintf("%c", b) + } + } + + return core.Yellow(s) +} + +func (d *BLERecon) showServices(p gatt.Peripheral, services []*gatt.Service) { + columns := []string{"Handles", "Service > Characteristics", "Properties", "Data"} + rows := make([][]string, 0) + + for _, svc := range services { + d.Session.Events.Add("ble.device.service.discovered", svc) + + name := svc.Name() + if name == "" { + name = svc.UUID().String() + } else { + name = fmt.Sprintf("%s (%s)", core.Green(name), core.Dim(svc.UUID().String())) + } + + row := []string{ + fmt.Sprintf("%04x -> %04x", svc.Handle(), svc.EndHandle()), + name, + "", + "", + } + + rows = append(rows, row) + + chars, err := p.DiscoverCharacteristics(nil, svc) + if err != nil { + log.Error("Error while enumerating chars for service %s: %s", svc.UUID(), err) + continue + } + + for _, ch := range chars { + d.Session.Events.Add("ble.device.characteristic.discovered", ch) + + name = ch.Name() + if name == "" { + name = " " + ch.UUID().String() + } else { + name = fmt.Sprintf(" %s (%s)", core.Green(name), core.Dim(ch.UUID().String())) + } + + props, isReadable := parseProperties(ch) + + data := "" + if isReadable { + raw, err := p.ReadCharacteristic(ch) + if err != nil { + data = core.Red(err.Error()) + } else { + data = parseRawData(raw) + } + } + + row := []string{ + fmt.Sprintf("%04x", ch.Handle()), + name, + strings.Join(props, ", "), + data, + } + + rows = append(rows, row) + } + } + + core.AsTable(os.Stdout, columns, rows) + d.Session.Refresh() +} + +func (d *BLERecon) enumAll(mac string) error { + dev, found := d.Session.BLE.Get(mac) + if found == false { + return fmt.Errorf("BLE device with address %s not found.", mac) + } + + services := dev.Device.Services() + if len(services) > 0 { + d.showServices(dev.Device, services) + } else { + d.setCurrentDevice(dev) + + log.Info("Connecting to %s ...", mac) + + if d.Running() { + d.gattDevice.StopScanning() + } + + go func() { + time.Sleep(d.connTimeout) + if d.currDevice != nil && d.connected == false { + d.Session.Events.Add("ble.connection.timeout", d.currDevice) + d.onPeriphDisconnected(nil, nil) + } + }() + + d.gattDevice.Connect(dev.Device) + } + + return nil +} + func (d *BLERecon) Stop() error { return d.SetRunning(false, func() { d.quit <- true diff --git a/modules/events_view_ble.go b/modules/events_view_ble.go index a701c2bc..90d898c9 100644 --- a/modules/events_view_ble.go +++ b/modules/events_view_ble.go @@ -46,10 +46,9 @@ func (s EventsStream) viewBLEEvent(e session.Event) { name, dev.Device.ID(), vend) - } else { - fmt.Printf("[%s] [%s] %v\n", + } /* else { + fmt.Printf("[%s] [%s]\n", e.Time.Format(eventTimeFormat), - core.Green(e.Tag), - e.Data) - } + core.Green(e.Tag)) + } */ } diff --git a/modules/net_recon_show.go b/modules/net_recon_show.go index 0ed426b5..d681da17 100644 --- a/modules/net_recon_show.go +++ b/modules/net_recon_show.go @@ -12,7 +12,6 @@ import ( "github.com/bettercap/bettercap/packets" "github.com/dustin/go-humanize" - "github.com/olekukonko/tablewriter" ) var ( @@ -114,15 +113,6 @@ func (d *Discovery) getRow(e *network.Endpoint, withMeta bool) []string { return row } -func (d *Discovery) showTable(header []string, rows [][]string) { - fmt.Println() - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(header) - table.SetColWidth(180) - table.AppendBulk(rows) - table.Render() -} - func (d *Discovery) Show(by string) error { targets := d.Session.Lan.List() if by == "seen" { @@ -166,7 +156,7 @@ func (d *Discovery) Show(by string) error { } } - d.showTable(colNames, rows) + core.AsTable(os.Stdout, colNames, rows) d.Session.Queue.Stats.RLock() fmt.Printf("\n%s %s / %s %s / %d pkts / %d errs\n\n", diff --git a/modules/wifi_recon.go b/modules/wifi_recon.go index 17d2f0cf..a72b869c 100644 --- a/modules/wifi_recon.go +++ b/modules/wifi_recon.go @@ -19,7 +19,6 @@ import ( "github.com/google/gopacket/pcap" "github.com/dustin/go-humanize" - "github.com/olekukonko/tablewriter" ) var maxStationTTL = 5 * time.Minute @@ -218,15 +217,6 @@ func mhz2chan(freq int) int { return 0 } -func (w *WiFiRecon) showTable(header []string, rows [][]string) { - fmt.Println() - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(header) - table.SetColWidth(80) - table.AppendBulk(rows) - table.Render() -} - func (w *WiFiRecon) isApSelected() bool { return w.ap != nil } @@ -274,7 +264,7 @@ func (w *WiFiRecon) Show(by string) error { } if nrows > 0 { - w.showTable(columns, rows) + core.AsTable(os.Stdout, columns, rows) } w.Session.Refresh() diff --git a/network/ble.go b/network/ble.go index 3c6d73b9..c28aec7e 100644 --- a/network/ble.go +++ b/network/ble.go @@ -7,7 +7,7 @@ import ( "sync" "time" - "github.com/bettercap/gatt" + "github.com/currantlabs/gatt" ) type BLEDevNewCallback func(dev *BLEDevice) diff --git a/network/ble_device.go b/network/ble_device.go index cadce4ce..331f2176 100644 --- a/network/ble_device.go +++ b/network/ble_device.go @@ -5,7 +5,7 @@ package network import ( "time" - "github.com/bettercap/gatt" + "github.com/currantlabs/gatt" ) type BLEDevice struct {