CCMP: Decrypting Frames and Calculating the MIC for WPA2

In the previous post, we explored the details of the RSNA 4-Way Handshake and key generation. In this post, we’ll focus on decrypting and calculating the MIC (Message Integrity Code) which ensures the integrity of a ciphered frame alongside additional authenticated data (AAD).

Remeber to check if needed the previous post for a deep dive into the RSNA Key Management:

https://1hundredwire.com/a-deep-dive-into-rsna-key-management-and-wpa2-personal-security/

If you’d like to follow along, you can download the PCAP file from the Wireshark repository: wpa2linkuppassphraseiswireshark.

For a better understanding of CCM (Counter with CBC-MAC), check out my detailed post: Explaining CCM Encryption – A Detailed Walkthrough. CCM is the foundation of CCMP, the encryption protocol defined by IEEE 802.11 and used in WPA2 (CCMP-AES).

CCM operates in two parts:

  • Counter mode encrypts the payload.
  • Cipher Block Chaining (CBC) calculates the MIC (Message Integrity Code), which is appended to the encrypted frame. The receiver then verifies the MIC to ensure the data hasn’t been tampered with.

In the last post, we derived the PTK and TK, which are used for encryption and authentication. Now, we’ll decrypt frame 12, which was sent immediately after the 4-Way Handshake.

802.11 Frame to be analyzed

The Data (62 bytes) has the ciphertext on the first 54 bytes and the MIC on the last 8 bytes.

The Cipher Text is: 0x425140326b1d4fd39c6d3a9247d3c82ec709c89a58457d06fb7062e892a08daaceb3023a3e71dd811fe08a3d82d6e03045942cdc55a2

The MIC is: 0x18bc3e0680faf030

CCM Inputs (Decryption)

The picture below shows the required inputs for CCM used in the decryption process:

The Encryption Key (K) is the Temporal Key (TK) we obtained in the last post: 0x99775e9a0854ac7899e11147547dd8f7.

The Ciphertext (C) is the encrypted data.

The Additional Authenticated Data (AAD) is derived from the 802.11 header. A detailed explanation will follow.

The Message Integrity Code (MIC) is 8 bytes long.

WPA2 uses the following CCMP parameters:

CCMP Parameters and Nonce

M is 8 bytes, and L is 2 bytes.

So:

M’ = (M / 2) – 1 = 3
L’ = L – 1 = 1

The Nonce used in CCM follows this structure:

Nonce Structure

Where:

Nonce Flags

The Nonce Flags has the length of 1 byte and has the fields:

  • Priority (Bits 0–3) – Indicates the priority of the MPDU.
  • Management (Bit 4) – Set to 1 if MFP is negotiated; otherwise, set to 0.
  • PV1 (Bit 5) – Set to 1 if the frame is PV1 (used by S1G 802.11ah Wi-Fi HaLow); otherwise, set to 0.
  • Reserved (Bits 6–7) – Always set to 0.
  • A2 – The Frame Address 2.
  • PN – The Packet Number, found in the CCMP Header.

Analyzing Frame 12, we have:

802.11 MAC Header of the Frame 12

The calculated Nonce Flag is 0x00 since:

  • The priority is 0.
  • It is not an MFP frame.
  • It is not a PV1 frame.

There is another header, the CCMP Header.

The CCMP Header has a hex value of 0x0100002000000000, which is decoded as follows:

802.11 CCMP Header
  • PN0 to PN5 encode the Packet Number (PN), where PN0 contains the least significant bits, and PN5 contains the most significant bits.
  • EXT IV (Extended IV) indicates that the CCMP Header extends the MPDU header by 8 octets. It is always 1 for CCMP.
  • Key ID specifies which key is used:
    • The PTK usually has a Key ID of 0.
    • The GTK usually has a Key ID of 1.
    • The Key ID is sent during the 4-way Handshake.

The Nonce consists of:

Nonce Flags (1 byte) | A2 (6 bytes) | PN (6 bytes)

So, we have:

0x00 | 0x500f807018d0 | 0x000000000001

Thus, the Nonce is:

0x00500f807018d0000000000001

CCMP Decryption with CTR

CCMP decryption relies on CTR mode, where the Counter is encrypted and then XORed with the Ciphertext to recover the plaintext.

CTR Encryption

CTR encryption and decryption follow the same logic; the only difference is that the XOR operation is performed with the ciphertext during decryption.

The ciphertext is divided into 128-bit blocks:

  • C_1 = 0x425140326b1d4fd39c6d3a9247d3c82e
  • C_2 = 0xc709c89a58457d06fb7062e892a08daa
  • C_3 = 0xceb3023a3e71dd811fe08a3d82d6e030
  • C_4 = 0x45942cdc55a2

The final block may contain fewer bytes. This frame consists of four blocks.

What makes CTR mode in CCMP unique is how the Counter is generated.

The Counter (A) is structured as follows:

Clock structure

Where i is the block counter, starting from 1.

The counter of i = 0 is used as part of the authentication process only.

The flag format is as follows:

Clock Flags

The Flags will be in binary 00000001 and in hex 0x01

A_n = Flags_Hex | Nonce | n

  • A_0 = 0100500f807018d00000000000010000
  • A_1 = 0100500f807018d00000000000010001
  • A_2 = 0100500f807018d00000000000010002
  • A_3 = 0100500f807018d00000000000010003
  • A_4 = 0100500f807018d00000000000010004

Now with the Clock (A) we will encrypted using AES each one of the clocks using the TK key: 0x99775e9a0854ac7899e11147547dd8f7

  • AES_A_0 = 0xf9dbe556fc2686c956daff0a5ea788c7 (Used in the authentication process)
  • AES_A_1 = 0xe8fb43326b1d47d3d96d3a8e47d3c82e
  • AES_A_2 = 0x380b7fa898ed19051b7062e983a16354
  • AES_A_3 = 0xceb3023a3e71dd811fe08a3d82d6e030
  • AES_A_4 = 0x45942cdc55a2ab4ce08517c562d0a90e

XOR with ciphertext

Now, each counter is XORed with its respective encrypted counter:

T_1 = AES_A_1 C_1

T_1 = 0xe8fb43326b1d47d3d96d3a8e47d3c82e 0x425140326b1d4fd39c6d3a9247d3c82e

T_1 = 0xaaaa0300000008004500001c00000000

T_2 = AES_A_2 C_2

T_2 = 0x380b7fa898ed19051b7062e983a16354 0xc709c89a58457d06fb7062e892a08daa

T_2 = 0xff02b732c0a86403e00000011101eefe

T_3 = AES_A_3 C_3

T_3 = 0xceb3023a3e71dd811fe08a3d82d6e030 0xceb3023a3e71dd811fe08a3d82d6e030

T_3 = 0x00000000000000000000000000000000

T_4 = AES_A_4 C_4

T_4 = 0x45942cdc55a2 0x45942cdc55a2

T_4 = 0x000000000000

The decrypted Plaintext is:

Decrypted Message: 0xaaaa0300000008004500001c00000000ff02b732c0a86403e00000011101eefe00000000000000000000000000000000000000000000

Cool, now we have a decrypted message! But before decoding it and figuring out what we’ve decrypted, let’s check if the MIC is correct. To do this, we’ll dive deeper into the Authentication Process.

Authentication

The authentication process uses CBC (Cipher Block Chaining) mode. It takes the last encrypted block and performs an XOR operation with the encrypted clock AES_A_0.

Before running CBC, we need to create the blocks. To make things easier, I will divide them into B_0, B_a, and B_m.

B_0

B_0 is created using the following structure:

B_0 Structure
B_0 – Flags

l(m) = Length of the message encoded in 2 bytes = 0x0036

Flags = 01011001 = 0x59

B_0 = 0x59 | 0x00500f807018d0000000000001 | 0x0036

B_0 = 0x5900500f807018d00000000000010036

B_a

The authentication process in CCMP authenticates the AAD (Additional Authenticated Data), which is not encrypted.

B_a consists of the length of the AAD concatenated with the AAD itself. If necessary, it is padded with zeros to create 128-bit blocks.

The AAD is derived from the 802.11 header, following specific rules:

AAD Masking Rules

Frame Control (FC):

  • Subtype (bits 4–6), Retry (bit 11), Power Management (bit 12), More Data (bit 13) → Masked to 0
  • Protected Frame (bit 14) → Always set to 1
  • +HTC (bit 15) → Masked to 0 if QoS Control is present, otherwise unmasked
  • Other subfields remain unchanged

MPDU Address Fields:

  • A1, A2, A3, and A4 (if present) included

Sequence Control (SC):

  • Sequence Number (bits 4–15) → Masked to 0
  • Fragment Number remains unchanged

QoS Control (QC) (if present):

  • TID used for AAD construction
  • In non-DMG BSS:
    • If both STAs support SPP A-MSDU, bit 7 (A-MSDU Present) is included
    • Otherwise, bits 4–6, 7 (if unsupported), and 8–15 are masked to 0
  • In DMG BSS:
    • Bits 7 (A-MSDU Present) and 8 (A-MSDU Type) included in AAD construction

After Masking:

  • FC = 0x8842
  • A1 = 0x4040a75073db, A2 = 0x500f807018d0, A3 = 0x1880909c6ae4
  • SC = 0x0000
  • QoS = 0x0000

The AAD is 0x88424040a75073db500f807018d01880909c6ae400000000

B_a is formed by concatenating the length of the AAD (l(a)) in two bytes with the AAD itself.

l(a) = 0x0018

  • B_a = 0x0018 | 0x88424040a75073db500f807018d01880909c6ae400000000
  • B_a = 0x001888424040a75073db500f807018d01880909c6ae400000000
  • B_a must consist of 128-bit blocks, so after padding with zeros, we obtain:

B_a = [ ‘0x001888424040a75073db500f807018d0’, ‘0x1880909c6ae400000000000000000000’ ]

B_m

The last block, B_m, is created by dividing the plaintext message into 128-bit blocks, and padding with zeros if necessary.

B_m = [‘0xaaaa0300000008004500001c00000000′,’0xff02b732c0a86403e00000011101eefe’,’0x00000000000000000000000000000000′,’0x00000000000000000000000000000000′]

Let’s call A_B the block created by combining all the other blocks.

A_B = B_0 | B_a | B_m

A_B = [‘0x5900500f807018d00000000000010036’, ‘0x001888424040a75073db500f807018d0’, ‘0x1880909c6ae400000000000000000000′,’0xaaaa0300000008004500001c00000000′,’0xff02b732c0a86403e00000011101eefe’,’0x00000000000000000000000000000000′,’0x00000000000000000000000000000000′]

CBC and XOR

Now we run the CBC with the A_B.

CBC block diagram

Doing the XOR with the previous output and running AES for each block.

The IV is ‘0x00000000000000000000000000000000’

C1 = AES(0x5900500f807018d00000000000010036 IV) = 0x8f2b25735dba7953cdac6d58c22b5577

C2 = AES(0x001888424040a75073db500f807018d0 0x8f2b25735dba7953cdac6d58c22b5577) = 0x0a05d04cb64b24b76efe433dff08d623

C3 = AES(0x1880909c6ae400000000000000000000 0a05d04cb64b24b76efe433dff08d623) = 0xe27d6e1b6b13543f9b9fdecdf93db9d9

C4 = AES(0xaaaa0300000008004500001c00000000 0xe27d6e1b6b13543f9b9fdecdf93db9d9) = 0x813064e26601dcc10e051d1707ea913b

C5 = AES(0xff02b732c0a86403e00000011101eefe 0x813064e26601dcc10e051d1707ea913b) = 0xaadc5bc6978d094b60bd98ed358f89d1

C6 = AES(0x00000000000000000000000000000000 0xaadc5bc6978d094b60bd98ed358f89d1) = 0xb9bc7b88e4768b73f4a079af132e0e07

C7 = AES(0x00000000000000000000000000000000 0xb9bc7b88e4768b73f4a079af132e0e07) = 0xe167db507cdc76f9ec030c951e766206

Authentication Final Step

The final step is to perform an XOR operation between the last block C7 (also known as T in the CCM spec) and the clock AES_A_0. The result is the first 16 bytes.

AES( 0xe167db507cdc76f9ec030c951e766206 0xf9dbe556fc2686c956daff0a5ea788c7)

= 18bc3e0680faf030bad9f39f40d1eac1

MIC = 0x18bc3e0680faf030

The MIC matches the one that we checked in the packet capture!

Ciphertext with MIC

Decoding the Data

The decrypted data was validated using the MIC. Now, let’s take a look at what the data is about:

Plaintext = 0xaaaa0300000008004500001c00000000ff02b732c0a86403e00000011101eefe00000000000000000000000000000000000000000000

LLCIPv4IGMPPadding

LLC Header: 0xaaaa030000000800 (LLC type 0800 tells that it is an IPv4);

Checking with Wireshark

The decrypted packet in wireshark is:

Frame 12 decrypted

And that’s it! We have successfully decrypted and verified the MIC.

The frame is a multicast membership query, which prompts a STA to send an IGMP membership report to join any multicast group the STA is interested in receiving traffic from.

What’s interesting here is that, although it’s a multicast message, the L2 address wasn’t a multicast address. This suggests that the infrastructure is converting multicast to unicast, likely to improve overall performance. This mechanism is outside the scope of this post, but it may be a topic for a future one.

To conclude, CCMP is an implementation of CCM that uses CTR mode for encryption and CBC for authentication.

I hope this has been informative for you!

Thanks,

Diego Capassi

References

IEEE Std 802.11™-2016, “IEEE Standard for Information technology—Telecommunications and information exchange between systems—Local and metropolitan area networks—Specific requirements—Part 11: Wireless LAN Medium Access Control (MAC) and Physical Layer (PHY) Specifications,” IEEE, 2016.

https://1hundredwire.com/a-deep-dive-into-rsna-key-management-and-wpa2-personal-security/

https://1hundredwire.com/explaining-ccm-encryption-a-detailed-walkthrough/

https://1hundredwire.com/understanding-aes-encryption-modes-from-ecb-to-cbc-and-ctr/