您的位置:首页 > 理论基础 > 计算机网络

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

2012-07-08 02:11 423 查看
注:本文已发表在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 (H***E_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吧。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: