项目时间:2012.3-2012.8
实验环境
硬件环境:VMware Workstation8.0虚拟机
测试系统:Windows XP SP2
系统结构
系统大体上可分为两大模块:内核层模块和应用层模块。整体框架如图所示
应用层模块又可细分为应用层控制模块和加密算法库模块,过滤加密规则库模块。内核层模块是系统的核心功能模块,是该系统的核心部分,下面详细叙述连个模块的功能分工。
(1)内核层模块:实现数据封包的获取与解析,匹配用户加密策略并完成网络数据包的透明加解密、数据包的分片和重组功能,保护数据传输的安全性,同时为应用层提供控制接口以便于用户策略的定制。
(2)应用层模块:控制内核驱动程序的启停,使系统处于保护和非保护状态;根据用户的需求定制过滤加密规则策略文件并导入注册表,如针对目的网段的加密策略,针对协议的加密策略等;初始化生成密钥信息,加密算法库,然后将其策略传递给内核驱动程序。
系统内核态实现
系统内核态模块主要实现的功能是对数据包的解析,以判断该数据包是否符合加密规则。若满足规则,则根据相应动作(接收/发送)采取不同的处理方式。若不满足规则,直接放行数据包。
对于加密后的数据包,我们使用如下包协议格式:
以太网帧头 |
IP头 |
加密协议头 |
加密后的TCP/UDP数据 |
对应加密协议头部的数据结构:
typedef struct _ENCRYPTION_HEADER { UCHAR Type; //加密类型 UCHAR Flag; //第1位:0不分段,1分段; 第2位:1第一段,0最后一段 USHORTIsEncrypt; //唯一标示是加密数据包 } ENCRYPTION_HEADER,*PENCRYPTION_HEADER;
数据包解析
由于数据包在内核中各层的传递都是以包描述符为单位的,因此我们若想要对数据包进行解析,则必须通过包描述符间接的获取数据包内容的实际存储区域,才能对其开始解析。
首先,通过NdisGetFirstBufferFromPacketSafe函数,获取该包描述符中的相关信息。
VOID NdisGetFirstBufferFromPacketSafe( IN PNDIS_PACKET Packet, //包描述符 OUT PNDIS_BUFFER *, //包描述符中对应的第一个缓冲区描述符 OUT PVOID *, //对应缓冲区起始地址 OUT PUINT FirstBufferLength, //缓冲区描述符的所对应缓冲区长度 OUT PUINT TotalBufferLength, //包描述符所对用缓冲区总长度 IN MM_PAGE_PRIORITY Priority );
接着,调用NdisAllocateMemoryWithTag函数分配一个与包描述符对应总大小的缓冲区,用于存放数据实际内容。
NDIS_STATUS NdisAllocateMemoryWithTag( OUT PVOID *, //分配缓冲区起始地址 IN UINT Length, //分配大小 IN ULONG Tag //内存标记 );
然后,通过NdisQueryBufferSafe函数获取每个缓冲区描述符所对应的缓冲区数据,并通过NdisMoveMemory函数将数据复制到我们之前分配的连续缓冲区,最后通过NdisGetNextBuffer函数顺序获取该包对应的缓冲区描述符,直至数据内容结尾。
VOID NdisQueryBufferSafe( IN PNDIS_BUFFER Buffer, //缓冲区描述度 OUT PVOID * OPTIONAL, //存放数据缓冲区地址 OUT PUINT Length, //缓冲区长度 IN MM_PAGE_PRIORITY Priority );
最后,对于刚才分配的连续缓冲区,已经存放好了用于分析的数据包内容,此时可以开始进行分析,通过与我们内核中存放的规则表比对,以判断数据包是否符合我们的加密规则。该比对内容调用了我们自定义的函数:
BOOLEAN IsPacketInRule(IN PUCHAR PacketContent); BOOLEAN IsInRuleList(IN PUCHAR PacketContent); BOOLEAN IsProtocolInRule( IN UCHAR Protocol, IN PRULE_LIST_ENTRY RuleListEntry );
这些函数主要是对我们的规则表进行遍历比对,以判断该数据包是否满足加密规则。
一个规则表项如下:
typedef struct _RULE_ITEM { UCHAR EncryptionType; PWCHAR Protocol; //需要保护的协议 USHORT ProtocolNumber; PUSHORT SrcPorts; USHORT SrcPortNumber; PUSHORT DestPorts; USHORT DestPortNumber; ULONG DestIPAddress; ULONG DestIPMask; }RULE_ITEM, *PRULE_ITEM;
解析包流程图如下:
规则表的获取
正如上节所提到的,在对数据包进行解析时,需要匹配加密规则表。规则表用struct _RULE_ITEM 结构体的链表来维护,而具体信息都是从系统注册表里获得。获取规则项的流程如下:
VOID UpdataRuleList()//来更新规则链表。 { ZwOpenKey( &hRegister, KEY_READ, &objectAttributes) //打开注册表的指定路径,即过滤规则存放的位置 ZwQueryKey( hRegister,KeyFullInformation,pfiFullInfo,uKeyLen,&uKeyLen ) //读取 KEY_FULL_INFORMATION 结构的数据, ZwEnumerateKey(hRegister,uLoopVar,KeyBasicInformation, pbiBasicInfo,uKeyLen, &uKeyLen) //枚举该键下的每个子键,获取相应信息,为每一组规则构造整路径,并利用获取每一组规则具体项数,路径信息。我们用这些信息来构造每一个子键,即每一组规则的完整路径,供下一步的读取使用。 //循环调用 GetRuleListEntryFromReg(IN PWCHAR SubKeyPath) 继续细化规则。 //循环结束 }
PRULE_LIST_ENTRYGetRuleListEntryFromReg( IN PWCHAR SubKeyPath ) { ZwOpenKey() //打开注册表。 ZwQueryKey() //读取KEY_FULL_INFORMATION 结构的数据。 //循环调用 ZwEnumerateValueKey( hRegister, uLoopVar, KeyValueFullInformation, pValueFullInfo, uKeyLen, &uKeyLen) //读取每一项规则的键值 FillRuleItem(&Item, pValueFullInfo) //填充规则项结构体Item 。 //循环结束 }
经过这样的过程,我们就完成了内核驱动对加密规则的获取。
数据包分片重组
由于物理网络层一般要限制每次发送数据帧的最大长度,任何时候IP层接收到一份要发送的IP数据报时,它要判断向本地哪个接口发送数据,并查询该接口获得MTU。IP把MTU与数据报长度进行比较,如果需要则进行分片。在正常的情况下IP数据包的分片是由路由器来完成的,路由器只负责数据包的分段操作,而数据包的分片重组操作是由目的主机来完成的。
但是在该系统的协议层驱动和小端口驱动之间加入一个中间层驱动,在中间层驱动中对数据包进行了加密的处理并附加了一些加密协议信息,那么分片后的数据包还有可能超过允许传输的最大长度限制,因而需要在中间层对其进行手动的分片与重组。
在发送端,只有符合加密规则的数据才有可能被进行分片操作。该分片操作并不在IP头的分片标记与分片偏移中体现,而是在我们的加密协议头中标识分片信息。
分片流程如下:
1. 判断原始数据包长度加上加密协议头部的长度后,是否超过了该环境下的MTU(系统默认以太网环境),如果超过MTU大小,则开始分片处理过程2,否则直接发送;
2.在加密协议头中定义一个我们自己的分片标识字段,如下:
#define ENCRYPTION_HEADER_FLAG_F 0x80 //分片标识 #define ENCRYPTION_HEADER_FLAG_MF 0X40 //第一片
根据此标识来识别关于分片的信息,并在处理数据填充加密协议头部的时候,将其写如协议头部相应字段Flag。
3. 根据分片中的类型,第一片/最后一片,计算好相应的数据包偏移与长度,并组装成两个不同长度的数据包,之后进入加密处理,最后发送分片数据包;
重组流程如下:
1.收到一个下层传来的数据包后,接收端先对其进行解析,获取传来的数据包内容。之后根据解析后的数据是否存在加密协议来决定数据处理流程。若解析的数据包中IsEncrypt字段有效则表明需要进行解密流程,此时留意加密协议头部中的flags字段;
2.若Flag中的字段为0,说明该数据包不是分片的数据包,此时直接解密处理;若Flag中为 ENCRYPTION_HEADER_FLAG_F| ENCRYPTION_HEADER_FLAG_MF,则表明该数据包是分片且为第一个分片包,此时需要将该IP头部的标识字段,源IP地址,以及数据包加密内容存在一个接收节点,转入过程3;
若Flag标识为ENCRYPTION_HEADER_FLAG_F,则表明着是一个分片处理包的最后一个分片,并将其也存入一个接收节点,转入过程3。该节点如下:
typedef struct _ReceiveListNode { structin_addr iaSrc;//IP包源地址 USHORTID; //IP包的标识字段 PUCHAR EncodeData; //加密的数据内容 USHORT BufferLenth; //长度 struct_ReceiveListNode* Next;//下一个接收节点 }ReceiveListNode, *pReceiveListNode;
3. 我们用一个队列构造一个待重组缓冲区,进入接收队列,将该接收节点与队列中的节点进行比较(比较IP源地址,IP标识字段是否相同),若相同则表明可以进行重组,从队列中取出相同的接收节点,并将两节点重组后解密,向上层传递;若没有相同的,则说明这是第一个到达接收端的分片,将其插入队列中,等待下一个相应分片到来。
数据包封包
对于加密处理后的数据,我们需要申请并初始化一个包描述符与缓冲区描述符,将加密处理数据,缓冲区描述符,包描述符关联起来形成一个新的数据封包,并向下发送。
包描述符处理:
(1)调用NdisAlloeatePacketPool申请并初始化包池。
VOID NdisAllocatePacketPool( OUT PNDIS STATUS Status, //返回封包池申请操作的状态 OUT PNDIS HANDLE PoolHandlc,//返回封包池句柄。 IN UINT NumberOfDeseriptors, //指定可以申请的封包描述表的数据量 IN UINT ProtocolReservedLength//指定要为每个封包描述表申请的自定义 ∥空间的大小 );
(2)调用NdisAllocatePacket从上面的封包池中申请封包描述符。
VOID NdisAllocatePacket( OUT PNDIS STATUS Status,//返回请求的最终状态 OUT PNDIS PACKET*Packet,//返回申请的封包描述表的指针 IN NDIS HANDLE PoolHandle//封包池句柄,要在这个池中申请封包 );
缓冲区描述符处理:
由于中问层驱动程序有可能对数据包进行加密处理,就必须分配缓冲区描述符来关联加密处理后的数据的缓冲区,该缓冲区由中间层驱动程序进行分配。
缓冲区描述符的分配与包描述符的分配类似,其过程如下:
(1)调用NdisAllocateBueffrPool获取缓冲池句柄。
(2)调用NdisAllocateMemoryWithTag分配缓冲区。
(3)调用NdisAllocateBuffer分配和设置缓冲区描述符,关联第(2)步分配的缓冲区。
(4)调用NdisChainBufferAtFront将缓冲区描述符链接到已分配的包描述符。
数据包发送处理
协议层驱动有数据包要向下层发送时,通过调用NdisSend/NdisSendPackets请求NDIS发送数据包,NDIS将调用中间层初始化时的注册函数MiniportSend/MiniportSendPackets对数据包进行相应的处理。由于我们的程序需要解析数据包,无论什么情况我们都要先获取数据包内容,因此,我们采用包描述符“重申请”的方式,根据原始包描述符创建一个新的包描述符。
处理过程:
1.首先要从内存池里分配一个数据包描述符,将传入的数据包拷贝到新分配的包描述符中(但不包括与原缓冲区描述符相关的信息)。因为我们需要判断该数据包是否符合加密规则,若符合规则,我们则需要对其内容进行修改,向下层发送新的数据包;只有不符合规则的数据包,我们才将原包描述符中与缓冲区描述符相关的信息拷贝,从而放行数据包。
2.解析数据包判断是否符合加密规则,符合则继续处理过程3;若不满足加密条件,则放行数据包。
3.由于使用的是RC4加密方式,数据包加密的长度是不变的。因此,我们根据原始数据加上加密协议头部后的长度判断其是否需要分片,若超过MTU大小,则对其进行分片处理。之后使用RC4算法对数据进行加密,构造出新的封包。
4.当处理完数据包后,中间层驱动通过调用NdisSend,将新的数据封包交给下层驱动。在NDIS中,对数据包的内存资源采取谁分配谁回收的策略。若此时发送直接成功,立马可以在此释放我们申请的资源,并通知上层驱动可以释放原始数据包资源;如果中间层驱动程序调用NdisSend后返回值NDIS_PENDING,这属于异步完成,此时我们已经失去了对数据包的控制权,这种情况下,只能等待下层驱动进行处理,当将数据包发送出去后,通过NDIS调用中间层驱动程序的注册函数ProtocoISendComplete函数通告中间层数据包已经发送完毕,此时中间层驱动就可以回收资源,并通知上层协议释放原始数据包资源。
发送数据包流程如下:
数据包接收处理
NDIS框架中的接收处理过程相对比较复杂,当网卡驱动程序收到数据包后,该驱动会根据不同情况调用不同的函数通知所绑定它的所有上层驱动,上层驱动根据底层驱动不同的通知函数也会有不同处理方式,因此,我们对数据包分析与解密处理的检查点,也要安插在不同的回调函数中,以便能够完全解密,接收流程如下:
- 底层驱动会使用NdisMIndicateReceive / NdisMEthIndicateReceive通知上层驱动已经收到数据报文。
- 若驱动中存在PtReceivePacke回调函数,则当下层驱动调NdisMIndicateReceive 函数时,表明已经获得完整数据包,PtReceivePacke被调用,此时检查点开始处理数据包。其他情况则调用PtReceive回调函数。在PtReceive中如果通过NdisGetReceivedPacket得到了一个完整的packet,就分配我们自己的MyPacket,根据底下传上来的packet设置MyPacket,此时检查点开始处理数据包,然后调用NdisMIndicateReceivePacket通知NDIS,NDIS会接着调用上层协议驱动的相应PtReceive例程。如果此时MyPacket的status是NDIS_STATUS_RESOURCES,我们就在本函数中释放我们分配的MyPacket;否则我们在上层发送4的时候,在MPReturnPacket中释放MyPacket.
- 在PtReceive中如果通过NdisGetReceivedPacket不能得到一个完整的packet,那我们就直接调用NdisMEthIndicateReceive等函数通知NDIS。
- 当上层协议驱动得到了一个完整的数据报文并且处理完毕以后,它会调用NdisReturnPacket,然后NDIS会调用我们的MPReturnPacket.
- 在我们的MPReturnPacket中,释放我们自己分配的MyPacket,然后同样的向下层调用NdisReturnPacket。下层会释放他们自己分配的packet
- 如果3发生,当底层miniport驱动收到了一个完整的数据报文,它会调用NdisMEthIndicateReceiveComplete,然后NDIS会调用我们的PtReceiveComplete
- 我们的PtReceiveComplete同样的会调用NdisMEthIndicateReceiveComplete,通知NDIS“我们已经收到了完整的报文”
- 当上层协议驱动得知底层已经收到了完整的数据报文以后,可能会调用NdisTransferData,要求下层把剩余的数据传上来。
- 8的调用会导致NDIS调用我们的MPTransferData例程。在MPTransferData中,做同样的调用NdisTransferData。注意该函数的返回值:如果返回success,说明剩余的数据立刻就传上来了。此时检查点开始处理数据包。10、11两步骤就不会调用;如果返回pending,表明底层在此阻塞,底层会在稍后的时候调用10
- 当底层miniport驱动做好了一个完整的packet,它会调用NdisTransferDataComplete
- 同样在我们的PtTransferDataComplete中,会作出同样的调用,此时检查点开始处理数据包。
检查点处理过程:
1、解析数据包看是否有加密标识,若没有则直接放行数据包,结束处理流程。
2、若是存在加密标识,则开始判断是否有分片标识。若是分片数据包,则开始解密,并将去除加密头部的数据以及IP头部相关信息作为一个接收节点,并该节点与分片等待队列中的节点进行匹配。若不存在匹配节点,直接插入分片等待队列。若存在对应的分片节点,我们便开始数据包的重组,并将该完整的数据包上传给上层驱动。
3、非分片数据则执行解密并去除加密头部后,直接上传上层驱动。
用户层模块的设计与实现
系统应用层模块主要负责与内核态程序的交互,包括内核模块的开启与关闭;负责加密/过滤策略文件的制定,包括对注册表的修改更新。下面将对各个环节进行详细的说明。
控制内核态
在用户态对内核模块进行控制,必须借助NDIS中的DeviceIoControl函数实现系统调用,传递自定义的I/O控制码(ioctl)及相关的缓冲区数据。
我们定义了两个控制码:加密保护开关和加密策略更新:
#defineIOCTL_PROTECTION_SWITCH_CHANGE _PTUSERIO_CTL_CODE(0x801,METHOD_BUFFERED, FILE_ANY_ACCESS) #defineIOCTL_RULE_LIST_CHANGE _PTUSERIO_CTL_CODE(0x802,METHOD_BUFFERED, FILE_ANY_ACCESS)
对内核模块的所有操作都是通过OpenHandler函数来完成的,一旦用户态更改规则或状态就会调用OpenHandler函数:
对注册表修改
我们将所有的规则信息全部存储在系统注册表中,这样可以使得我们的内核驱动可以在每次开机时读取最新的规则和状态信息,即保留最近一次修改的结果。
我们在 “HKEY_LOCAL_MACHINE\\System\\CurrentControlSet\\services\\passthru\\UserConfig”的位置存放内核驱动的相关信息。
在用户态调用RegCreateKeyEx()函数创建键,用RegSetValueEx()函数来设置相应键值,用户层模块在规则改变时修改注册表,以供内核模块读取。三者关系如下图所示: