术语解释:

octet: octet这个词在rfc中反复出现,是数据长度单位,通常是指8bit。但是在某些架构的机器上这个值不等于8bit。如果你正在使用的是x86的处理器,那么你就把octet当作Byte来看就可以了。

悄然丢弃(silently discard):当服务器收到错误的包时,一般会给发送方发一个错误回复。而 silently discard的意思就是不发送错误回复,直接把这个包丢弃。

一:认证流程:

1.802.1x的身份认证,认证成功之前此端口属于受限端口,认证成功之后该端口被打开,数据可自由通过

2.客户机发出dhcp请求,通过dhcp过程,获得ip地址

3.定时刷新,大概10秒左右。如果2-3分钟内收不到有效的刷新回复,服务器停止计费,并将此端口关闭(改为受限端口)

二、协议模型:

802.1x认证过程中采用的协议可以分为3层:

Extensible Authentication Protocol (EAP) (rfc3748)
EAPOL PAE/802.1x
ethernet(rfc894)

dhcp过程采用的是标准的dhcp协议

应用层 DHCP
传输层 TCP/UDP
网络层 IP
物理链路层 ethernet

三、协议详解

1、Ethernet frame:

本文不讨论基于802.3以太网。

以太网上的数据按帧(frame)传输,每个帧的格式为

以太网头(14 octet)
数据 尾部校验
目的地物理地址 源物理地址 ethernet type code data CRC

对于ethernet,物理地址,即MAC地址,都是48bit。

本文所说的所有包,除去dhcp过程的包,都是EAPOL EAP/802.1x类型,ethernet type code 用的是0x888e

而dhcp是基于IP协议的TCP/IP应用层协议。对于ipv4,ethernet type code 用的是0x0800.(矿大目前还没通ipv6,即将)

ethernet的包的最大长度(以太网头+数据)是1500 octet,最小是60 octet.

当包的实际长度小于ethernet要求的最小长度时,需要做尾部填充。

尾部填充规则:

先填充12位的这种
static u_char packet_pad[12]={
0x6C,0x69,0x6E,0x6B,0x61,0x67,0x65,0x00,0x00,0xC9,0x00,0x19
};

如果长度还是不够,就以0补足。

2、EAPOL PAE/802.1x:

 

1 2 3 4 5...
version type length data

version:

长度为1 octet,值必须为1

type:

a) EAP-Packet. A value of 0000 0000 indicates that the frame carries an EAP packet.
b) EAPOL-Start. A value of 0000 0001 indicates that the frame is an EAPOL-Start frame.
c) EAPOL-Logoff. A value of 0000 0010 indicates that the frame is an explicit EAPOL-Logoff
request frame.
d) EAPOL-Key. A value of 0000 0011 indicates that the frame is an EAPOL-Key frame.
e) EAPOL-Encapsulated-ASF-Alert. A value of 0000 0100 indicates that the frame carries an
EAPOL-Encapsulated-ASF-Alert.

length:

从这个字段后面开始算,不算后面的尾部填充

data:

可选。如果type为EAPOL-Start或EAPOL-Logoff,则length等于0,data为空。

Extensible Authentication Protocol (EAP):

EAP 包的标准格式

1 2 3 4 5...
code Identifier Length Data ...

这个是在rfc3748的第19页定义的。

Code:

长度为一个字节,指明此EAP包的类型

EAP Codes 字段可取的值为:

1 请求
2 回答
3 成功
4 失败

如果接受到的值不在上述4个之列,必须悄然丢弃。

字段说明:

Identifier

长度必须为1个octet

Identifier字段的值在请求和回复的过程中必须一直是一样的。即使是超时重传,这个值也不能变。

如果服务器发送的请求包的Identifier字段的值为a,而客户端的回复为b,且a不等于b,那么这个回复必须被悄然丢弃。(silently discard)

Length

Length 字段的长度是2 octet,指明了包的长度(指code,Identifier,Length,Data四个字段长度之和)。超过这个长度的部分,被视作链路层的尾部填充(padding)而在处理中被忽略。如果length字段的值大于包的总长度,那么这个包必须被悄然丢弃。

(由于我们现在所用的是基于EAPOL PAE/802.1x 的EAP,而EAPOL PAE/802.1x也有一个length字段,不过这个length字段是从EAPOL PAE/802.1x头之后开始算,而EAP的length字段是从EAP部分的第一个字节开始算,所以这两个字段的值,必须是一样的)

Data

Data字段的长度可以是0个,或者多个octet.Data字段的格式由Code字段所决定。

如果Code字段是3或者4,那么Data字段必须为空(长度为0),且Length字段必须为4。但是矿大似乎不是这样。认证成功返回的包也许长度会是5,data字段有1个octet的type.这个type的值通常为1

协议说明:

rfc3748中的authenticator是指我们所说的认证服务器,peer是指我们的客户机。

服务器发给客户机的称为请求包,code字段的值必须为1。

客户机发给服务器的称为应答包,code字段的值必须为2。

每个请求包,的data字段,必须含有1个octet的type字段。type字段的作用是指明服务器正在请求什么样的服务。

发送的请求包可能因为种种原因需要重发。重新发送的请求包的Identifier字段必须和原来的请求包的Identifier字段含有相同的值。

如果服务器发来的请求包是有效的,那么客户机必须发送一个应答。

应答包仅仅用来回复一个有效的请求包,而且,绝对不允许在超时后重传。

由于线路故障等原因,客户机可能会接受到重复的请求包。如果客户机收到了重复的请求包,而这个请求刚刚已经回复过一次了,那么客户机必须把以前的答复重新发送一次,而不是再次处理这个请求。

客户机必须按(请求包)接收到的顺序回复服务器的请求。

请求包和答复包的格式,可以统一的表述如下

1 2 3 4 5 6...
code Identifier Length type type Data ...

type:

请求包和应答包的type字段必须相同。

rfc3748中对于这个字段的建议值是

1 Identity 请求/应答 用户名
2 Notification

对于请求,意味着服务器在发送 displayable message。客户端应答时必须还是以type2应答,type-data字段长度必须为0

3 Nak (Response only)  
4 MD5-Challenge  
5 One Time Password (OTP)  
6 Generic Token Card (GTC)  
0xFE Expanded Types  
0xFF Experimental use  

对于矿大的802.1x协议,就我所抓到的包而言,type字段可能的取值为:

0x01 用户名
0x99 密码

认证过程的实例分析:

下面的过程中,我把ethernet头都省略了,没有写。因为我不想公布我的MAC地址

第一步,client给server发特殊的广播包

packet data: 1 1 0 0 108 105 110 107 97 103 101 0 0 201 0 25 0 0 0 0

这是一个简单的EAPOL PAE/802.1x包,data字段为空,长度为0

Ethernet 头部
EAPOL PAE/802.1x 头部  
目的MAC地址(48bit) 源MAC地址(48bit) ethernet type code(16bit) 1(8bit) 1(8bit) 0(8bit) 0(8bit) 尾部填充


其中目的MAC地址是在协议标准中被指定的。矿大用的是1-80-c2-0-0-3,这是一个MAC广播地址


第二步,用户名

服务器发EAP请求包,请求用户名
packet data: 1 0 0 5 1 191 0 5 1

client 给server发用户名

  0 1 2 3 4 5 6 7 8 9 a b c d e f
0 1 0 0 21 2 191 0 21 1 50 50 48 57 48 48 48
  PAE version PAE type PAE LENGTH code id 包长度 type 卡号...
1 50 56 50 48 51 64 101 100 117 108 105 110 107 97 103 101
  ...卡号 '@' 联网类型 尾部填充...


从ethernet type之后开始看,
0x00-0x03是EAPOL PAE/802.1x 头,
其中0x00是version,一般都是1.
0x01是EAP packet type,见前面的参考
0x02-0x03是包的大小,从这个字段后面开始算,不算后面的尾部填充,此处应该为 用户名长度+5
0x04是EAP code,2代表应答
0x05是session id,每个认证过程一个,从server的reply中copy过来
0x06-0x07是长度,必须和EAPOL PAE/802.1x头中(0x02-0x03)的长度相同。
0x08是EAP data type
0x09-0x18是用户名, 变长
0x09-0x14是卡号,即"220900028203",0x15是'@',0x16-0x18是"edu",合起来就是"220900028203@edu"
0x19以后是尾部填充

 

第二步,密码
server向客户端请求密码:

captured length: 60
packet length: 60
packet data: 1 0 0 21 1 191 0 21 153 0 0 58 147 0 0 20 58 0 0 82 170 0 0 76 167

  0 1 2 3 4 5 6 7 8 9 a b c d e f
0 1 0 0 21 1 191 0 21 153 0 0 0 147 0 0 20
  PAE version PAE type LENGTH EAP code id LENGTH type 密钥...
1 58 0 0 82 170 0 0 76 167 108 105 110 107 97 103 101
2 ...密钥 0 0 0 0 0    

0x00-0x03 EAPOL PAE/802.1x头,同上
0x04 code,因为是server发的请求包,所以为1
0x05 session id
0x06-0x07 包长度
0x08是type,此处是153,代表请求密码
0x09-0x18 密钥

client 给server发密码
packet data: 1 0 0 22 2 191 0 22 153 18 141 24 209 30 50 43 53 148 79 176 174 75 179 48 92 149

首先将16Byte的密钥和1 Byte的session id(密钥在前,id在后)共17位,做md5,结果记做md5Dig

将16Byte的密钥求和,然后取个位数,记做di.

将di左移4bit

然后将md5Dig与varPublic+di做按位xor,共16*8bit。(varPublic是个奇怪的东西,见最后面的参考)

再将上面按位xor得到的结果与password做按位xor,此处要求密码不得长于16位,所得的结果就是要被发送的,经加密过后的密码。

然后包的类型还是按照上面,加上连创特有的包头,但是正文一开始,必须等于18。然后是16位的经加密过后的密码。然后是尾部填充。

如果你看不懂我上面写了什么,这里是我写的一个实现,看代码也许比看文档更容易


///BUG!
///如果这里发生了意外的边界对齐
///例如,把len提升为4个字节,和后面的16字节的md5Dig恰好对齐
///那么认证一定会失败
struct {
u_char len;
u_char md5Dig[16];
} result;
result.len=18;

///声明并初始化md5Buffer
u_char md5Buffer[64];
memset(md5Buffer,0,sizeof(md5Buffer));

///md5Buffer里面要填充的内容是key+id
memcpy(md5Buffer,key,16);
md5Buffer[16]=id;

///这里不能直接用标准库的std::accumlulate
///因为结果类型是int
//u_char di=std::accumulate(key,key+10,0)%10;
u_char* p_end=key+16;
int sum(0);
for(u_char* p=key;p!=p_end;++p)
sum+=*p;

///取个位数字
int di=sum%10;

///作md5
MD5_CTX context;
MD5Init(&context);
MD5Update(&context,md5Buffer,17);
MD5Final(result.md5Dig,&context);

di<<=4;
for (int i=0;i!=0x10;i++)
result.md5Dig[i] ^= varPublic[di+i];

const char* password=getPassword();
if(password==NULL)
return -1;
size_t len=strlen(password);

for(size_t i(0);i<len;++i)
result.md5Dig[i]^=password[i];

第三步

如果认证成功,服务器会返回一个code等于3的EAP包。

如果失败,服务器会返回一个code等于4的EAP包。

然后就是刷新链接了,方式很简单,服务器会不断请求发送用户名,然后客户端不断发送就是了

第四步,断开链接

发一个不含data的EAPOL PAE/802.1x包,类型为EAPOL-Logoff

关于所使用包:

第一个寻找服务器的包和最后一个logoff是标准的PAE包,0x888e后面是4个字节PAE头,然后就没了,就是尾部填充。

其余的,都是在EAPOL PAE/802.1x 上传输的EAP包

参考:

密码加密中采用的varPublic

static unsigned char varPublic[0x100] = {
0x75, 0x73, 0x65, 0x20, 0x6D, 0x64, 0x35, 0x20,
0x65, 0x72, 0x72, 0x6F, 0x72, 0x21, 0x21, 0x00,
0x8B, 0xF4, 0x6A, 0x01, 0xA1, 0x00, 0x2C, 0x43,
0x00, 0x50, 0xFF, 0x15, 0x00, 0x68, 0x43, 0x00,
0x3B, 0xF4, 0xE8, 0x70, 0xB7, 0xFF, 0xFF, 0x8B,
0xF4, 0x6A, 0x01, 0xA1, 0x8C, 0x2D, 0x43, 0x00,
0x50, 0xFF, 0x15, 0x00, 0x68, 0x43, 0x00, 0x3B,
0xF4, 0xE8, 0x59, 0xB7, 0xFF, 0xFF, 0x8B, 0xF4,
0x6A, 0x01, 0xA1, 0x98, 0x30, 0x43, 0x00, 0x50,
0xFF, 0x15, 0x00, 0x68, 0x43, 0x00, 0x3B, 0xF4,
0xE8, 0x42, 0xB7, 0xFF, 0xFF, 0x8B, 0xF4, 0x6A,
0x01, 0xA1, 0x10, 0x2C, 0x43, 0x00, 0x50, 0xFF,
0x15, 0x00, 0x68, 0x43, 0x00, 0x3B, 0xF4, 0xE8,
0x2B, 0xB7, 0xFF, 0xFF, 0x8B, 0xF4, 0x6A, 0x01,
0xA1, 0x04, 0x2C, 0x43, 0x00, 0x50, 0xFF, 0x15,
0x00, 0x68, 0x43, 0x00, 0x3B, 0xF4, 0xE8, 0x14,
0xB7, 0xFF, 0xFF, 0x8B, 0xF4, 0x6A, 0x01, 0xA1,
0x90, 0x2D, 0x43, 0x00, 0x50, 0xFF, 0x15, 0x00,
0x68, 0x43, 0x00, 0x3B, 0xF4, 0xE8, 0xFD, 0xB6,
0xFF, 0xFF, 0x8B, 0xF4, 0x6A, 0x01, 0xA1, 0xF0};

版权说明:

以上抓包过程是我“以研究和学习为目的”独自在FreeBSD下完成。

所用的抓包器是我自己写的,基于BSD特有的bpf协议。

密码的加密算法是我参考hjkl(dayongxie@163.com)同学的代码写的。

协议分析是参考多篇rfc文档而写的,主要有

rfc3580 IEEE 802.1X Remote Authentication Dial In User Service (RADIUS)

rfc3748 Extensible Authentication Protocol (EAP)

rfc3579 RADIUS (Remote Authentication Dial In User Service) Support For Extensible Authentication Protocol (EAP)

hjkl开发了一个客户端可以在Linux和FreeBSD下使用,基于libpcap和libnet.以GPL方式发布。需要者可向其写信索取。

我也写了一个客户端仅能在FreeBSD下使用,基于BPF,需要者可向来信索取。snnn119@gmail.com