0×00 前言
0×01 “主机发现”原理
0×02 基于ARP的主机发现
0×03基于ICMP的主机发现
0×04 端口扫描的原理
0×05 基于connect的端口扫描
0×06 基于SYN、FIN的端口扫描
0×07 优化
0×08总结与预告
在上一章节,我们学习了winpcap入门知识,基本环境的搭建与基础程序的编写,本章我们将继续深入探索winpcap在各种应用场景下的强大魅力。本章节将详细介绍利用winpcap进行网络节点存活性探测及端口开放状态扫描的“姿势”。
网络节点存活性探测,又称“主机发现”,其目的在于确定目标主机是否在线。网络管理员通常使用其维护网络,确定网络中主机的通联状况,及时发现掉线或宕机的机器;而渗透测试人员则利用其确定目标网络拓扑及在线主机,根据绘制的在线网络拓扑进行渗透测试,如端口扫描、系统探测等,从而避免发送大量探测包到不在线的主机,提高探测效率。
通常我们都会使用系统自带的ping程序进行主机存活性探测。
使用wireshark进行抓包可以看出其使用的是ICMP协议
百科Tips
ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。
从上图可以看出ICMP是在IP协议头后,但是需要注意,该协议是TCP/IP协议集中的一个子协议,属于网络层协议,也就是和IP协议属于同一层,并不是和TCP/UDP一样具有端口。从抓包数据可以看出,向目标主机发送ICMP request请求包,目标主机收到后会进行reply回复。
有些同学会说,我用ping是下面这个样子。
这种情况的原因很可能是下列几种(排名分先后顺序):
1.目标主机不在线
2.本地主机与目标主机之间不通联
3.目标主机开启的防火墙过滤了ICMP协议
4.本地主机不在线
5.数据包被外星人抓走了。。。
6.。。。。。。。。。。。。。。。。。。。。。。
需要注意的是,windows防火墙在默认配置情况下开启,只会过滤request请求包的接收,所以本地主机的防火墙开启后不会影响到存活性探测。
在介绍基于ICMP主机发现技术之前,先来回顾一下,在上一章节中我们介绍了ARP数据包的发送。
可以发现,我们可以利用ARP协议进行主机发现,针对目标主机IP广播ARP查询包,如果目标主机在线则会响应。
优点:防火墙不会对此进行过滤
缺点:ARP广播包不能跨网段,则该主机发现方法只适用于局域网内部
测试:
此处我们所用的代码依旧来源于ARPSPOOF源码,其中函数EnumLanHost(argv[2], argv[3])用于局域网的ARP广播,该处调用了iphlpapi.dll中SendARP函数进行广播包的发送。关键代码如下:
// 网段内主机信息双向链表 typedef struct _LAN_HOST_INFO { char IpAddr[4 * 4]; /* 主机IP地址 */ char HostName[25]; /* 主机名 */ unsigned char ucMacAddr[4]; /* 主机网卡地址 */ BOOL bIsOnline; /* 是否在线 */ struct _LAN_HOST_INFO *prev; /* 上一个主机的指针 */ struct _LAN_HOST_INFO *next; /* 下一个主机的指针 */ }LAN_HOST_INFO, *PLAN_HOST_INFO; // 开始进行多线程ARP扫描,创建uHostNum个线程扫描 // 扫描端口范围 1 ~ uHostNum for (i = 0, uHostByte ++; i < uHostNum; i ++, uHostByte ++) { // 构造IP地址 memset(TempIpAddr, 0, strlen(TempIpAddr)); sprintf(TempIpAddr, "%d.%d.%d.%d", (uHostByte & 0xff000000) >> 0x18, (uHostByte & 0x00ff0000) >> 0x10, (uHostByte & 0x0000ff00) >> 0x08, (uHostByte & 0x000000ff)); // 构造链表 pNextHostInfo = (PLAN_HOST_INFO) malloc(sizeof(LAN_HOST_INFO)); memset(pNextHostInfo, 0, sizeof(LAN_HOST_INFO)); memcpy(pLanHostInfo->IpAddr, TempIpAddr, sizeof(TempIpAddr)); pLanHostInfo->next = pNextHostInfo; pNextHostInfo->prev = pLanHostInfo; pNextHostInfo->next = NULL; if ((hThread[i]=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) scan_lan, pLanHostInfo, 0, &dwThreadID))==NULL) { printf("[!] Create thread error! IP is %s/n",TempIpAddr); } pLanHostInfo = pLanHostInfo->next; Sleep(2); // 等待参数传递完毕,再重新赋值 } // 等待线程返回,退出函数 WaitForMultipleObjects(uHostNum,hThread,TRUE,-1);
可以看到代码中为了加快发送速度,使用了多线程技术。
测试结果:
可以看到,扫描了192.168.0.1/24网段,共消耗了3982ms,一共发现11台主机,除去自身,共10台主机存活。下面我们再来看看ICMP的战绩。
在0×01节中,已经介绍了ICMP主机发现的基本原理,现利用这一原理进行实验测试。
优点:不局限与局域网,可探测任意可达网络内的主机
缺点:防火墙会针对ICMP协议进行过滤
测试:
为了与基于ARP的主机发现测试进行时间对比,此次测试我们同样扫描192.168.0.1/24网段程序原理:
调用winpcap针对网内所有IP地址发送ICMP request包,同时进行数据包的接收,筛选出ICMP reply包,凡是有reply包的IP地址即为存活主机IP。
关键代码:
// 打开网卡
if ((adhandle = OpenAdapter(0, szIPSelf, ucSelf, szIPGate)) == NULL)
{
printf("[!] Open adatper error!/n");
return FALSE;
}
DWORD dwThreadID; // 线程ID
HANDLE hThread; //该线程用于ICMP的接收
if ((hThread=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) revICMPData,adhandle, 0, &dwThreadID))==NULL)
{
printf("[!] Create thread error! /n");
}
ICMPScan(adhandle);//该函数用于发送ICMP request探测包
WaitForSingleObject(hThread,INFINITE);//等待hThread线程终止
发送ICMP request包的关键代码
pcap_t *adhandle = (pcap_t *)p;
u_char ucFrame[ICMP_LEN];
//设置Ethernet头
ETHeader eh = { 0 };
memset(eh.dhost,0xff, 6);//目的MAC
memcpy(eh.shost, ucSelf, 6);//源MAC
eh.type = htons(ETHERTYPE_IP);//下层协议类型
memcpy(ucFrame, &eh, sizeof(eh));//将设置好的Ethernet头进行填充
//设置IP头
IPHeader ph = { 0 };
ph.iphVerLen=0x45;//版本号和头长度(各占4位)
ph.ipTOS=0;//服务类型
ph.ipLength=htons(sizeof(IPHeader)+sizeof(ICMPHeader));//封包总长度,即整个IP报的长度
ph.ipID=htons(0x00a3);//封包标识,惟一标识发送的每一个数据报
ph.ipFlags=0;//标志
ph.ipTTL=128;//生存时间,就是TTL
ph.ipProtocol=1;//协议
ph.ipChecksum=0;//校验和置0
ph.ipSource = inet_addr(szIPSelf);//源IP地址
ph.ipDestination = inet_addr("192.168.0.10"); //目的IP地址
ph.ipChecksum=checkicmpsum((USHORT*)&ph,sizeof(IPHeader));//校验和计算
memcpy(&ucFrame[sizeof(ETHeader)], &ph, sizeof(ph));//将设置好的IP头进行填充
// 设置ICMP头
ICMPHeader ih = { 0 };
ih.i_cksum=0;//校验和置0
memcpy(ih.i_data,"abcdefghijklmnopqrstuvwabcdefghi",32);//填充数据
ih.i_seq=htons(0x0028);//序列号
ih.i_id =htons(1);//id
ih.i_code = 0;//代码
ih.i_type=8;//ICMP类型
ih.i_cksum= checkicmpsum((USHORT*)&ih, sizeof(ICMPHeader));//校验和计算
memcpy(&ucFrame[sizeof(ETHeader)+sizeof(IPHeader)], &ih, sizeof(ih));//填充
char IPbuf[20]={0};
//针对192.168.0.1/24网段内的所有IP地址进行探测
for(int i=1; i<255 ; i++)
{ sprintf(IPbuf,"192.168.0.%d",i);
ph.ipDestination = inet_addr(IPbuf);
ph.ipChecksum=0;
ph.ipChecksum=checkicmpsum((USHORT*)&ph,sizeof(IPHeader));
memcpy(&ucFrame[sizeof(ETHeader)], &ph, sizeof(ph));
if(pcap_sendpacket(adhandle, (const unsigned char *) ucFrame, ICMP_LEN) < 0)
{
printf("Send Packet Error/n");
return;
}
}
从代码中可以看到,并未采用多线程来发送探测包,那此次测试与上一节的测试相比,会有优势吗?
测试结果:
82ms!!!!是的,你没有看错,完全秒杀上一节的测试。为了排除程序的不稳定性造成的时间误差,下面是两测试的五次实验时间的对比:
从上图可以清晰看出基于ARP的测试虽然采用多线程技术,但耗费时间依旧明显过长,经过对代码的分析,发现原因在于,基于ARP的测试虽然针对每个IP都开启一个线程进行请求,但是SendARP函数属于堵塞式函数,即需要得到回复结果或者超时才会返回,这意味着在探测不在线的主机时,需要等到函数超时才能返回,根据时间结果我们可以猜测到其函数超时限制大概在4秒。
而基于ICMP的测试,虽然只是利用1个线程在发送探测包,但是由于pcap_sendpacket发送函数不需要等待,所以瞬间会完全本网段数据包的发送,而在线主机在接收到数据包后也会立即作出响应,而使得接收数据包的函数快速捕获到ICMP reply包。可以看出整个过程数据异步工作模式,极大提升效率。
在确定存活主机后,针对端口状态的探测也是一项重要工作,网络管理员可以根据端口开放状况确定对应的服务是否在正常运转,渗透测试员可根据主机的端口开发状况计划下一步的渗透计划。
本地利用Netstat程序可以列出端口开放状况
而针对远程主机的端口则需要从网络协议的角度出发,发送相关的探测数据包,根据响应确定端口状态。
熟悉socket编程的同学,在进行连接时均调用connect函数来建立TCP连接,下图为一次典型的TCP连接的建立和断开过程。
根据上图可以看出,如果端口处于开放,则在接收到连接请求后,会进行三次握手来建立连接,而端口关闭的话,则不会发送SYN/ACK确认包。根据这一特性,我们进行下面的实验测试。
利用套接字的connect函数,我们可以方便的确定端口是否开放。
关键代码: SOCKADDR_IN addrServ; addrServ.sin_family=AF_INET; addrServ.sin_addr.S_un.S_addr=inet_addr(szSelfIP); addrServ.sin_port=htons(_port); SOCKET sClient = NULL; if (sClient == NULL) { //创建套接字 sClient=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); if(sClient==INVALID_SOCKET) { cout<<"创建客户端socket失败!"<<<"port "< <<" is not open!"< <<"port "< <<" open on host!"<
测试:
先选用单线程进行扫描,即扫描完一个端口后再进行下一个端口的扫描。
在经过半分钟后,笔者已被这龟速深深伤害,毫不犹豫Ctrl+C结束了它的使命。上图可以看出,connect探测一个端口需要消耗1秒左右的时间,而端口的范围是0-65535,这样扫下去需要65535秒,也就是18小时!!!
为了加快速度,我们采用多线程技术进行测试,测试的端口范围0-1000
关键代码: //初始化Windows Sockets 动态库 HANDLE *hThread; // 线程数组指针 DWORD dwThreadID; // 线程ID int uPortNum = 1000; WSADATA wsaData; if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0) { cout<<"找不到可使用的WinSock dll!"<
从实验结果可以看到,虽然采用了多线程技术,但是扫描1000个端口是速度依旧比较慢,耗费了15秒,这样的扫描速度对于大规模的网络探测依旧不够理想。此处的时间耗费和ARP实验类似,connect函数也属于阻塞函数,需要建立完整的连接或等待超时才可返回。
优点:直接调用winsock库套接字函数,开发方便
缺点:由于建立连接需要完成三次握手,则耗费时间久,数据量大,而且会被防火墙记录
针对connect扫描的缺陷,可简化三次握手的步骤,直接发送SYN包进行探测,根据目标端口是否回复SYN/ACK确认包来判断端口的开放状况,这样会大大替身扫描速度,而且没有建立完整的三次握手,不会被防火墙进行记录。由于只是进行了建立连接的前半部分,所以SYN扫描又称“半开放式扫描”。
下图是目标端口80开放时,探测包的回应状况。
而对于未开放的端口,会收到RST/ACK的响应包断开连接。
SYN端口扫描关键代码:
//SYN探测包构建函数 unsigned char* BuildSYN(WORD dst_port) { static struct ip_s_packet syn_packet; static struct ps_s_tcp fake_tcp; //以太网帧头 memcpy(syn_packet.eth.source_mac,szSelfmac,6); //本机mac memcpy(syn_packet.eth.dest_mac,szdestmac,6);//目标mac syn_packet.eth.eh_type = htons(0x0800);//协议类型,ip //ip头部 syn_packet.ip.ver_ihl = 0x45; //协议版本及头长 syn_packet.ip.tos = 0x00;//服务类型 syn_packet.ip.tlen = htons(0x002c);//总长度 syn_packet.ip.identification = 123; //标识 syn_packet.ip.flags_fo = 0x0000; //标志位,段偏移 syn_packet.ip.ttl = 128; syn_packet.ip.proto = 0x06; syn_packet.ip.crc = htons(0x0000); syn_packet.ip.src_addr = inet_addr(szSelfIP); //源ip syn_packet.ip.dst_addr = inet_addr(szDestIP); syn_packet.ip.crc = htons(IPcheck((unsigned char*)&syn_packet.ip));//ip头部校验和 //tcp头部 srand(time(NULL)); syn_packet.tcp.s_port = htons(rand()%65535);//源端口 syn_packet.tcp.d_port = htons(dst_port);//目标端口 syn_packet.tcp.sn = htonl(rand()%65535);//序列号 syn_packet.tcp.an = 0; //确认号 syn_packet.tcp.other = htons(0x6002);//头长及六个标识位 syn_packet.tcp.window_size = htons(0x0c00);//窗口大小 syn_packet.tcp.check_sum = 0;//tcp校验和 syn_packet.tcp.urgent_pointer = 0;//紧急指针 //tcp伪头部 fake_tcp.source_address = syn_packet.ip.src_addr; fake_tcp.dest_address = syn_packet.ip.dst_addr; fake_tcp.placeholder = 0; fake_tcp.protocol = syn_packet.ip.proto; fake_tcp.tcp_length = htons(sizeof(syn_packet.tcp)); fake_tcp.tcptou = syn_packet.tcp; syn_packet.tcp.check_sum = TCPcheck((WORD*)&fake_tcp,36); return (unsigned char*)&syn_packet; } //探测包发送函数 void SYNScanSend(pcap_t * adhandle) { unsigned char *packet; WORD count = 65535 - 1; WORD *Buf = Sort(1,65535);//端口生产函数 int i = 0; while(i<=count) { packet = BuildSYN(Buf[i]); pcap_sendpacket(adhandle, packet, 58); i++; } return; }
测试结果:
从测试结果可以明显看出,效率极高,扫描65535个端口只耗费了不到3秒。
SYN扫描优点:速度快、安全性强;
细心的同学在进行SYN扫描的时候会发现如下情况:
开放端口会重复发送SYN/ACK确认包,通常会发送3次确认包。这是因为我们并没有进行三次握手的第三个数据包的放松,所以目标端口会认为自己的包未送达,而进行了超时重传。
FIN端口扫描也是利用端口对FIN包的响应判断开放状态。如果发送一个FIN标志的TCP报文到一个关闭的端口,那么应该返回一个RST报文,如果发送到一个开放的端口,那么应该没有任何反应。如果收到ICMP端口不可达错误数据包,则不能确认是否开放或者关闭,把它称为状态未知端口。
关键代码:
//构建FIN包 //dst_port:目标端口 unsigned char* BuildFIN(WORD dst_port) { static struct ip_f_packet fin_packet; static struct ps_f_tcp fake_tcp; //以太网帧头 memcpy(syn_packet.eth.source_mac,szSelfmac,6); //本机mac memcpy(syn_packet.eth.dest_mac,szdestmac,6);//目标mac syn_packet.eth.eh_type = htons(0x0800);//协议类型,ip //ip头部 fin_packet.ip.ver_ihl = 0x45; //协议版本及头长 fin_packet.ip.tos = 0x00;//服务类型 fin_packet.ip.tlen = htons(0x0028);//总长度 fin_packet.ip.identification = 123; //标识?????? fin_packet.ip.flags_fo = 0x0000; //标志位,段偏移 fin_packet.ip.ttl = 128; fin_packet.ip.proto = 0x06; fin_packet.ip.crc = htons(0x0000); fin_packet.ip.src_addr = inet_addr(szSelfIP); //源ip?????????? fin_packet.ip.dst_addr = inet_addr(szDestIP); // syn_packet.ip.op_pad 扫描包的ip选项部分为空 fin_packet.ip.crc = htons(IPcheck((unsigned char*)&fin_packet.ip));//ip头部校验和 //tcp头部 srand(time(NULL)); fin_packet.tcp.s_port = htons(rand()%65535);//源端口 fin_packet.tcp.d_port = htons(dst_port);//目标端口 fin_packet.tcp.sn = htonl(rand()%65535);//序列号 fin_packet.tcp.an = 0; //确认号 fin_packet.tcp.other = htons(0x5001);//头长及六个标识位 fin_packet.tcp.window_size = htons(0x0c00);//窗口大小 fin_packet.tcp.check_sum = 0;//tcp校验和 fin_packet.tcp.urgent_pointer = 0x0000;//紧急指针 //tcp伪头部 fake_tcp.source_address = fin_packet.ip.src_addr; fake_tcp.dest_address = fin_packet.ip.dst_addr; fake_tcp.placeholder = 0; fake_tcp.protocol = fin_packet.ip.proto; fake_tcp.tcp_length = htons(sizeof(fin_packet.tcp)); fake_tcp.tcptou = fin_packet.tcp; fin_packet.tcp.check_sum = TCPcheck((WORD*)&fake_tcp,32); return (unsigned char*)&fin_packet; }
需要注意,在发送FIN探测包后,如果收不到任何回复,并不代表端口一定是打开的,有可能由于网络环境的复杂性,或者防火墙或其他网络过滤设备,阻碍了正常的数据流程。
另外笔者在针对windows系统进行测试时,会发现所有的端口都不进行响应。
该方法是对windows系统无效的,只能针对类Unix操作系统。
除了上述方法外,我们可以充分利用TCP的标志位,进行其他类型的端口扫描,例如TCP ACK扫描、TCP NULL扫描、TCP Xmas Tree扫描等等,有兴趣的同学可以查阅相关资料完成测试实验。
在进行主机发现过程中,由于受到防火墙等设备的影响,很可能阻断了ICMP包的传输,而影响到探测的准确度,所以有必要利用其他方法来拟补这一缺陷。
大名鼎鼎的Nmap在主机发现中采用的方法是,发送四种数据包探测目标主机是否在线:
1.ICMPecho request
2.TCP SYN packet to port 443
3.TCP ACK packet to port 80
4.ICMP timestamp request
这种方法叫做 “综合探测法” ,结合可端口扫描的方法,对目标进行存活性探测,一定程度上规避了防火墙对测试结果的影响。
在端口扫描时,一次性向目标主机大量发送探测包势必会触发防火墙报警,所以有必要采取一些防范措施,在上面的测试中,探测包的源端口均采用随机数,而目标端口也并没有按照从0到65535进行顺序扫描,而是利用sort函数生存随机端口序列。
WORD* Sort(WORD Begin,WORD End) { WORD count = End - Begin;//Begin:起始端口,End:结束端口,count:扫描的端口数 WORD i ,Temp,Rand; if(count<0) return NULL; srand(time(NULL)); for(i=0;i<=count;i++)//将端口号存入数组 buf[i]=Begin+i; for(i=0;i<=count;i++)//将原端口位置与随机生成位置调换 { Rand = rand()%count; Temp = buf[i]; buf[i]=buf[Rand]; buf[Rand]=Temp; } return (WORD *)buf; }
针对端口扫描,我们还可以整理一份常用端口列表,先扫描一些常用端口,从而尽快发现所需要的端口信息。在Nmap中,其端口扫描便是这样实现的。
在本章中,我们结合上一章的入门知识,深入学习了winpcap在扫描探测场景中的应用方法,理解了主机发现和端口扫描的原理,掌握了一些常用发放手段,并提出了优化建议。在下一章中,我们会学习如何利用winpcap进行ARP欺骗和中间人攻击。