6.2 Sunday搜索內存特徵

Sunday 算法是一種字符串搜索算法,由Daniel M.Sunday於1990年開發,該算法用於在較長的字符串中查找子字符串的位置。算法通過將要搜索的模式的字符與要搜索的字符串的字符進行比較,從模式的最左側位置開始。如果發現不匹配,則算法將模式向右滑動一定數量的位置。這個數字是由當前文本中當前模式位置的最右側字符確定的。相比於暴力方法,該算法被認爲更加高效。

6.2.1 字符串與特徵碼轉換

GetSignatureCodeArray函數,該函數用於將給定的十六進制串表示的字節碼特徵碼轉換爲十進制數,存儲在一個整型數組中,以便後續進行搜索。同時,特徵碼中的未知標記符號?會被用256 替代,方便後續搜索對特徵碼的匹配。

其中,參數SignatureCode爲一串十六進制字符串,描述要搜索的字節碼特徵碼,參數BytesetSequence爲一個整型數組,用於存儲將十六進制數轉爲十進制後的結果。該函數首先計算給定的十六進制串中包含的字節碼個數,因爲每個字節對應兩個十六進制字符,再加上每兩個字符間的空格,故需要將十六進制字符串長度除以三,再加上一。

接下來,函數逐個字符讀入特徵碼串中的每一個十六進制數,如果是有效的十六進制數,則轉化爲十進制數存入BytesetSequence數組中。如果遇到未知的標記符號?,則在BytesetSequence數組中用256表示該位置的值。最後,返回特徵碼數組中字節碼的個數。

// 定義全局變量
#define BLOCKMAXSIZE 409600  // 每次讀取內存的最大大小
BYTE* MemoryData;            // 每次將讀取的內存讀入這裏
SHORT Next[260];             // 搜索下一個內存區域

// 將傳入的SignatureCode特徵碼字符串轉換爲BytesetSequence特徵碼字節集
WORD GetSignatureCodeArray(char* SignatureCode, WORD* BytesetSequence)
{
    int len = 0;

    // 用於存儲特徵碼數組長度
    WORD SignatureCodeLength = strlen(SignatureCode) / 3 + 1;

    // 將十六進制特徵碼轉爲十進制
    // 依次遍歷SignatureCode中的每一個十六進制數
    for (int i = 0; i < strlen(SignatureCode);)
    {
        char num[2];

        // 分別取出第一個和第二個十六進制字符
        num[0] = SignatureCode[i++];
        num[1] = SignatureCode[i++];
        i++;

        // 如果兩個字符都是有效的十六進制數,則將它們轉換成十進制並存儲到 BytesetSequence 中
        if (num[0] != '?' && num[1] != '?')
        {
            int sum = 0;
            WORD a[2];

            // 分別將兩個十六進制字符轉換成十進制數
            for (int i = 0; i < 2; i++)
            {
                // 如果是數字
                if (num[i] >= '0' && num[i] <= '9')
                {
                    a[i] = num[i] - '0';
                }
                // 如果是小寫字母
                else if (num[i] >= 'a' && num[i] <= 'z')
                {
                    a[i] = num[i] - 87;
                }
                // 如果是大寫字母
                else if (num[i] >= 'A' && num[i] <= 'Z')
                {
                    a[i] = num[i] - 55;
                }
            }

            // 計算兩個十六進制數轉換後的十進制數,並將其存儲到 BytesetSequence 數組中
            sum = a[0] * 16 + a[1];
            BytesetSequence[len++] = sum;
        }
        else
        {
            BytesetSequence[len++] = 256;
        }
    }
    return SignatureCodeLength;
}

6.2.2 搜索內存區域特徵

SearchMemoryBlock函數,該函數用於在指定進程的某一塊內存中搜索給定的字節碼特徵碼,查找成功則將匹配地址存入結果數組中。其中,參數hProcess爲指向要搜索內存塊所在進程的句柄,SignatureCode爲給定特徵碼的數組指針,SignatureCodeLength爲特徵碼長度,StartAddress爲搜索的起始地址,size爲搜索內存的大小,ResultArray爲存儲搜索結果的數組引用。

通過調用ReadProcessMemory函數讀取進程內存中指定地址和大小的數據,將讀取的數據存入變量MemoryData中,然後對讀取的數據進行匹配,查找特徵碼。若匹配成功,則將特徵碼匹配的起始地址存入結果數組中。在匹配時,採用了KMP算法。如果找到與特徵碼中的字節碼不匹配的字節,就根據Next數組記錄的回溯位置,重新從失配的位置開始匹配,以降低匹配的時間複雜度,提高搜索效率。在代碼中,若特徵碼中存在問號,則匹配位置從問號處開始重新匹配,如果沒有則繼續按照Next數組回溯進行匹配。

// 獲取GetNextArray數組
void GetNextArray(short* next, WORD* SignatureCode, WORD SignatureCodeLength)
{
    // 特徵碼字節集的每個字節的範圍在0-255(0-FF)之間
    // 256用來表示問號,到260是爲了防止越界
    for (int i = 0; i < 260; i++)
    {
        next[i] = -1;
    }
    for (int i = 0; i < SignatureCodeLength; i++)
    {
        next[SignatureCode[i]] = i;
    }
}

// 搜索一塊內存區域中的特徵
void SearchMemoryBlock(HANDLE hProcess, WORD* SignatureCode, WORD SignatureCodeLength, unsigned __int64 StartAddress, unsigned long size, vector<unsigned __int64>& ResultArray)
{
    // 讀取指定進程的內存數據到MemoryData緩衝區中
    if (!ReadProcessMemory(hProcess, (LPCVOID)StartAddress, MemoryData, size, NULL))
    {
        return;
    }

    // 循環遍歷內存數據緩衝區
    for (int i = 0, j, k; i < size;)
    {
        j = i; k = 0;

        // 逐個比對內存數據緩衝區中的字節和特徵碼中的字節
        for (; k < SignatureCodeLength && j < size && (SignatureCode[k] == MemoryData[j] || SignatureCode[k] == 256); k++, j++);

        // 如果特徵碼完全匹配到內存數據緩衝區中的一段數據
        if (k == SignatureCodeLength)
        {
            // 將該段數據的起始地址保存到結果數組中
            ResultArray.push_back(StartAddress + i);
        }

        // 如果已經處理到緩衝區的末尾
        if ((i + SignatureCodeLength) >= size)
        {
            return;
        }

        int num = Next[MemoryData[i + SignatureCodeLength]];

        // 如果特徵碼中有問號,從問號處開始匹配
        if (num == -1)
        {
            // 如果特徵碼有問號,就從問號處開始匹配,如果沒有就 i += -1
            i += (SignatureCodeLength - Next[256]);
        }
        else
        {
            // 否則從匹配失敗的位置開始
            i += (SignatureCodeLength - num);
        }
    }
}

6.2.3 搜索整塊內存區域

SearchMemory函數,該函數用於在指定進程的內存空間中搜索給定特徵碼的內存塊,並把搜索到的內存地址存入結果數組中。函數爲一層循環枚舉給定的內存塊,內部則調用SearchMemoryBlock函數進行內存塊搜索。其中,參數hProcess爲指向要搜索內存塊所在進程的句柄,SignatureCode爲給定特徵碼的字符串指針,StartAddress爲搜索的起始地址,EndAddress爲搜索的結束地址,InitSize爲搜索結果數組初始空間大小,ResultArray爲存儲搜索結果的數組引用。

該函數首先通過調用VirtualQueryEx函數獲取可讀可寫和可讀可寫可執行的內存塊信息,並遍歷每個內存塊,對內存塊進行搜索。之所以不直接搜索整個內存區域,是因爲那樣可以減少非必要的搜索,提高效率。

內存塊的搜索通過調用SearchMemoryBlock函數實現。搜索採用了KMP算法,先通過GetNextArray函數和GetSignatureCodeArray函數將特徵碼轉換爲對應的變量,再對每個內存塊逐個匹配,在匹配過程中若找到與特徵碼中的字節碼不匹配的字節,就根據Next數組記錄的回溯位置從失配的位置開始重新匹配,以降低匹配的時間複雜度。在內存塊搜索過程中,若匹配成功,則將特徵碼匹配的起始地址存入結果數組中,最終函數返回結果數組大小。

// 實現搜索整個程序
int SearchMemory(HANDLE hProcess, char* SignatureCode, unsigned __int64 StartAddress, unsigned __int64 EndAddress, int InitSize, vector<unsigned __int64>& ResultArray)
{
    int i = 0;
    unsigned long BlockSize;
    MEMORY_BASIC_INFORMATION mbi;

    WORD SignatureCodeLength = strlen(SignatureCode) / 3 + 1;
    WORD* SignatureCodeArray = new WORD[SignatureCodeLength];

    // 實現特徵碼字符串與數組轉換
    GetSignatureCodeArray(SignatureCode, SignatureCodeArray);
    GetNextArray(Next, SignatureCodeArray, SignatureCodeLength);

    // 初始化結果數組
    ResultArray.clear();
    ResultArray.reserve(InitSize);

    // 查詢內存屬性並循環
    while (VirtualQueryEx(hProcess, (LPCVOID)StartAddress, &mbi, sizeof(mbi)) != 0)
    {
        // 判斷並獲取具有PAGE_READWRITE讀寫,或者PAGE_EXECUTE_READWRITE讀寫執行權限的內存
        if (mbi.Protect == PAGE_READWRITE || mbi.Protect == PAGE_EXECUTE_READWRITE)
        {
            i = 0;

            // 得到當前塊長度
            BlockSize = mbi.RegionSize;
            
            // 搜索這塊內存
            while (BlockSize >= BLOCKMAXSIZE)
            {
                // 調用內存塊搜索功能依次搜索內存
                SearchMemoryBlock(hProcess, SignatureCodeArray, SignatureCodeLength, StartAddress + (BLOCKMAXSIZE * i), BLOCKMAXSIZE, ResultArray);
                BlockSize -= BLOCKMAXSIZE;
                i++;
            }
            SearchMemoryBlock(hProcess, SignatureCodeArray, SignatureCodeLength, StartAddress + (BLOCKMAXSIZE * i), BlockSize, ResultArray);
        }

        // 開始地址增加下一塊長度繼續搜索
        StartAddress += mbi.RegionSize;
        if (EndAddress != 0 && StartAddress > EndAddress)
        {
            return ResultArray.size();
        }
    }

    // 釋放特徵碼數組並返回搜索計數器
    free(SignatureCodeArray);
    return ResultArray.size();
}

將上述代碼理解後讀者可以自行使用

int main(int argc, char *argv[])
{
    // 通過進程名獲取進程PID號
    DWORD Pid = GetPidByName("PlantsVsZombies.exe");
    printf("[*] 獲取進程PID = %d \n", Pid);

    // 初始化MemoryData大小
    MemoryData = new BYTE[BLOCKMAXSIZE];

    // 存儲搜索返回值
    vector<unsigned __int64> ResultArray;

    // 通過進程ID獲取進程句柄
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, Pid);

    // 開始搜索
    // 搜索特徵碼 FF 25 ?? 從0x0000000到0xFFFFFFF 初始長度爲3 返回值放入ResultArray
    SearchMemory(hProcess, "FF 25 ??", 0x0000000, 0xFFFFFFF, 3, ResultArray);

    // 輸出結果
    for (vector<unsigned __int64>::iterator it = ResultArray.begin(); it != ResultArray.end(); it++)
    {
        printf("0x%08X \n", *it);
    }

    system("pause");
    return 0;
}

編譯並運行上述程序片段,則會枚舉hProcess進程內特徵碼時FF 25 ??的片段,枚舉位置爲0x0000000-0xFFFFFFF枚舉長度爲3個特徵,最終將枚舉結果輸出到ResultArray數組內,輸出效果圖如下所示;

本文作者: 王瑞
本文鏈接: https://www.lyshark.com/post/ae682eb.html
版權聲明: 本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章