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程序进行主机存活性探测。

Clipboard Image.png

使用wireshark进行抓包可以看出其使用的是ICMP协议

Clipboard Image.png

百科Tips

ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。

Clipboard Image.png

从上图可以看出ICMP是在IP协议头后,但是需要注意,该协议是TCP/IP协议集中的一个子协议,属于网络层协议,也就是和IP协议属于同一层,并不是和TCP/UDP一样具有端口。从抓包数据可以看出,向目标主机发送ICMP request请求包,目标主机收到后会进行reply回复。

Clipboard Image.png

有些同学会说,我用ping是下面这个样子。

Clipboard Image.png

这种情况的原因很可能是下列几种(排名分先后顺序):


1.目标主机不在线

2.本地主机与目标主机之间不通联

3.目标主机开启的防火墙过滤了ICMP协议

4.本地主机不在线

5.数据包被外星人抓走了。。。

6.。。。。。。。。。。。。。。。。。。。。。。

需要注意的是,windows防火墙在默认配置情况下开启,只会过滤request请求包的接收,所以本地主机的防火墙开启后不会影响到存活性探测。

0×02 基于ARP的主机发现

在介绍基于ICMP主机发现技术之前,先来回顾一下,在上一章节中我们介绍了ARP数据包的发送。

Clipboard Image.png

可以发现,我们可以利用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);

可以看到代码中为了加快发送速度,使用了多线程技术。

测试结果:

Clipboard Image.png

可以看到,扫描了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;

}

}

从代码中可以看到,并未采用多线程来发送探测包,那此次测试与上一节的测试相比,会有优势吗?

测试结果:

Clipboard Image.png

Clipboard Image.png

82ms!!!!是的,你没有看错,完全秒杀上一节的测试。为了排除程序的不稳定性造成的时间误差,下面是两测试的五次实验时间的对比:

Clipboard Image.png

从上图可以清晰看出基于ARP的测试虽然采用多线程技术,但耗费时间依旧明显过长,经过对代码的分析,发现原因在于,基于ARP的测试虽然针对每个IP都开启一个线程进行请求,但是SendARP函数属于堵塞式函数,即需要得到回复结果或者超时才会返回,这意味着在探测不在线的主机时,需要等到函数超时才能返回,根据时间结果我们可以猜测到其函数超时限制大概在4秒。

而基于ICMP的测试,虽然只是利用1个线程在发送探测包,但是由于pcap_sendpacket发送函数不需要等待,所以瞬间会完全本网段数据包的发送,而在线主机在接收到数据包后也会立即作出响应,而使得接收数据包的函数快速捕获到ICMP reply包。可以看出整个过程数据异步工作模式,极大提升效率。

0×04  端口扫描的原理

在确定存活主机后,针对端口状态的探测也是一项重要工作,网络管理员可以根据端口开放状况确定对应的服务是否在正常运转,渗透测试员可根据主机的端口开发状况计划下一步的渗透计划。

本地利用Netstat程序可以列出端口开放状况

Clipboard Image.png

而针对远程主机的端口则需要从网络协议的角度出发,发送相关的探测数据包,根据响应确定端口状态。

熟悉socket编程的同学,在进行连接时均调用connect函数来建立TCP连接,下图为一次典型的TCP连接的建立和断开过程。

Clipboard Image.png

根据上图可以看出,如果端口处于开放,则在接收到连接请求后,会进行三次握手来建立连接,而端口关闭的话,则不会发送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失败!"<<endl;

                return ;

            }

        }

//连接服务器

if(connect(sClient,(sockaddr *)&addrServ,sizeof(sockaddr))==SOCKET_ERROR)

{

//cout<<"port "<<ntohs(addrServ.sin_port)<<" is not open!"<<endl;

  //closesocket(sClient);

//return 1;

}

else{

cout<<"port "<<ntohs(addrServ.sin_port)<<" open on host!"<<endl;

closesocket(sClient);

sClient = NULL;

}

测试:

先选用单线程进行扫描,即扫描完一个端口后再进行下一个端口的扫描。

Clipboard Image.png

在经过半分钟后,笔者已被这龟速深深伤害,毫不犹豫Ctrl+C结束了它的使命。上图可以看出,connect探测一个端口需要消耗1秒左右的时间,而端口的范围是0-65535,这样扫下去需要65535秒,也就是18小时!!!

Clipboard Image.png

为了加快速度,我们采用多线程技术进行测试,测试的端口范围0-1000


关键代码:

    //初始化Windows Sockets 动态库

HANDLE *hThread; // 线程数组指针

DWORD dwThreadID; // 线程ID

int uPortNum = 1000;

    WSADATA wsaData;

    if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)

    {

        cout<<"找不到可使用的WinSock dll!"<<endl;

        return 1;

    }

hThread = (HANDLE *)malloc(sizeof(HANDLE) * uPortNum);

int *lPort= (int *)malloc(sizeof(int) * uPortNum);

    for (int _port = 0;_port<uPortNum;_port++)

{

       

lPort[_port]=_port;

if ((hThread[_port]=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) connect_scan, 

&lPort[_port], 0, &dwThreadID))==NULL)

{

printf("[!] Create thread error! \n");

}

}

// 等待线程返回,退出函数

SyncWaitForMultipleObjs(hThread,uPortNum);

printf("finished! \n");

从实验结果可以看到,虽然采用了多线程技术,但是扫描1000个端口是速度依旧比较慢,耗费了15秒,这样的扫描速度对于大规模的网络探测依旧不够理想。此处的时间耗费和ARP实验类似,connect函数也属于阻塞函数,需要建立完整的连接或等待超时才可返回。

优点:直接调用winsock库套接字函数,开发方便

缺点:由于建立连接需要完成三次握手,则耗费时间久,数据量大,而且会被防火墙记录

0×06  基于SYN、FIN的端口扫描

针对connect扫描的缺陷,可简化三次握手的步骤,直接发送SYN包进行探测,根据目标端口是否回复SYN/ACK确认包来判断端口的开放状况,这样会大大替身扫描速度,而且没有建立完整的三次握手,不会被防火墙进行记录。由于只是进行了建立连接的前半部分,所以SYN扫描又称“半开放式扫描”。

下图是目标端口80开放时,探测包的回应状况。

Clipboard Image.png

而对于未开放的端口,会收到RST/ACK的响应包断开连接。

Clipboard Image.png

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;

}

测试结果:

Clipboard Image.png

从测试结果可以明显看出,效率极高,扫描65535个端口只耗费了不到3秒。

SYN扫描优点:速度快、安全性强;

细心的同学在进行SYN扫描的时候会发现如下情况:

Clipboard Image.png

开放端口会重复发送SYN/ACK确认包,通常会发送3次确认包。这是因为我们并没有进行三次握手的第三个数据包的放松,所以目标端口会认为自己的包未送达,而进行了超时重传。

FIN端口扫描也是利用端口对FIN包的响应判断开放状态。如果发送一个FIN标志的TCP报文到一个关闭的端口,那么应该返回一个RST报文,如果发送到一个开放的端口,那么应该没有任何反应。如果收到ICMP端口不可达错误数据包,则不能确认是否开放或者关闭,把它称为状态未知端口。

Clipboard Image.png

关键代码:


//构建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系统进行测试时,会发现所有的端口都不进行响应。

Clipboard Image.png

该方法是对windows系统无效的,只能针对类Unix操作系统。

除了上述方法外,我们可以充分利用TCP的标志位,进行其他类型的端口扫描,例如TCP ACK扫描、TCP NULL扫描、TCP Xmas Tree扫描等等,有兴趣的同学可以查阅相关资料完成测试实验。

0×07 优化

在进行主机发现过程中,由于受到防火墙等设备的影响,很可能阻断了ICMP包的传输,而影响到探测的准确度,所以有必要利用其他方法来拟补这一缺陷。

Clipboard Image.png

大名鼎鼎的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中,其端口扫描便是这样实现的。

0×08 总结与预告

在本章中,我们结合上一章的入门知识,深入学习了winpcap在扫描探测场景中的应用方法,理解了主机发现和端口扫描的原理,掌握了一些常用发放手段,并提出了优化建议。在下一章中,我们会学习如何利用winpcap进行ARP欺骗和中间人攻击。