16.1 Socket 端口扫描技术
端口扫描是一种网络安全测试技术,该技术可用于确定对端主机中开放的服务,从而在渗透中实现信息搜集,其主要原理是通过发送一系列的网络请求来探测特定主机上开放的TCP/IP端口。具体来说,端口扫描程序将从指定的起始端口开始,向目标主机发送一条TCP或UDP消息(这取决于端口的协议类型)。如果目标主机正在监听该端口,则它将返回一个确认消息,这表明该端口是开放的。如果没有响应,则说明该端口是关闭的或被过滤。
首先我们来了解一下阻塞与非阻塞模式:
阻塞模式是指当I/O操作无法立即完成时,应用程序会阻塞并等待操作完成。例如,在使用阻塞套接字接收数据时,如果没有数据可用,则调用函数将一直阻塞,直到有数据可用为止。在这种模式下,I/O操作将会一直阻塞应用程序的进程,因此无法执行其他任务。
非阻塞模式是指当I/O操作无法立即完成时,应用程序会立即返回并继续执行其他任务。例如,在使用非阻塞套接字接收数据时,如果没有数据可用,则调用函数将立即返回,并指示操作正在进行中,同时应用程序可以执行其他任务。在这种模式下,应用程序必须反复调用I/O操作以检查其完译状态,这通常是通过轮询或事件通知机制实现的。非阻塞模式允许应用程序同时执行多个任务,但每个I/O操作都需要增加一定的额外开销。
要实现端口探测我们可以通过connect()这个函数来实现,利用connect函数实现端口开放检查的原理是通过TCP协议的三次握手过程来探测目标主机是否开放目标端口。
在TCP协议的三次握手过程中,客户端向服务器发送一个SYN标志位的TCP数据包。如果目标主机开放了目标端口并且正在监听连接请求,则服务器会返回一个带有SYN和ACK标志位的TCP数据包,表示确认连接请求并请求客户端确认。此时客户端回应一个ACK标志位的TCP数据包,表示确认连接请求,并建立了一个到服务器端口的连接。此时客户端和服务器端之间建立了一个TCP连接,可以进行数据传输。
如果目标主机没有开放目标端口或者目标端口已经被占用,则服务器不会响应客户端的TCP数据包,客户端会在一定时间后收到一个超时错误,表示连接失败。
因此,通过调用connect函数,可以向目标主机发送一个SYN标志位的TCP数据包并等待服务器响应,从而判断目标端口是否开放。如果connect函数返回0,则表示连接成功,目标端口开放;否则,连接失败,目标端口未开放或目标主机不可达。
// 探测网络端口开放情况
BOOL PortScan(char *Addr, int Port)
{
WSADATA wsd;
SOCKET sHost;
SOCKADDR_IN servAddr;
// 初始化套接字库
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
return FALSE;
}
// 创建套接字
sHost = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == sHost)
{
return FALSE;
}
// 设置连接地址和端口
servAddr.sin_family = AF_INET;
servAddr.sin_addr.S_un.S_addr = inet_addr(Addr);
servAddr.sin_port = htons(Port);
// 连接测试
int retval = connect(sHost, (LPSOCKADDR)&servAddr, sizeof(servAddr));
if (retval != SOCKET_ERROR)
{
return TRUE;
}
WSACleanup();
closesocket(sHost);
return FALSE;
}
int main(int argc, char* argv[])
{
int port_list[] = { 80, 443, 445, 135, 139, 445 };
int port_size = sizeof(port_list) / sizeof(int);
for (int x = 0; x < port_size; x++)
{
int ret = PortScan("8.141.58.64", port_list[x]);
printf("循环次数: %d 端口: %d 状态: %d \n", x + 1, port_list[x], ret);
}
system("pause");
return 0;
}
上述代码片段则是一个简单的端口探测案例,当运行后程序会调用connect函数向目标主机发送一个SYN标志位的TCP数据包,探测目标端口是否开放。如果目标主机响应带有SYN和ACK标志位的TCP数据包,则表示连接请求成功并请求确认,操作系统在自动发送带ACK标志位的TCP数据包进行确认,建立TCP连接;
如果目标主机没有响应或者响应带有RST标志位的TCP数据包,则表示连接请求失败,目标端口为未开放状态。通过此方式,程序可以快速检测多个端口是否开放,该程序运行后输出效果如下图所示;

上述代码虽然可以实现端口扫描,但是读者应该会发现此方法扫描很慢,这是因为扫描器每次只能链接一个主机上的端口只有当connect函数返回后才会执行下一次探测任务,而如果需要提高扫描效率那么最好的方法是采用非阻塞的扫描模式,使用非阻塞模式我们可以在不使用多线程的情况下提高扫描速度。
非阻塞模式所依赖的核心函数为select()函数是一种用于多路I/O复用的系统调用,在Windows中提供了对该系统调用的支持。select()函数可以同时监听多个文件或套接字(socket)的可读、可写和出错状态,并返回有状态变化的文件或套接字的数量,在使用该函数时读者应率先调用ioctlsocket()函数,并设置FIONBIO套接字为非阻塞模式。
select 函数的基本语法如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
- nfds:需要监听的文件或套接字最大编号加1
- readfds:可读文件或套接字集合
- writefds:可写文件或套接字集合
- exceptfds:出错文件或套接字集合
- timeout:超时时间,如果为NULL,则表示一直等待直到有事件发生
select 函数会阻塞进程,直到在需要监听的文件或套接字中有一个或多个文件或套接字发送了需要监听的事件,或者超时时间到达。当select()函数返回时,可以通过fd_set集合来查询有状态变化的文件或套接字。
select 函数的原理是将调用进程的文件或套接字加入内核监测队列,等待事件发生。当某个文件或套接字有事件发生时,内核会将其添加到内核缓冲区中,同时在返回时告诉进程有哪些套接字可以进行I/O操作,进程再根据文件或套接字的状态进行相应的处理。使用select()函数可以大大提高I/O操作的效率,减少资源占用。
如下代码实现的是一段简单的端口扫描程序,用于检查目标主机的一段端口范围内是否有端口处于开放状态。该函数中通过设置fd_set类型的掩码(mask)并加入套接字,使用select()函数查询该套接字的可写状态,并设置超时时间为1毫秒,如果返回值为0,则目标端口未开放,继续下一个端口的扫描。如果返回值为正数,则目标端口已成功连接(开放),输出扫描结果并继续下一个端口的扫描。
该代码中使用了非阻塞套接字和select()函数的组合来实现非阻塞IO。非阻塞套接字可以使程序不会在等待数据到来时一直阻塞,而是可以在等待数据到来的同时进行其他操作,从而提高程序的效率。select()函数则可以同时等待多个套接字的数据到来,从而使程序更加高效地进行I/O操作。
// 非阻塞端口探测
void PortScan(char *address, int StartPort, int EndPort)
{
SOCKADDR_IN ServAddr;
TIMEVAL TimeOut;
FD_SET mask;
TimeOut.tv_sec = 0;
// 设置超时时间为500毫秒
TimeOut.tv_usec = 1000;
// 指定模式
unsigned long mode = 1;
// 循环扫描端口
for (int port = StartPort; port <= EndPort; port++)
{
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
ServAddr.sin_family = AF_INET;
ServAddr.sin_addr.S_un.S_addr = inet_addr(address);
ServAddr.sin_port = htons(port);
FD_ZERO(&mask);
FD_SET(sock, &mask);
// 设置为非阻塞模式
ioctlsocket(sock, FIONBIO, &mode);
connect(sock, (struct sockaddr *)&ServAddr, sizeof(ServAddr));
// 查询可写入状态 如果不为0则说明这个端口是开放的
int ret = select(0, 0, &mask, 0, &TimeOut);
if (ret != 0 && ret != -1)
{
printf("扫描地址: %-13s --> 端口: %-5d --> 状态: [Open] \n", address, port);
}
else
{
printf("扫描地址: %-13s --> 端口: %-5d --> 状态: [Close] \n", address, port);
}
}
}
int main(int argc, char *argv[])
{
char *Addr[2] = { "192.168.1.1", "192.168.1.10" };
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
{
exit(0);
}
for (int x = 0; x < 2; x++)
{
PortScan(Addr[x], 1, 255);
}
WSACleanup();
system("pause");
return 0;
}
读者可自行编译并运行上述代码片段,默认会扫描Addr[2]数组内的两个IP地址的1-255端口范围开放情况,读者可感觉到效率上变得快了许多,输出效果如下图所示;

上述代码虽然增加的扫描速度但是还可以进一步优化,我们可以通过增加信号机制,通过使用信号可以很好的控制扫描并发连接数,增加了线程控制将会使扫描器更加稳定,同时我们还引用了多线程模式,通过两者的结合可以极大的提高扫描质量和效率。
基于信号的端口扫描,也称为异步IO端口扫描,是一种高效的端口扫描技术,可以利用操作系统的信号机制提高网络I/O的效率。基于信号的端口扫描具有非阻塞和异步的特性,可以最大限度地提高网络I/O效率,同时在大并发量下表现出更好的性能。但是,使用时需要小心处理信号的相关问题,避免死锁和数据不一致。
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32")
typedef struct _THREAD_PARAM
{
char *HostAddr; // 扫描主机
DWORD dwStartPort; // 端口号
HANDLE hEvent; // 事件句柄
HANDLE hSemaphore; // 信号量句柄
}THREAD_PARAM;
// 最大线程数,用于控制信号量数量
#define MAX_THREAD 10
// 线程扫描函数
DWORD WINAPI ScanThread(LPVOID lpParam)
{
// 拷贝传递来的扫描参数
THREAD_PARAM ScanParam = { 0 };
MoveMemory(&ScanParam, lpParam, sizeof(THREAD_PARAM));
// 设置信号
SetEvent(ScanParam.hEvent);
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
// 初始化套接字
SOCKET s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sockaddr;
// 填充扫描地址与端口
sockaddr.sin_family = AF_INET;
sockaddr.sin_addr.S_un.S_addr = inet_addr(ScanParam.HostAddr);
sockaddr.sin_port = htons(ScanParam.dwStartPort);
// 开始连接
if (connect(s, (SOCKADDR*)&sockaddr, sizeof(SOCKADDR)) == 0)
{
printf("地址: %-16s --> 端口: %-5d --> 信号量: %-5d 状态: [Open] \n",
ScanParam.HostAddr, ScanParam.dwStartPort, ScanParam.hSemaphore);
}
else
{
printf("地址: %-16s --> 端口: %-5d --> 信号量: %-5d 状态: [Close] \n",
ScanParam.HostAddr, ScanParam.dwStartPort, ScanParam.hSemaphore);
}
closesocket(s);
WSACleanup();
// 释放一个信号量
ReleaseSemaphore(ScanParam.hSemaphore, 1, NULL);
return 0;
}
int main(int argc, char *argv[])
{
// 线程参数传递
THREAD_PARAM ThreadParam = { 0 };
// 设置线程信号
SetEvent(ThreadParam.hEvent);
// 创建事件
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
// 创建信号
HANDLE hSemaphore = CreateSemaphore(NULL, MAX_THREAD, MAX_THREAD, NULL);
ThreadParam.hEvent = hEvent;
ThreadParam.hSemaphore = hSemaphore;
ThreadParam.HostAddr = "59.110.117.109";
for (DWORD port = 1; port < 4096; port++)
{
// 判断信号量
DWORD dwWaitRet = WaitForSingleObject(hSemaphore, 200);
if (dwWaitRet == WAIT_OBJECT_0)
{
ThreadParam.dwStartPort = port;
// 启动扫描线程
HANDLE hThread = CreateThread(NULL, 0, ScanThread, (LPVOID)&ThreadParam, 0, NULL);
// 等待事件
WaitForSingleObject(hEvent, INFINITE);
// 重置信号
ResetEvent(hEvent);
}
else if (dwWaitRet == WAIT_TIMEOUT)
{
continue;
}
}
system("pause");
return 0;
}
读者可自行编译并运行上述代码,将对特定IP地址进行端口探测,每次启用10个线程,即实现了控制线程并发,又实现了端口多线程扫描效果,如下图所示;

本文作者: 王瑞
本文链接: https://www.lyshark.com/post/e9090338.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
16.1 Socket 端口扫描技术的更多相关文章
- Hacker(16)----防范端口扫描与嗅探
端口扫描与嗅探都是黑客常用的招数,其目的是定位目标计算机和窃取隐私信息.为确保自己计算机的安全,用户需要掌握防范嗅探与端口扫描的常见措施,保障个人隐私信息安全. 一.掌握防范端口扫描的常用措施 防范端 ...
- 一、TCP扫描技术
一.TCP扫描技术 常用的端口扫描技术有很多种,如 TCP connect() 扫描 .TCP SYN 扫描.TCP FIN 扫描 等,网络上也有很多文章专门介绍,比如 :http://www.ant ...
- 小白日记10:kali渗透测试之端口扫描-UDP、TCP、僵尸扫描、隐蔽扫描
端口扫描 二三四层发现的目的只是为了准确发现所有活着主机IP,确定攻击面,端口扫描即发现攻击点,发现开放端口.端口对应网络服务及应用端程序,服务端程序的漏洞通过端口攻入.[所有的扫描结果,都不要完全相 ...
- Kali linux 2016.2(Rolling)中metasploit的端口扫描
目前常见的端口扫描技术一般有如下几类: TCP Connect.TCP SYN.TCP ACK.TCP FIN. Metasploit中的端口扫描器 Metasploit的辅助模块中提供了几款实用的 ...
- 端口复用技术简单了解;重用端口;socket复用端口
端口复用相关点 多个应用复用端口,只有最后一个绑定的socket可以接受数据,所有socket都可以发送数据 使用端口复用技术时,所有的socket都开启端口复用,才可以实现端口复用 黑客技术,使用标 ...
- 端口扫描程序nmap使用手册
其实还是建议看英文的man,对以后学习其他东西很有帮助的:) 摘要 nmap是一个网络探测和安全扫描程序,系统管理者和个人可以使用这个软件扫描大型的网络,获取那台主机正在运行以及提供什么服务等 ...
- ★Kali信息收集★8.Nmap :端口扫描
★Kali信息收集~ 0.Httrack 网站复制机 http://www.cnblogs.com/dunitian/p/5061954.html ★Kali信息收集~ 1.Google Hackin ...
- Kail Linux渗透测试教程之ARP侦查Netdiscover端口扫描Zenmap与黑暗搜索引擎Shodan
Kail Linux渗透测试教程之ARP侦查Netdiscover端口扫描Zenmap与黑暗搜索引擎Shodan ARP侦查工具——Netdiscover Netdiscover是一个主动/被动的AR ...
- 用Python实现一个端口扫描,只需简单几步就好
一.常见端口扫描的原理 0.秘密扫描 秘密扫描是一种不被审计工具所检测的扫描技术. 它通常用于在通过普通的防火墙或路由器的筛选(filtering)时隐藏自己. 秘密扫描能躲避IDS.防火墙.包过滤器 ...
- Kali linux 2016.2(Rolling)中的Nmap的端口扫描功能
不多说,直接上干货! 如下,是使用Nmap对主机202.193.58.13进行一次端口扫描的结果,其中使用 root@kali:~# nmap -sS -Pn 202.193.58.13 Starti ...
随机推荐
- selenium元素定位---ElementNotInteractableException(元素不可交互异常)解决方法
方法一: 增加强制等待时间 方法二: 使用js点击 element = self.browser.find_element(By.XPATH, "//td[@class='el-table_ ...
- 使用C#编写.NET分析器(完结)
译者注 这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断).IDE.诊断 ...
- 利用InnoStep在VS编译时自动构建安装包
摘要 很多同学在C/S开发领域或多或少都可能会遇到需要制作安装包的场景,打包的工具也是五花八门,例如有NSIS.InstallShield.Wix Toolset.ClickOnce等等,这里以Inn ...
- tensorflow.js 多分类,机器学习区分企鹅种类
前言: 在规则编码中,我们常常会遇到需要通过多种区间判断某种物品分类.比如二手物品的定价,尽管不是新品没有 SKU 但是基本的参数是少不了.想通过成色来区分某种物品,其实主要是确定一些参数.然后根据参 ...
- 定义一个类,在实例化的时候,抛出NameError异常
代码1:class cla: def __init__(self): #raise NameError # 抛出异常 print(r) cla() 运行截图:
- C#中多线程的用法
1.在C#中使用多线程可以使用Thread 代码例子: public class ThreadExample { public static void ThreadProc() { for (int ...
- [kubernetes]二进制部署k8s集群-基于containerd
0. 前言 k8s从1.24版本开始不再直接支持docker,但可以自行调整相关配置,实现1.24版本后的k8s还能调用docker.其实docker自身也是调用containerd,与其k8s通过d ...
- 你的开发套件已到货「GitHub 热点速览」
这周的 GitHub 热点榜,撇开上周的介绍过的几个项目,剩下就两字:套件.像是搜罗了大量黑客工具的 hackingtool,还有打算一统米哈游游戏客户端的 Starward,以及好用的 CV 库 s ...
- shell编程之存储读写测试实战脚本
Shell编程是一种在命令行环境中编写程序的技术,常用于Linux和Unix系统.它主要使用Shell脚本语言来编写程序.Shell编程常用于系统管理.自动化任务.批处理等领域. 常用的Shell脚本 ...
- Selenium 学习笔记
Selenium 学习笔记 Selenium 框架是时下在 Web 领域中被使用得最为广泛的自动化测试工具集之一,它能帮助程序员们面向指定的 Web 前端应用快速地开发出自动化测试用例,且能实现跨各种 ...