网络数据包捕获与发送的多重实现 (学习)

注:本文已发表在2008年第10期《黑客防线》,转载请注明来源。

 

对网络数据进行研究,归根到底离不开对数据包的捕获和发送这两个关键环节,而其他操作都是建立在这个基础上的。捕获数据包有多种方法,常见的有原始套接字和Libpcap(它在Windows下的版本是WinPcap);而发送数据包同样可以使用原始套接字,但更好的方式是使用Libnet,操作简单、功能强大、使用方便、效果稳定,实属居家旅行、杀人放火之必备良品。

    在阅读以下内容之前,您最好首先能够熟练掌握一些常见协议格式,例如TCP、UDP、IP、ARP等,并对OSI模型有着比较清晰的了解。

使用原始套接字

    原始套接字是允许访问底层传输协议的一种套接字类型,它工作在OSI协议模型的网络层,允许对底层的传输协议加以控制,对IP头信息进行实际的操作。而此前我们所了解的TCP或UDP等协议都是工作在OSI协议模型的传输层,利用了网络层提供的服务。

    原始套接字有两种类型,其一是在IP头中使用预定义的协议,如ICMP等,其二是在IP头中使用自定义的协议,这时我们甚至可以构造属于自己的协议类型。

    创建原始套接字同样是使用socket或WSASocket函数,只不过需要将套接字类型指定为SOCK_RAW,如下代码所示:SOCKET s = socket(AF_INET,SOCK_RAW,IPPROTO_ICMP);其中第三个参数将成为IP头中协议域的值。

    在完成了原始套接字的创建之后,就可以在发送和接收调用中使用对应的套接字句柄。无论在创建原始套接字时是否设定了IP_HDRINCL选项,IP头都会包含在接收到的任何返回数据中。

    由于原始套接字可以控制基层传输机制,存在着可能被恶意利用的安全隐患,因此在Windows NT/2000/XP上,必须是具有管理员权限的用户才能创建原始套接字,如果没有管理员权限,创建原始套接字仍然会成功,但到bind操作时就会返回失败。为绕过这一限制,可禁止对原始套接字的安全检查,方法是在注册表中创建变量:“HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Afd\Parameters\DisaleRaw-Security”,并将它的值设为1(DWORD类型)。

下面一段代码演示了使用原始套接字捕获数据报的方法。

#include <winsock2.h>
#include <stdio.h>

#pragma comment(lib,"ws2_32")
#define SIO_RCVALL _WSAIOW(IOC_VENDOR,1) 

// TCP头结构
typedef struct _TCPHeader
{
 USHORT  sourcePort;
 USHORT  destinationPort;
 ULONG   sequenceNumber;
 ULONG   acknowledgeNumber;
 UCHAR   dataoffset;
 UCHAR   flags;
 USHORT  windows;
 USHORT  checksum;
 USHORT  urgentPointer;
}TCPHeader, *PTCPHeader;

// UDP头结构
typedef struct _UDPHeader
{
 USHORT  sourcePort;
 USHORT  destinationPort;
 USHORT  len;
 USHORT  checksum;
}UDPHeader, *PUDPHeader;

// IP头结构
typedef struct _IPHeader
{
 UCHAR   iphVerLen;
 UCHAR   ipTOS;
 USHORT  ipLength;
 USHORT ipID;
 USHORT  ipFlags;
 UCHAR   ipTTL;
 UCHAR   ipProtocol;
 USHORT  ipChecksum;
 ULONG   ipSource;
 ULONG   ipDestination;
}IPHeader, *PIPHeader;

void DecodeTCPPacket(char *pData);
void DecodeUDPPacket(char *pData);
void DecodeIPPacket(char *pData);

void main()
{
 WSADATA ws;
 WSAStartup(MAKEWORD(2,2),&ws);
 // 创建原始套接字
 SOCKET sRaw = socket(AF_INET,SOCK_RAW,IPPROTO_IP);
 // 得到本机IP,使用原始套接字时必须明确指定一个本机的通信对象
 char szHostName[56];
 SOCKADDR_IN addr_in;
 struct hostent *pHost;
 gethostname(szHostName,56);
 if((pHost = gethostbyname((char *)szHostName)) == NULL)
  return;
 addr_in.sin_family = AF_INET;
 addr_in.sin_port   = htons(0);
 memcpy(&addr_in.sin_addr.S_un.S_addr,pHost->h_addr_list[0],pHost->h_length);
 printf("Binding To Interface : %s\n",::inet_ntoa(addr_in.sin_addr));
 if(bind(sRaw,(SOCKADDR *)&addr_in,sizeof(addr_in)) == SOCKET_ERROR)
  return;
 
 // 将网卡设置为混杂模式
 DWORD dwValue = 1;
 if(ioctlsocket(sRaw,SIO_RCVALL,&dwValue) != 0)
  return;
 
 char buffer[1024];
 int  nRet;
 while(TRUE)
 { // 接收数据包并进行解析
  nRet = recv(sRaw,buffer,1024,0);
  if(nRet > 0)
   DecodeIPPacket(buffer);
 }
 closesocket(sRaw);
}
// 解析TCP数据包
void DecodeTCPPacket(char *pData)
{
 TCPHeader *pTcpHdr = (TCPHeader *)pData;
 printf("\tTCP Port:%d -> %d\n",ntohs(pTcpHdr->sourcePort),ntohs(pTcpHdr->destinationPort));
}
// 解析UDP数据包
void DecodeUDPPacket(char *pData)
{
 UDPHeader *pUdpHdr = (UDPHeader *)pData;
 printf("\tUDP Port:%d -> %d\n",ntohs(pUdpHdr->sourcePort),ntohs(pUdpHdr->destinationPort));
}
// 解析IP数据包
void DecodeIPPacket(char *pData)
{
 IPHeader *pIpHdr = (IPHeader *)pData;
 in_addr source,dest;
 char szSourceIp[32],szDestIp[32];
 source.S_un.S_addr = pIpHdr->ipSource;
 dest.S_un.S_addr   = pIpHdr->ipDestination;
 strcpy(szSourceIp,::inet_ntoa(source));
 strcpy(szDestIp,::inet_ntoa(dest));
 printf("\t%s -> %s\n",szSourceIp,szDestIp);
 // 计算头部长度
 int nHeaderLen = (pIpHdr->iphVerLen & 0xF) * sizeof(ULONG);
 switch(pIpHdr->ipProtocol)
 {
 case IPPROTO_TCP:
  DecodeTCPPacket(pData + nHeaderLen);
  break;
 case IPPROTO_UDP:
  DecodeUDPPacket(pData + nHeaderLen);
  break;
 case IPPROTO_ICMP:
  break;
 default:
  break;
 }
}

    使用原始套接字捕获数据报的代码想必大家都能看懂,下面我来解释一下解析数据报的过程。由于原始套接字工作在网络层,因此我们得到的都是IP报文,在IP报文中使用IP地址寻址,因此我们可以找到报文的源地址与目的地址并输出;同时,在IP报文中有个字段标明了它的上层协议,例如上层协议是TCP,则网络层把报文交给传输层处理的时候就要进行TCP解析,此处我们必须自己完成解析,大体过程就是把IP报文头部去掉后再进行TCP或UDP解析。使用原始套接字发送数据报也不是很难,不过它比较麻烦,需要自己手动填写IP、TCP等协议头部,还要自行计算校验和。下面一段代码给出了填充协议头部并将数据发送出去的主要过程。

// 构造并发送数据包
ip_header.iphVerLen  = (4<<4 | sizeof(ip_header)/sizeof(ULONG));
ip_header.ipTOS     = 0;
ip_header.ipLength   = htons(sizeof(ip_header) + sizeof(tcp_header));
ip_header.ipID  = 1;
ip_header.ipFlags = 0;
ip_header.ipTTL  = 128;
ip_header.ipProtocol  = IPPROTO_TCP;
ip_header.ipChecksum = 0;
ip_header.ipSource    = inet_addr(inet_ntoa(szHost.sin_addr));
ip_header.ipDestination  = inet_addr(“127.0.0.1”);
ip_header.ipChecksum  = CheckSum((USHORT *)&ip_header,sizeof(ip_header));
memcpy(szBuffer,&ip_header,sizeof(IPHeader));

tcp_header.DestPort    = htons(DestPort);
tcp_header.SourcePort  = htons(SourcePort);
tcp_header.SeqNumber  = htonl(seq);
tcp_header.AckNumber  = 0;
tcp_header.DataOffset  = (sizeof(tcp_header)/4<<4 | 0);
tcp_header.Flags       = TCP_SYN;
tcp_header.Window     = htons(5647);
tcp_header.UrgentPointer= 0;
tcp_header.CheckSum   = 0;
// 计算伪首部校验和
ComputeTcpPseudoHeaderCheckum(&ip_header,&tcp_header,NULL,0);
memcpy(&szBuffer[sizeof(IPHeader)],&tcp_header,sizeof(TCPHeader));
// 发送数据
int iError = 0;
int nLen = sizeof(ip_header) + sizeof(tcp_header);
if(sendto(sRaw,szBuffer,nLen + 34,0,(struct sockaddr *)&szDest,sizeof(szDest)) == SOCKET_ERROR)
{
 printf("Send Data Error!\n");
 closesocket(sRaw);
 
 iError = WSAGetLastError();
 printf("Error Code = %d\n",iError);
 
 return -1;
}
printf("Send Data Success!\n",);

    使用WinPcapWinPcap是Win 32 环境下的数据包捕获开发包,它其实是将UNIX环境下的Libpcap移植到了Windows系统。WinPcap可用于网络分析、网络故障诊断、网络数据包嗅探、监视以及流量分析统计等各种程序中,它主要提供了以下四个方面的功能:


捕获网络原始数据包;
根据用户定义的规则过滤数据包;
发送用户自己构造的数据包到网络;
统计网络流量。

    WinPcap由数据包捕获驱动、底层动态链接库(Packet.dll)和高层静态链接库(wpcap.lib)三部分组成。wpcap.lib使用Packet.dll提供的服务,为用户提供了一个更简单易用的接口,因此被使用的非常普遍,我们今天就是直接使用wpcap.lib提供的API。下面的代码演示了使用WinPcap捕获数据包的基本流程,它只是最基本的做法,实际上WinPcap还支持用户自己设定过滤器来过滤消息等更多复杂的功能,此处限于篇幅只能介绍最基本的捕获数据功能:

#include "pcap.h"

#pragma comment(lib,"wpcap.lib")
#pragma comment(lib,"ws2_32.lib")

void packet_handler(u_char *param,const struct pcap_pkthdr *header,const u_char *pkt_data);

int main()
{
 pcap_if_t *alldevs;
 pcap_if_t *d;
 int    iNum;
 int    i = 0;
 pcap_t   *adhandle;
 char   ErrBuff[PCAP_ERRBUF_SIZE];
 // 查找可用网络接口
 if(pcap_findalldevs(&alldevs,ErrBuff) == -1)
 {
  fprintf(stderr,"Error in pcap_findalldevs:%s\n",ErrBuff);
  return -1;
 }
 // 输出全部网络接口
 for(d = alldevs; d; d = d->next)
 {
  printf("%d.%s",++i,d->name);
  if(d->description)
   printf("(%s)\n",d->description);
  else
   printf("No description available!\n");
 }
 if(i == 0)
 {
  printf("\nNo Interface Found!\nMake Sure WinPcap is installed!\n");
  return -1;
 }
 // 提示用户选择相应接口
 printf("Input The Interface Number(1-%d):",i);
 scanf("%d",&iNum);
 if(iNum < 1 || iNum > i)
 {
  printf("\nInterface Number Out of Range.\n");
  pcap_freealldevs(alldevs);
  return -1;
 }
 // 跳转到指定接口
 for(d = alldevs,i = 0; i < iNum - 1; d = d->next, i ++);
 // 打开接口设备开始监听
 if((adhandle = pcap_open_live(d->name,65536,1,1000,ErrBuff)) == NULL)
 {
  fprintf(stderr,"\nUnable to Open the Adapter.%s is not Supported by WinPcap.\n");
  pcap_freealldevs(alldevs);
  return -1;
 }
 printf("\nListening On %s...\n",d->description);
 pcap_freealldevs(alldevs);
 // 捕获数据并通过回调函数处理
 pcap_loop(adhandle,0,packet_handler,NULL);
 return 0;
}
// 回调函数
void packet_handler(u_char *param,const struct pcap_pkthdr *header,const u_char *pkt_data)
{
 struct tm *ltime;
 char   timestr[16];
 
 ltime = localtime(&header->ts.tv_sec);
 strftime(timestr,sizeof(timestr),"%H:%M:%S",ltime);
 printf("%s,%.6d len:%d\n",timestr,header->ts.tv_usec,header->len);
}

使用Libnet

    Libnet也是一个知名的开发包,它为构造并发送数据包提供了一种简单而强大的方法,使用Libnet可以轻松地构造出各种类型协议的数据包例如TCP、UDP、ICMP等。
    与libpcap之于WinPcap不同,libnet没有直接提供在Windows平台上的移植包,它需要我们自己编译生成相应的DLL和LIB文件。通常我们只能下载到一个“libnet.tar.gz”文件,不过可以使用WinRar将其解压,其中有个“Win32”文件夹,由于我们要在VC 6.0下使用,因此需要打开“Libnet.dsw”工程文件进行编译,它的编译过程比较复杂,同时需要WinPcap和较高版本SDK的支持,我已经将其编译好了,随文附上。
    下面的代码来自libnet提供的sample,它演示了构造并发送ARP数据包的过程:

#if (HAVE_CONFIG_H)
#if ((_WIN32) && !(__CYGWIN__)) 
#include "../include/win32/config.h"
#else
#include "../include/config.h"
#endif
#endif
#include "./libnet_test.h"

int main(int argc, char *argv[])
{
 int c;
 u_int32_t i;
 libnet_t *l;
 libnet_ptag_t t;
 char *device = NULL;
 u_int8_t *packet;
 u_int32_t packet_s;
 char errbuf[LIBNET_ERRBUF_SIZE];
 
 printf("libnet 1.1 packet shaping: ARP[link -- autobuilding ethernet]\n");
 if (argc > 1)
 {
  device = argv[1];
 }
 l = libnet_init(  LIBNET_LINK_ADV,  device, errbuf);
 if (l == NULL)
 {
  fprintf(stderr, "%s", errbuf);
  exit(EXIT_FAILURE);
 }
 else
     i = libnet_get_ipaddr4(l);
 
 t = libnet_autobuild_arp(
  ARPOP_REPLY,                      /* operation type */
  enet_src,                                /* sender hardware addr */
  (u_int8_t *)&i,                          /* sender protocol addr */
  enet_dst,                                /* target hardware addr */
  (u_int8_t *)&i,                          /* target protocol addr */
  l);                                    /* libnet context */
 if (t == -1)
 {
  fprintf(stderr, "Can't build ARP header: %s\n", libnet_geterror(l));
  goto bad;
 }
 t = libnet_autobuild_ethernet(enet_dst, ETHERTYPE_ARP, l); 
 if (t == -1)
 {
  fprintf(stderr, "Can't build ethernet header: %s\n",
   libnet_geterror(l));
  goto bad;
 }
 if (libnet_adv_cull_packet(l, &packet, &packet_s) == -1)
 {
  fprintf(stderr, "%s", libnet_geterror(l));
 }
 else
 {
  fprintf(stderr, "packet size: %d\n", packet_s);
  libnet_adv_free_packet(l, packet);
 }
 
 c = libnet_write(l);
 if (c == -1)
 {
  fprintf(stderr, "Write error: %s\n", libnet_geterror(l));
  goto bad;
 }
 else
 {
  fprintf(stderr, "Wrote %d byte ARP packet from context \"%s\"; "
   "check the wire.\n", c, libnet_cq_getlabel(l));
 }
 libnet_destroy(l);
 return (EXIT_SUCCESS);
bad:
 libnet_destroy(l);
 return (EXIT_FAILURE);
}

    事实上,由于libnet没有对应的Windows版本,其对于一些基础较为薄弱的读者来说使用起来还是有一定难度的,不容易理解,工程复杂,因此它的使用场合并不多,但如果对它熟悉了,使用起来还是很方便的,但我们需要自己从头构造数据包的地方其实不多,因此常规的Winsock应该可以基本满足我们的编程需要了。

    最后再感叹一下, VC 6.0用的多了真的感觉很不爽,虽然很稳定,但SDK版本实在太低了,而更新SDK后遇到一些老的程序就又容易出现问题,换VS.NET吧。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值