用C#下的Raw Socket编程实现网络封包监视

谈起 socket 编程 大家也 许会想起 QQ IE 没错 还有许多网络工具如 P2P NetMeeting 等在 应用层实现的应用程序 也是用 socket 来实现的 Socket 是一 个网络编程接口 实现于网络应用层 Windows Socket 包括了一套系 统组件 充分利用了 Microsoft Windows 消息 驱动的特点 Socket 规范 1.1 版是在 1993 1 发行的 并广泛用于此后出现的 Windows9x 操作系 统中 Socket 规范 2.2 其在 Windows 平台上的版本是 Winsock2.2 也叫 Winsock2 1996 5 发行 Windows NT 5.0 及以后版本的 Windows 统支持 Winsock2 Winsock2 支持多 个传输协议的原始套接字 I/O 模型 务质量控制等

本文向大家介Windows Sockets的一些于用C#实现的原始套接字(Raw Socket)程,以及在此基实现网络封包监视。同Winsock1相比,Winsock2最明的就是支持了Raw Socket套接字型,使用Raw Socket,可把置成混模式,在这种模式下,我可以收到网络上的IP包,然包括目的不是本机的IP包,通原始套接字,我也可以更加自如地控制Windows下的多种协议,而且能够对网络传输机制行控制。

在本文例子中,我在nbyte.BasicClass命名空间实现RawSocket包含了我们实现数据包监视的核心技。在实现这个类之前,需要先IP头结构来暂时存放一些有关网络封包的信息:


[StructLayout(LayoutKind.Explicit)]
 public struct IPHeader
 {
  [FieldOffset(0)] public byte    ip_verlen;        //I4
位首部+4IP版本
  [FieldOffset(1)] public byte    ip_tos;            //8
位服务类TOS
  [FieldOffset(2)] public ushort  ip_totallength; //16
据包总长度(字
  [FieldOffset(4)] public ushort  ip_id;             //16
标识
  [FieldOffset(6)] public ushort  ip_offset;       //3
志位
  [FieldOffset(8)] public byte    ip_ttl;            //8
位生存时间 TTL
  [FieldOffset(9)] public byte    ip_protocol;    //8
协议(TCP, UDP, ICMP, Etc.)
  [FieldOffset(10)] public ushort ip_checksum; //16
IP首部校
  [FieldOffset(12)] public uint   ip_srcaddr;     //32
位源IP地址
  [FieldOffset(16)] public uint   ip_destaddr;   //32
位目的IP地址
 }

这样每一封包到达时候,可以用强制化把包中的据流个个IPHeader象。
下面就RawSocket了,一始,先定个参数,包括

  private bool error_occurred;          //套接字在接收包时是否产生错误
  public bool KeepRunning;              //
是否继续进
  private static int len_receive_buf; //
得到的据流的
  byte [] receive_buf_bytes;          //
收到的字
  private Socket socket = null;       //
明套接字
有一常量:
const int SIO_RCVALL = unchecked((int)0x98000001);//
听所有的据包

里的SIO_RCVALL是指示RawSocket接收所有的据包,在以后的IOContrl中要用,在下面的造函中,实现一些参数的初始化:

  public RawSocket()                    //造函
  {
   error_occurred=false;
   len_receive_buf = 4096;
   receive_buf_bytes = new byte[len_receive_buf];
  }

下面的函数实现RawSocket它与终结点(IPEndPoint:本机IP和端口)定:
  public void CreateAndBindSocket(string IP)                  //
建立并绑定套接字
  {
   socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);
   socket.Blocking = false;                                         //
socket非阻塞状态
   socket.Bind(new IPEndPoint(IPAddress.Parse(IP), 0)); //
定套接字

   if (SetSocketOption()==false) error_occurred=true;
  }
其中,在建套接字的一句socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP);中有3个参数

第一个参数定地址族,MSDN上的描述是指定 Socket 例用解析地址的址方案要把套接字定到终结点(IPEndPoint,需要使用InterNetwork,即采用IP版本4的地址格式,也是今大多套接字程所采用一个寻址方案(AddressFamily)。

第二个参数设置的套接字型就是我使用的Raw型了,SocketType是一举数型,Raw套接字型支持础传输协议访问。通使用 SocketType.Raw不光可以使用传输控制协议(Tcp)和用户数报协议(Udp)行通信,也可以使用网际消息控制协议 (Icmp) Internet 管理协议 (Igmp) 来进行通信。在用程序必提供完整的 IP 标头。所接收的在返回时会保持其 IP 标头选项

第三个参数设协议类型,Socket 使用 ProtocolType 举数型向 Windows Socket API 通知所求的协议里使用的是IP协议,所以要采用ProtocolType.IP参数

CreateAndBindSocket中有一自定SetSocketOptionSocket中的SetSocketOption不同,我里定的是具有IO控制功能的SetSocketOption的定如下:

  private bool SetSocketOption()                           //raw socket

  {
   bool ret_value = true;
   try
   {
    socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, 1);
    byte []IN = new byte[4]{1, 0, 0, 0};
    byte []OUT = new byte[4];
 
    //
级别操作模式,接受所有的据包,一步是关键,必socketrawIP Level才可用SIO_RCVALL
    int ret_code = socket.IOControl(SIO_RCVALL, IN, OUT);
    ret_code = OUT[0] + OUT[1] + OUT[2] + OUT[3];//
48位字合成一32位整
    if(ret_code != 0) ret_value = false;
   }
   catch(SocketException)
   {
    ret_value = false;
   }
   return ret_value;
  }

其中,置套接字选项时使套接字包含IP,否无法IPHeader结构,也无法据包信息。
int ret_code = socket.IOControl(SIO_RCVALL, IN, OUT);是函中最关键的一步了,因,在windows中我不能用Receive数来接收raw socket上的据,是因,所有的IP包都是先核心,然后再传输到用程序,当发送一raws socket包的候(比如syn),核心不知道,也这个数据被送或者接建立的记录,因此,当远端主机回候,系核心就把包都全部掉,而到不了用程序上。所以,就不能简单地使用接收函数来接收。要到接收据的目的,就必采用嗅探,接收所有通据包,然后筛选,留下符合我需要的。可以通过设SIO_RCVALL,表示接收所有网络上的据包。接下一下IOControlMSDN释它说置套接字级别操作模式,怎么级别操作法?其实这个数与API中的WSAIoctl很相似。WSAIoctl如下:

int WSAIoctl(
  SOCKET s,                                                                          //
指定的套接字
  DWORD dwIoControlCode,                                                      //
控制操作

LPVOID lpvInBuffer,                                                              //指向输入数据流的指针
  DWORD cbInBuffer,                                                              //
输入数据流的大小节数
  LPVOID lpvOutBuffer,                                                           //
指向输出数据流的指针
  DWORD cbOutBuffer,                                                           //
输出数据流的大小节数
  LPDWORD lpcbBytesReturned,                                               //
指向输出字节流数目的实数值
  LPWSAOVERLAPPED lpOverlapped,                                         //
指向一WSAOVERLAPPED结构

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//指向操作完成时执行的例程
);

C#IOControl不像WSAIoctl么复杂,其中只包括其中的控制操作入字流、出字流三个参数,不过这个参数了。我看到函中定了一节数组byte []IN = new byte[4]{1, 0, 0, 0}实际是一个值为1DWORD或是Int32,同byte []OUT = new byte[4];也是,整和了一int,WSAIoctl参数lpcbBytesReturned指向的
为设置套接字选项时可能会发错误,需要用一个值传递错误标志:

 public bool ErrorOccurred
  {
   get
   {
    return error_occurred;
   }
  }

下面的函数实现据包的接收:

 //解析接收的据包,形成PacketArrivedEventArgs事件类对象,PacketArrival事件
  unsafe private void Receive(byte [] buf, int len)
  {
   byte temp_protocol=0;
   uint temp_version=0;
   uint temp_ip_srcaddr=0;
   uint temp_ip_destaddr=0;
   short temp_srcport=0;
   short temp_dstport=0;
   IPAddress temp_ip;
  
   PacketArrivedEventArgs e=new PacketArrivedEventArgs();//
网络数据包信息事件

   fixed(byte *fixed_buf = buf)
   {
    IPHeader * head = (IPHeader *) fixed_buf;//
据流整和IPHeader结构
    e.HeaderLength=(uint)(head->ip_verlen & 0x0F) << 2;
   
    temp_protocol = head->ip_protocol;
    switch(temp_protocol)//
提取协议类
    {
     case 1: e.Protocol="ICMP";     break;
     case 2: e.Protocol="IGMP";     break;
     case 6: e.Protocol="TCP";      break;
     case 17: e.Protocol="UDP";     break;
     default: e.Protocol= "UNKNOWN"; break;
    }

    temp_version =(uint)(head->ip_verlen & 0xF0) >> 4;//提取IP协议版本
    e.IPVersion = temp_version.ToString();

                   //以下句提取出了PacketArrivedEventArgs象中的其他参数

   temp_ip_srcaddr = head->ip_srcaddr;
    temp_ip_destaddr = head->ip_destaddr;
    temp_ip = new IPAddress(temp_ip_srcaddr);
    e.OriginationAddress =temp_ip.ToString();
    temp_ip = new IPAddress(temp_ip_destaddr);
    e.DestinationAddress = temp_ip.ToString();

    temp_srcport = *(short *)&fixed_buf[e.HeaderLength];
    temp_dstport = *(short *)&fixed_buf[e.HeaderLength+2];
    e.OriginationPort=IPAddress.NetworkToHostOrder(temp_srcport).ToString();
    e.DestinationPort=IPAddress.NetworkToHostOrder(temp_dstport).ToString();

    e.PacketLength =(uint)len;
    e.MessageLength =(uint)len - e.HeaderLength;

    e.ReceiveBuffer=buf;
    //
buf中的IP头赋给PacketArrivedEventArgs中的IPHeaderBuffer
    Array.Copy(buf,0,e.IPHeaderBuffer,0,(int)e.HeaderLength);
    //
buf中的包中赋给PacketArrivedEventArgs中的MessageBuffer
    Array.Copy(buf,(int)e.HeaderLength,e.MessageBuffer,0,(int)e.MessageLength);
   }
   //
PacketArrival事件
   OnPacketArrival(e);
  }

大家注意到了,在上面的函中,我使用了指针这种的不安全代,可C#中指和移位些原始操作也可以程序员带来编程上的便利。在函PacketArrivedEventArgs类对象,以便通OnPacketArrival(e)事件把据包信息传递出去。其中PacketArrivedEventArgsRawSocket中的嵌套它继承了系事件(Event,封装了据包的IP、端口、协议等其他据包中包含的信息。在启动接收据包的函中,我使用了步操作的方法,以下函数开启听的接口:

public void Run() //
{
 IAsyncResult ar = socket.BeginReceive(receive_buf_bytes, 0, len_receive_buf, SocketFlags.None, new AsyncCallback(CallReceive), this);
}

Socket.BeginReceive返回了一个异步操作的接口,在此接口的生成函BeginReceive明了步回CallReceive把接收到的网络数据流传给receive_buf_bytes这样就可用一个带步操作的接口参数步回地接收据包:

  private void CallReceive(IAsyncResult ar)//步回

{
   int received_bytes;
   received_bytes = socket.EndReceive(ar);
   Receive(receive_buf_bytes, received_bytes);
   if (KeepRunning) Run();
  }

此函数当挂起或取后去接收一新的据包,这样能保证让每一个数据包都能被程序探到。
下面通过声明代理事件句柄来实现和外界的通信:

public delegate void PacketArrivedEventHandler(Object sender, PacketArrivedEventArgs args);
//
事件句柄:包到达时事件
public event PacketArrivedEventHandler PacketArrival;//
时间句柄函

这样就可以实现对数据包信息的取,采用步回,可以提高接收据包的效率,代理事件把封包信息传递到外界。然能把所有的封包信息传递出去,就可以实现对数据包的分析了:)不RawSocket的任务还没有完,最后不要望了关闭套接字

 public void Shutdown()                                       //关闭raw socket
  {
   if(socket != null)
   {
    socket.Shutdown(SocketShutdown.Both);
    socket.Close();
   }
  }

以上介RawSocket过构IP头获取了包中的信息,过异步回数实现据包的接收,使用时间代理句柄和自定据包信息事件据包信息送出去,实现网络数据包的监视这样就可以在外部添加一些函数对数据包行分析了

你可能感兴趣的:(socket编程)