Outbreak Labs

I can do anything I want to. And so can you.

Decrypting WEP using C#

The WPA2 decryption from the earlier posts was challenging to implement. I knew that WEP used a simple RC4 decryption, and didn't think its implementation would warrant a post. And while it's true that it was trivial to implement the decryption, less so was performing the CRC validation. The display of the WEP ICV in Wireshark was very misleading here, because it is *not* the expected CRC. As it turns out, that value is also encrypted, so it must be decrypted along with the data. Here's the implementation:


public public static bool TryDecryptWEP(IEEE_802_11<Generic> encryptedPacket, byte[] key, out IPacket decrypted)
        {
            
            if (key.Length != 5 && key.Length != 13 && key.Length != 16 && key.Length != 29 && key.Length != 61)
            {                
                decrypted = null;
                return false;
            }

            if (encryptedPacket.FrameType != (int)FrameTypes.Data || !encryptedPacket.IsWep)
            {
                decrypted = null;
                return false;
            }

            var keyWithIV = new byte[key.Length + 3];
            Array.Copy(encryptedPacket.CCMP_WEP_Data, keyWithIV, 3);
            Array.Copy(key, 0, keyWithIV, 3, key.Length);

            var rc4 = new RC4(keyWithIV);
            var encryptedPayload = encryptedPacket.Payload.Concat(BitConverter.GetBytes(ByteOrder.NetworkToHostOrder(encryptedPacket.WEP_ICV))).ToArray();
            var dec = new byte[encryptedPayload.Length];
            rc4.decrypt(encryptedPayload, encryptedPayload.Length, dec);

            var expectedCRC = BitConverter.ToUInt32(dec, dec.Length - 4);
            dec = dec.Take(dec.Length - 4).ToArray();


            if (Crc32.Crc32Algorithm.Compute(dec) == expectedCRC)
            {
                var decryptedBytes = encryptedPacket.Take(26).Concat(dec).ToArray();
                decryptedBytes[1] &= 191; //Everything except protected
                decrypted = Generic.ParseAsNew(decryptedBytes, IEEE_802_11.ParseAsNew);
                return true;
            }
            else
            {
                decrypted = null;
                return false;
            }
        }
First, some simple sanity checks - a valid key length, and a data packet encrypted under WEP. Next, we append the key to the IV from the packet. The WEP_ICV is then appended to the data, and decrypted using the IV+Key. The result is our data + expected CRC. We slice off the expected CRC, then compute a standard CRC32 checksum over the allegedly decrypted data. If the calculated CRC matches the expected CRC, then the decryption was successful. As with the WPA2 decryption implementation, we have to fudge the packet header a bit, flipping the protected bit to 0 and excluding the encryption information. PacketGremlin can now handle WEP and WPA2/AES, only WPA/TKIP remains.

WiFi Decryption resources and test data

I thought I'd share some of the resources I used in the recent WiFi decryption series of posts.

First, much time was saved by my friend Daniel Smullen, who put together a set of clean captures of WEP, WPA, and WPA2 for me. The captures and the associated passwords are available for download here: MyTestWAP.zip (151.2KB)

The Aircrack-ng project was invaluable; it likely would have taken many months to accomplish what I did, were it not for the source code of this tool suite being available.

Finally, here are some miscellaneous resources that were useful in understanding the various protocols and encryption schemes involved:

http://www.willhackforsushi.com/papers/80211_Pocket_Reference_Guide.pdf
http://svn.fonosfera.org/fon-ng/trunk/openwrt/package/broadcom-wl/src/driver/proto/eapol.h
http://www.xirrus.com/cdn/pdf/wifi-demystified/documents_posters_encryption_plotter
http://my.safaribooksonline.com/book/networking/wireless/0596001835/802dot11-framing-in-detail/wireless802dot11-chp-4-sect-3
https://chromium.googlesource.com/chromiumos/third_party/hostap/+/0.12.369.B/wlantest/ccmp.c
http://security.stackexchange.com/questions/46670/does-using-wpa2-enterprise-just-change-the-attack-model-vs-wpa2-psk
http://www.seas.gwu.edu/~cheng/388/LecNotes/CCMP.pdf
http://stackoverflow.com/questions/12018920/wpa-handshake-with-python-hashing-difficulties
http://stackoverflow.com/questions/19144775/4-way-handshake-simulation-in-c-sharp?rq=1
http://stackoverflow.com/questions/2465690/pbkdf2-hmac-sha1
http://hashcat.net/forum/thread-1745.html

Decrypting CCMP using C#

The final step in the WiFi decryption saga is to deal with Counter Mode Cipher Block Chaining Message Authentication Code Protocol, commonly abbreviated to CCMP. It is the encryption standard used by WPA2, and requires everything done up to this point to generate the temporal key used as input. Here is the code.

public static bool TryDecryptCCMP(IEEE_802_11 encryptedPacket, byte[] temporalKey, bool strip80211Header, out IPacket decrypted)
        {
            if (temporalKey.Length != 16) throw new ArgumentException("temporalKey must be 16 bytes");
            var decryptedBytes = encryptedPacket.ToArray();
            int z, data_len, blocks, last, offset;
            bool is_a4, is_qos;

            var B0 = new byte[16];
            var B = new byte[16];
            var MIC = new byte[16];
            var PacketNumber = new byte[6];
            var AdditionalAuthData = new byte[32];

            is_a4 = (decryptedBytes[1] & 3) == 3;
            is_qos = (decryptedBytes[0] & 0x8C) == 0x88;
            z = 24 + 6 * (is_a4 ? 1 : 0);
            z += 2 * (is_qos ? 1 : 0);

            PacketNumber[0] = decryptedBytes[z + 7];
            PacketNumber[1] = decryptedBytes[z + 6];
            PacketNumber[2] = decryptedBytes[z + 5];
            PacketNumber[3] = decryptedBytes[z + 4];
            PacketNumber[4] = decryptedBytes[z + 1];
            PacketNumber[5] = decryptedBytes[z + 0];

            data_len = decryptedBytes.Length - z - 8 - 8;

            B0[0] = 0x59;
            B0[1] = 0;
            Array.Copy(decryptedBytes, 10, B0, 2, 6);
            Array.Copy(PacketNumber, 0, B0, 8, 6);
            B0[14] = (byte)((data_len >> 8) & 0xFF);
            B0[15] = (byte)(data_len & 0xFF);

            AdditionalAuthData[2] = (byte)(decryptedBytes[0] & 0x8F);
            AdditionalAuthData[3] = (byte)(decryptedBytes[1] & 0xC7);
            Array.Copy(decryptedBytes, 4, AdditionalAuthData, 4, 3 * 6);
            AdditionalAuthData[22] = (byte)(decryptedBytes[22] & 0x0F);

            if (is_a4)
            {
                Array.Copy(decryptedBytes, 24, AdditionalAuthData, 24, 6);

                if (is_qos)
                {
                    AdditionalAuthData[30] = (byte)(decryptedBytes[z - 2] & 0x0F);
                    AdditionalAuthData[31] = 0;
                    B0[1] = AdditionalAuthData[30];
                    AdditionalAuthData[1] = 22 + 2 + 6;
                }
                else
                {
                    AdditionalAuthData[30] = 0;
                    AdditionalAuthData[31] = 0;

                    B0[1] = 0;
                    AdditionalAuthData[1] = 22 + 6;
                }
            }
            else
            {
                if (is_qos)
                {
                    AdditionalAuthData[24] = (byte)(decryptedBytes[z - 2] & 0x0F);
                    AdditionalAuthData[25] = 0;
                    B0[1] = AdditionalAuthData[24];
                    AdditionalAuthData[1] = 22 + 2;
                }
                else
                {
                    AdditionalAuthData[24] = 0;
                    AdditionalAuthData[25] = 0;

                    B0[1] = 0;
                    AdditionalAuthData[1] = 22;
                }
            }

            using (RijndaelManaged aesFactory = new RijndaelManaged())
            {
                aesFactory.Mode = CipherMode.ECB;
                aesFactory.Key = temporalKey;
                ICryptoTransform aes = aesFactory.CreateEncryptor();

                aes.TransformBlock(B0, 0, B0.Length, MIC, 0);
                XOR(MIC, 0, AdditionalAuthData, 16);
                aes.TransformBlock(MIC, 0, MIC.Length, MIC, 0);
                XOR(MIC, 0, AdditionalAuthData.Skip(16).ToArray(), 16);
                aes.TransformBlock(MIC, 0, MIC.Length, MIC, 0);

                B0[0] &= 0x07;
                B0[14] = 0;
                B0[15] = 0;
                aes.TransformBlock(B0, 0, B0.Length, B, 0);
                XOR(decryptedBytes, decryptedBytes.Length - 8, B, 8);

                blocks = (data_len + 16 - 1) / 16;
                last = data_len % 16;
                offset = z + 8;

                for (int i = 1; i <= blocks; i++)
                {
                    var n = (last > 0 && i == blocks) ? last : 16;

                    B0[14] = (byte)((i >> 8) & 0xFF);
                    B0[15] = (byte)(i & 0xFF);

                    aes.TransformBlock(B0, 0, B0.Length, B, 0);
                    XOR(decryptedBytes, offset, B, n);
                    XOR(MIC, 0, decryptedBytes.Skip(offset).ToArray(), n);
                    aes.TransformBlock(MIC, 0, MIC.Length, MIC, 0);

                    offset += n;
                }
            }
            if (strip80211Header)
            {
                decryptedBytes[1] &= 191; // Remove the protected bit
                var decryptedBytesList = decryptedBytes.ToList();
                decryptedBytesList.RemoveRange(34 - 8, 8); // Remove CCMP Parameters (otherwise, without the protected bit it will break the parsing)
                decrypted = Generic.ParseAsNew(decryptedBytesList, IEEE_802_11.ParseAsNew).Layers().ElementAt(1); // Remove the altered 802.11 header
            }
            else
            {
                decrypted = Generic.ParseAsNew(decryptedBytes, IEEE_802_11.ParseAsNew);
            }

            return memcmp(decryptedBytes.Skip(offset).Take(8).ToArray(), MIC.Take(8).ToArray()) == 0;

        }
This is nearly a direct port from airdecap-ng, except for the actual cryptography which of course uses the managed equivalents. First let's get the most obvious parameters out of the way: We need an 802.11 packet to decrypt; the "encryptedPacket" parameter. As before, this isn't the entirety of the frame, which might have radiotap or other things attached, it's just the 802.11 layer and above. We also need a place to put the decrypted result, that's the "decrypted" parameter.

The "strip80211Header" option is one that I'm not too happy with. When the decryption is complete, you're left with the 802.11 header essentially untouched, which indicates that the contents is encrypted. I need to be able to return the full packet that was inputted for verification purposes, but beyond that the more useful output is the actual decrypted contents. So this parameter allows the consumer of the function to choose whether the output will be at the 802.11 layer, or at the decrypted protocol layer.

The temporalKey parameter is of course the most interesting. The Temporal Key is 16 bytes long, 32 bytes into the Pairwise Transit Key (PTK). I discussed how to get the PTK in a previous post.

I won't go into detail about the implementation of this method, as it's largely just data manipulation, but I'll discuss it at a high level. We start by moving bytes around to come up with the B0 and AdditionalAuthData arrays. Next, we prepare an AES encryptor in ECB mode. This mode is important, because normally you'd need to provide an Initialization Vector as well as a key to AES. We use the encryptor to come up with the MIC which we can use to validate our results, and also to perform the actual decryption of the data (which is done not all at once, but in blocks). As a final processing step, remove the 802.11 header if requested, and parse the resulting decrypted packet data. Lastly, we use the MIC we calculated to verify if the decryption was successful.

I was able to put together the PMK, PTK, and CCMP functions that I've gone over in this series to successfully decrypt my WPA2 test capture. Before I can call Packet Gremlin's WiFi decryption support complete, I'll need to also handle TKIP and WEP. There's a lot of overlap between these two; they both use the RC4 encryption algorithm, so it shouldn't take much time to implement. Later, I will post the test captures I've used, as well as links to many of the resources that were helpful in coming up with these implementations.

Validating the WPA2 PTK using C#

Continuing on the quest to decrypt WiFi traffic, I've written code to validate the WPA PTK. This wasn't strictly necessary, and I tried to avoid doing so if only to get to the end goal faster, but when I started implementing the CCMP decryption and it wasn't working, I needed more visibility into what was going on. As before, I arrived at this code by studying various online resources and the Aircrack-ng source code.


public static bool PtkIsValid(byte[] ptk, IEEE_802_1X<IEEE_802_1X.Key> eapolKey)
        {
            bool tkip = (eapolKey.Payload.KeyInformation & 7) == 1;
            bool isValid;
            var MIC = eapolKey.Payload.MIC;
            eapolKey.Payload.MIC = new byte[MIC.Length];
            if (tkip)
            {
                var hmacmd5 = new HMACMD5(ptk.Take(16).ToArray());
                hmacmd5.ComputeHash(eapolKey.ToArray());
                isValid = hmacmd5.Hash.Take(16).SequenceEqual(MIC.Take(16));
            }
            else
            {
                var hmacsha1 = new HMACSHA1(ptk.Take(16).ToArray());
                hmacsha1.ComputeHash(eapolKey.ToArray());
                isValid = hmacsha1.Hash.Take(16).SequenceEqual(MIC.Take(16));
            }
            eapolKey.Payload.MIC = MIC;
            return isValid;
        }
We take the PTK (of course) and an EAPOL Key packet as parameters. This EAPOL packet is part of the four way authentication handshake. Of the four messages in the handshake, any of the latter three will do, though the fourth would probably be quickest to process because it contains no WPA key data. The first message in the handshake is insufficient, as it has no MIC with which we can verify. It's important to note that this isn't the entirety of the frame, which would include the 802.11 header as well as any cruft from the wireless driver (such as the radiotap header); it's just the 802.1x and above. This is one of the nice things about the Packet Gremlin packet structure - instead of accepting a large array and getting offsets into it, I can more clearly express what data I need.

We start by determining if the handshake was done with AES or TKIP. This is done by checking the KeyInformation field of the EAPOL key packet. We need to know this to decide whether to use the MD5 or SHA1 algorithm.

Next, we take the MIC out of the EAPOL packet and zero it out. Figuring out that this needed to be done was tricky; the Aircrack-ng unit test has the frame data with this already zeroed out, and the MIC stored separately.

If the handshake was done with TKIP, we initialize the HMACMD5 algorithm with the first 16 bytes of the PTK, and use it to compute the hash over the EAPOL frame with the MIC zeroed out. We compare the first 16 bytes of the resulting hash with the first 16 bytes of the MIC we copied out of the EAPOL packet. If they are equal, then the PTK is valid.

Similarly, if the handshake was done with AES, we initialize the HMACSHA1 algorithm with the first 16 bytes of the PTK, and use it to compute the hash over the EAPOL frame with the MIC zeroed out. We compare the first 16 bytes of the resulting hash with the first 16 bytes of the MIC we copied out of the EAPOL packet. If they are equal, then the PTK is valid.

Before we're done, we put the MIC back in the EAPOL packet. I don't like having to do this temporary corruption of the packet. I could make a copy first, but I think that would be more of a performance hit than it's worth.

And there you have it! In the next post, I'll go over CCMP decryption, the final step in the process.