Apple client fails with mandatory PMF on 802.1X SSID

Introduction

We recently got a bug entry claiming that an iPhone with iOS 10.3.2 fails to connect to a 802.1X SSID if PMF is mandatory/required, but can connect to our AP if WPA-PSK is used instead. Since we do have iPhones here, it was easy to reproduce the issue and take a look at the connection behavior of AP and station (STA), which reveals where things go wrong. Furthermor not only iOS devices like iPhone and iPad are affected, but also macOS clients like a Macbook Pro.

Apple and PSK

PSK and PMF optional

Let’s start simple with a SSID that uses WPA2-PSK for encryption and mark this SSID PMF opional/capable so that the Auth Key Management (AKM) type is set to “PSK (0x02)”. The iPhone will connect to the SSID just fine.

PSK_pmf_opt_sha1

PSK and PMF required

So far so good, we go on with WPA2-PSK and PMF mandatory/required. Looking at the RSN ASM type, we do see “PSK (SHA256) (0x06)” for key hashes with SHA-256 now. Again, the client can connect to the SSID.

PSK_pmf_mandatory_sha256

Apple and 802.1X

802.1X and PMF optional

Now, let’s repeat this for an EAP-PEAP based 802.1X SSID and PMF optional/capable. Within Beacons and Probe Requests, the AP announces RSN AKM type “WPA (0x01)” and the client can connect with the right credentials. The “Association Request” from the STA contains a RSN element with AKM type “WPA” and “Management Frame Protection Capable” capability.

1x_pmf_opt_sha1

1x_pmf_opt_sha1_assoc_req

802.1X and PMF required

The critical part is 802.1X with PMF required, where the AP uses RSN AKM type “WPA (SHA256) (0x05)” and advertises “Management Frame Protection Capable” and “Management Frame Protection Required” capabilities.

1x_pmf_man_sha256_probe_resp

Somehow this type seems to be missing within Apples implementation on iOS and macOS, because both OSes will tell the user that he tries to connect to a WPA-PSK SSID! My guess is, that this is the fallback/default AKM within the OS. I took a screenshot on a Macbook with WiFi Explorer in the background showing AKM Suite “WPA (SHA256)” as well for a PMF mandatory 802.1X SSID.

Macbook_WPA-PSK-PMF

Furthermore, after the Authentication frames, the client sends an Association Request with no RSN information element present. This is causing the connection problem.

1x_pmf_man_sha256_assoc_req

Without the RSN element, the AP still answers with an Association Response, frame 13, but does not start the EAP phase afterwards.

Is it really Apple messing things up?

You could still blame that our LANCOM AP does something wrong so the STA gets confused and misbehaves. So I double checked with a Cisco WLC 2504 and AP3600 and got the same behavior.

Furthermore I got a hold on a Nokia/Microsoft Lumia 950, which is Wi-Fi Alliance 802.11ac certified [1] (PMF certification is a must for that), and checked against a LANCOM AP with 802.1X, PMF mandatory/required. The phone sends the required RSN information element within its “Association Request” and can connect! You can see the EAP phase starting right after the “Association Response” from the AP.

lumia_1x_pmf_man_sha256_assoc_req

Conclusion

Altough Apple is quite keen on getting into the enterprise business with their devices and they recently announced partnership with Cisco Systems, the typical enterprise authentication method 802.1X together with PMF is not fully supported. It is suprising to see that Apple fails on both OSes, iOS and macOS, with the same error.

While Apple did WFA certification for 11n devices up to iPhone 5s, there has been no 802.11ac WFA certification for an iPhone. [2]

[1] https://wi-fi.org/content/search-page?keys=Lumia%20950

[2] https://wi-fi.org/product-finder-results?sort_by=default&sort_order=desc&capabilities=4&keywords=iPhone

Fake a WLAN connection via Scapy

Based on my previous post on how to send WLAN frames via Scapy, see Using Scapy to send WLAN frames, I want to demonstrate how to establish a connection to an unencrypted SSID with a WLAN interface that we control. I highly recommend reading my previous post if you aren’t already familiar with Scapy and Python.

Pleate note: The following commands were made on an Ubuntu 16.04 Linux system with a TP-Link TL-WN727N (v4.1) WLAN USB Stick. This WLAN USB stick is capable of sending ACKs within the required timeout, although the device is used in monitor mode, as long as its real MAC address is used as STA MAC. The scapy package can be installed as python-scapy (Ubuntu) or just scapy (Fedora), wireshark is also required. Your linux and python skills should be quite solid and include knowledge about the use of ifconfig and iwconfig in linux as well as class definition and inheritance in python.

Required packets for a WLAN connection

Without going into too much detail here, we basically just need two types of packets to connect to an AP – an Authentication frame and an Association Request.

Authentication Frame

For an authentication frame, Scapy already offers the required protocols Dot11() and Dot11Auth(). All we need to do is determine a STA MAC and a BSSID. With algo=0, the open system authentication algorithm is used, a sequence number of 1 is chosen by seqnum=0x0001 and the status code 0 means successful. This is the full command for the authentication frame:

packet = Dot11(addr1=[RECEIVER MAC], addr2=[SENDER MAC], addr3=[BSSID]) / Dot11Auth(algo=0, seqnum=0x0001, status=0x0000)

If we take a look at the packet in wireshark, we can see that the given interpretation of the fields fits:

Scapy Auth Packet in Wireshark

Association Request

This one is a rather difficult frame to create. It requires knowledge about parameters that are present in the Beacon like the supported rates field. This data has to be present within our association request, otherwise a LANCOM AP will reject the association of this client. In the following example we want to connect to an AP that supports .11g and .11n, but no .11b. If you are using a different AP or setting, some other rates/fields might be mandatory as well.

Let’s start with the basic packet that consists of Dot11, Dot11AssoReq and Dot11Elt. Besides the BSSID and STA MAC, the SSID NAME (ESSID) needs to be known as well to create the association request packet. The SSID is written to the info field of an 802.11 information element (Dot11Elt) with ID=0.

packet = Dot11(addr1=[RECEIVER MAC], addr2=[SENDER MAC], addr3=[BSSID])/Dot11AssoReq(cap=0x1100, listen_interval=0x00a) / Dot11Elt(ID=0, info="SSID NAME")

Again we take a look at the packet in wireshark:

Scapy Assoc Packet in WiresharkAs I’ve mentioned before, we also need to add the information field for supported rates, that matches the one from the AP Beacons. We can just sniff the Beacons and copy the values to our own definition of a supported rates information field. How to do it is given in the section Creating a customized packet in my previous post. So I just show you the Python code below.

Supported Rates Information Field
Supported Rates field in Beacon
from scapy.all import *

class Dot11EltRates(Packet):
    """ Our own definition for the supported rates field """
    name = "802.11 Rates Information Element"
    # Our Test STA supports the rates 6, 9, 12, 18, 24, 36, 48 and 54 Mbps
    supported_rates = [0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c]
    fields_desc = [ByteField("ID", 1), ByteField("len", len(supported_rates))]
    for index, rate in enumerate(supported_rates):
        fields_desc.append(ByteField("supported_rate{0}".format(index + 1),
                                     rate))

Python Files

Receiving and Sending packets

We create a Python file to receive and send packets with our monitoring interface and name it “monitor_ifc.py”. First of all, we add our Rates Info element as a class named “Dott11EltRates” and then create a new class called “Monitor”. The class is initialized with the name of the monitor interface, a STA MAC and the BSSID to connect to. The boolean variables “auth_found” and “assoc_found” are used to check if authentication/association responses were seen. Our Dot11EltRates class is assigned to a dot11_rates variable.

from scapy.all import *

class Dot11EltRates(Packet):
    """
    Our own definition for the supported rates field
    """
    name = "802.11 Rates Information Element"
    # Our Test AP has the rates 6, 9, 12 (B), 18, 24, 36, 48 and 54, with 12
    # Mbps as the basic rate - which does not have to concern us.
    supported_rates = [0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c]

    fields_desc = [
        ByteField("ID", 1),
        ByteField("len", len(supported_rates))
        ]

    for index, rate in enumerate(supported_rates):
        fields_desc.append(ByteField("supported_rate{0}".format(
            index + 1), rate))

class Monitor:
    def __init__(self, mon_ifc, sta_mac, bssid):
        """

        :param mon_ifc: WLAN interface to use as a monitor
        :param channel: Channel to operate on
        :param sta_mac: MAC address of the STA
        :param bssid: BSSID of the AP to attack
        """
        self.mon_ifc = mon_ifc
        self.sta_mac = sta_mac
        self.bssid = bssid
        self.auth_found = False
        self.assoc_found = False
        self.dot11_rates = Dot11EltRates()

Out first method of the Monitor class is “send_packet”, which simply sends a given Scapy packet. However, if the packet type is an Association Request, we add the dot11_rates informatation prior to sending it.

    def send_packet(self, packet, packet_type=None):
        """

        :param packet_type: Specific types require a special handling
        :param packet: This is our packet to be sent
        :return:
        """
        # Send out the packet
        if packet_type is None:
            send(packet)
        elif packet_type == "AssoReq":
            packet /= self.dot11_rates
            send(packet)
        else:
            print("Packet Type '{0}' unknown".format(packet_type))

As we need to check if the AP sends an Authentication Response, the method “search_auth()” uses the sniffing function from Scapy to look for packets matching the lfilter, x is a sniffed packet, as long as no timeout occurs or the stop_filter is True. The method “check_auth()” as stop filter just looks for matching addresses within a given frame, the matching frame type is checked by “x.haslayer(Dot11Auth)” within the lfilter of search_auth(). So if a packet has the Dot11Auth layer, we check if the addresses also match to our defined ones. The parameter mp_queue is a multiprocessing queue to return values from multiprocessed processes, which returns True or False in this case.

    def check_auth(self, packet):
        """
        Try to find the Authentication from the AP

        :param packet: sniffed packet to check for matching authentication
        """
        seen_receiver = packet[Dot11].addr1
        seen_sender = packet[Dot11].addr2
        seen_bssid = packet[Dot11].addr3

        if self.bssid == seen_bssid and \
            self.bssid == seen_sender and \
                self.sta_mac == seen_receiver:
            self.auth_found = True
            print("Detected Authentication from Source {0}".format(
                seen_bssid))
        return self.auth_found

    def search_auth(self, mp_queue):
        print("\nScanning max 5 seconds for Authentication "
              "from BSSID {0}".format(self.bssid))
        sniff(iface=self.mon_ifc, lfilter=lambda x:
                  x.haslayer(Dot11Auth),
              stop_filter=self.check_auth,
              timeout=5)
        mp_queue.put(self.auth_found)

As we also need to check for an Association Response, we nearly do the same, except that we check if the packet has the Dot11AssoResp layer.

    def check_assoc(self, packet):
        """
        Try to find the Association Response from the AP

        :param packet: sniffed packet to check for matching association
        """
        seen_receiver = packet[Dot11].addr1
        seen_sender = packet[Dot11].addr2
        seen_bssid = packet[Dot11].addr3

        if self.bssid == seen_bssid and \
            self.bssid == seen_sender and \
                self.sta_mac == seen_receiver:
            self.assoc_found = True
            print("Detected Association Response from Source {0}".format(
                seen_bssid))
        return self.assoc_found

    def search_assoc_resp(self, mp_queue):
        print("\nScanning max 5 seconds for Association Response "
              "from BSSID {0}".format(self.bssid))
        sniff(iface=self.mon_ifc, lfilter=lambda x: x.haslayer(Dot11AssoResp),
              stop_filter=self.check_assoc,
              timeout=5)
        mp_queue.put(self.assoc_found)

To wrap things up, here is the complete code of the “monitor_ifc.py”:

from scapy.all import *

class Dot11EltRates(Packet):
    """
    Our own definition for the supported rates field
    """
    name = "802.11 Rates Information Element"
    # Our Test AP has the rates 6, 9, 12 (B), 18, 24, 36, 48 and 54, with 12
    # Mbps as the basic rate - which does not have to concern us.
    supported_rates = [0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c]

    fields_desc = [
        ByteField("ID", 1),
        ByteField("len", len(supported_rates))
        ]

    for index, rate in enumerate(supported_rates):
        fields_desc.append(ByteField("supported_rate{0}".format(
            index + 1), rate))

class Monitor:
    def __init__(self, mon_ifc, sta_mac, bssid):
        """

        :param mon_ifc: WLAN interface to use as a monitor
        :param channel: Channel to operate on
        :param sta_mac: MAC address of the STA
        :param bssid: BSSID of the AP to attack
        """
        self.mon_ifc = mon_ifc
        self.sta_mac = sta_mac
        self.bssid = bssid
        self.auth_found = False
        self.assoc_found = False
        self.dot11_rates = Dot11EltRates()

    def send_packet(self, packet, packet_type=None):
        """
        Send and display a packet.

        :param packet_type: Specific types require
        :param packet:
        :return:
        """
        # Send out the packet
        if packet_type is None:
            send(packet)
        elif packet_type == "AssoReq":
            packet /= self.dot11_rates
            send(packet)
        else:
            print("Packet Type '{0}' unknown".format(packet_type))

    def check_auth(self, packet):
        """
        Try to find the Authentication from the AP

        :param packet: sniffed packet to check for matching authentication
        """
        seen_receiver = packet[Dot11].addr1
        seen_sender = packet[Dot11].addr2
        seen_bssid = packet[Dot11].addr3

        if self.bssid == seen_bssid and \
            self.bssid == seen_sender and \
                self.sta_mac == seen_receiver:
            self.auth_found = True
            print("Detected Authentication from Source {0}".format(
                seen_bssid))
        return self.auth_found

    def check_assoc(self, packet):
        """
        Try to find the Association Response from the AP

        :param packet: sniffed packet to check for matching association
        """
        seen_receiver = packet[Dot11].addr1
        seen_sender = packet[Dot11].addr2
        seen_bssid = packet[Dot11].addr3

        if self.bssid == seen_bssid and \
            self.bssid == seen_sender and \
                self.sta_mac == seen_receiver:
            self.assoc_found = True
            print("Detected Association Response from Source {0}".format(
                seen_bssid))
        return self.assoc_found

    def search_auth(self, mp_queue):
        print("\nScanning max 5 seconds for Authentication "
              "from BSSID {0}".format(self.bssid))
        sniff(iface=self.mon_ifc, lfilter=lambda x: x.haslayer(Dot11Auth),
              stop_filter=self.check_auth,
              timeout=5)
        mp_queue.put(self.auth_found)

    def search_assoc_resp(self, mp_queue):
        print("\nScanning max 5 seconds for Association Response "
              "from BSSID {0}".format(self.bssid))
        sniff(iface=self.mon_ifc, lfilter=lambda x: x.haslayer(Dot11AssoResp),
              stop_filter=self.check_assoc,
              timeout=5)
        mp_queue.put(self.assoc_found)

Establish the connection

So let’s move on to the actual connection establishment. We create a new file called “connection_phase.py”, import multiprocessing, scapy and the Monitor class from the “monitor_ifc.py”. Then we create the class “ConnectionPhase” with a connection state as a string, the monitor interface to be used, the STA MAC and the BSSID.

import multiprocessing
from scapy.all import *

from monitor_ifc import Monitor

class ConnectionPhase:
    """
    Establish a connection to the AP via the following commands
    """

    def __init__(self, monitor_ifc, sta_mac, bssid):
        self.state = "Not Connected"
        self.mon_ifc = monitor_ifc
        self.sta_mac = sta_mac
        self.bssid = bssid

Our first method creates an authentication packet and uses a list of jobs for multiprocessing. Since we want to send the packet and check if we receive an answer from the AP, we parallelize the sending and receiving jobs via multiprocessing.Process(target=[…],args=(arg1, …)) and add them to the list of jobs which will be started and joined afterwards. If the return value in our message queue is True, we will set the internal state to “Authenticated”.

    def send_authentication(self):
        """
        Send an Authentication Request and wait for the Authentication Response.
        Which works if the user defined Station MAC matches the one of the
        WLAN ifc itself.

        :return: -
        """
        packet = Dot11(
            addr1=self.bssid,
            addr2=self.sta_mac,
            addr3=self.bssid) / Dot11Auth(
                algo=0, seqnum=0x0001, status=0x0000)
        packet.show()

        jobs = list()
        result_queue = multiprocessing.Queue()
        receive_process = multiprocessing.Process(
            target=self.mon_ifc.search_auth,
            args=(result_queue, ))
        jobs.append(receive_process)
        send_process = multiprocessing.Process(
            target=self.mon_ifc.send_packet,
            args=(packet, ))
        jobs.append(send_process)

        for job in jobs:
            job.start()
        for job in jobs:
            job.join()

        if result_queue.get():
            self.state = "Authenticated"

For the Association Request frame most of the code is the same, we first check if our internal state matches “Authenticated” before creating the frame based on the Dot11AssReq from Scapy. The parameter “ssid” will be part of the 802.11 information element that includes the SSID name (ESSID). If we get a True in our message queue, the internal state is set to “Associated”.

    def send_assoc_request(self, ssid):
        """
        Send an Association Request and wait for the Association Response.
        Which works if the user defined Station MAC matches the one of the
        wlan ifc itself.

        :param ssid: Name of the SSID (ESSID)
        :return: -
        """
        if self.state != "Authenticated":
            print("Wrong connection state for Association Request: {0} "
                  "- should be Authenticated".format(self.state))
            return 1

        packet = Dot11(
            addr1=self.bssid,
            addr2=self.sta_mac,
            addr3=self.bssid) / Dot11AssoReq(
                cap=0x1100, listen_interval=0x00a) / Dot11Elt(
                    ID=0, info="{}".format(ssid))
        packet.show()
jobs = list()
        result_queue = multiprocessing.Queue()
        receive_process = multiprocessing.Process(
            target=self.mon_ifc.search_assoc_resp,
            args=(result_queue,))
        jobs.append(receive_process)
        send_process = multiprocessing.Process(
            target=self.mon_ifc.send_packet,
            args=(packet, "AssoReq", ))
        jobs.append(send_process)

        for job in jobs:
            job.start()
        for job in jobs:
            job.join()

        if result_queue.get():
            self.state = "Associated"

Last but not least we need code to establish the connection by calling these methods from the ConnectionPhase class. In this example, a main function within the “connection_phase.py” will do this. We also use fix values for our monitoring interface, STA MAC, BSSID and ESSID, but it is also possible to hand them over as arguments from the command line. Check your “iwconfig” to get the full name of the monitor WLAN interface and “ifconfig” to get its MAC. Furthermore the BSSID and the ESSID have to be known.

def main():
    monitor_ifc = "wlx60e327xxyyzz"
    sta_mac = "60:e3:27:xx:yy:zz"
    bssid = "00:a0:57:aa:bb:cc"
    conf.iface = monitor_ifc

    # MACs are converted to always use lowercase
    mon_ifc = Monitor(monitor_ifc, sta_mac.lower(), bssid.lower())

    connection = ConnectionPhase(mon_ifc, sta_mac, bssid)
    connection.send_authentication()
    if connection.state == "Authenticated":
        print("STA is authenticated to the AP!")
    else:
        print("STA is NOT authenticated to the AP!")
    time.sleep(1)
    connection.send_assoc_request(ssid="SSID-NAME")

    if connection.state == "Associated":
        print("STA is connected to the AP!")
    else:
        print("STA is NOT connected to the AP!")

if __name__ == "__main__":
    sys.exit(main())

Our code of “connection_phase.py” is now complete, the code is given below.

import multiprocessing
from scapy.all import *

from monitor_ifc import Monitor

class ConnectionPhase:
    """
    Establish a connection to the AP via the following commands
    """

    def __init__(self, monitor_ifc, sta_mac, bssid):
        self.state = "Not Connected"
        self.mon_ifc = monitor_ifc
        self.sta_mac = sta_mac
        self.bssid = bssid

    def send_authentication(self):
        """
        Send an Authentication Request and wait for the Authentication Response.
        Which works if the user defined Station MAC matches the one of the
        wlan ifc itself.

        :return: -
        """
        packet = Dot11(
            addr1=self.bssid,
            addr2=self.sta_mac,
            addr3=self.bssid) / Dot11Auth(
                algo=0, seqnum=0x0001, status=0x0000)

        packet.show()

        jobs = list()
        result_queue = multiprocessing.Queue()
        receive_process = multiprocessing.Process(
            target=self.mon_ifc.search_auth,
            args=(result_queue, ))
        jobs.append(receive_process)
        send_process = multiprocessing.Process(
            target=self.mon_ifc.send_packet,
            args=(packet, ))
        jobs.append(send_process)

        for job in jobs:
            job.start()
        for job in jobs:
            job.join()

        if result_queue.get():
            self.state = "Authenticated"

    def send_assoc_request(self, ssid):
        """
        Send an Association Request and wait for the Association Response.
        Which works if the user defined Station MAC matches the one of the
        wlan ifc itself.

        :param ssid: Name of the SSID (ESSID)
        :return: -
        """
        if self.state != "Authenticated":
            print("Wrong connection state for Association Request: {0} "
                  "- should be Authenticated".format(self.state))
            return 1

        packet = Dot11(
            addr1=self.bssid,
            addr2=self.sta_mac,
            addr3=self.bssid) / Dot11AssoReq(
                cap=0x1100, listen_interval=0x00a) / Dot11Elt(
                    ID=0, info="{}".format(ssid))
        packet.show()
jobs = list()
        result_queue = multiprocessing.Queue()
        receive_process = multiprocessing.Process(
            target=self.mon_ifc.search_assoc_resp,
            args=(result_queue,))
        jobs.append(receive_process)
        send_process = multiprocessing.Process(
            target=self.mon_ifc.send_packet,
            args=(packet, "AssoReq", ))
        jobs.append(send_process)

        for job in jobs:
            job.start()
        for job in jobs:
            job.join()

        if result_queue.get():
            self.state = "Associated"

def main():
    monitor_ifc = "wlx60e327xxyyzz"
    sta_mac = "60:e3:27:xx:yy:zz"
    bssid = "00:a0:57:aa:bb:cc"
    conf.iface = monitor_ifc

    # mac configuration per command line arguments, MACs are converted to
    # always use lowercase
    mon_ifc = Monitor(monitor_ifc, sta_mac.lower(), bssid.lower())

    connection = ConnectionPhase(mon_ifc, sta_mac, bssid)
    connection.send_authentication()
    if connection.state == "Authenticated":
        print("STA is authenticated to the AP!")
    else:
        print("STA is NOT authenticated to the AP!")
    time.sleep(1)
    connection.send_assoc_request(ssid="SSID-NAME")

    if connection.state == "Associated":
        print("STA is connected to the AP!")
    else:
        print("STA is NOT connected to the AP!")

if __name__ == "__main__":
    sys.exit(main())

We can now execute the code in a Linux bash via “sudo python3 connection_phase.py”, the sudo rights are required to send/receive with our monitor interface. Given below is the output of our tool, with the made up MACs for security reasons.

###[ 802.11 ]###
 subtype = 11
 type = Management
 proto = 0
 FCfield =
 ID = 0
 addr1 = 00:a0:57:aa:bb:cc
 addr2 = 60:e3:27:xx:yy:zz
 addr3 = 00:a0:57:aa:bb:cc
 SC = 0
 addr4 = 00:00:00:00:00:00
 ###[ 802.11 Authentication ]###
 algo = open
 seqnum = 1
 status = success

Scanning max 5 seconds for Authentication from BSSID 00:a0:57:aa:bb:cc
 .
 Sent 1 packets.
 Detected Authentication from Source 00:a0:57:aa:bb:cc
STA is authenticated to the AP!

###[ 802.11 ]###
 subtype = 0
 type = Management
 proto = 0
 FCfield =
 ID = 0
 addr1 = 00:a0:57:aa:bb:cc
 addr2 = 60:e3:27:xx:yy:zz
 addr3 = 00:a0:57:aa:bb:cc
 SC = 0
 addr4 = 00:00:00:00:00:00
 ###[ 802.11 Association Request ]###
 cap = ESS+privacy
 listen_interval= 10
 ###[ 802.11 Information Element ]###
 ID = SSID
 len = None
 info = 'SSID-NAME'
 ###[ 802.11 Rates Information Element ]###
 ID = 1
 len = 8
 supported_rate1= 12
 supported_rate2= 18
 supported_rate3= 24
 supported_rate4= 36
 supported_rate5= 48
 supported_rate6= 72
 supported_rate7= 96
 supported_rate8= 108

Scanning max 5 seconds for Association Response from BSSID 00:a0:57:aa:bb:cc
 .
 Sent 1 packets.
 Detected Association Response from Source 00:a0:57:aa:bb:cc
STA is connected to the AP!

From this point on, you can send any frame, even frames that usually require a valid connection, to interact with the AP. May this little tool be useful to you and if you got any questions, feel free to ask them within the comment section. Thanks for reading.

Using Scapy to send WLAN frames

Scapy[1] is one mighty python tool to create, receive and manipulate various network packets and it comes with a very handy CLI as well. If you ever had the need to create specific network packets within a program, I suggest you use python together with scapy to do so. Let’s start with a short introduction to the scapy CLI and end with a small example of scapy within python code. With the right kind of WLAN interface it is even possible to create a connection to an unencrypted SSID and I am going to publish the steps to do so in another post.

Pleate note: The following commands were made on a Fedora 23 Linux system with an TP-Link TL-WN727N (v4.1) WLAN USB Stick. The scapy package can be installed as python-scapy (Ubuntu) or just scapy (Fedora), wireshark is also required. Your linux and python skills should be quite solid and include knowledge about the use of ifconfig and iwconfig in linux as well as class definition and inheritance in python.

Scapy CLI

After using a bash to call “scapy”, the CLI should show up and should look like this (Your version might be different):

Welcome to Scapy (2.2.0)
>>>

You can take a look at all the predefined protocols via the “ls()” command…

>>> ls()
ARP : ARP
ASN1_Packet : None
BOOTP : BOOTP
CookedLinux : cooked linux
DHCP : DHCP options [...]

… and get some details about them via “ls(<PROTOCOL>)” e.g. ls(ARP):

>>> ls(ARP)
hwtype : XShortField = (1)
ptype : XShortEnumField = (2048)
hwlen : ByteField = (6)
plen : ByteField = (4)
op : ShortEnumField = (1)
hwsrc : ARPSourceMACField = (None)
psrc : SourceIPField = (None)
hwdst : MACField = ('00:00:00:00:00:00')
pdst : IPField = ('0.0.0.0')

Since we want to send WLAN packets, the following predefined protocols can be used “out of the box”:

Dot11 : 802.11
Dot11ATIM : 802.11 ATIM
Dot11AssoReq : 802.11 Association Request
Dot11AssoResp : 802.11 Association Response
Dot11Auth : 802.11 Authentication
Dot11Beacon : 802.11 Beacon
Dot11Deauth : 802.11 Deauthentication
Dot11Disas : 802.11 Disassociation
Dot11Elt : 802.11 Information Element
Dot11ProbeReq : 802.11 Probe Request
Dot11ProbeResp : 802.11 Probe Response
Dot11QoS : 802.11 QoS
Dot11ReassoReq : 802.11 Reassociation Request
Dot11ReassoResp : 802.11 Reassociation Response
Dot11WEP : 802.11 WEP packet
[...]
RadioTap : RadioTap dummy

A quick example of how to send a Dot11 frame, e.g. a Null Frame, from an AP (e.g. MAC = 00:a0:57:98:76:54) to a Client (STA) (e.g. MAC = 00:a0:57:12:34:56) is:

>>> ls(Dot11)
subtype : BitField = (0)
type : BitEnumField = (0)
proto : BitField = (0)
FCfield : FlagsField = (0)
ID : ShortField = (0)
addr1 : MACField = ('00:00:00:00:00:00')
addr2 : Dot11Addr2MACField = ('00:00:00:00:00:00')
addr3 : Dot11Addr3MACField = ('00:00:00:00:00:00')
SC : Dot11SCField = (0)
addr4 : Dot11Addr4MACField = ('00:00:00:00:00:00')
>>> packet = Dot11(addr1="00:a0:57:12:34:56", addr2="00:a0:57:98:76:54", addr3="00:a0:57:98:76:54", type=2, subtype=4)

The addr1 field defines the destination/receiver, addr2 the source/transmitter and addr3 the BSSID of the frame. The Null frame is defined as type 2 and subtype 4. One of the most powerful possibilities of the scapy CLI is the direct call of wireshark to inspect a packet. This is as handy as:

>>> wireshark(packet)

A wireshark window should open and display the packet as shown in the following screenshot:
Scapy Packet in Wireshark

 

Creating a customized packet

Although there are a lot of predefined protocols/packets, we do need to create certain packets/protocols like e.g. information fields manually. This is where python comes into play and a more elaborate introduction can be found at [2].

We import scapy and create a class that is derived from scapy.Packet. Besides the class member name, the fields_desc is the most important variable. With the help of ByteField from scapy, we can create new information elements like ID and length of the included elements as well as the supported rates themselves by setting up a list of supported rates in hexadecimal notation and insert these values in ByteFields. Since each ByteField is unique, the names of the fields for supported rates need to differ, that’s why we enumerate the values and create them with supported_rate1, supported_rate2 and so on.

from scapy.all import *

class Dot11EltRates(Packet):
    """ Our own definition for the supported rates field """
    name = "802.11 Rates Information Element"
    # Our Test STA supports the rates 6, 9, 12, 18, 24, 36, 48 and 54 Mbps
    supported_rates = [0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c]
    fields_desc = [ByteField("ID", 1), ByteField("len", len(supported_rates))]
    for index, rate in enumerate(supported_rates):
        fields_desc.append(ByteField("supported_rate{0}".format(index + 1),
                                     rate))

You can either enter the values of the defined fields as hexadecimal ones with “0x” in front or as integers like the value of the “ID” ByteField in the example above. It is important that the byte value of the “len” field matches length of the successive byte elements.

Sending a (customized) packet

If we want to send a packet via python/scapy, we first have to set up our monitoring interface, which will be our USB WLAN Stick. We can use the command “iwconfig” in a bash to find the all the installed WLAN interfaces, use “ifconfig” to identify the right one via MAC address if you got more than one WLAN interface. In my case the TP-Link WLAN USB Stick is named “wlp0s29u1u7”. For the following steps, you need to be root or use sudo to configure the stick to the required monitor mode.

1) Turn the interface down via “ifconfig wlp0s29u1u7 down”
2) Set the device to monitor mode via “iwconfig wlp0s29u1u7 mode monitor”
3) Turn the interface back on via “ifconfig wlp0s29u1u7 up”
4) Set the interface to a certain channel via “iwconfig wlp0s29u1u7 chan 6”
5) If the settings were correctly adapted, the output of “iwconfig wlp0s29u1u7” includes “Mode:Monitor” and “Frequency:2.437 GHz” (for channel 6 in this example).

We can now use this interface to send packets via scapy. The following python code builds an association request packet via the predefined Dot11, Dot11AssoReq and Dot11Elt packets and extends the packet with rate information that is defined in the class given above. Afterwards we use the “sendp()” command to send the packet on the defined monitoring interface.

packet = Dot11(
    addr1="00:a0:57:98:76:54",
    addr2="00:a0:57:12:34:56",
    addr3="00:a0:57:98:76:54") / Dot11AssoReq(
        cap=0x1100, listen_interval=0x00a) / Dot11Elt(
            ID=0, info="MY_BSSID")
packet /= Dot11EltRates()
sendp(packet, iface="wlp0s29u1u7")
packet.show()

The code has to be executed as root or via sudo due to the control over the network interface. The script will output a warning, that no route for IPv6 destination :: could be found (happens during import of scapy and can be ignored) afterwards a “.” and the message “Sent 1 packets” should be visible, which means that the created packet was sent.

WARNING: No route found for IPv6 destination :: (no default route?). This affects only IPv6
.
Sent 1 packets.

Due to the packet.show() command at the end of our code, scapy will also print the packet to the standard output:

###[ 802.11 ]###
 subtype = 0
 type = Management
 proto = 0
 FCfield = 
 ID = 0
 addr1 = 00:a0:57:98:76:54
 addr2 = 00:a0:57:12:34:56
 addr3 = 00:a0:57:98:76:54
 SC = 0
 addr4 = 00:00:00:00:00:00
###[ 802.11 Association Request ]###
 cap = ESS+privacy
 listen_interval= 10
###[ 802.11 Information Element ]###
 ID = SSID
 len = None
 info = 'MY_BSSID'
###[ 802.11 Rates Information Element ]###
 ID = 1
 len = 8
 supported_rate1= 12
 supported_rate2= 18
 supported_rate3= 24
 supported_rate4= 36
 supported_rate5= 48
 supported_rate6= 72
 supported_rate7= 96
 supported_rate8= 108

You might have noticed that scapy does not print the length of the 802.11 Information Element containing the SSID info. The length is calculated automatically while sending the packet and not while it is printed. See the following screenshot in the Conclusion for proof.

Conclusion

Within the examples given in this post, we are able to create packets based on predefined ones in scapy and are able modify or create certain elements via python. If you are able to sniff the packet on the air, we can take a look at it in wireshark:

Scapy Assoc Packet in Wireshark

Following up is the complete code.

from scapy.all import *

class Dot11EltRates(Packet):
    """ Our own definition for the supported rates field """
    name = "802.11 Rates Information Element"
    # Our Test STA supports the rates 6, 9, 12, 18, 24, 36, 48 and 54 Mbps
    supported_rates = [0x0c, 0x12, 0x18, 0x24, 0x30, 0x48, 0x60, 0x6c]
    fields_desc = [ByteField("ID", 1), ByteField("len", len(supported_rates))]
    for index, rate in enumerate(supported_rates):
        fields_desc.append(ByteField("supported_rate{0}".format(index + 1),
                                     rate))

packet = Dot11(
    addr1="00:a0:57:98:76:54",
    addr2="00:a0:57:12:34:56",
    addr3="00:a0:57:98:76:54") / Dot11AssoReq(
        cap=0x1100, listen_interval=0x00a) / Dot11Elt(
            ID=0, info="MY_BSSID")
packet /= Dot11EltRates()
sendp(packet, iface="wlp0s29u1u7")
packet.show()

Links

[1] http://www.secdev.org/projects/scapy/
[2] http://www.secdev.org/projects/scapy/doc/build_dissect.html

A look back on WLAN in 2015

Introduction

Like last year’s post about WLAN in 2014, you can read it here, it is time to look back what happened in the wireless space this year and give a forecast for topics of 2016.

Looking Back

867 Mbps becoming the new basic speed for 5GHz APs

We currently see an increasing demand for 802.11ac APs with two spatial streams as some sort of the default AP. Up to this year the 300 Mbps 802.11n APs were the high running product for most and 450 Mbps 802.11n or 1300 Mbps 802.11ac APs for the top notch installations. Since only Apple (and to a certain degree Dell) released 11ac Clients with 3 spatial streams, the vast majority of smartphones, tablets and laptops includes 1 or 2 spatial stream adapters, so a 11ac AP Wave 1 with 2 spatial stream is a perfect fit for today’s demand for bandwidth and it’s always great to see more and more 5 GHz installations.

A new channel bandwidth: 160 MHz

With the release of 802.11ac Wave 2 Access Points, an increase in the maximum channel bandwidth from 80 to 160 MHz took place. This of course is an 5 GHz only feature since it requires 8 x 20 MHz channels. This feature looks great on paper, it can nearly double the throughput of an 80 MHz AP creating really flashy numbers, but it lacks benefit in the real world. Besides some installations, mainly in the consumer space in a rural area, most enterprise customers are better of with 8 individual 20 MHz channels or at least 4 individual 40 MHz channels than a single 160 MHz channel causing co-channel congestion on all the non-160 MHz APs and vice versa.

Another spatial stream: No. 4

With Wave 2 came the fourth spatial stream to the APs, since I haven’t been able to find any client with a 4 stream WLAN adapter, this increase in spatial streams is more or less a simple benefit for MU-MIMO. I will explain this in the next paragraph.

MU-MIMO in action

Marketing departments hoped for a new flashy feature in 802.11ac besides the increase in spatial streams, channel bandwidth and Modulation and Coding Schemes to tell customers about the exciting new possibilities with the newer 802.11 standard. Multi-User MIMO promised to deliver a new way of how the medium can be accessed and could enhance the total throughput for client devices with less spatial streams than the AP has. By transmitting to other clients on antennas that are unused for a single client with less spatial streams than the AP can offer, the total throughput of all the transmissions should be greater than the one for the single transmission alone. Unfortunately we saw the first enterprise APs with this feature (e.g. Ruckus) and missed the clients to support this. At Wireless Field Day 8, Ruckus showed their feature against prototype clients, you can watch this video here: Ruckus demos MU-MIMO

We have to consider that APs will always “loose” one antenna for proper beamforming. So MU-MIMO can only take off if an AP has three or four spatial streams, which leads to a new notation of Tx Antennas, Rx Antennas, MIMO Spatial Streams and MU-MIMO Streams like 3×3:3:2 or 4×4:4:3. As long as no one figures out, how to utilize all antennas for MU-MIMO, the two stream APs would be 2×2:2:1, which means that there can be no second (beamformed) transmission at the same time.

With the release of the Nexus 5X, the first MU-MIMO capable client came to market and we can hope that the other vendors than Google will follow with their smartphone refresh in 2016.

Location, Location, Location

All big enterprise WLAN companies are offering Location Based Service (LBS) engines and customers start to figure out what to do with it. The whole LBS game is relatively new but very promising for the industry because the LBS-customers do not only offer wi-fi to their customers to keep them happy, but also get something back that is very useful in our modern, digital world: data from customers. Especially the retailers out there want to dig deeper into their costumer behavior and LBS promises to delivers this, quite good at the moment but even better in the near future.

As Cisco sets the bar quite high with their “hyperlocation” promising down to 1m accuracy (for associated clients), see their demo here, the rest of the enterprise will soon step up to the game. While all the Cisco methods still rely on RSSI of broadcast or data packets, it will be interesting to see if another, more precise method can be established next year to increase accuracy not only for associated clients.

Forecast

60 GHz and 802.11ad

I’ve written about this in my last year’s post about WLAN in 2014 within the forecast and now there is the first official slide claiming 802.11ad is not dead! Qualcomm will offer official support for the new IEEE standard in the 60 GHz band with their upcoming SoC – the Snapdragon 820. The slide is taken from the article at Anandtech about that SoC, you can find it here. We still have to wait and see how the AP side will react to this (first announcement in the customer space can be found here). I guess we will all have to wait and see how the enterprise vendors will adapt and promote this new standard this year.

Snapdragon 820 vs. 810

Please note: Since 60 GHz is suitable for Line of Sight (LoS) connections within short range, we might see some adaption in conference rooms/centers in the enterprise space, while the main installation will remain on 2.4/5 GHz.

 Conclusion

Most of the topics from 2015 have to do with 802.11ac, which is becoming the new standard while Wave 2 added some new features or enhancements to it that still need to be used by clients in the near future.

What is your opinion on the given topics? Which hot topics of 2015 do you think I missed? Comments are welcome.

Go for high bandwidth in 2.4 GHz! Oh wait…

Introduction

Though some WLAN experts claim that 2.4 GHz is dead and we all should move to 5 GHz, customers still rely on 2.4 GHz due to legacy clients or even new clients with 11bgn only wi-fi adapters. Some of these customers might tell you that they want to use 40 MHz channels in 2.4 GHz because they think they can achieve a very high performance and that would be essential for their typical workloads. As some might scream “If you want high performance, go to 5 GHz and be happy”, a statement that I do support very much, a customer can still point a finger at you and tell you that your Access Points (APs) might support 40 MHz in 2.4 GHz so it is his right to use it.

First of all, the use of 40 MHz channel bandwidth requires an RF-environment with no 11b/g-only BSSIDs, otherwise the AP is forced to use 20 MHz. Although it might be possible to use 40 MHz at the initial start of an BSS, the AP has to monitor the environment to fall back to 20 MHz as soon as he discovers an BSS with 11b/g only. This method is required by the IEEE 802.11 standard. A client has the possibility to inform the AP that he has to switch to 20 MHz via an Action Frame.

If you live in a rural area, you only use a single AP and the next 2.4 GHz AP is miles/kilometers away, we all can agree that 40 MHz might be an option. But what about an enterprise setup with multiple APs, SSIDs and neighbors? You got channel 12 and 13 in Europe so you could do a two channel model with grouping of the 40 MHz channel pairs 1,5 and 9,13, so the 40 MHz channels don’t overlap. This might be a good setup in theory, so what can go wrong? Well, here are two things that we experienced:

1) Client Behavior

A client might not like a switch from 40 MHz to 20 MHz during an established connection. We saw this with an Intel Dual Band Wireless-AC 7260 adapter running driver version 17.13.12.1 (2014-12-16). Traffic is send from AP to client and suddenly the client notifies the AP of an intolerant BSS, so that the AP has to disable 40 MHz channel bandwidth (see pictures below).

Client Action Frame

I’ve added the column “40 MHz support” to my Wireshark columns, so that you can see directly that the Beacon before the Action frame had 20 and 40 MHz support, the one after the action frame only 20 MHz support. Following the switch in the Beacon, the client sends a Deauthentication to the AP and the Probe Request/Response Ping-Pong begins.

Client Deauth

At the end, since the AP sends a Disassociate to the Client (frame 67) the client has to go through Authentication and Association again (followed by the Block ACK negotiation).Client ReAuthenticate

As shown above, the Intel client looses the connection to the AP when 40 MHz operation is stopped. Though you might claim that this happens due to a single AP setup, we have a costumer complaining about disconnections with Intel clients in his site with multiple APs installed.

2) Performance

We configured a traffic source to send 15 Mbps of throughput via Ethernet to the AP, which sends them via WLAN to the Intel client. We used 1400 Byte packets to achieve this load and chose a time interval of 10.0 seconds for the following graphs, so each bar in the graphs represents 10.0 seconds. The equivalent WLAN throughput over 10 seconds would be 150 Mbps.

Please note that the sniffer used in this test did not capture all the packets, we have sent 1 million test packets (excluding retries) and about 84% of these test packets have been captured. There have been other APs on the used channels, causing additional traffic that did not belong to the test transmission and this additional traffic is split into “Beacons” and “Other” in the following graphs. Let’s take a look at the throughput over time:

Throughput over Time

TPT-LegendTPT-Graph

As long as the 40 MHz channel bandwidth was used, we can see that the total “Downlink (DL) to Client” throughput, including initial and retry transmissions, varies a lot around 150 Mbps/10.0 seconds, including a certain amount of retransmissions. We don’t meet the 150 Mbps for each of the 10 seconds steps, sometimes it is a bit lower, sometimes much higher. After the switch to 20 MHz channel bandwidth and the client reconnecting to the AP, we see a rather constant 150 Mbps/10.0 seconds of total DL throughput with only a small amount of retransmissions.

Medium Allocation over Time

We also plot the air times of the given categories as percentages of the time interval and call it “Medium Allocation”. As the time interval is defined as 10.0 seconds, 10 seconds are 100%. The air time is defined by “packet length in bits” divided by “data rate in bits”. The “Unused time” in this case is defined as: “10.0 seconds” – “Total DL time” – “Total UL time” – “Total Other time”. With the constant 15 Mbps throughput, the medium was not saturated. We can also see that the “Other Beacons” take a certain amount of time due to a rather low basic data rate.

 

MA-Legend MA-GraphThe air time of the total DL transmissions for 40 MHz is a little lower than for 20 MHz, for most of the 10 seconds time steps. It varies between 5.8 and 14.4 % (Average: 8.2%, Median: 7.7%). Due to the higher data rate, this was expected, but the 20 MHz total DL transmission are nearly constantly using 12.1 to 12.3 % of air time (Average: 12.2%, Median: 12.3%), which means that the total 20 MHz DL transmission time is not twice as much as the total 40 MHz DL transmissions time. So yes you can lower the medium allocation/DL air time by using 40 MHz channels, but you loose efficiency due to more retransmissions.

Summary

A 40 MHz channel model in 2.4 GHz in an enterprise environment does not show the benefits that you might expect. We saw that Intel clients do not honor the switch from 40 MHz to 20 MHz channel bandwidth and require a reconnection. Furthermore, the throughput of 15 Mbps was rather constant with 20 MHz since less retransmissions occurred and so the air time of the total DL transmission is not twice as much as for the 40 MHz bandwidth.

If we take into consideration that the customer could use a four channel model with 20 MHz instead of a two channel model with 40 MHz, the total throughput of four parallel 20 MHz transmissions might give him way more efficiency over all four channels than two parallel 40 MHz transmissions. We can assume this by expecting more retransmissions for intended loads that saturate the medium (much) more than the 15 Mbps in this example and more “Other” traffic than in this example from neighboring companies using the same channels. The question is, if it is possible at all to achieve a higher load than the total net throughput of the 144.4 Mbps data rate (highest 20 MHz data rate with short Guard Interval)?!

Have made a similar experience? Have you found a client that dealt with the 40->20 MHz channel bandwidth switch without any troubles? Do you got any customers demanding 40 MHz in 2.4 GHz? Please share your opinion on this topic in the comments section.

Germany’s new draft law for wi-fi hotspots

As I have explained in my previous blog post about the “Störerhaftung” for guest access to WLAN hotspots, offering free Wi-Fi in Germany does not come without any risks. The latest draft law tries to remove the accountability of wi-fi hotspot operators in case a wi-fi user breaches the law.

If the law passes the german parliament, the “Bundestag”, an operator has to use encryption or let the users voluntarily register for the service. It is also necessary to force each user to accept the General Terms and Conditions including a term like “I won’t use this wi-fi hotspot for any illegal activity”.

The combination of encryption and GTC acceptance is criticized since it would require individual access codes for each user, a requirement that is hard to meet with most commercial wi-fi equipment. Let’s wait and see if the law will pass unchanged and if it can improve the hotspot situation in Germany.

WLPC_EU 2014 Presentation “How to Secure your Wireless LAN”

In October 2014, Keith Parsons brought the Wireless LAN Professionals Conference (WLPC) to Maastricht in the Netherlands. It was a great event for all the attendees to get to know other WLAN folks and talk about our common passion – improving the Wi-Fi experience by sharing our knowledge. There will be another WLPC_EU this October, so if you are interested, I highly recommend it.

One of the presentation slots was given to my presentation about WLAN and security. You can watch a recording of that presentation at Vimeo or right below.

Tagline: “Everybody can do 802.11i, but what other security concepts can or should play a role in designing a secure network, not only in regards to WLAN? This session is about protected management frames and the german/LANCOM view on secure (wireless) network infrastructure.”


This presentation will also be part of WiFivaganza at the end of April in Maastricht, another new WLAN conference focusing on the community but not related to the WLPC(_EU). Feel free to get in touch with me there.

You can check out a lot of other WLPC_EU presentations via this link and read more about Protected Management Frames in my blog post.