Building a Wi-Fi scanner with Scapy

The idea for this post came about from just wanting to play around with Scapy. Not having spent any time on it before, it was high time to see what this excellent packet crafting tool was all about. There was no well-defined end goal here, only a desire to dissect some packets. In the end (about 20 hours later), what came out of it is essentially a Wi-Fi network reconnaissance tool. As such, it almost feels obligatory to have a disclaimer stating that the content provided here is for educational purposes only, if it wasn’t for the fact that the tool is harmless (besides exposing networks with weak security that is…).

What does the Wi-Fi scanner tool do? It uses the Scapy sniff module to capture wireless frames, and analyses their contents to display a list of wireless networks and their properties. It displays BSSID, SSID, RSSI, frequency on which the frame was received, as well as the cipher & AKM suites which give us the configured security level. The last parameter is the age of the beacon, the entries are aged out after 30 seconds, but this is configurable. Each entry is updated in real time, then the results are sorted by RSSI and displayed using varying colours from strongest to weakest.

A few words on the lab environment. Here we have a Debian Buster VM (Raspberry Pi Desktop) running on a Windows 10 host using VMware Workstation 15 Player. The wireless adapter is a tp-link Archer T9UH using the freely available aircrack-ng drivers. Getting a combination of hardware and software suitable for this task (i.e. wireless sniffing) can be a hassle, as can getting it running even after you find a combination that works. For a bit more on this see an earlier post titled Wireless Capture Analysis. As Python (3) comes pre-installed on Debian the only additional software is Scapy itself, easily added using pip3 install scapy.

All of the required information is available by looking into the various frame headers of the beacon frame, or layers, as they are referred to in Scapy. The link below shows a Scapy dump of all available layers from a captured beacon frame.

scapy_beacon_layers.txt

Now its just a case of extracting the information we are interested in.

RSSI and frequency are in the RadioTap header/layer, and give us the parameters of the frame as it was received. The RadioTap header is added by the driver of the interface performing the capture.

The BSSID can be found in the 802.11-FCS layer (or Frame Control Field in 802.11 parlance), and corresponds to the transmitting station’s MAC address. Recall that 802.11 communication uses up to four MAC addresses for each frame, depending on the type of transmission. Address 2 (addr2) represents the TA or Transmitter Address. For a Beacon frame this is the radio MAC address of the transmitting AP, exactly what we are looking for.

For the SSID value we have to look at the Dot11Beacon layer, or specifically, the Information Element immediately following the Dot11Beacon layer. This also doesn’t quite correspond to the way that (say) Wireshark would decode the information, the IE order is different, but it doesn’t really affect anything from a scripting point of view.

Last but not least, the network security information can be found in the RSN Information Element. Scapy presents this as two layers, RSNCipherSuite and AKMSuite, and also decodes the value for each as a decimal number. The tables below show the possible values for Cipher and AKM suites, representing the configured network encryption and authentication types respectively.

RawEncryptionScapy
00-0F-AC-01WEP401
00-0F-AC-02TKIP2
00-0F-AC-04CCMP4
00-0F-AC-05WEP1045
Cipher suite
RawAuthenticationScapy
00-0F-AC-01802.1X1
00-0F-AC-02PSK2
00-0F-AC-03802.1X (FT)3
Authentication Key Management (AKM) Suite

To capture wireless frames the network interface needs to be in monitor mode. This can be achieved with the following commands:

sudo ip link set wlan0 down
sudo iw wlan0 set type monitor
sudo ip link set wlan0 up
sudo iw wlan0 set channel 36

There are some caveats however, as there are a number of system services that can interfere with the scanning/sniffing process and should be disabled. The airmon-ng tool is very useful here and will provide a list of services that may need to be disabled.

After some testing though, it seems that the dhcpcd and wpa_supplicant services are the real culprits here (at least in this case). Particularly the dhcpcd service, as it appears to initiate periodic channel scans (!?) about once every second, which means you may miss traffic if trying to capture only a single channel. In this case we are not capturing all traffic, only beacons, so it’s not as much of a problem. However, if you do want to scan a single channel, it is best to disable the dhcpcd service, and possibly the wpa_supplicant service too.

Scanning can also be performed while cycling through all channels, allowing for a wider view of available networks. For this, instead of writing code to change channels periodically, we can just use the channel scanning ‘feature’ of dhcpcd, and simply leave the dhcpcd service enabled.

Pseudocode

Function

IF packet has Dot11Beacon layer
  Set Timestamp from current time
  Load RadioTap layer
  Set Signal variable from RadioTap field dBm_AntSignal
  Set Frequency variable from RadioTap field ChannelFrequency
  Load Dot11FCS layer
  Set Bssid variable from Dot11FCS field addr2
  Load Dot11Beacon layer
  Load next Information Element
  Set Ssid variable from Information Element field info
  IF packet has RSNCipherSuite layer
    Load RSNCipherSuite layer
    Set Cipher variable from RSNCipherSUite field cipher (map against list of known values)
  ELSE Set Cipher variable to Open
  IF packet has AKMSuite layer
    Load AKMSuite layer
    Set Akm variable from AKMSuite field suite (map against list of known values)
  ELSE Set Akm variable to Open
  Assign Bssid, Signal, Ssid, Timestamp, Frequency, Cipher, Akm to nested AP dictionary
  Clear screen
  Print headings
  LOOP through AP dictionary, sorted by Signal value
    Set Beacon_age = Timestamp – Saved AP value
    IF Beacon_age less than Max_beacon_age
      IF Signal below -65
        Print Ssid, Signal, Frequency, Cipher, Akm in green
      IF Signal between -65 and -50
        Print Ssid, Signal, Frequency, Cipher, Akm in yellow
      IF Signal between -49 and -40
        Print Ssid, Signal, Frequency, Cipher, Akm in red
      IF Signal above -40
        Print Ssid, Signal, Frequency, Cipher, Akm in magenta
    ELSE delete AP record

Main code

Set Max_beacon_age
Define empty AP dictionary
Run Scapy sniff module, pass each packet to Function, print output from Function

Real code

scapy.wifi.scanner.py