diff --git a/modules/events_view.go b/modules/events_view.go index 7a1f73da..4c4a9b74 100644 --- a/modules/events_view.go +++ b/modules/events_view.go @@ -77,6 +77,19 @@ func (s *EventsStream) viewWiFiEvent(e session.Event) { tui.Dim(desc), tui.Bold(probe.SSID), tui.Yellow(rssi)) + } else if e.Tag == "wifi.client.handshake" { + hand := e.Data.(WiFiHandshakeEvent) + ss := "s" + if hand.NewPackets == 1 { + ss = "" + } + fmt.Fprintf(s.output, "[%s] [%s] captured a full handshake for station %s and AP %s (saved %d new packet%s to %s)\n", + e.Time.Format(eventTimeFormat), + tui.Green(e.Tag), + hand.Station.String(), + hand.AP.String(), + hand.NewPackets, ss, + hand.File) } } diff --git a/modules/wifi.go b/modules/wifi.go index 881ada2d..92eb95cf 100644 --- a/modules/wifi.go +++ b/modules/wifi.go @@ -17,6 +17,7 @@ import ( "github.com/google/gopacket/layers" "github.com/google/gopacket/pcap" + "github.com/evilsocket/islazy/fs" "github.com/evilsocket/islazy/tui" ) @@ -35,6 +36,7 @@ type WiFiModule struct { pktSourceChanClosed bool deauthSkip []net.HardwareAddr deauthSilent bool + shakesFile string apRunning bool apConfig packets.Dot11ApConfig writes *sync.WaitGroup @@ -123,6 +125,11 @@ func NewWiFiModule(s *session.Session) *WiFiModule { } })) + w.AddParam(session.NewStringParameter("wifi.handshakes.file", + "~/bettercap-wifi-handshakes.pcap", + "", + "File path of the pcap file to save handshakes to.")) + w.AddParam(session.NewStringParameter("wifi.ap.ssid", "FreeWiFi", "", @@ -221,6 +228,14 @@ func (w *WiFiModule) Configure() error { return err } + if err, w.shakesFile = w.StringParam("wifi.handshakes.file"); err != nil { + return err + } else if w.shakesFile != "" { + if w.shakesFile, err = fs.Expand(w.shakesFile); err != nil { + return err + } + } + if w.source != "" { if w.handle, err = pcap.OpenOffline(w.source); err != nil { return err @@ -353,6 +368,7 @@ func (w *WiFiModule) Start() error { w.discoverProbes(radiotap, dot11, packet) w.discoverAccessPoints(radiotap, dot11, packet) w.discoverClients(radiotap, dot11, packet) + w.discoverHandshakes(radiotap, dot11, packet) w.updateInfo(dot11, packet) w.updateStats(dot11, packet) } diff --git a/modules/wifi_recon.go b/modules/wifi_recon.go index b3673f5b..0fa7f64a 100644 --- a/modules/wifi_recon.go +++ b/modules/wifi_recon.go @@ -11,6 +11,8 @@ import ( "github.com/google/gopacket" "github.com/google/gopacket/layers" + + "github.com/evilsocket/islazy/tui" ) var maxStationTTL = 5 * time.Minute @@ -23,6 +25,13 @@ type WiFiProbe struct { RSSI int8 } +type WiFiHandshakeEvent struct { + File string + NewPackets int + AP net.HardwareAddr + Station net.HardwareAddr +} + func (w *WiFiModule) stationPruner() { w.reads.Add(1) defer w.reads.Done() @@ -121,3 +130,76 @@ func (w *WiFiModule) discoverClients(radiotap *layers.RadioTap, dot11 *layers.Do } }) } + +func allZeros(s []byte) bool { + for _, v := range s { + if v != 0 { + return false + } + } + return true +} + +func (w *WiFiModule) discoverHandshakes(radiotap *layers.RadioTap, dot11 *layers.Dot11, packet gopacket.Packet) { + if keyLayer := packet.Layer(layers.LayerTypeEAPOLKey); keyLayer != nil { + if key := keyLayer.(*layers.EAPOLKey); key.KeyType == layers.EAPOLKeyTypePairwise { + staMac := net.HardwareAddr{} + apMac := net.HardwareAddr{} + if dot11.Flags.FromDS() { + staMac = dot11.Address1 + apMac = dot11.Address2 + } else if dot11.Flags.ToDS() { + staMac = dot11.Address2 + apMac = dot11.Address1 + } + + if station, found := w.Session.WiFi.GetClient(staMac.String()); found { + // ref. https://wlan1nde.wordpress.com/2014/10/27/4-way-handshake/ + if !key.Install && key.KeyACK && !key.KeyMIC { + // [1] (ACK) AP is sending ANonce to the client + log.Debug("[%s] got frame 1/4 of the %s <-> %s handshake (anonce:%x)", + tui.Green("wifi"), + apMac, + staMac, + key.Nonce) + station.Handshake.AddFrame(0, packet) + } else if !key.Install && !key.KeyACK && key.KeyMIC && !allZeros(key.Nonce) { + // [2] (MIC) client is sending SNonce+MIC to the API + log.Debug("[%s] got frame 2/4 of the %s <-> %s handshake (snonce:%x mic:%x)", + tui.Green("wifi"), + apMac, + staMac, + key.Nonce, + key.MIC) + station.Handshake.AddFrame(1, packet) + } else if key.Install && key.KeyACK && key.KeyMIC { + // [3]: (INSTALL+ACK+MIC) AP informs the client that the PTK is installed + log.Debug("[%s] got frame 3/4 of the %s <-> %s handshake (mic:%x)", + tui.Green("wifi"), + apMac, + staMac, + key.MIC) + station.Handshake.AddFrame(2, packet) + } + + numUnsaved := station.Handshake.NumUnsaved() + doSave := numUnsaved > 0 + if doSave && w.shakesFile != "" { + log.Debug("saving handshake frames to %s", w.shakesFile) + if err := w.Session.WiFi.SaveHandshakesTo(w.shakesFile, w.handle.LinkType()); err != nil { + log.Error("error while saving handshake frames to %s: %s", w.shakesFile, err) + } + } + + if doSave && station.Handshake.Complete() { + w.Session.Events.Add("wifi.client.handshake", WiFiHandshakeEvent{ + File: w.shakesFile, + NewPackets: numUnsaved, + AP: apMac, + Station: staMac, + }) + } + } + } + } +} diff --git a/modules/wifi_show.go b/modules/wifi_show.go index 4cfffff7..7f3f7207 100644 --- a/modules/wifi_show.go +++ b/modules/wifi_show.go @@ -48,11 +48,20 @@ func (w *WiFiModule) getRow(station *network.Station) ([]string, bool) { if len(station.Cipher) > 0 { encryption = fmt.Sprintf("%s (%s, %s)", station.Encryption, station.Cipher, station.Authentication) } + if encryption == "OPEN" || encryption == "" { encryption = tui.Green("OPEN") ssid = tui.Green(ssid) bssid = tui.Green(bssid) + } else { + // this is ugly, but necessary in order to have this + // method handle both access point and clients + // transparently + if ap, found := w.Session.WiFi.Get(station.HwAddress); found && ap.HasHandshakes() { + encryption = tui.Red(encryption) + } } + sent := "" if station.Sent > 0 { sent = humanize.Bytes(station.Sent) @@ -318,7 +327,7 @@ func (w *WiFiModule) ShowWPS(bssid string) (err error) { fmt.Println() fmt.Printf("* %s (%s ch:%d):\n", tui.Bold(ssid), tui.Dim(station.BSSID()), station.Channel()) keys := []string{} - for name, _ := range station.WPS { + for name := range station.WPS { keys = append(keys, name) } sort.Strings(keys) diff --git a/network/wifi.go b/network/wifi.go index 389b8960..79b95921 100644 --- a/network/wifi.go +++ b/network/wifi.go @@ -2,9 +2,16 @@ package network import ( "encoding/json" + "os" "strconv" "sync" "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/google/gopacket/pcapgo" + + "github.com/evilsocket/islazy/fs" ) func Dot11Freq2Chan(freq int) int { @@ -174,3 +181,37 @@ func (w *WiFi) Clear() error { w.aps = make(map[string]*AccessPoint) return nil } + +func (w *WiFi) SaveHandshakesTo(fileName string, linkType layers.LinkType) error { + w.Lock() + defer w.Unlock() + + doHead := !fs.Exists(fileName) + + fp, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666) + if err != nil { + return err + } + defer fp.Close() + + writer := pcapgo.NewWriter(fp) + + if doHead { + if err = writer.WriteFileHeader(65536, linkType); err != nil { + return err + } + } + + for _, ap := range w.aps { + for _, station := range ap.Clients() { + station.Handshake.EachUnsavedPacket(func(pkt gopacket.Packet) { + err = writer.WritePacket(pkt.Metadata().CaptureInfo, pkt.Data()) + }) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/network/wifi_ap.go b/network/wifi_ap.go index 7486359d..ff2f67ec 100644 --- a/network/wifi_ap.go +++ b/network/wifi_ap.go @@ -96,3 +96,16 @@ func (ap *AccessPoint) Clients() (list []*Station) { } return } + +func (ap *AccessPoint) HasHandshakes() bool { + ap.Lock() + defer ap.Unlock() + + for _, c := range ap.clients { + if c.Handshake.Complete() { + return true + } + } + + return false +} diff --git a/network/wifi_handshake.go b/network/wifi_handshake.go new file mode 100644 index 00000000..c97a046f --- /dev/null +++ b/network/wifi_handshake.go @@ -0,0 +1,67 @@ +package network + +import ( + "github.com/google/gopacket" + "sync" +) + +type Handshake struct { + sync.Mutex + + Challenges []gopacket.Packet + Responses []gopacket.Packet + Confirmations []gopacket.Packet + unsaved []gopacket.Packet +} + +func NewHandshake() *Handshake { + return &Handshake{ + Challenges: make([]gopacket.Packet, 0), + Responses: make([]gopacket.Packet, 0), + Confirmations: make([]gopacket.Packet, 0), + unsaved: make([]gopacket.Packet, 0), + } +} + +func (h *Handshake) AddFrame(n int, pkt gopacket.Packet) { + h.Lock() + defer h.Unlock() + + switch n { + case 0: + h.Challenges = append(h.Challenges, pkt) + case 1: + h.Responses = append(h.Responses, pkt) + case 2: + h.Confirmations = append(h.Confirmations, pkt) + } + + h.unsaved = append(h.unsaved, pkt) +} + +func (h *Handshake) Complete() bool { + h.Lock() + defer h.Unlock() + + nChal := len(h.Challenges) + nResp := len(h.Responses) + nConf := len(h.Confirmations) + + return nChal > 0 && nResp > 0 && nConf > 0 +} + +func (h *Handshake) NumUnsaved() int { + h.Lock() + defer h.Unlock() + return len(h.unsaved) +} + +func (h *Handshake) EachUnsavedPacket(cb func(gopacket.Packet)) { + h.Lock() + defer h.Unlock() + + for _, pkt := range h.unsaved { + cb(pkt) + } + h.unsaved = make([]gopacket.Packet, 0) +} diff --git a/network/wifi_station.go b/network/wifi_station.go index ed70412a..4c3212b1 100644 --- a/network/wifi_station.go +++ b/network/wifi_station.go @@ -14,6 +14,7 @@ type Station struct { Cipher string `json:"cipher"` Authentication string `json:"authentication"` WPS map[string]string `json:"wps"` + Handshake *Handshake `json:"-"` } func cleanESSID(essid string) string { @@ -35,6 +36,7 @@ func NewStation(essid, bssid string, frequency int, rssi int8) *Station { Frequency: frequency, RSSI: rssi, WPS: make(map[string]string), + Handshake: NewHandshake(), } } diff --git a/packets/dot11_wps_attrs.go b/packets/dot11_wps_attrs.go index ae805383..0bab99c2 100644 --- a/packets/dot11_wps_attrs.go +++ b/packets/dot11_wps_attrs.go @@ -37,83 +37,83 @@ var ( } wpsDeviceTypes = map[uint16]wpsDevType{ - 0x0001: wpsDevType{"Computer", map[uint16]string{ + 0x0001: {"Computer", map[uint16]string{ 0x0001: "PC", 0x0002: "Server", 0x0003: "Media Center", }}, - 0x0002: wpsDevType{"Input Device", map[uint16]string{}}, - 0x0003: wpsDevType{"Printers, Scanners, Faxes and Copiers", map[uint16]string{ + 0x0002: {"Input Device", map[uint16]string{}}, + 0x0003: {"Printers, Scanners, Faxes and Copiers", map[uint16]string{ 0x0001: "Printer", 0x0002: "Scanner", }}, - 0x0004: wpsDevType{"Camera", map[uint16]string{ + 0x0004: {"Camera", map[uint16]string{ 0x0001: "Digital Still Camera", }}, - 0x0005: wpsDevType{"Storage", map[uint16]string{ + 0x0005: {"Storage", map[uint16]string{ 0x0001: "NAS", }}, - 0x0006: wpsDevType{"Network Infra", map[uint16]string{ + 0x0006: {"Network Infra", map[uint16]string{ 0x0001: "AP", 0x0002: "Router", 0x0003: "Switch", }}, - 0x0007: wpsDevType{"Display", map[uint16]string{ + 0x0007: {"Display", map[uint16]string{ 0x0001: "TV", 0x0002: "Electronic Picture Frame", 0x0003: "Projector", }}, - 0x0008: wpsDevType{"Multimedia Device", map[uint16]string{ + 0x0008: {"Multimedia Device", map[uint16]string{ 0x0001: "DAR", 0x0002: "PVR", 0x0003: "MCX", }}, - 0x0009: wpsDevType{"Gaming Device", map[uint16]string{ + 0x0009: {"Gaming Device", map[uint16]string{ 0x0001: "XBox", 0x0002: "XBox360", 0x0003: "Playstation", }}, - 0x000F: wpsDevType{"Telephone", map[uint16]string{ + 0x000F: {"Telephone", map[uint16]string{ 0x0001: "Windows Mobile", }}, } wpsAttributes = map[uint16]wpsAttr{ - 0x104A: wpsAttr{Name: "Version", Desc: wpsVersionDesc}, - 0x1044: wpsAttr{Name: "State", Desc: map[string]string{ + 0x104A: {Name: "Version", Desc: wpsVersionDesc}, + 0x1044: {Name: "State", Desc: map[string]string{ "01": "Not Configured", "02": "Configured", }}, - 0x1012: wpsAttr{Name: "Device Password ID", Desc: map[string]string{ + 0x1012: {Name: "Device Password ID", Desc: map[string]string{ "0000": "Pin", "0004": "PushButton", }}, - 0x103B: wpsAttr{Name: "Response Type", Desc: map[string]string{ + 0x103B: {Name: "Response Type", Desc: map[string]string{ "00": "Enrollee Info", "01": "Enrollee", "02": "Registrar", "03": "AP", }}, - 0x1054: wpsAttr{Name: "Primary Device Type", Func: dot11ParseWPSDeviceType}, - 0x1049: wpsAttr{Name: "Vendor Extension", Func: dot11ParseWPSVendorExtension}, - 0x1053: wpsAttr{Name: "Selected Registrar Config Methods", Func: dot11ParseWPSConfigMethods}, - 0x1008: wpsAttr{Name: "Config Methods", Func: dot11ParseWPSConfigMethods}, - 0x103C: wpsAttr{Name: "RF Bands", Func: dott11ParseWPSBands}, + 0x1054: {Name: "Primary Device Type", Func: dot11ParseWPSDeviceType}, + 0x1049: {Name: "Vendor Extension", Func: dot11ParseWPSVendorExtension}, + 0x1053: {Name: "Selected Registrar Config Methods", Func: dot11ParseWPSConfigMethods}, + 0x1008: {Name: "Config Methods", Func: dot11ParseWPSConfigMethods}, + 0x103C: {Name: "RF Bands", Func: dott11ParseWPSBands}, - 0x1057: wpsAttr{Name: "AP Setup Locked"}, - 0x1041: wpsAttr{Name: "Selected Registrar"}, - 0x1047: wpsAttr{Name: "UUID-E"}, - 0x1021: wpsAttr{Name: "Manufacturer", Type: wpsStr}, - 0x1023: wpsAttr{Name: "Model Name", Type: wpsStr}, - 0x1024: wpsAttr{Name: "Model Number", Type: wpsStr}, - 0x1042: wpsAttr{Name: "Serial Number", Type: wpsStr}, - 0x1011: wpsAttr{Name: "Device Name", Type: wpsStr}, - 0x1045: wpsAttr{Name: "SSID", Type: wpsStr}, - 0x102D: wpsAttr{Name: "OS Version", Type: wpsStr}, + 0x1057: {Name: "AP Setup Locked"}, + 0x1041: {Name: "Selected Registrar"}, + 0x1047: {Name: "UUID-E"}, + 0x1021: {Name: "Manufacturer", Type: wpsStr}, + 0x1023: {Name: "Model Name", Type: wpsStr}, + 0x1024: {Name: "Model Number", Type: wpsStr}, + 0x1042: {Name: "Serial Number", Type: wpsStr}, + 0x1011: {Name: "Device Name", Type: wpsStr}, + 0x1045: {Name: "SSID", Type: wpsStr}, + 0x102D: {Name: "OS Version", Type: wpsStr}, } wpsConfigs = map[uint16]string{