网络数据包捕获与发送的多重实现

 

注:本文已发表在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",);

 

使用WinPcap

 

WinPcap是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,                    /* injection type */

            device,                                 /* network interface */

            errbuf);                                 /* 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,                             /* ethernet destination */

            ETHERTYPE_ARP,                      /* protocol type */

            l);                                     /* libnet handle */

    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吧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值