一般都熟悉sniffer这个工具,它可以捕捉流经本地网卡的所有数据包。抓取网络数据包进行分析有很多用处,如分析网络是否有网络病毒等异常数据,通信协议的分析(数据链路层协议、IP、UDP、TCP、甚至各种应用层协议),敏感数据的捕捉等。下面我们就来看看在windows下如何实现数据包的捕获。

下面先对网络嗅探器的原理做简单介绍。

嗅探器设计原理

  嗅探器作为一种网络通讯程序,也是通过对网卡的编程来实现网络通讯的,对网卡的编程也是使用通常的套接字(socket)方式来进行。但是,通常的套接字程序只能响应与自己硬件地址相匹配的或是以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口但却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地址之后将不引起响应,也就是说应用程序无法收取到达的数据包。而网络嗅探器的目的恰恰在于从网卡接收所有经过它的数据包,这些数据包即可以是发给它的也可以是发往别处的。显然,要达到此目的就不能再让网卡按通常的正常模式工作,而必须将其设置为混杂模式。

  具体到编程实现上,这种对网卡混杂模式的设置是通过原始套接字(raw socket)来实现的,这也有别于通常经常使用的数据流套接字和数据报套接字。在创建了原始套接字后,需要通过setsockopt()函数来设置IP头操作选项,然后再通过bind()函数将原始套接字绑定到本地网卡。为了让原始套接字能接受所有的数据,还需要通过ioctlsocket()来进行设置,而且还可以指定是否亲自处理IP头。至此,实际就可以开始对网络数据包进行嗅探了,对数据包的获取仍象流式套接字或数据报套接字那样通过recv()函数来完成。但是与其他两种套接字不同的是,原始套接字此时捕获到的数据包并不仅仅是单纯的数据信息,而是包含有 IP头、 TCP头等信息头的最原始的数据信息,这些信息保留了它在网络传输时的原貌。通过对这些在低层传输的原始信息的分析可以得到有关网络的一些信息。由于这些数据经过了网络层和传输层的打包,因此需要根据其附加的帧头对数据包进行分析。下面先给出结构.数据包的总体结构:

数据包
IP头 TCP头(或其他信息头) 数据

数据在从应用层到达传输层时,将添加TCP数据段头,或是UDP数据段头。其中UDP数据段头比较简单,由一个8字节的头和数据部分组成,具体格式如下:

16位 16位
源端口 目的端口
UDP长度 UDP校验和

而TCP数据头则比较复杂,以20个固定字节开始,在固定头后面还可以有一些长度不固定的可选项,下面给出TCP数据段头的格式组成:

16位 16位
源端口 目的端口
        顺序号
        确认号
TCP头长 (保留)7位 URG ACK PSH RST SYN FIN 窗口大小
校验和 紧急指针
           可选项(0或更多的32位字)
           数据(可选项)

对于此TCP数据段头的分析在编程实现中可通过数据结构_TCP来定义:

  1. typedef struct _TCP{
  2. WORD SrcPort;  // 源端口
  3. WORD DstPort;  // 目的端口
  4. DWORD SeqNum;  // 顺序号
  5. DWORD AckNum;  // 确认号
  6. BYTE DataOff;  // TCP头长
  7. BYTE Flags;   // 标志(URG、ACK等)
  8. WORD Window;   // 窗口大小
  9. WORD Chksum;   // 校验和
  10. WORD UrgPtr;   // 紧急指针
  11. } TCP;
  12. typedef TCP *LPTCP;
  13. typedef TCP UNALIGNED * ULPTCP;

网络层,还要给TCP数据包添加一个IP数据段头以组成IP数据报。IP数据头以大端点机次序传送,从左到右,版本字段的高位字节先传输(SPARC是大端点机;Pentium是小端点机)。如果是小端点机,就要在发送和接收时先行转换然后才能进行传输。IP数据段头格式如下:

16位 16位
版本 IHL 服务类型 总长
标识 标志 分段偏移
生命期 协议 头校验和
源地址
目的地址
选项(0或更多)

同样,在实际编程中也需要通过一个数据结构来表示此IP数据段头,下面给出此数据结构的定义:

  1. typedef struct _IP{
  2. union{
  3. BYTE Version;   // 版本
  4. BYTE HdrLen;    // IHL
  5. };
  6. BYTE ServiceType;   // 服务类型
  7. WORD TotalLen;     // 总长
  8. WORD ID;        // 标识
  9. union
  10. {
  11. WORD Flags;    // 标志
  12. WORD FragOff;   // 分段偏移
  13. };
  14. BYTE TimeToLive;    // 生命期
  15. BYTE Protocol;     // 协议
  16. WORD HdrChksum;    // 头校验和
  17. DWORD SrcAddr;    // 源地址
  18. DWORD DstAddr;     // 目的地址
  19. BYTE Options;     // 选项
  20. } IP;
  21. typedef IP * LPIP;
  22. typedef IP UNALIGNED * ULPIP;

在明确了以上几个数据段头的组成结构后,就可以对捕获到的数据包进行分析了。

嗅探器实现

嗅探器实质就是从网络上获取数据包的一种工具,它可以捕捉流经本地网卡的所有数据包。抓取网络数据包进行分析有很多用处,如分析网络是否有网络病毒等异常数据,通信协议的分析(数据链路层协议、IP、UDP、TCP、甚至各种应用层协议),敏感数据的捕捉等。下面我们就来看看在windows下如何实现数据包的捕获。

WINSOCK本身就提供了抓取流经网卡的所有数据包的函数,虽然只能在IP协议层上捕捉,但只要您的工作没有涉及到数据链路层的话,这也就足够用了。抓取数据包的编程方法基本和编写其它网络应用程序一样,只需多一个步骤,即将SOCKET设置为接收所有数据的模式,这是用WSAIoctl来实现的。

编程实现主要有以下几个步骤:
    1. 初始化WINSOCK库;
    2. 创建SOCKET句柄;
    3. 绑定SOCKET句柄到一个本地地址;
    4. 设置该SOCKET为接收所有数据的模式;
    5. 接收数据包;
    6. 关闭SOCKET句柄,清理WINSOCK库;

(1)初始化winsock库

Winsock是Windows下的网络编程接口,它是由Unix下的BSD Socket发展而来,是一个与网络协议无关的编程接口。Winsock在常见的Windows平台上有两个主要的版本,即Winsock1和Winsock2。编写与Winsock1兼容的程序你需要引用头文件WINSOCK.H,如果编写使用Winsock2的程序,则需要引用WINSOCK2.H。此外还有一个MSWSOCK.H头文件,它是专门用来支持在Windows平台上高性能网络程序扩展功能的。使用WINSOCK.H头文件时,同时需要库文件WSOCK32.LIB,使用WINSOCK2.H时,则需要WS2_32.LIB,如果使用MSWSOCK.H中的扩展API,则需要MSWSOCK.LIB。正确引用了头文件,并链接了对应的库文件,你就构建起编写WINSOCK网络程序的环境了。

每个Winsock程序必须使用WSAStartup载入合适的Winsock动态链接库,如果载入失败,WSAStartup将返回SOCKET_ERROR,这个错误就是WSANOTINITIALISED,WSAStartup的定义如下:

  1. int WSAStartup(
  2. WORD wVersionRequested,
  3. LPWSADATA lpWSAData
  4. );

wVersionRequested指定了你想载入的Winsock版本,其高字节指定了次版本号,而低字节指定了主版本号。你可以使用宏MAKEWORD(x, y)来指定版本号,这里x代表主版本,而y代表次版本。lpWSAData是一个指向WSAData结构的指针,WSAStartup会向该结构中填充其载入的Winsock动态链接库的信息。

当你使用完Winsock接口后,要调用下面的函数对其占用的资源进行释放:

  1. int WSACleanup(void);

如果调用该函数失败也没有什么问题,因为操作系统为自动将其释放,对应于每一个WSAStartup调用都应该有一个WSACleanup调用.

错误处理
    
   Winsock函数调用失败大多会返回 SOCKET_ERROR(实际上就是-1),你可以调用WSAGetLastError得到错误的详细信息:

  1. int WSAGetLastError (void);

对该函数的调用将返回一个错误码,其码值在WINSOCK.H或WINSOCK2.H(根据其版本)中已经定义,这些预定义值都以WSAE开头.同时你还可以使用WSASetLastError来自定义错误码值.

下面是我的winsock初始化例子:

  1. WORD wVersion;
  2. WSADATA wsadata;
  3. int err;
  4. wVersion = MAKEWORD(,);
  5. // WSAStartup() initiates the winsock,if successful,the function returns zero
  6. err = ::WSAStartup(wVersion,&wsadata);
  7. if(err!=)
  8. {
  9. printf("Couldn't initiate the winsock!\n");
  10. }

【注意】使用WORD 、WSADATA 和WSAStartup等时必须包含其头文件,加入语句:

  1. #include "winsock2.h"
  2. #include "windows.h"

我在初始化winsock库时遇到了一个问题,调用WSAStartUp(),编译后出现unresolved external symbol_WSAStartup@8,开始觉得很奇怪,因为头文件之类的都包含了怎么会出错呢?后来上网查了下才知道,需要包含一个动态链接库WS2_32.LIB,方法有两种:

第一种:

在菜单   project ->settings -> link  -> object/library   modules   下面输入ws2_32.lib  然后确定即可

第二种:

在头文件中加入语句#pragma   comment(   lib,   "ws2_32.lib"   )  来显式加载。 即:

  1. #include <winsock2.h>
  2. #pragma comment(lib, "WS2_32")

(2)创建socket句柄

winsock库初始化成功后就可以创建socket句柄了,使用函数socket即可。socket函数原型为:

  1. int socket(int domain, int type, int protocol);

domain指明所使用的协议族,通常为AF_INET,表示互联网协议族(TCP/IP协议族);type参数指定socket的类型:SOCK_STREAM 或SOCK_DGRAM,Socket接口还定义了原始Socket(SOCK_RAW),允许程序使用低层协议;protocol通常赋值"0"。Socket()调用返回一个整型socket描述符,你可以在后面的调用使用它。

创建了socket句柄后,要将该句柄与本地IP绑定后才能使用。在进行绑定之前,要先获得本地机器的相关信息,包括主机名,主机IP地址等。最后进行绑定。我的代码如下:

  1. SOCKET ServerSock=socket(AF_INET,SOCK_RAW,IPPROTO_IP);
  2. char mname[];
  3. struct hostent* pHostent;
  4. sockaddr_in myaddr;
  5. //Get the hostname of the local machine
  6. if( - == gethostname(mname, sizeof(mname)))
  7. {
  8. closesocket(ServerSock);
  9. printf("%d",WSAGetLastError());
  10. exit(-);
  11. }
  12. else
  13. {
  14. //Get the IP adress according the hostname and save it in pHostent
  15. pHostent=gethostbyname((char*)mname);
  16. //填充sockaddr_in结构
  17. myaddr.sin_addr = *(in_addr *)pHostent->h_addr_list[];
  18. myaddr.sin_family = AF_INET;
  19. myaddr.sin_port = htons();//对于IP层可随意填
  20. //bind函数创建的套接字句柄绑定到本地地址
  21. if(SOCKET_ERROR==bind(ServerSock,(struct sockaddr *)&myaddr,sizeof(myaddr)))
  22. {
  23. closesocket(ServerSock);
  24. cout<<WSAGetLastError<<endl;
  25. exit(-);
  26. }
  27. }

接下来的工作就是把该socket设为接收所有数据的模式。

  1. //设置该SOCKET为接收所有流经绑定的IP的网卡的所有数据,包括接收和发送的数据包
  2. u_long sioarg = ;
  3. DWORD dwValue=;
  4. if( SOCKET_ERROR == WSAIoctl( ServerSock, SIO_RCVALL , &sioarg,sizeof(sioarg),NULL,,&dwValue,NULL,NULL ) )
  5. {
  6. closesocket(ServerSock);
  7. cout << WSAGetLastError();
  8. exit(-);
  9. }

接收网络数据的工作了。

【注意】这里一定要保证gethostname、gethostbyname、bind、ioctlsocket等函数都能够被正确执行,我在开始时就因为几个参数设置不对而导致bind和ioctlsocket执行错误,费了半天周折才搞定。

以下是我的完整代码:

  1. WORD wVersion;
  2. WSADATA wsadata;
  3. int err;
  4. wVersion = MAKEWORD(,);
  5. // WSAStartup() initiates the winsock,if successful,the function returns zero
  6. err = ::WSAStartup(wVersion,&wsadata);
  7. if(err!=)
  8. {
  9. printf("Couldn't initiate the winsock!\n");
  10. }
  11. else
  12. {
  13. // create a socket
  14. SOCKET ServerSock=socket(AF_INET,SOCK_RAW,IPPROTO_IP);
  15. char mname[];
  16. struct hostent* pHostent;
  17. sockaddr_in myaddr;
  18. //Get the hostname of the local machine
  19. if( - == gethostname(mname, sizeof(mname)))
  20. {
  21. closesocket(ServerSock);
  22. printf("%d",WSAGetLastError());
  23. exit(-);
  24. }
  25. else
  26. {
  27. //Get the IP adress according the hostname and save it in pHostent
  28. pHostent=gethostbyname((char*)mname);
  29. //填充sockaddr_in结构
  30. myaddr.sin_addr = *(in_addr *)pHostent->h_addr_list[];
  31. myaddr.sin_family = AF_INET;
  32. myaddr.sin_port = htons();//对于IP层可随意填
  33. //bind函数创建的套接字句柄绑定到本地地址
  34. if(SOCKET_ERROR==bind(ServerSock,(struct sockaddr *)&myaddr,sizeof(myaddr)))
  35. {
  36. closesocket(ServerSock);
  37. cout<<WSAGetLastError<<endl;
  38. printf("..............................Error……");
  39. getchar();
  40. exit(-);
  41. }
  42.  
  43. //设置该SOCKET为接收所有流经绑定的IP的网卡的所有数据,包括接收和发送的数据包
  44. u_long sioarg = ;
  45. DWORD dwValue=;
  46. if( SOCKET_ERROR == WSAIoctl( ServerSock, SIO_RCVALL , &sioarg,sizeof(sioarg),NULL,,&dwValue,NULL,NULL ) )
  47. {
  48. closesocket(ServerSock);
  49. cout << WSAGetLastError();
  50. exit(-);
  51. }
  52. //获取分析数据报文
  53. char buf[];
  54. int len = ;
  55. listen(ServerSock,);
  56. do
  57. {
  58. len = recv( ServerSock, buf, sizeof(buf),);
  59. if( len > )
  60. {
  61. //报文处理
  62. }
  63. }while( len > );
  64. }
  65. }
  66. ::WSACleanup();

用C++实现网络编程---抓取网络数据包的实现方法的更多相关文章

  1. Asp.net 使用正则和网络编程抓取网页数据(有用)

    Asp.net 使用正则和网络编程抓取网页数据(有用) Asp.net 使用正则和网络编程抓取网页数据(有用) /// <summary> /// 抓取网页对应内容 /// </su ...

  2. Fiddler:在PC和移动设备上抓取HTTPS数据包

    Fiddler是一个免费的Web调试代理,支持任何浏览器.系统以及平台.这个工具是进行Web和App网络开发的必备工具,戳此处下载. 根据Fiddler官网的描述,具有以下六大特点: Web调试 性能 ...

  3. 图解Fiddler如何抓取Android数据包

    介绍Fiddler抓取Android数据包希望对大家的工作和学习有所帮助! 电脑开启wifi热点 首先在电脑上下载一个wifi软件,我这里用的是猎豹wifi,电脑开启wifi热点后,如下图所示:  设 ...

  4. Fiddler基础用法-抓取浏览器数据包

    Fiddler基础知识 Fiddler是强大的抓包工具,它的原理是以web代理服务器的形式进行工作的,使用的代理地址是:127.0.0.1,端口默认为8888,我们也可以通过设置进行修改. 代理就是在 ...

  5. wireshark抓取OpenFlow数据包

    在写SDN控制器应用或者改写控制器源码的时候,经常需要抓包,验证网络功能,以及流表的执行结果等等,wireshark是个很好的抓包分析包的网络工具,下面简介如何用wireshark软件抓取OpenFl ...

  6. Wireshark学习笔记——怎样高速抓取HTTP数据包

    0.前言     在火狐浏览器和谷歌浏览器中能够很方便的调试network(抓取HTTP数据包),可是在360系列浏览器(兼容模式或IE标准模式)中抓取HTTP数据包就不那么那么方便了.尽管也可使用H ...

  7. Charles + Android 抓取Https数据包 (适用于Android 6.0及以下)

    通过Charles代理,我们能很轻易的抓取手机的Http请求,因为Http属于明文传输,所以我们能直接获取到我们要抓取的内容.但是Https内容本身就是加密的,这时我们会发现内容是加密的了.本文我们来 ...

  8. Charles 如何抓取https数据包

    Charles可以正常抓取http数据包,但是如果没有经过进一步设置的话,无法正常抓取https的数据包,通常会出现乱码.举个例子,如果没有做更多设置,Charles抓取https://www.bai ...

  9. Fiddler抓取https数据包

    Wireshark和Fiddler的优缺点: ①Wireshark是一种在网络层上工作的抓包工具,不仅自带大量的协议分析器,而且可以通过编写Wireshark插件来识别自定义的协议.虽然Wiresha ...

随机推荐

  1. [Vue]学习中遇到的疑点

    computed:计算属性,官方api上说计算属性的结果会被缓存,除非依赖的响应式属性变化才会重新计算.但是经过测试并没有缓存.案例: computed: { now: function () { c ...

  2. 发布在IIS上的Web程序,调用服务器的COM组件

    场景大致是这样的,在工厂中分布着许多的PDA点,这些PDA点都要进行实时的扫描--打印操作.实现方法是采用网络打印机,然后服务器安装驱动,管理着所有的打印机.然后服务器,发布一个WebService, ...

  3. 将博客搬至51CTO

    为了统一博客文章,将文章搬至51cto个人博客

  4. Restful风格的简单实现办法

    如果实在着急上Restful的URL在项目里 , 可以使用turkey的urlrewrite. 先在web.xml中加入如下代码 <!-- URL ReWrite --> <filt ...

  5. 给Mac下的iTerm2增加配色

    iterm2就不说了,Mac下非常好用的终端,这里就先谈谈如何给其增加配色,效果如下图 可以来这下载theme : http://iterm2colorschemes.com/ 1.先编辑你的prof ...

  6. dede文章页调用当前栏目链接方法

    DedeCMS内容页调用当前栏目其实用下来是调用不出来的,{dede:field.typename/}是有效的,可是 {dede:field.typeurl/}却调不出文档当前栏目所在目录链接URL. ...

  7. java 、android 知识图谱

    java知识图谱: android知识图谱: 照此图练习,神功自成.....

  8. POJ 1743 后缀数组不重叠最长重复子串

    #include<stdio.h> #include<string.h> #include<algorithm> #define maxn 30000 using ...

  9. 剑指offer习题集

    1.重载赋值运算符函数:(具体见代码) //普通做法 CMyString& CMyString::operator=(const CMyString& str) { if (this ...

  10. Count the Colors(线段树染色)

    Count the Colors Time Limit:2000MS    Memory Limit:65536KB    64bit IO Format:%lld & %llu Submit ...