In this section we'll have a look how the IEEE 802.11 protocol is implemented in libtins.
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.
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 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:
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");
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.
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:
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:
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
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".
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);
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:
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:
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.