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.

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:

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:

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:

- 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 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:

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:

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:


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.

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!

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:

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/