一、前言

本文將介紹特徵碼掃描與啓發式掃描。特徵碼和啓發掃描其實大家都在殺毒軟件裏面聽過,網上也有很多關於特徵碼和啓發掃描實現的輪子,但無一例的是,這些基本都是對新人不太友好的,或者功能太”重的”(有些甚至用了神經網絡….新人入門一臉懵逼)。本文會盡量以簡單易懂的形式介紹.希望能幫助後人設計輪子時候有個參考。

之前的文章: FPS遊戲反作弊系統設計: API調用回溯

二、特徵碼掃描與啓發掃描技術實現思路

在瞭解特徵碼掃描之前你需要了解所有程序最終都是一堆機器碼組成的彙編指令.拖進od或者ida或者x64dbg你會看到這樣子:

無論如何 ,程序都會這樣子.因此 所爲特徵碼掃描 我們只需要 提取其中的一段或者多段機器碼,然後存入數據庫,當掃描時候,從數據庫提取機器碼出來,遍歷這個程序做匹配即可(說起來容易)

對於啓發掃描,我們需要觀察程序行爲.如果一個程序同時設置開機啓動/創建服務/網絡通訊/寫入文件這些行爲,說明很有可能是病毒,但是也可能是正常軟件(所爲存在誤報就是這樣子),那麼怎麼監視程序行爲呢?大部分殺毒軟件通過自己的虛擬機,沙盒,去讓程序運行,同時剝離掉程序的sleep這種休眠函數.執行個20秒10秒,再把進行的行爲進行個判斷.再或者通過API調用關係進行處理(可以去vxjump看看裏面的深入文章如果對這種技術感興趣的).當然我這邊是反外掛的,沒有做那麼高端的(因爲大部分外掛會有登陸窗口,所以虛擬機類啓發其實對這種外掛很無能爲力),我這邊使用的玩具級別的啓發掃描.

三、實現特徵碼掃描

在此之前,我是取內存特徵碼,因爲內存特徵碼能無視一些加密外殼.但內存特徵碼有壞處是有些內存地址是隨機的.下次開機就不是這個地址了.比如:

mov eax,1212(隨機地址)

 call eax

如果我們取mov eax 12 12 call eax 這個作爲特徵碼,那麼下次啓動,匹配的可能是mov eax 88 88 call eax ,1212會發生改變!所以我們需要通配符號 ?? :

mov eax ?? ?? call eax 這樣,無論1212變成啥 我們都可以正確匹配到

再來過一次流程:

1. 讀內存中的文件
2.找到代碼段,其他區段我們不掃描.
3.隨機取三段位置,得到其特徵碼
4.對於特徵碼的隨機變化部位加通配符
5.匹配特徵碼.

直接上代碼

得到base地址,這裏是因爲計劃連內存模塊也掃描的,但是想了想沒必要,就只掃主程序了

HMODULE Megrez_GetBase(HANDLE hProcess)
{
    HMODULE hModule[100] = { 0 };
    DWORD dwRet = 0;
    BOOL bRet = ::EnumProcessModulesEx(hProcess, (HMODULE*)(hModule), sizeof(hModule), &dwRet, LIST_MODULES_ALL);
    if (FALSE == bRet)
    {
#ifdef DEBUG
        WCHAR _buf[256] = { 0 };
        swprintf_s(_buf, 256, L"特徵碼掃描枚舉模塊失敗");
        OutputDebugStringW(_buf);
#endif
        ::CloseHandle(hProcess);
        return NULL;
    }
    // 獲取第一個模塊加載基址
    HMODULE pProcessImageBase = hModule[0];
    return pProcessImageBase;
}

得到代碼段:

DWORD Megrez_GetCodeSegAttr(HANDLE hProcess, HMODULE hBase, OUT PDWORD pSizeofCode)
{
    PBYTE pSection = (PBYTE)hBase;
    SIZE_T dReadNum;
    DWORD dPE = NULL;
    ReadProcessMemory(hProcess, (PBYTE)hBase + offsetof(_IMAGE_DOS_HEADER, e_lfanew), &dPE, 4, &dReadNum);
    pSection += dPE;
    pSection += 4;
    pSection += sizeof(IMAGE_FILE_HEADER);
    DWORD dBaseOfCode, dSizeOfCode;
    ReadProcessMemory(hProcess, (PBYTE)pSection + offsetof(_IMAGE_OPTIONAL_HEADER, SizeOfCode), &dSizeOfCode, 4, &dReadNum);
    ReadProcessMemory(hProcess, (PBYTE)pSection + offsetof(_IMAGE_OPTIONAL_HEADER, BaseOfCode), &dBaseOfCode, 4, &dReadNum);
    *pSizeofCode = dSizeOfCode;
    return dBaseOfCode;
}

得到隨機三段特徵碼:

void Megrez_GetSigCode(HANDLE hProcess, PBYTE pTEXTInMemory, DWORD dSizeOfText, OUT std::vector<PBYTE>& vecSigCode)
{
    std::random_device rd;
    std::default_random_engine e(rd());
    SIZE_T Temp;
    DWORD dSigRVA[3] = { 0 };
    dSigRVA[0] = e() % (dSizeOfText - 25);
    dSigRVA[1] = e() % (dSizeOfText - 25);
    dSigRVA[2] = e() % (dSizeOfText - 25);
    /*for (int i = 0; i < 3; i++)
    {
        printf("%p ", dSigRVA[i]);
    }
    printf("\n");*/
    for (int i = 0; i < 3; i++)
    {
        PBYTE pTemp = (PBYTE)malloc(100);
        PBYTE pTempTemp = pTemp;
        memset(pTemp, 0, 100);
        ReadProcessMemory(hProcess, pTEXTInMemory + dSigRVA[i], pTemp, 25, &Temp);
        std::string Temp;
        for (int j = 0; j < 20; j++)
        {
            char subStr[3] = { 0 };
            sprintf(subStr, "%02x", *pTemp);
            Temp += subStr;
            Temp += ' ';
            pTemp++;
        }
        PBYTE SigCode = (PBYTE)malloc(100);
        memset(SigCode, 0, 100);
        memcpy(SigCode, Temp.c_str(), Temp.length() + 1);
        vecSigCode.push_back(SigCode);
        free(pTempTemp);
    }
}

匹配特徵碼:

DWORD Megrez_StringMatching(HANDLE hProcess, std::vector<PBYTE> vec, PBYTE pTEXTInMemory, DWORD dSizeOfText)
{
    std::string A((char*)vec[0]);
    const char* pat1 = A.c_str();
    DWORD firstMatch1 = 0;
    DWORD dCodeEnd1 = 0;
    PBYTE pMemory = (PBYTE)malloc(dSizeOfText);
    memset(pMemory, 0, dSizeOfText);
    SIZE_T dReadSize;
    ReadProcessMemory(hProcess, pTEXTInMemory, pMemory, dSizeOfText, &dReadSize);
    for (PBYTE pCur = pMemory; pCur < pMemory + dSizeOfText; pCur++)
    {
        if (dCodeEnd1 == 0)
        {
            if (!*pat1)//我的字符串結束
            {
                dCodeEnd1 = 1;
            }
            if (dCodeEnd1 == 0)
            {
                if (*(PBYTE)pat1 == '?' || *pCur == getByte(pat1))//  匹配上了
                {
                    if (!firstMatch1)
                        firstMatch1 = 1;
                    if (!pat1[2])
                    {
                        dCodeEnd1 = 1;
                    }
                    if (dCodeEnd1 == 0)
                    {
                        if (*(PWORD)pat1 == '\?\?' || *(PBYTE)pat1 != '\?')
                            pat1 += 3;
                        else
                            pat1 += 2; //one ?
                    }
                }
                else
                {
                    pat1 = A.c_str();                              //
                    firstMatch1 = 0;
                }
            }
        }
      //省略一些特殊xxoo技巧
      //.....
    }
    free(pMemory);
    return firstMatch1 & firstMatch2 & firstMatch3;
}

就這樣.恭喜你實現了一個價值五十萬的殺毒軟件(doge

四、實現啓發掃描

其實,搞殺毒軟件的那一套虛擬機啓發掃描對外掛不實用因爲外掛都有登陸註冊,虛擬機無法判斷登錄後的結果.因此這邊我使用一個玩具級別的啓發掃描那就掃導入表.

來複習一邊導入表: 程序要調用的一些api 會提前寫在程序導入表裏面,掃描導入表意味着,可以判斷程序調用了什麼api,判斷程序調用了掃描api,就知道程序的行爲

所以流程如下:

1. 讀入內存文件
2.解析內存文件導入表裏面的api給這些api分類,高危,中危,低危.
3.掃描導入表,高危類api(比如讀寫進程,dll注入)加30分,中危20,低危10分
4.當分數大於某個特定的值,可以說這個東西疑似外掛.上傳給雲端.

直接上代碼:

讀入內存文件:

HMODULE CHeuristicScan::Megrez_GetBase(HANDLE hProcess)
{
    HMODULE hModule[100] = { 0 };
    DWORD dwRet = 0;
    BOOL bRet = ::EnumProcessModulesEx(hProcess, (HMODULE*)(hModule), sizeof(hModule), &dwRet, LIST_MODULES_ALL);
    if (FALSE == bRet)
    {
        ::CloseHandle(hProcess);
        return NULL;
    }
    // 獲取第一個模塊加載基址
    HMODULE pProcessImageBase = hModule[0];
    return pProcessImageBase;
}

解析導入表,給api分級:

PBYTE CHeuristicScan::Megrez_GetScetionBaseAndSize(DWORD RVA, PDWORD pSize)
{
    SIZE_T sReadNum;
    for (int i = 0; i < this->image_file_header.NumberOfSections; i++)
    {
        DWORD dVirtualSize, dVirtualAddress;
        ReadProcessMemory(this->hProcess, this->pFirstSectionTable + offsetof(IMAGE_SECTION_HEADER, Misc.VirtualSize ) + sizeof(IMAGE_SECTION_HEADER) * i, &dVirtualSize, 4, &sReadNum);
        ReadProcessMemory(this->hProcess, this->pFirstSectionTable + offsetof(IMAGE_SECTION_HEADER, VirtualAddress) + sizeof(IMAGE_SECTION_HEADER) * i, &dVirtualAddress, 4, &sReadNum);
        if (RVA >= dVirtualAddress && RVA < dVirtualAddress + dVirtualSize)
        {
            *pSize = dVirtualSize;
            return this->pImageBase + dVirtualAddress;
        }
        
    }
    return NULL;
}

DWORD CHeuristicScan::Scan()
{   
    DWORD dScore = 0;
    SIZE_T dReadNum;
    DWORD dCodeSecSize;
    PBYTE pSectionAddr = this->Megrez_GetScetionBaseAndSize(this->image_data_directory.VirtualAddress, &dCodeSecSize);
    
    this->pSectionBase = (PBYTE)malloc(dCodeSecSize);
    ReadProcessMemory(this->hProcess, pSectionAddr, this->pSectionBase, dCodeSecSize, &dReadNum);
    PBYTE pSectionBaseTemp = this->pSectionBase;
    DWORD dOffset = Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, this->image_data_directory.VirtualAddress);
    unordered_set<string> hashsetProcNameHTemp(hashsetProcNameH);
    unordered_set<string> hashsetProcNameMTemp(hashsetProcNameM);
    unordered_set<string> hashsetProcNameLTemp(hashsetProcNameL);
    //遍歷
    for (int i = 0; *(PDWORD)(this->pSectionBase + dOffset) != 0 ; i++ , dOffset += sizeof(IMAGE_IMPORT_DESCRIPTOR))
    {
        DWORD dINTOff = Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, *(PDWORD)(this->pSectionBase + dOffset));
       // printf("%s\n", this->pSectionBase + Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, *(PDWORD)(this->pSectionBase + dOffset + 12)));
        for (int j = 0; *(PDWORD)(this->pSectionBase + dINTOff) != 0; j++ , this->isX64 ? dINTOff += 8: dINTOff += 4)
        {
            if (this->isX64)
            {
                if ((*(unsigned long long*)(this->pSectionBase + dINTOff) & 0x8000000000000000) == 0x8000000000000000)
                {
                    continue;
                }
                else
                {
                    DWORD dStringOff = Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, *(PDWORD)(this->pSectionBase + dINTOff)) + 2;


                    auto iter = hashsetProcNameHTemp.find((char*)(this->pSectionBase + dStringOff));
                    if (iter != hashsetProcNameHTemp.end())
                    {
                        //高危api
                        printf("30:%s\n", (char*)(this->pSectionBase + dStringOff));
                        dScore += 30;
                        Remove((char*)(this->pSectionBase + dStringOff), hashsetProcNameHTemp);
                    }
                    iter = hashsetProcNameMTemp.find((char*)(this->pSectionBase + dStringOff));
                    if (iter != hashsetProcNameMTemp.end())
                    {
                        //中危api
                        printf("10:%s\n", (char*)(this->pSectionBase + dStringOff));
                        dScore += 10;
                        Remove((char*)(this->pSectionBase + dStringOff), hashsetProcNameMTemp);
                    }
                    iter = hashsetProcNameLTemp.find((char*)(this->pSectionBase + dStringOff));
                    if (iter != hashsetProcNameLTemp.end())
                    {
                        //低危
                        printf("5:%s\n", (char*)(this->pSectionBase + dStringOff));
                        dScore += 5;
                        Remove((char*)(this->pSectionBase + dStringOff), hashsetProcNameLTemp);
                    }
                }


            }
            else
            {
               DWORD dStringOff = Megrez_GetOffsetOfSectoin(this->pImageBase, pSectionAddr, *(PDWORD)(this->pSectionBase + dINTOff)) + 2;
               //..省略,跟上面一樣
            }
            
        }
    }
    if (dScore >= 大於某個值)
    {
        this->bIsGameTool = TRUE;
        //上報
        //....
    }
    return dScore;
}

運行結果:

可以看到,可以成功識別遊戲外掛,dll注入器.

恭喜你,完成了一個價值100w的啓發式掃描引擎(doge

五、擴展與反思

事實上,這玩意也不是什麼新奇事物了.對抗方法有很多,特徵掃描最常見的就是加個虛擬化殼動態虛擬化.這樣子特徵掃描就gg了,但是對於反外掛來說.沒有什麼正常程序會加虛擬化殼,所以遇到非正常程序 比如不是c++ c# vc+6.0 deiph的程序一律上報雲端然後做判斷處理.

對抗啓本文髮式掃描的方法最簡單的是動態調用,但動態調用也可以直接通過內存掃描call函數地址來判斷行爲.

攻防無止境.還得多多學習

*本文作者:huoji120,轉載請註明來自FreeBuf.COM

相關文章