diff --git a/modules/events_stream/events_view.go b/modules/events_stream/events_view.go index 8d13312e..06dad9e3 100644 --- a/modules/events_stream/events_view.go +++ b/modules/events_stream/events_view.go @@ -160,6 +160,8 @@ func (mod *EventsStream) View(e session.Event, refresh bool) { mod.viewWiFiEvent(e) } else if strings.HasPrefix(e.Tag, "ble.") { mod.viewBLEEvent(e) + } else if strings.HasPrefix(e.Tag, "hid.") { + mod.viewHIDEvent(e) } else if strings.HasPrefix(e.Tag, "mod.") { mod.viewModuleEvent(e) } else if strings.HasPrefix(e.Tag, "net.sniff.") { diff --git a/modules/events_stream/events_view_hid.go b/modules/events_stream/events_view_hid.go new file mode 100644 index 00000000..f603b30b --- /dev/null +++ b/modules/events_stream/events_view_hid.go @@ -0,0 +1,26 @@ +package events_stream + +import ( + "fmt" + + "github.com/bettercap/bettercap/network" + "github.com/bettercap/bettercap/session" + + "github.com/evilsocket/islazy/tui" +) + +func (mod *EventsStream) viewHIDEvent(e session.Event) { + dev := e.Data.(*network.HIDDevice) + if e.Tag == "hid.device.new" { + fmt.Fprintf(mod.output, "[%s] [%s] new HID device %s detected on channel %s.\n", + e.Time.Format(eventTimeFormat), + tui.Green(e.Tag), + tui.Bold(dev.Address), + dev.Channels()) + } else if e.Tag == "hid.device.lost" { + fmt.Fprintf(mod.output, "[%s] [%s] HID device %s lost.\n", + e.Time.Format(eventTimeFormat), + tui.Green(e.Tag), + tui.Red(dev.Address)) + } +} diff --git a/modules/hid_recon/hid_recon.go b/modules/hid_recon/hid_recon.go new file mode 100644 index 00000000..a2e06da8 --- /dev/null +++ b/modules/hid_recon/hid_recon.go @@ -0,0 +1,262 @@ +package hid_recon + +import ( + "sync" + "time" + + "github.com/bettercap/bettercap/modules/utils" + "github.com/bettercap/bettercap/network" + "github.com/bettercap/bettercap/session" + + "github.com/bettercap/nrf24" + + "github.com/evilsocket/islazy/tui" +) + +type HIDRecon struct { + session.SessionModule + dongle *nrf24.Dongle + waitGroup *sync.WaitGroup + channel int + hopPeriod time.Duration + pingPeriod time.Duration + sniffPeriod time.Duration + lastHop time.Time + lastPing time.Time + useLNA bool + sniffLock *sync.Mutex + sniffAddrRaw []byte + sniffAddr string + pingPayload []byte + inSniffMode bool + inPromMode bool + selector *utils.ViewSelector +} + +func NewHIDRecon(s *session.Session) *HIDRecon { + mod := &HIDRecon{ + SessionModule: session.NewSessionModule("hid.recon", s), + waitGroup: &sync.WaitGroup{}, + sniffLock: &sync.Mutex{}, + hopPeriod: 100 * time.Millisecond, + pingPeriod: 100 * time.Millisecond, + sniffPeriod: 500 * time.Millisecond, + lastHop: time.Now(), + lastPing: time.Now(), + useLNA: true, + channel: 1, + sniffAddrRaw: nil, + sniffAddr: "", + inSniffMode: false, + inPromMode: false, + pingPayload: []byte{0x0f, 0x0f, 0x0f, 0x0f}, + } + + mod.AddHandler(session.NewModuleHandler("hid.recon on", "", + "Start HID recon.", + func(args []string) error { + return mod.Start() + })) + + mod.AddHandler(session.NewModuleHandler("hid.recon off", "", + "Stop HID recon.", + func(args []string) error { + return mod.Stop() + })) + + sniff := session.NewModuleHandler("hid.sniff ADDRESS", `(?i)^hid\.sniff ([a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}:[a-f0-9]{2}|clear)$`, + "TODO TODO", + func(args []string) error { + return mod.setSniffMode(args[0]) + }) + + sniff.Complete("hid.sniff", s.HIDCompleter) + + mod.AddHandler(sniff) + + mod.AddHandler(session.NewModuleHandler("hid.show", "", + "TODO TODO", + func(args []string) error { + return mod.Show() + })) + + mod.selector = utils.ViewSelectorFor(&mod.SessionModule, "hid.show", []string{"mac", "seen"}, "mac desc") + + return mod +} + +func (mod HIDRecon) Name() string { + return "hid.recon" +} + +func (mod HIDRecon) Description() string { + return "TODO TODO" +} + +func (mod HIDRecon) Author() string { + return "Simone Margaritelli " +} + +func (mod *HIDRecon) Configure() error { + var err error + + if mod.dongle, err = nrf24.Open(); err != nil { + return err + } + + mod.Debug("using device %s", mod.dongle.String()) + + if mod.useLNA { + if err = mod.dongle.EnableLNA(); err != nil { + return err + } + mod.Debug("LNA enabled") + } + + return nil +} + +func (mod *HIDRecon) setSniffMode(mode string) error { + mod.sniffLock.Lock() + defer mod.sniffLock.Unlock() + + mod.inSniffMode = false + if mode == "clear" { + mod.Debug("restoring recon mode") + mod.sniffAddrRaw = nil + mod.sniffAddr = "" + } else { + if err, raw := nrf24.ConvertAddress(mode); err != nil { + return err + } else { + mod.Info("sniffing device %s ...", tui.Bold(mode)) + mod.sniffAddr = network.NormalizeHIDAddress(mode) + mod.sniffAddrRaw = raw + } + } + return nil +} + +func (mod *HIDRecon) doHopping() { + if mod.inPromMode == false { + if err := mod.dongle.EnterPromiscMode(); err != nil { + mod.Error("error entering promiscuous mode: %v", err) + } else { + mod.inSniffMode = false + mod.inPromMode = true + mod.Info("device entered promiscuous mode") + } + } + + if time.Since(mod.lastHop) >= mod.hopPeriod { + mod.channel++ + if mod.channel > nrf24.TopChannel { + mod.channel = 1 + } + if err := mod.dongle.SetChannel(mod.channel); err != nil { + mod.Warning("error hopping on channel %d: %v", mod.channel, err) + } else { + mod.lastHop = time.Now() + } + } +} + +func (mod *HIDRecon) doPing() { + if mod.inSniffMode == false { + if err := mod.dongle.EnterSnifferModeFor(mod.sniffAddrRaw); err != nil { + mod.Error("error entering sniffer mode for %s: %v", mod.sniffAddr, err) + } else { + mod.inSniffMode = true + mod.inPromMode = false + mod.Info("device entered sniffer mode for %s", mod.sniffAddr) + } + } + + if time.Since(mod.lastPing) >= mod.pingPeriod { + // try on the current channel first + if err := mod.dongle.TransmitPayload(mod.pingPayload, 250, 1); err != nil { + for mod.channel = 1; mod.channel <= nrf24.TopChannel; mod.channel++ { + if err := mod.dongle.SetChannel(mod.channel); err != nil { + mod.Error("error setting channel %d: %v", mod.channel, err) + } else if err = mod.dongle.TransmitPayload(mod.pingPayload, 250, 1); err == nil { + mod.lastPing = time.Now() + return + } + } + } + } +} + +func (mod *HIDRecon) Start() error { + if err := mod.Configure(); err != nil { + return err + } + + return mod.SetRunning(true, func() { + mod.waitGroup.Add(1) + defer mod.waitGroup.Done() + + mod.Info("hopping on %d channels every %s", nrf24.TopChannel, mod.hopPeriod) + for mod.Running() { + isSniffing := mod.sniffAddrRaw != nil + if !isSniffing { + mod.doHopping() + } else { + mod.doPing() + } + + buf, err := mod.dongle.ReceivePayload() + if err != nil { + mod.Warning("error receiving payload from channel %d: %v", mod.channel, err) + continue + } + + sz := len(buf) + if isSniffing { + if sz > 0 && buf[0] == 0x00 { + buf = buf[1:] + mod.Debug("sniffed payload %x for %s", buf, mod.sniffAddr) + + if dev, found := mod.Session.HID.Get(mod.sniffAddr); found { + dev.LastSeen = time.Now() + dev.AddPayload(buf) + dev.AddChannel(mod.channel) + } else { + mod.Warning("got a payload for unknown device %s", mod.sniffAddr) + } + } + } else { + if sz >= 5 { + addr, payload := buf[0:5], buf[5:] + mod.Debug("detected device %x on channel %d (payload:%x)\n", addr, mod.channel, payload) + if isNew, dev := mod.Session.HID.AddIfNew(addr, mod.channel, payload); isNew { + // sniff for a while in order to detect the device type + go func() { + defer func() { + mod.sniffLock.Unlock() + mod.setSniffMode("clear") + }() + + mod.setSniffMode(dev.Address) + // make sure nobody can sniff to another + // address until we're not done here... + mod.sniffLock.Lock() + + time.Sleep(mod.sniffPeriod) + }() + } + } + } + } + + mod.Debug("stopped") + }) +} + +func (mod *HIDRecon) Stop() error { + return mod.SetRunning(false, func() { + mod.waitGroup.Wait() + mod.dongle.Close() + mod.Debug("device closed") + }) +} diff --git a/modules/hid_recon/hid_show.go b/modules/hid_recon/hid_show.go new file mode 100644 index 00000000..986cf379 --- /dev/null +++ b/modules/hid_recon/hid_show.go @@ -0,0 +1,125 @@ +package hid_recon + +import ( + "fmt" + "os" + "sort" + "time" + + "github.com/bettercap/bettercap/network" + + "github.com/dustin/go-humanize" + + "github.com/evilsocket/islazy/tui" +) + +var ( + AliveTimeInterval = time.Duration(5) * time.Minute + PresentTimeInterval = time.Duration(1) * time.Minute + JustJoinedTimeInterval = time.Duration(10) * time.Second +) + +func (mod *HIDRecon) getRow(dev *network.HIDDevice) []string { + sinceLastSeen := time.Since(dev.LastSeen) + seen := dev.LastSeen.Format("15:04:05") + + if sinceLastSeen <= JustJoinedTimeInterval { + seen = tui.Bold(seen) + } else if sinceLastSeen > PresentTimeInterval { + seen = tui.Dim(seen) + } + + return []string{ + dev.Address, + dev.Type.String(), + dev.Channels(), + humanize.Bytes(dev.PayloadsSize()), + seen, + } +} + +func (mod *HIDRecon) doFilter(dev *network.HIDDevice) bool { + if mod.selector.Expression == nil { + return true + } + return mod.selector.Expression.MatchString(dev.Address) +} + +func (mod *HIDRecon) doSelection() (err error, devices []*network.HIDDevice) { + if err = mod.selector.Update(); err != nil { + return + } + + devices = mod.Session.HID.Devices() + filtered := []*network.HIDDevice{} + for _, dev := range devices { + if mod.doFilter(dev) { + filtered = append(filtered, dev) + } + } + devices = filtered + + switch mod.selector.SortField { + case "mac": + sort.Sort(ByHIDMacSorter(devices)) + case "seen": + sort.Sort(ByHIDSeenSorter(devices)) + } + + // default is asc + if mod.selector.Sort == "desc" { + // from https://github.com/golang/go/wiki/SliceTricks + for i := len(devices)/2 - 1; i >= 0; i-- { + opp := len(devices) - 1 - i + devices[i], devices[opp] = devices[opp], devices[i] + } + } + + if mod.selector.Limit > 0 { + limit := mod.selector.Limit + max := len(devices) + if limit > max { + limit = max + } + devices = devices[0:limit] + } + + return +} + +func (mod *HIDRecon) colNames() []string { + colNames := []string{"MAC", "Type", "Channels", "Data", "Seen"} + switch mod.selector.SortField { + case "mac": + colNames[0] += " " + mod.selector.SortSymbol + case "seen": + colNames[4] += " " + mod.selector.SortSymbol + } + return colNames +} + +func (mod *HIDRecon) Show() (err error) { + var devices []*network.HIDDevice + if err, devices = mod.doSelection(); err != nil { + return + } + + rows := make([][]string, 0) + for _, dev := range devices { + rows = append(rows, mod.getRow(dev)) + } + + tui.Table(os.Stdout, mod.colNames(), rows) + + if mod.sniffAddrRaw == nil { + fmt.Printf("\nchannel:%d\n\n", mod.channel) + } else { + fmt.Printf("\nchannel:%d sniffing:%s\n\n", mod.channel, mod.sniffAddr) + } + + if len(rows) > 0 { + mod.Session.Refresh() + } + + return nil +} diff --git a/modules/hid_recon/hid_show_sort.go b/modules/hid_recon/hid_show_sort.go new file mode 100644 index 00000000..57bb2cc3 --- /dev/null +++ b/modules/hid_recon/hid_show_sort.go @@ -0,0 +1,19 @@ +package hid_recon + +import ( + "github.com/bettercap/bettercap/network" +) + +type ByHIDMacSorter []*network.HIDDevice + +func (a ByHIDMacSorter) Len() int { return len(a) } +func (a ByHIDMacSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByHIDMacSorter) Less(i, j int) bool { + return a[i].Address < a[j].Address +} + +type ByHIDSeenSorter []*network.HIDDevice + +func (a ByHIDSeenSorter) Len() int { return len(a) } +func (a ByHIDSeenSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByHIDSeenSorter) Less(i, j int) bool { return a[i].LastSeen.Before(a[j].LastSeen) } diff --git a/modules/modules.go b/modules/modules.go index 951b08bd..f1ba1365 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -10,6 +10,7 @@ import ( "github.com/bettercap/bettercap/modules/dns_spoof" "github.com/bettercap/bettercap/modules/events_stream" "github.com/bettercap/bettercap/modules/gps" + "github.com/bettercap/bettercap/modules/hid_recon" "github.com/bettercap/bettercap/modules/http_proxy" "github.com/bettercap/bettercap/modules/http_server" "github.com/bettercap/bettercap/modules/https_proxy" @@ -56,4 +57,5 @@ func LoadModules(sess *session.Session) { sess.Register(update.NewUpdateModule(sess)) sess.Register(wifi.NewWiFiModule(sess)) sess.Register(wol.NewWOL(sess)) + sess.Register(hid_recon.NewHIDRecon(sess)) } diff --git a/network/hid.go b/network/hid.go new file mode 100644 index 00000000..e84c73ba --- /dev/null +++ b/network/hid.go @@ -0,0 +1,91 @@ +package network + +import ( + "sync" + "time" +) + +type HIDDevNewCallback func(dev *HIDDevice) +type HIDDevLostCallback func(dev *HIDDevice) + +type HID struct { + sync.RWMutex + devices map[string]*HIDDevice + newCb HIDDevNewCallback + lostCb HIDDevLostCallback +} + +func NewHID(newcb HIDDevNewCallback, lostcb HIDDevLostCallback) *HID { + return &HID{ + devices: make(map[string]*HIDDevice), + newCb: newcb, + lostCb: lostcb, + } +} + +func (b *HID) Get(id string) (dev *HIDDevice, found bool) { + b.RLock() + defer b.RUnlock() + dev, found = b.devices[id] + return +} + +func (b *HID) AddIfNew(address []byte, channel int, payload []byte) (bool, *HIDDevice) { + b.Lock() + defer b.Unlock() + + id := HIDAddress(address) + if dev, found := b.devices[id]; found { + dev.LastSeen = time.Now() + dev.AddChannel(channel) + dev.AddPayload(payload) + return false, dev + } + + newDev := NewHIDDevice(address, channel, payload) + b.devices[id] = newDev + + if b.newCb != nil { + b.newCb(newDev) + } + + return true, newDev +} + +func (b *HID) Remove(id string) { + b.Lock() + defer b.Unlock() + + if dev, found := b.devices[id]; found { + delete(b.devices, id) + if b.lostCb != nil { + b.lostCb(dev) + } + } +} + +func (b *HID) Devices() (devices []*HIDDevice) { + b.Lock() + defer b.Unlock() + + devices = make([]*HIDDevice, 0) + for _, dev := range b.devices { + devices = append(devices, dev) + } + return +} + +func (b *HID) EachDevice(cb func(mac string, d *HIDDevice)) { + b.Lock() + defer b.Unlock() + + for m, dev := range b.devices { + cb(m, dev) + } +} + +func (b *HID) Clear() { + b.Lock() + defer b.Unlock() + b.devices = make(map[string]*HIDDevice) +} diff --git a/network/hid_device.go b/network/hid_device.go new file mode 100644 index 00000000..ba043ba9 --- /dev/null +++ b/network/hid_device.go @@ -0,0 +1,160 @@ +package network + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" +) + +type HIDType int + +const ( + HIDTypeUnknown HIDType = 0 + HIDTypeLogitech HIDType = 1 + HIDTypeAmazon HIDType = 2 + HIDTypeMicrosoft HIDType = 3 + HIDTypeDell HIDType = 4 +) + +func (t HIDType) String() string { + switch t { + case HIDTypeLogitech: + return "Logitech" + case HIDTypeAmazon: + return "Amazon" + case HIDTypeMicrosoft: + return "Microsoft" + case HIDTypeDell: + return "Dell" + } + return "" +} + +type HIDPayload []byte + +type HIDDevice struct { + sync.Mutex + LastSeen time.Time + Type HIDType + Address string + RawAddress []byte + channels map[int]bool + payloads []HIDPayload + payloadsSz uint64 +} + +func NormalizeHIDAddress(address string) string { + parts := strings.Split(address, ":") + for i, p := range parts { + if len(p) < 2 { + parts[i] = "0" + p + } + } + return strings.ToLower(strings.Join(parts, ":")) + +} + +func HIDAddress(address []byte) string { + octects := []string{} + for _, b := range address { + octects = append(octects, fmt.Sprintf("%02x", b)) + } + return strings.ToLower(strings.Join(octects, ":")) +} + +func NewHIDDevice(address []byte, channel int, payload []byte) *HIDDevice { + dev := &HIDDevice{ + LastSeen: time.Now(), + Type: HIDTypeUnknown, + RawAddress: address, + Address: HIDAddress(address), + channels: make(map[int]bool), + payloads: make([]HIDPayload, 0), + payloadsSz: 0, + } + + dev.AddChannel(channel) + dev.AddPayload(payload) + + return dev +} + +func (dev *HIDDevice) AddChannel(ch int) { + dev.Lock() + defer dev.Unlock() + + dev.channels[ch] = true +} + +func (dev *HIDDevice) Channels() string { + dev.Lock() + defer dev.Unlock() + + chans := []string{} + for ch, _ := range dev.channels { + chans = append(chans, fmt.Sprintf("%d", ch)) + } + + sort.Strings(chans) + return strings.Join(chans, ",") +} + +// credits to https://github.com/insecurityofthings/jackit/tree/master/jackit/plugins +func (dev *HIDDevice) onEventFrame(p []byte, sz int) { + // return if type has been already determined + if dev.Type != HIDTypeUnknown { + return + } + + if sz == 6 { + dev.Type = HIDTypeAmazon + return + } + + if sz == 10 && p[0] == 0x00 && p[1] == 0xc2 { + dev.Type = HIDTypeLogitech // mouse movement + return + } else if sz == 22 && p[0] == 0x00 && p[1] == 0xd3 { + dev.Type = HIDTypeLogitech // keystroke + return + } else if sz == 5 && p[0] == 0x00 && p[1] == 0x40 { + dev.Type = HIDTypeLogitech // keepalive + return + } else if sz == 10 && p[0] == 0x00 && p[0] == 0x4f { + dev.Type = HIDTypeLogitech // sleep timer + return + } + + if sz == 19 && (p[0] == 0x08 || p[0] == 0x0c) && p[6] == 0x40 { + dev.Type = HIDTypeMicrosoft + return + } + + // TODO: Dell +} + +func (dev *HIDDevice) AddPayload(payload []byte) { + dev.Lock() + defer dev.Unlock() + + sz := len(payload) + if payload != nil && sz > 0 { + dev.payloads = append(dev.payloads, payload) + dev.payloadsSz += uint64(sz) + dev.onEventFrame(payload, sz) + } +} + +func (dev *HIDDevice) NumPayloads() int { + dev.Lock() + defer dev.Unlock() + return len(dev.payloads) +} + +func (dev *HIDDevice) PayloadsSize() uint64 { + dev.Lock() + defer dev.Unlock() + return dev.payloadsSz +} diff --git a/session/session.go b/session/session.go index 79053290..c531b2e8 100644 --- a/session/session.go +++ b/session/session.go @@ -81,6 +81,7 @@ type Session struct { Lan *network.LAN `json:"lan"` WiFi *network.WiFi `json:"wifi"` BLE *network.BLE `json:"ble"` + HID *network.HID `json:"hid"` Queue *packets.Queue `json:"packets"` StartedAt time.Time `json:"started_at"` Active bool `json:"active"` @@ -199,6 +200,16 @@ func (s *Session) BLECompleter(prefix string) []string { return macs } +func (s *Session) HIDCompleter(prefix string) []string { + macs := []string{""} + s.HID.EachDevice(func(mac string, dev *network.HIDDevice) { + if prefix == "" || strings.HasPrefix(mac, prefix) { + macs = append(macs, mac) + } + }) + return macs +} + func (s *Session) Module(name string) (err error, mod Module) { for _, m := range s.Modules { if m.Name() == name { @@ -292,6 +303,12 @@ func (s *Session) Start() error { s.Firewall = firewall.Make(s.Interface) + s.HID = network.NewHID(func(dev *network.HIDDevice) { + s.Events.Add("hid.device.new", dev) + }, func(dev *network.HIDDevice) { + s.Events.Add("hid.device.lost", dev) + }) + s.BLE = network.NewBLE(func(dev *network.BLEDevice) { s.Events.Add("ble.device.new", dev) }, func(dev *network.BLEDevice) {