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);
printf("循环次数: %d 端口: %d 状态: %d \n", x + 1, port_list, ret);
}
system("pause");
return 0;
}上述代码片段则是一个简单的端口探测案例,当运行后程序会调用connect函数向目标主机发送一个SYN标志位的TCP数据包,探测目标端口是否开放。如果目标主机响应带有SYN和ACK标志位的TCP数据包,则表示连接请求成功并请求确认,操作系统在自动发送带ACK标志位的TCP数据包进行确认,建立TCP连接;
如果目标主机没有响应或者响应带有RST标志位的TCP数据包,则表示连接请求失败,目标端口为未开放状态。通过此方式,程序可以快速检测多个端口是否开放,该程序运行后输出效果如下图所示;
https://img2023.cnblogs.com/blog/1379525/202305/1379525-20230505202941423-848808132.png
上述代码虽然可以实现端口扫描,但是读者应该会发现此方法扫描很慢,这是因为扫描器每次只能链接一个主机上的端口只有当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
页:
[1]