本节讨论 CCM 在 WiFi 中的实际应用 -- CCMP 协议

根据 RFC 3610,完成 CCMP 报文的加解密,需要提供:分组密钥(K)、随机数(Nonce)、附加认证数据(AAD),这三个参数从哪里来?

另外, 作为处理对象的 CCMP 报文又来自哪里? 正常是通过抓包获取,但无线报文比普通的有线(以太)报文抓取相对麻烦点
幸运的是,万能的 Internet 已经给我们准备好了,在 Wireshark 网站 -- wiki.wireshark.org -- 中有个网页链接
主流协议的报文都被世界各地的网友抓取并上传到链接指向的页面上
进入页面,下载其中一个叫 wpa-Induction.cap 的抓包文件,该文件将作为后续的解密报文

至于分组密钥 K,它与 STA 如何接入 WiFi 网络有关,具体而言
如果 STA 是通过 WPA-PSK/WPA2-PSK 模式(这是家用无线路由器中使用最多的模式)接入,则 K 来源于配置此模式时输入的密码(后面记为 PSK)
如果 STA 是通过 WPA-Enterprise/WPA2-Enterprise 模式接入,则 K 来自 802.1X 认证过程中协商出来的密钥(后续会单独讨论 802.1X 认证)
这两种模式下,STA/AP 双方最终都会得到名为 PMK 的密钥
PMK 的作用类似一个密钥种子,它衍生出(更为准确说是协商出)密钥 PTK(其中就包括分组密钥 K)

就文件 wpa-Induction.cap 而言,它抓取的是 WPA-PSK 模式下 STA 与 AP 的交互报文,AP 的密码(PSK)为 Induction

本节讨论 WPA-PSK 模式下:无线网络密码(PSK) --> PMK --> PTK --> K 的具体变化情况

在无线路由器上配置过 WPA-PSK 模式的网友知道,配置时除了要输入 PSK,还要指定无线网络名(标准名为 SSID)
SSID 有两个作用:标识 AP 自己的名称(与其他 AP 区分出来),另一个就是参与 PMK 的构造
构造过程遵循 PKCS #5 v2.0 中的 PBKDF2 标准,简言之,我们可以认为 PMK = PBKDF2(PSK, SSID)

wpa-Induction.cap 中的 SSID 又是多少?这需要查看类型为 Beacon 的报文
其中有个字段解析为 SSID parameter set: Coherer,即 SSID 名称为 Coherer

这里我们看到,一旦输入的 PSK 和 SSID 固定,PMK 就不再变化,这带来了一定的安全性问题
因为知道 PSK 的 STA 可以通过抓取四次握手报文,嗅探别的 STA 与 AP 之间的流量(见后面详细说明)
在 WPA-Enterprise/WPA2-Enterprise 模式中,PMK 是动态生成的,避免了上述担心

脚本 PBKDF2.pl 是生成 PMK 过程的代码实现,下面是其内容及执行结果

 1 use Digest::HMAC_SHA1 qw(hmac_sha1 hmac_sha1_hex);
 2 use Digest::SHA1  qw(sha1 sha1_hex sha1_base64);
 3 # PBKDF2(passphrase, ssid, 4096, 256) -- underlying function is HMAC-SHA1
 4 
 5 #(The first argument to the pseudorandom function PRF serves as HMAC's
 6 #   "key," and the second serves as HMAC's "text." 
 7 #   In the case of PBKDF2, the "key" is thus the password and the "text" is the salt.)  HMAC-
 8 #   SHA-1 has a variable key length and a 20-octet (160-bit) output
 9 #   value.
10 
11 if( $#ARGV != 1)
12 {
13   print "Usage: perl $0 passphrase ssid -- ASCII string form\n";
14   exit 0;
15 }
16 
17 $count = 4096;
18 $salt = $ARGV[1];
19 $password  = $ARGV[0];
20 print "Salt = SSID = $salt\nPassword    = $password\n";
21 
22 #PBKDF2 (P, S, c, dkLen)
23 #   Input:          P          password, an octet string
24 #                   S          salt, an octet string
25 #                   c          iteration count, a positive integer
26 #                   dkLen      intended length in octets of the derived
27 #                              key, a positive integer, at most
28 #                              (2^32 - 1) * hLen
29 #
30 #   Output:         DK         derived key, a dkLen-octet string
31 #
32 #      2. Let l be the number of hLen-octet blocks in the derived key,
33 #         rounding up, and let r be the number of octets in the last
34 #         block:
35 #
36 #                   l = CEIL (dkLen / hLen) ,
37 #                   r = dkLen - (l - 1) * hLen .
38 #
39 #         Here, CEIL (x) is the "ceiling" function, i.e. the smallest
40 #         integer greater than, or equal to, x.
41 #
42 $round = 2; # ceiling(256 bit/20)
43 #
44 #      3. For each block of the derived key apply the function F defined
45 #         below to the password P, the salt S, the iteration count c, and
46 #         the block index to compute the block:
47 #
48 #                   T_1 = F (P, S, c, 1) ,
49 #                   T_2 = F (P, S, c, 2) ,
50 #                   ...
51 #                   T_l = F (P, S, c, l) ,
52 #
53 for $i (1..$round)
54 {
55   $r .= &F($password, $salt, $count, $i);
56 }
57 $r = substr $r, 0, 32;
58 print "WPA-PSK:PMK = ", uc unpack('H*', $r);
59 sub F
60 {
61   ($password, $salt, $count, $ii) = @_;
62   $int_4 = pack 'N', $ii;
63   $yyy = $xxx = hmac_sha1(join ('',$salt,$int_4), $password);
64   for $i (2..$count)
65   {
66     $xxx = hmac_sha1($xxx, $password);
67     $yyy = $yyy ^ $xxx;
68   }
69   return $yyy;
70 }
View Code

C:\>perl PBKDF2.pl Induction Coherer
输出如下:
Salt = SSID = Coherer
Password    = Induction
WPA-PSK:PMK = A288FCF0CAAACDA9A9F58633FF35E8992A01D9C10BA5E02EFDF8CB5D730CE7BC

前已述及,PMK 会协商出 PTK,这个协商过程就是著名的 EAPOL-Key 四次握手
具体而言,四次握手的前两个报文(分别由 STA/AP 发出)各自包括了一串名为 WPA Key Nonce 的随机数(分别记为 ANonce/SNonce)

在 wpa-Induction.cap 中,EAPOL-Key 四次握手报文分别为 #87 #89 #92 #94,其中
#87 报文包含 WPA Key Nonce: 3e8e967dacd960324cac5b6aa721235bf57b949771c867989f49d04ed47c6933 -- ANonce
#89 报文包含 WPA Key Nonce: cdf405ceb9d889ef3dec42609828fae546b7add7baecbb1a394eac5214b1d386 -- SNonce

光有这两个随机数还不够,IEEE 802.11i 中还规定,STA 和 AP 的无线 MAC 地址(分别记为 AA/SPA)也要参与 PTK 的生成
最终生成 PTK 需要五个参数:PMK、AA、SPA、ANonce、SNonce

脚本 PMK2PTK.pl 对上述过程进行了实现,下面是其内容及执行结果

  1 use Digest::HMAC_SHA1 qw(hmac_sha1);
  2 
  3 if( $#ARGV != 4)
  4 {
  5   print "Usage: perl $0 PMK AA SPA ANonce SNonce\n";
  6   print "       PMK     -- Pairwise Master Key       32 bytes\n";
  7   print "       AA      -- Authenticator MAC address  6 bytes\n";
  8   print "       SPA     -- Supplicant MAC address     6 bytes\n";
  9   print "       ANonce  -- Authenticator Nonce       32 bytes\n";
 10   print "       SNonce  -- Supplicant Nonce          32 bytes\n";
 11   print "All input should be hexadecimal characters\n";
 12   exit 0;
 13 }
 14 
 15 if(length $ARGV[0] !=64)
 16 {
 17   print "PMK MUST be 32 bytes\n";
 18   exit 0;
 19 }
 20 if(length $ARGV[1] !=12)
 21 {
 22   print "AA MUST be 6 bytes\n";
 23   exit 0;
 24 }
 25 if(length $ARGV[2] !=12)
 26 {
 27   print "SPA MUST be 6 bytes\n";
 28   exit 0;
 29 }
 30 if(length $ARGV[3] !=64)
 31 {
 32   print "ANonce MUST be 32 bytes\n";
 33   exit 0;
 34 }
 35 if(length $ARGV[4] !=64)
 36 {
 37   print "SNonce MUST be 32 bytes\n";
 38   exit 0;
 39 }
 40 
 41 $ARGV[0]=uc $ARGV[0];
 42 $ARGV[1]=uc $ARGV[1];
 43 $ARGV[2]=uc $ARGV[2];
 44 $ARGV[3]=uc $ARGV[3];
 45 $ARGV[4]=uc $ARGV[4];
 46 
 47 $pmk    = pack 'H*', $ARGV[0];
 48 $aa     = pack 'H*', $ARGV[1];
 49 $spa    = pack 'H*', $ARGV[2];
 50 $anonce = pack 'H*', $ARGV[3];
 51 $snonce = pack 'H*', $ARGV[4];
 52 @z      = sort $ARGV[1],$ARGV[2];
 53 $bb     = $z[0].$z[1];
 54 @z      = sort $ARGV[3],$ARGV[4];
 55 $bb    .= $z[0].$z[1];
 56 $b      = pack 'H*', $bb;
 57 
 58 my $debug = 0;
 59 
 60 if ($debug)
 61 {
 62   print <<QQQ;
 63     以下摘自 IEEE 802.11i
 64     PTK ←  PRF-X(PMK, "Pairwise key expansion", Min(AA,SPA) || Max(AA,SPA) || Min(ANonce,SNonce) || Max(ANonce,SNonce))
 65           = PRF-X(K,    A,                       B)
 66     
 67     TKIP uses X = 512 and CCMP uses X = 384
 68     
 69     PRF-384(K, A, B) = PRF(K, A, B, 384)
 70     PRF-512(K, A, B) = PRF(K, A, B, 512)
 71     
 72     PRF(K, A, B, Len)
 73       for i  ← 0 to (Len+159)/160 do
 74         R ← R || H-SHA-1(K, A, B, i)
 75       return L(R, 0, Len)
 76     
 77     H-SHA-1(K, A, B, X) ← HMAC-SHA-1(K, A || Y || B || X)
 78     K is a key
 79     A is a unique label for each different purpose of the PRF
 80     B is a variable-length string
 81     Y is a single octet containing 0
 82     X is a single octet containing the loop parameter i
 83     || denotes concatenation
 84     
 85     总结
 86     B = min(AA,SPA) || Max(AA,SPA) || Min(ANonce,SNonce) || Max(ANonce,SNonce)
 87     计算   hmac_sha1("Pairwise key expansion"||0x00||B||0x00, PMK)
 88         || hmac_sha1("Pairwise key expansion"||0x00||B||0x01, PMK)
 89         || hmac_sha1("Pairwise key expansion"||0x00||B||0x02, PMK)
 90         || hmac_sha1("Pairwise key expansion"||0x00||B||0x03, PMK)
 91         || ...
 92     取上述结果前 384/512 Bit
 93 QQQ
 94 }
 95 
 96 undef $R;
 97 $Len = 512/8; # 384 -- TKIP, 512 -- CCMP
 98 $zero = pack 'H2',0;
 99 $a = "Pairwise key expansion";
100 for $i (0 ... ($Len+19)/20)
101 {
102   $idx = pack 'h2',$i;
103   $R .= hmac_sha1($a.$zero.$b.$idx, $pmk);
104 }
105 
106 print "\n";
107 print 'EAPOL-Key Confirm Key               ', unpack('H*',substr($R, 0, 16)),"\n";
108 print 'EAPOL-Key Encrypt Key               ', unpack('H*',substr($R, 16, 16)),"\n";
109 print '(TKIP|CCMP) Temporal Key            ', unpack('H*',substr($R, 32, 16)),"\n";
110 print 'TKIP MIC Key(for Authenticator Tx)  ', unpack('H*',substr($R, 48, 8)),"\n";
111 print 'TKIP MIC Key(for Supplicant Tx)     ', unpack('H*',substr($R, 56, 8)),"\n";
View Code

C:\>perl PMK2PTK.pl a288fcf0caaacda9a9f58633ff35e8992a01d9c10ba5e02efdf8cb5d730ce7bc 000c4182b255 000d9382363a
         3e8e967dacd960324cac5b6aa721235bf57b949771c867989f49d04ed47c6933
         cdf405ceb9d889ef3dec42609828fae546b7add7baecbb1a394eac5214b1d386
上面命令行实际在一行内输入,只处为折行显示

输出如下:
EAPOL-Key Confirm Key               b1cd792716762903f723424cd7d16511
EAPOL-Key Encrypt Key               82a644133bfa4e0b75d96d2308358433
(TKIP|CCMP) Temporal Key            15798d511beae0028313c8ab32f12c7e
TKIP MIC Key(for Authenticator Tx)  cb71c893482669da
TKIP MIC Key(for Supplicant Tx)     af0e9223fe1c0aed

从输出结果中看到,PTK 并不是一个单独的密钥,而是分成若干部分
其中 EAPOL-Key Confirm Key 和 EAPOL-Key Encrypt Key 仅用于 EAPOL-Key 的四次握手(对握手报文起加密和认证作用)
第三行 (TKIP|CCMP) Temporal Key 参与报文的加解密,在我们讨论的上下文中,就是 CCM 中的分组密钥 K
最后二行 TKIP MIC Key(...) 仅适用于 TKIP 协议,与 CCMP 协议无关

下面以报文 #99 为例,剖析 CCMP 的具体操作,报文内容如下
  08 41 2c 00 00 0c 41 82 b2 55 00 0d 93 82 36 3a -- IEEE 802.11 Data 报文头
  ff ff ff ff ff ff b0 01 01 00 00 20 00 00 00 00
  7e cc f6 0a c1 dd ff b0 47 96 c3 0b a1 9c 92 c6 -- 此行开始共 336 字节(共 21 行)为密文部分
  6f 4d 1c e7 27 08 c2 95 cf 58 19 45 8c 18 d5 1f    提取为文件 wpa-Induction-cipher.txt
  64 56 7a 7c c5 ff 85 e7 a6 8b 23 8a 33 5e 44 44
  f7 de 0c 5e ef 72 1d 9f db 0d 51 44 03 d1 c9 06
  46 15 23 3e fc e2 4b 41 6d 53 8c 88 84 5e 46 0d
  29 63 0e da 72 97 fd db b5 66 ac 0a 05 f9 21 1f
  bf 24 39 9a 15 a9 15 11 04 39 bd 0c 0c 51 0a 08
  4a 88 90 50 01 fc 64 cc 9a 4f ca d2 51 d6 e0 f1
  55 00 b7 13 fb 42 c2 44 60 58 2a 68 d0 a5 b9 9c
  80 8e 01 2c 20 0a c5 27 b0 eb 32 0f 75 7d 60 ea
  01 fa 79 f6 5c 2f c3 55 66 90 62 d9 25 e3 e4 4c
  02 91 c1 a7 36 d5 0f 0b 8c 6c 68 de 9e 53 6e d9
  7f eb 43 93 82 80 4b 73 92 3a 61 7f cc ef 37 60
  cf 65 98 f7 7e 39 b9 90 a6 d1 67 ab 5c a6 a9 57
  76 38 fe a8 34 2c 97 ab d5 54 f5 6f ea 48 eb 48
  be 52 df c8 27 66 7b 1c 09 08 78 58 b9 96 9a 74
  10 2d 53 e3 7d 35 2e e4 62 44 84 3d 02 f5 1b 04
  43 64 cb 26 33 fd 2e 8c 16 0a 21 31 24 56 e5 74
  74 89 33 e0 d8 49 5b e8 23 97 d8 9c b7 39 f7 ab
  a0 e8 44 c2 b8 dc 3a 3d 57 d1 a7 b0 7e a9 ff 97
  a3 d7 17 ff 02 83 0b 58 2c a8 94 27             -- 本行中前 8 字节为认证字段部分,后 4 字节为 FCS
                                                     CCMP 协议规定:认证字段长度 M = 8

现在已经知道分组密钥 K = 15798d511beae0028313c8ab32f12c7e
另两个参数 AAD/Nonce,它们在 IEEE 802.11i 有详细定义,这里直接给出结果

AAD = l(a) || a,构造比较复杂,我们估且认为它是下列字段的组合(实际上做了一些修正,感兴趣可参考标准)
      l(a) || Frame Control || Address 1 || Address 2 || Address 3 || Sequence Control || Address 4 || Qos Control
长度    2           2             6            6            6               2                6             2
其中最后两个字段 Address 4、Qos Control 为可选字段, 所以 AAD 长度范围是 24-32 字节
将 AAD 以 16 字节为单位分组,得到 B_1、B_2 两个附加认证分组,其中 B_2 可能要用 0 填充

在本例中,实际的 AAD 内容为(最后两个字段 Address 4、Qos Control 不存在,末尾填充 0)
00 16 08 41 00 0c 41 82 b2 55 00 0d 93 82 36 3a ff ff ff ff ff ff 00 00 00 00 00 00 00 00 00 00
l(a)  Fc    Address 1         Address 2         Address 3         此处开始用 0 填充

生成 AAD 分组文件
C:\>perl -e "binmode STDOUT; print pack('H*','00160841000c4182b255000d9382363affffffffffff00000000000000000000')" > aad.txt

Nonce 相对简单,其长度 = 15 - L = 15 - 2 = 13,格式为
Nonce = Priority Octet || A2                || PN
      = 00             || 00 0d 93 82 36 3a || 00 00 00 00 00 01
      = 00 00 0d 93 82 36 3a 00 00 00 00 00 01

B_0 = Flags || Nonce                                  || l(m)
    = 59    || 00 00 0d 93 82 36 3a 00 00 00 00 00 01 || 01 50 -- 0x0150 = 336
    = 59 00 00 0d 93 82 36 3a 00 00 00 00 00 01 01 50
生成 B_0 分组文件
C:\>perl -e "binmode STDOUT; print pack('H*','5900000d9382363a0000000000010150')" > b_0.txt

计算 A_i ...
A_0 = 01 00 00 0d 93 82 36 3a 00 00 00 00 00 01 00 00
A_1 = 01 00 00 0d 93 82 36 3a 00 00 00 00 00 01 00 01
生成 A_1 || A_2 || ... 分组文件
C:\>perl -e "binmode STDOUT; for($i=1;$i<22;$i++){print pack('H28n','0100000d9382363a000000000001',$i)}" > a_i.txt
# 循环 21 次,因为 21 * 16 = 336,索引为何从 1 开始,参见上节

计算密钥流 E( K, A_1 || A_2 || ... )
C:\>openssl enc -aes-128-ecb -K 15798d511beae0028313c8ab32f12c7e -iv 0 -nopad -in a_i.txt > enc_key.txt

密文 与 加密密钥 异或,得到明文

 1 if ( $#ARGV <= 0 )
 2 {
 3   print <<QQQ;
 4     用法:perl $0 file1 file2 ...
 5     举例:perl $0 a.txt b.txt c.txt
 6     说明:文件个数必须 >= 2
 7 QQQ
 8   exit 0;
 9 }
10 
11 my @file_content;
12 
13 binmode STDOUT;
14 
15 # 检查其余入参是否为十六进制和长度相等
16 foreach (@ARGV){
17   open INFILE, "<", $_ or die $!;
18   binmode INFILE;
19   local $/ = undef;
20   $buffer = <INFILE>; # 一次性读入所有文件内容
21   close (INFILE);
22   push @file_content, $buffer;
23 }
24 
25 my $result = pack('H2', "00");
26 # 逐一提取每个文件内容,进行 XOR
27 foreach (@file_content){
28   $result = $result ^ $_;
29 }
30 
31 #print uc unpack('H*', $result);
32 print $result;
View Code

C:\>perl xor_file.pl wpa-Induction-cipher.txt enc_key.txt > wpa-Induction-plain.txt

明文内容是什么呢?我们对下答案,看看 Wireshark 自身的 CCMP 解密结果,如下图

原来是 DHCP Request 报文,这也说明 WiFi 安全属于二层安全范畴
仔细对比明文 wpa-Induction-plain.txt 的内容,完全符合

仅仅满足数据的机密性还是不够的,如何判断报文是否被篡改过?
CCMP 是通过附在密文后的 8 字节认证字段(红色显示部分)来解决的,下面按照 RFC 3610 中的规定进行验证

已知 A_0 = 01 00 00 0d 93 82 36 3a 00 00 00 00 00 01 00 00
生成 A_0 分组文件
C:\>perl -e "binmode STDOUT; print pack('H*','0100000d9382363a0000000000010000')" > a_0.txt

生成数据校验密钥 E(K, A_0)
C:\>openssl enc -aes-128-ecb -K 15798d511beae0028313c8ab32f12c7e -iv 0 -nopad -in a_0.txt > mac_key.txt
C:\>od -An -tx1 mac_key.txt
 37 ef 73 a5 87 be 43 61 92 b3 6e ed c2 01 a3 49 -- 取前 8 字节 37 ef 73 a5 87 be 43 61

数据校验密钥 与 认证字段 异或,得到 X_n+1(即 CBC-MAC)的期望值
C:\>perl xor.pl 37ef73a587be4361 a3d717ff02830b58
结果为 XOR = 9438645A853D4839

而真实的 CBC-MAC 由密钥 K 对 -- B_0 || AAD分组 || WiFi明文分组(wpa-Induction-plain.txt) -- 进行 CBC 加密得到

合并 B_0 || AAD分组 || WiFi明文分组
C:\>perl -pe "BEGIN{binmode STDOUT;}" b_0.txt aad.txt wpa-Induction-plain.txt > b.txt

AES 加密计算 CBC-MAC

 1 use Crypt::CBC;
 2 
 3 if( $#ARGV != 3)
 4 {
 5   print "Usage: perl $0 encrypt/decrypt Key(hex) IV(hex) infile\n";
 6   exit 0;
 7 }
 8 
 9 $cipher = Crypt::CBC->new(
10               -key          =>  pack('H*', $ARGV[1]),
11               -cipher       =>  'Rijndael',
12               -header       =>  'none',
13               -iv           =>  pack('H*', $ARGV[2]),
14               -padding      =>  'none',
15               -keysize      =>  16,
16               -blocksize    =>  16,
17               -literal_key  =>  1,
18 );
19 
20 open INFILE, "<", $ARGV[3] or die $!;
21 binmode INFILE;
22 binmode STDOUT;
23 if ($ARGV[0] =~ /^e/)
24 {
25   $cipher->start('encrypting');
26   local $/ = undef;
27   $buffer = <INFILE>; # 一次性读入所有文件内容
28   print $cipher->encrypt($buffer);
29 }
30 elsif ($ARGV[0] =~ /^d/)
31 {
32   $cipher->start('decrypting');
33   local $/ = undef;
34   $buffer = <INFILE>; # 一次性读入所有文件内容
35   print $cipher->decrypt($buffer);
36 }
37 else
38 {
39   print "not encrypt/decrypt";
40 }
View Code

C:\>perl AES-CBC.pl encrypt 15798d511beae0028313c8ab32f12c7e 00000000000000000000000000000000 b.txt > cbc-mac.txt
文件 cbc-mac.txt 的最后一个分组为
 94 38 64 5A 85 3D 48 39 5B 35 A9 89 E6 47 A3 3B
其前 8 字节 9438645A853D4839期望值 相同,校验成功,说明报文未被篡改

再看报文最后一个字段 FCS(蓝色显示部分),在 CCMP 中也是起校验作用
不过校验范围为除 FCS 外的整个报文,即:报文头 || 密文 || 认证字段,不像 WEP 报文中是针对明文校验
同样也可以利用 perl 自带的 crc32.bat 脚本进行验证