注:本文已发表在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吧。