0×00 前言 0×01 “主机发现”原理 0×02 基于ARP的主机发现 0×03基于ICMP的主机发现 0×04 端口扫描的原理 0×05 基于connect的端口扫描 0×06 基于SYN、FIN的端口扫描 0×07 优化 0×08总结与预告 0×00 前言在上一章节,我们学习了winpcap入门知识,基本环境的搭建与基础程序的编写,本章我们将继续深入探索winpcap在各种应用场景下的强大魅力。本章节将详细介绍利用winpcap进行网络节点存活性探测及端口开放状态扫描的“姿势”。 0×01 主机发现”原理网络节点存活性探测,又称“主机发现”,其目的在于确定目标主机是否在线。网络管理员通常使用其维护网络,确定网络中主机的通联状况,及时发现掉线或宕机的机器;而渗透测试人员则利用其确定目标网络拓扑及在线主机,根据绘制的在线网络拓扑进行渗透测试,如端口扫描、系统探测等,从而避免发送大量探测包到不在线的主机,提高探测效率。 通常我们都会使用系统自带的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请求包的接收,所以本地主机的防火墙开启后不会影响到存活性探测。 0×02 基于ARP的主机发现在介绍基于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×03 基于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包。可以看出整个过程数据异步工作模式,极大提升效率。 0×04 端口扫描的原理在确定存活主机后,针对端口状态的探测也是一项重要工作,网络管理员可以根据端口开放状况确定对应的服务是否在正常运转,渗透测试员可根据主机的端口开发状况计划下一步的渗透计划。 本地利用Netstat程序可以列出端口开放状况
而针对远程主机的端口则需要从网络协议的角度出发,发送相关的探测数据包,根据响应确定端口状态。 熟悉socket编程的同学,在进行连接时均调用connect函数来建立TCP连接,下图为一次典型的TCP连接的建立和断开过程。
根据上图可以看出,如果端口处于开放,则在接收到连接请求后,会进行三次握手来建立连接,而端口关闭的话,则不会发送SYN/ACK确认包。根据这一特性,我们进行下面的实验测试。 0×05 基于connect的端口扫描利用套接字的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失败!"<
测试: 先选用单线程进行扫描,即扫描完一个端口后再进行下一个端口的扫描。
在经过半分钟后,笔者已被这龟速深深伤害,毫不犹豫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库套接字函数,开发方便 缺点:由于建立连接需要完成三次握手,则耗费时间久,数据量大,而且会被防火墙记录 0×06 基于SYN、FIN的端口扫描针对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; |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|