IEEE 802.11

libtins has great support for the IEEE 802.11 protocol. Tools such as those included in the aircrack-ng suite should be very simple to implement using this library.

The whole protocol is implemented using a base class, Dot11, which contains fields shared by every frame in it. Every frame type is represented by a certain class that inherits from it.

Management frames

Let's have a look at management frames, which are represented by the abstract class Dot11Management. This class contains several helper methods which allow you to search and add tagged options from the frame. We'll take as an example Dot11Beacon, which is the class that represents beacon frames:

Dot11Beacon beacon;
// Make this a broadcast frame. Note that Dot11::BROADCAST
// is just the same as "ff:ff:ff:ff:ff:ff"
beacon.addr1(Dot11::BROADCAST);
// We'll set the source address to some arbitrary address
beacon.addr2("00:01:02:03:04:05");
// Set the bssid, to the same one as above
beacon.addr3(beacon.addr2());

// Let's add an ssid option
beacon.ssid("libtins");
// Our current channel is 8
beacon.ds_parameter_set(8);
// This is our list of supported rates:
beacon.supported_rates({ 1.0f, 5.5f, 11.0f });

// Encryption: we'll say we use WPA2-psk encryption
beacon.rsn_information(RSNInformation::wpa2_psk());

// The beacon's ready to be sent!

Note that the above curly brace syntax used when calling Dot11Beacon::supported_rates will only work in C++11, the latest C++ ISO standard. That method takes a std::vector<float> as its argument, so we're calling vector's constructor from an initializer_list. If we didn't use this feature, we'd have to create a temporary vector, fill it in, and pass it as argument. If you haven't checked out C++11, you should, it's awesome.

Data frames

Data frames should be easy to deal with if you have a basic notion of how higher protocols are encapsulated within 802.11.

Let's asume we've got no encryption in our data frames. A TCP packet will look like this in libtins:

Dot11Data

Here, the SNAP class, represents the LLC+SNAP encapsulation. You should only care about this structure if you're actually crafting packets. While sniffing, you should always use the PDU::find_pdu method template to find the layer you're looking for. Note that instead of SNAP + IP + TCP, there might be just a RawPDU if the data is encrypted. In order to create a packet like the one shown above, you could do the following:

// Yikes! Simple stuff :D
Dot11Data data = Dot11Data() / SNAP() / IP() / TCP() / RawPDU("Hallou");

Decrypting data frames

libtins supports the decryption of WEP and WPA2 (both AES and TKIP) encrypted 802.11 frames. Of course, the library will not crack the password/PSK used to encrypt the packets, you need to provide it yourself.

Decrypting WEP encrypted frames

The WEPDecrypter class, which lies the namespace Crypto, is the one that handles WEP decryption. You create an object of that type, add tuples (bssid, password), and let it decrypt packets. Let's see an example:

Crypto::WEPDecrypter decrypter;
// Packets sent to/from the AP whose BSSID is the one provided below,
// will be decrypted using the password "passw"
decrypter.decrypter.add_password("00:01:02:03:04:05", "passw");
// Just asume we get an encrypted frame from somewhere
Dot11Data some_data = generate_dot11data();
if (decrypter.decrypt(some_data)) {
    // Data was decrypted
    std::cout << "Data was successfully decrypted!\n";
}
else {
    // Data couldn't be decrypted
    std::cout << "Decryption failed!\n";
}

Okay, the example is pretty straightforward. One thing I'd like to point out is why would decryption fail? Imagining the BSSID associated with the data frame was actually the one for which we provided a password, it could have happened that after decryption, the checksum field in the data frame was invalid. In this case, the RawPDU that contained the data, would be removed, so some_data.inner_pdu() would be effectively nullptr. I'll explain why does the class behave this way below.

In case decryption was successful, the RawPDU would be replaced by a SNAP PDU, followed by whatever that packet contained. As an example, this picture illustrates what would happen if you successfully decrypted a TCP packet:

Dot11Decrypt

Note that everything that follows the SNAP PDU obviously depends on the actual packet being decrypted. Moreover, if the data frame were not encrypted, or the associated BSSID was not the one given, the packet will be left intact.

If you read above and wondered why packets for which the checksum is invalid are modified, and the RawPDU which contains the encrypted data is removed, then here's the reason. The situation given in the snippet shown a few lines up is not very common. If you want to decrypt a packet, you are most surely taking it out from either your network interface, or a pcap file. The WEPDecrypter class was designed to be plugged in between a sniffer and the sniffer callback. That can be achieved using the DecrypterProxy class template. This is how you'd do it:

bool handler(PDU &pdu) {
    // process....
    return true;
}
// ....

// This creates a decrypter proxy class. 
auto decrypt_proxy = make_wep_decrypter_proxy(&handler);
// Same as in the previous example.
decrypt_proxy.decrypter().add_password("00:01:02:03:04:05", "passw");

// Create a sniffer
Sniffer sniffer("wlan0", Sniffer::PROMISC);
// Sniff and decrypt!
sniffer.sniff_loop(decrypt_proxy);

The code shown above would be a more common situation in which you'd use a WEPDecrypter. Let's analyze it:

  • We define a handler function, called "handler".
  • We create a decrypter proxy. The "auto" specifier, again a C++11 feature, is used so we don't actually write down the type of that object which should be something like DecrypterProxy<bool(*)(PDU&), WEPDecrypter>. Yes, it's a long name, that's why I used that specifier. This proxy class implements an operator(), suitable to be used as the argument to Sniffer::sniff_loop. It also stores a decrypter, in this case a WEPDecrypter, which you can access using the method DecrypterProxy::decrypter
  • The same tuple (bssid, password) is added to the decrypter held by the proxy class.
  • A sniffer is created, which will interpret packets from the wlan0 interface.
  • A sniffing loop is started. This is where the proxy does its magic. Instead of simply calling our handler, it will intercept data frames that, in this case, are sent to/from the AP identified by the BSSID provided, and it will decrypt the content. When the "handler" callback is executed, you know that if the PDU argument is a data frame that should be decrypted, then it will be. Frames from BSSIDs other than the ones provided will be left intact, and you'll still be able to process them in the same callback. Packets for which the checksum is invalid will be discarded. This somehow explains the behaviour pointed out a few lines up.

Using this design, when new decrypters are added, you'll simply chain them all together and use a somewhat "decrypting chain".

Decrypting WPA2 encrypted frames

As from libtins v1.1, the library supports the decryption of WPA2(both AES and TKIP) encrypted frames. This is done through the WPA2Decrypter class.

WPA2 decryption is more complex than WEP decryption, since you can't just pick a random packet and decrypt it. There's a handshake between each client and the access point in which some nonces, which are later needed during decryption, are exchanged.

Moreover, the access point's SSID is required in order to create the first set of keys(the PMK), so you have to provide that as well. The object will be analyzing beacon frames looking for that SSID so that handshakes performed against that access point can be filtered using the appropriate BSSID.

WPA2Decrypter will be looking for 4-way handshakes. Everytime one is detected, the PTK keys are generated(the MIC in the handshake is verified), and from that point, every packet sent from/to that client will be decrypted, just like WEPDecrypter does.

Let's see a small example on how to do this:

bool handler(PDU &pdu) {
    // pdu here is not encrypted!
    return true;
}
// ....

// This creates a decrypter proxy class. 
auto decrypt_proxy = make_wpa2_decrypter_proxy(&handler);
// Same as in the previous example.
decrypt_proxy.decrypter().add_ap_data("my_secure_psk", "my_access_point_ssid");

// Create a sniffer
Sniffer sniffer("wlan0", Sniffer::PROMISC);
// Sniff and decrypt!
sniffer.sniff_loop(decrypt_proxy);

802.11 encapsulation

As you may know, network interface drivers typically use some form of encapsulation rather than using raw 802.11 frames. A widely used form of encapsulation is the RadioTap protocol. If your driver uses RadioTap, then a packet taken from it will look like this:

RadioTap encapsulation

The RadioTap protocol gives you more information about 802.11 frames. You can retrieve info such as the signal strength, noise, whether a Frame Check Sequence is used to provide integrity, etc.

The RadioTap class uses some useful defaults, so you don't have to provide every option, since most of the time you'll use the same value for them. As an example, this snippet:

#include <tins/tins.h>

using namespace Tins;

int main() {
    RadioTap radio = RadioTap() / Dot11Beacon();
    PacketWriter writer("/tmp/output.pcap", PacketWriter::RADIOTAP);
    writer.write(radio);
}

Generates the following packet:

RadioTap Wireshark

Note that some drivers actually use raw 802.11 frames, without any encapsulation. Others may use AVS or Prism, but unfortunately, libtins does not support them yet. However, adding them is in the TODO list, so they'll probably be included soon.

Previous part: Protocols
Next part: Adding new protocols