Gh0st通信协议解析(7)

分类: windows程序ring3 |
接下来就是被控端向主控端进行连接的地方,主要是调用了CClientSocket::Connect这个函数:
http://fmn.rrimg.com/fmn060/20120605/0325/b_large_xjLH_2fb70000115a1263.jpg
连接之前,首先要执行CClientSocket::Disconnect这个函数,目的就是清除一下socket资源。这里面有个险中取胜的一个地方,在Disconnect函数中有一个对m_hEvent进行置信的操作,要知道在连接主控端的这个大循环中是不断的循环测试这个值的,如果这个值受信了则就退出这个连接循环,那客户端岂不是就掉线了?而问题的解决方案就在这里,在Connect这个函数中调用了Disconnect之后紧接着调用了ResetEvent这个函数,马上将m_hEvent设置为未受信的状态。
http://fmn.rrimg.com/fmn059/20120605/0325/b_large_76qW_11f5000037e61263.jpg
因为我们忽略了代理服务器,因此在这里所有对代理服务器的操作我们都可以忽略到,除了这些我们会发现,上面一段代码就是创建了一个用于连接的套接字,然后连接主控端。
http://fmn.rrimg.com/fmn057/20120605/0325/b_large_njDv_02c500002cc11263.jpg
连接到主控端之后,设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:
设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDING的IO操作,则马上会失败返回。
上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定主控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。
接下来就创建了一个无限循环的工作线程,这个工作线程的主要任务就是监听来自客户端的命令请求,关于这个线程的分析,我们稍后再表。
让我们话分两路,去看看当有被控端主动去连接到主控端的时候,主控端会有怎样的操作。
http://fmn.rrimg.com/fmn056/20120605/0325/b_large_vLzu_4fba000087891263.jpg
http://fmn.rrimg.com/fmn064/20120605/0325/b_large_1b9P_0ad9000039831263.jpg
当有被控端连接到主控端的时候,在监听套接字上会有网络事件发生,因此阻塞在m_hEvent这个事件对象上的线程会被唤醒,接下来会详细判断出发生在监听套接字上的这个网络事件具体是否为FD_ACCEPT,因为我们在监听套接字上,只对这个网络事件感兴趣。如果确实为FD_ACCEPT这个网络事件的发生的话,那么就要调用CIOCPSserver::OnAccept这个函数,对到来的连接进行处理。
我们先来看看这一段接收所用到得API函数的功能进行一个简单的说明。
1:reinterpret_cast:CIOCPServer* pThis = reinterpret_cast(lParam);
The
这个操作符允许你将任意类型的指针转化成其它类型的指针,并且允许你将整形转换成任意类型的指针,反之亦然。错误的使用这个操作符可以轻易的使你的程序处于不安全的状态。2:WaitForSingleObject:
DWORD
WaitForSingleObject(
);
The DWORD
WSAWaitForMultipleEvents
);
The 当所有指定的对象受信的时候或者只有一个对象受信的时候,又或者等待的时间超时的时候,这个函数才会返回。
4
:WSAEnumNetworkEvents
int
WSAEnumNetworkEvents (
);
The
Windows SocketsWSAEnumNetworkEventsfunction discovers
occurrences of network events for the indicated socket, clear
internal network event records, and reset event objects
(optional).这个函数会识别指定的socket上发生的网络事件,并且会清除内部的网络事件记录,还会重置事件对象。
http://fmn.rrimg.com/fmn059/20120605/0330/b_large_QD6F_724600010f911263.jpg
http://fmn.rrimg.com/fmn061/20120605/0330/b_large_1SoN_398b00000f671263.jpg
对以上代码进行说明:
首先,创建了一个与被控端进行通信的clientSocket,这个clientSocket是主控端与被控端进行信息交互的传输媒介。接下来是为了与被控端进行信息交互而创建了一个保存客户端数据的变量ClientContext*
pContext =
AllocateContext();我们看以下这个函数的实现:
http://fmn.rrimg.com/fmn057/20120605/0330/b_large_p2l2_7c080000abff1263.jpg
首先是用临界区CLock
cs(CIOCPServer::m_cs,
"AllocateContext")锁住了这块代码,以使得这个线程独占的访问该代码。
接着判断m_listFreePool这个链表里面是否还有元素存在,注意这个连表里的每一个元素都是一个指针,指向一个ClientContext结构。有的话直接从这个连表里摘取一个下来,否则的话需要从新申请一个ClientContext结构。我们对这个结构的成员变量进行一番说明。
http://fmn.rrimg.com/fmn065/20120605/0340/b_large_69Xz_398b00000f711263.jpg
至此,这个CBuffer这个类的成员变量以及成员函数我们就看到这里。我们继续回到这个类——ClientContext。
CBuffer
将要发送的数据
CBuffer
接收到的压缩的数据
CBuffer
解压后的数据
CBuffer
上次发送的数据包,接收失败时重发时用
int
第一个int是类型,第二个是CDialog的地址
int
记录传输的速度
//
Input Elements for
Winsock
WSABUF
BYTE
以上两个值是给非阻塞函数WSARecv函数作为参数用的,具体的用法,看下面:
*******************************************************************************
pContext->m_wsaInBuffer.buf
=
(char*)pContext->m_byInBuffer;
pContext->m_wsaInBuffer.len
=
sizeof(pContext->m_byInBuffer);
UINT
nRetVal =
WSARecv(pContext->m_Socket,
首先,将m_wsaInBuffer
这个变量的两个成员变量赋值为ClientContext里的成员变量m_byInBuffer。然后再WSARecv这个函数里会用到m_wsaInBuffer。在这里我们要第一次简单的初探主控端与被控端的交互过程:我打算从两个不同角度去简要的叙述一下主控端与被控端之间的交互过程。
第一:数据的发送过程。
1
:在CIOCPServer::Send函数中准备好待发送的数据。就是将需要发送的数据先存储在ClientContext::
m_WriteBuffer
这个缓冲区中,主控端主动向被控端发送的数据基本上都是一些命令数据,因此,没有将命令数据进行压缩传输。但是,在传输的过程中可能会引起数据丢失,需要备份将要发送的数据,因此,在ClientContext::
m_ResendWriteBuffer
中备份了这些命令数据。
2
:准备好将要发送的数据之后,使用
OVERLAPPEDPLUS
* pOverlap = new
OVERLAPPEDPLUS(IOWrite);
PostQueuedCompletionStat
向完成端口投递一个发送数据的请求,这个时候的数据并没有送出到网卡的数据缓冲区,当然也就没有被发送出去,这个时候的数据甚至都可能没有发送至TCP/IP协议栈的缓冲区中。
3
:守候在完成端口上的工作线程会因为这里投递了一个发送数据的请求而被唤醒,这个时候BOOL
bIORet =
GetQueuedCompletionStatu
等待在此函数上的线程会被唤醒,这个函数会返回,并且在lpClientContext,会返回由PostQueuedCompletionStat
PostQueuedCompletionStat
GetQueuedCompletionStatu
这两个函数的参数是一一对应的。
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) \
#define
END_IO_MSG_MAP() \
return
bRet; \
}
接下来,我们需要看看使用这个宏的地方的定义:
BEGIN_IO_MSG_MAP()
END_IO_MSG_MAP()
对这组宏调用进行宏展开,展开之后的情形为:
public:
Bool
ProcessIOMessage(IOType clientIO, ClientContext* pContext, DWORD
dwSize = 0)\
{
bool
bRet = false;
if
(IORead == clientIO) \
if
(IOWrite == clientIO) \
if
(IOInitialize == clientIO)
\
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,
将含有待发送数据的缓冲区地址赋给我们使用WSASend函数的参数,然后将数据发送出去,这样就完成了整个数据的发送过程。而且这整个过程也都是由动作驱动的,有数据发送,则主动投递发送请求。
第二:数据的接收过程
首先说明一点,数据的接收的过程是由程序自身驱动的,我们必须自己先调用WSARecv函数,通知完成端口一旦在该套接字上有数据到达即调用为完成端口服务的线程中分发函数进行处理到来的数据。这一整个过程可以描述如下。
1
:当有客户连接到来的时候,即调用
PostRecv(pContext);
OVERLAPPEDPLUS
* pOverlap = new
OVERLAPPEDPLUS(IORead);
ULONG
DWORD
UINT
nRetVal = WSARecv(pContext->m_Socket,
在这个函数中,调用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
这个成员变量就是用来给WSASend作为函数参数来使用的,它的使用方式我们在上面也已经说过,在这里就不再赘述。
HANDLE
这个变量在这里,我先临时定为无意义的一个变量,因为我确实没看到这个变量有被初始化。
//
Message counts... purely for example
purposes
LONG
LONG
以上两个变量记录发送出去,或者接收到得数据包的个数。
BOOL
是不是主socket
这两个变量并没有被启用。
ClientContext*
ClientContext*
接下来,让我们回到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
(!AssociateSocketWithCompl
{
pContext
= NULL;
closesocket(
clientSocket );
}
接下来,我们重点看看CIOCPServer::AssociateSocketWithCompl
http://fmn.rrimg.com/fmn065/20120605/0340/b_large_4pA7_02c500002cd01263.jpg
关于CreateIoCompletionPort这个函数的具体使用方法,在前面的我们有提到,在这里我们再回顾一下:
The
也就是说这个函数既可以将某个打开的“文件”句柄与新创建的或者已经存在的输入输出端口相关联起来,也可以仅仅创建一个完成端口而不与某个“文件”相关联。上图所示即是将主控端与被控端端进行通信的套接字句柄与先前创建的那个完成端口相关联。
我们继续看CIOCPServer::OnAccept()这个函数未解读的部分
http://fmn.rrimg.com/fmn064/20120605/0340/b_large_wdjE_062e00005d291261.jpg
设置了该套接字的一个保活特性,关于这部分的内容我们在Gh0st通信协议分析(1)里有也有讲过,在这里我们再回顾一下这种使用方法:
设置了SIO_KEEPALIVE_VALS后,激活包由TCP STACK来负责。当网络连接断开后,TCP STACK并不主动告诉上层的应用程序,但是当下一次RECV或者SEND操作进行后,马上就会返回错误告诉上层这个连接已经断开了如果检测到断开的时候,在这个连接上有正在PENDING的IO操作,则马上会失败返回。
上面这句代码的含义是:每隔m_nKeepLiveTime的时间,开始向受控端发送激活包,重复发送五次,每次发送的时间间隔是10秒钟,如果在十秒钟之内都没能得到回复,则判定受控端已经掉线。对掉线后的处理,在这里我必须要说说:由于TCP STACK并不主动告诉上层的应用程序,只有当下一次发送数据,或者接收数据的时候才会被发现。
继续分析CIOCPServer::OnAccept()这个函数未解读的部分
http://fmn.rrimg.com/fmn056/20120605/0340/b_large_FOfp_036e0000f0951261.jpg
保存这个会话数据结构到m_listContexts这个变量中,接下来向完成端口投递一个名称为IOInitialize的请求,请求完成端口能处理这个请求,而完成端口对这个请求的处理,我们根据前面的分析,应该由CIOCPServer::OnClientInitializing:这个函数来处理,我们看看这个函数的实现方式:
http://fmn.rrimg.com/fmn061/20120605/0340/b_large_dUHi_7ac3000073dd1261.jpg
这个函数就是向完成端口投递一个接收数据的请求,以待后续有数据传输过来的时候,会有完成端口的工作线程负责调用相应的函数去处理。
至此,主控端就顺利的完成了一次被上线主机的一个接收过程,接下来的过程,是受控端主动主控端发送上线包的这么一个过程,以及,被控端对各种控制命令的一个响应的过程,关于这部分的内容,我们在Gh0st通信协议解析(8)里在讨论。