6.1 KMP演算法搜尋機器碼

lyshark發表於2023-09-20

KMP演算法是一種高效的字串匹配演算法,它的核心思想是利用已經匹配成功的子串字首的資訊,避免重複匹配,從而達到提高匹配效率的目的。KMP演算法的核心是構建模式串的字首陣列Next,Next陣列的意義是:當模式串中的某個字元與主串中的某個字元失配時,Next陣列記錄了模式串中應該回退到哪個位置,以便繼續匹配。Next陣列的計算方法是找出模式串每一個字首中最長的相等字首和字尾,並記錄下來它們的長度,作為Next陣列中的對應值。

在字串匹配時,KMP演算法從主串和模式串的開頭開始逐個字元比較,若發現匹配失敗,則根據Next陣列中的值進行回退,從失配位置的下一位重新開始比較。這樣回退的次數比暴力匹配方式要少得多,因此匹配效率得到了大幅提升。

6.1.1 遍歷輸出程式記憶體

首先需要實現取程式PID的功能,當使用者傳入一個程式名稱時則輸出該程式的PID號,透過封裝GetPidByName函式,該函式用於根據指定的程式名稱,獲取該程式的程式PID,以便於後續針對程式進行操作。函式引數name為指定的程式名稱字串。該函式透過呼叫CreateToolhelp32Snapshot函式建立一個系統快照,返回系統中所有程式的快照控制程式碼。然後使用該快照控制程式碼,透過程式快照函式Process32FirstProcess32Next函式逐個對比程式的名稱,找到程式名稱匹配的PID,返回該PID。若無法找到匹配的程式名稱,則返回0。讀者需要注意,當使用程式遍歷功能時通常需要引入<tlhelp32.h>庫作為支援;

// 根據程式名得到程式PID
DWORD GetPidByName(const char* name)
{
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) };
    DWORD pid = 0;

    if (Process32First(snapshot, &pe32))
    {
        do
        {
            if (_stricmp(pe32.szExeFile, name) == 0)
            {
                pid = pe32.th32ProcessID;
                break;
            }
        } while (Process32Next(snapshot, &pe32));
    }
    CloseHandle(snapshot);
    return pid;
}

在開始使用KMP列舉特徵碼之前我們需要實現簡單的記憶體讀寫功能,透過封裝一個MemoryTraversal函式,該函式接收三個引數分別是,程式PID,程式開始記憶體地址,以及程式結束記憶體地址,該函式輸出當前程式記憶體機器碼,每次讀入4096位元組,然後每16個字元換一次行,遍歷記憶體0x00401000 - 0x7FFFFFFF這篇記憶體區域,這段程式碼實現如下所示;

// 遍歷並輸出程式記憶體
VOID MemoryTraversal(DWORD PID, const DWORD beginAddr, const DWORD endAddr)
{
    const DWORD pageSize = 4096;

    // 開啟並獲取程式控制程式碼
    HANDLE process = ::OpenProcess(PROCESS_ALL_ACCESS, false, PID);

    BOOL _break = FALSE;
    BYTE page[pageSize];
    DWORD tmpAddr = beginAddr;

    // 迴圈列舉程式
    while (tmpAddr <= endAddr)
    {
        // 每次讀入記憶體
        ReadProcessMemory(process, (LPCVOID)tmpAddr, &page, pageSize, 0);

        // 依次迴圈每一個位元組
        for (int x = 0; x < 4096; x++)
        {
            // 每16個字元換一行
            if (x % 15 != 0)
            {
                DWORD ch = page[x];

                if (ch >= 0 && ch <= 15)
                {
                    printf("0%x ", ch);
                }
                else
                {
                    printf("%x ", ch);
                }
            }
            else
            {
                printf(" | %x \n", tmpAddr+x);
            }
        }
        tmpAddr += pageSize;
    }
}

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

    // 輸出記憶體遍歷0x401000-0x7FFFFFFF
    MemoryTraversal(Pid, 0x401000, 0x7FFFFFFF);

    system("pause");
    return 0;
}

讀者可自行編譯這段程式碼片段,並執行特定程式,當程式執行後即可輸出PlantsVsZombies.exe程式內的機器碼,並以16個字元為一個單位進行輸出,其效果圖如下所示;

6.1.2 使用KMP搜尋特徵碼

為了能讓讀者更好的理解KMP特徵碼搜尋的實現原理,這裡筆者依然在MemoryTraversal函式基礎之上進行一定的改進在本次改進中,我們增加了memcmp函式,透過使用該函式我們可以很容易的實現對特定記憶體區域的相同比較,讀者在呼叫ScanMemorySignatureCode函式時需要傳入,開始地址,結束地址,特徵碼,以及特徵碼長度,當找到特定記憶體後則返回該記憶體的所在位置。

// 記憶體特徵碼搜尋
ULONG ScanMemorySignatureCode(DWORD Pid, DWORD beginAddr, DWORD endAddr, unsigned char *ShellCode, DWORD ShellCodeLen)
{
    unsigned char *read = new unsigned char[ShellCodeLen];

    // 開啟程式
    HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, Pid);

    // 開始搜尋記憶體特徵
    for (int x = 0; x < endAddr; x++)
    {
        DWORD addr = beginAddr + x;

        // 每次讀入ShellCodeLen位元組特徵
        ReadProcessMemory(process, (LPVOID)addr, read, ShellCodeLen, 0);
        int a = memcmp(read, ShellCode, ShellCodeLen);

        if (a == 0)
        {
            printf("%x :", addr);
            for (int y = 0; y < ShellCodeLen; y++)
            {
                printf("%02x ", read[y]);
            }
            printf(" \n");
            return addr;
        }
    }
    return 0;
}

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

    // 開始搜尋特徵碼
    unsigned char ScanOpCode[3] = { 0x56, 0x57, 0x33 };

    // 依次傳入開始地址,結束地址,特徵碼,以及特徵碼長度
    ULONG Address = ScanMemorySignatureCode(Pid, 0x401000, 0x7FFFFFFF, ScanOpCode, 3);

    printf("[*] 找到記憶體地址 = 0x%x \n", Address);

    system("pause");
    return 0;
}

上述程式執行後,將列舉當前程式0x401000-0x7FFFFFFF區域中特徵碼為0x56, 0x57, 0x33的記憶體地址,列舉到以後則輸出該記憶體地址的位置,輸出效果圖如下圖所示;

有了上面的模板我們只需要在此基礎之上增加KMP列舉方法即可實現,如下程式碼則是替換具有KMP功能的搜尋模式,在程式碼中可看出我們僅僅只是將ScanMemorySignatureCode函式內部的memcmp函式替換為了KMPSearchString函式,其他位置並沒有任何變化,此處主要增加的函式有GetNextval以及KMPSearchString,這兩個函式的核心思想是利用KMP演算法,在主字串中尋找子字串時,遇到匹配失敗的字元時,能夠跳過一些已經比較過的字元,重複利用部分匹配的結果,提高字串匹配的效率。將子串的每個字元失配時應該跳轉的位置透過GetNextval函式計算得出,然後在KMPSearchString函式中透過這個陣列進行跳轉和匹配。該演算法的時間複雜度為O(m+n),其中mn分別表示主串和模式串的長度。

#include <iostream>
#include <windows.h>
#include <tlhelp32.h>

using namespace std;

// 根據程式名得到程式PID
DWORD GetPidByName(const char* name)
{
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32 pe32 = { sizeof(PROCESSENTRY32) };
    DWORD pid = 0;

    if (Process32First(snapshot, &pe32))
    {
        do
        {
            if (_stricmp(pe32.szExeFile, name) == 0)
            {
                pid = pe32.th32ProcessID;
                break;
            }
        } while (Process32Next(snapshot, &pe32));
    }
    CloseHandle(snapshot);
    return pid;
}

/*
* P 為模式串,下標從 0 開始。
* nextval 陣列是模式串 SubString 中每個字元失配時應該回溯的位置。
*/
void GetNextval(string SubString, int nextval[])
{
    int SubStringLen = SubString.size(); // 計算模式串的長度
    int i = 0;                           // 子串的指標
    int j = -1;                          // 字首的指標
    nextval[0] = -1;                     // 初始化 nextval 陣列,將第一個值設為 -1

    while (i < SubStringLen - 1)
    {
        if (j == -1 || SubString[i] == SubString[j]) // 如果子串和字首相等,或 j==-1
        {
            i++; j++;                                // 子串指標和字首指標分別加一
            if (SubString[i] != SubString[j])        // 如果下一個字元不相等
            {
                nextval[i] = j;                      // 將字首指標 j 的值賦給 nextval 陣列中的當前位置 i
            }
            else                                     // 如果下一個字元相等
            {
                nextval[i] = nextval[j];             // 已經有 nextval[j],所以將它賦給 nextval[i]
            }
        }
        else                                        // 如果子串和字首不相等
        {
            j = nextval[j];                        // 更新字首指標 j 的值,指向 nextval[j]
        }
    }
}

/* 在 MainString 中找到 SubString 第一次出現的位置 下標從0開始*/
int KMPSearchString(string MainString, string SubString, int next[])
{
    GetNextval(SubString, next);

    int MainStringIndex = 0;                 // 儲存主字串下標
    int SubStringIndex = 0;                  // 儲存子字串下標
    int MainStringLen = MainString.size();   // 主字串大小
    int SubStringLen = SubString.size();     // 子字串大小

    // 迴圈遍歷字串,因為末尾 '\0' 的存在,所以不會越界
    while (MainStringIndex < MainStringLen && SubStringIndex < SubStringLen)
    {
        // MainString 的第一個字元不匹配或 MainString[] == SubString[]
        if (SubStringIndex == -1 || MainString[MainStringIndex] == SubString[SubStringIndex])
        {
            MainStringIndex++; SubStringIndex++;
        }
        else   // 當字串匹配失敗則跳轉
        {
            SubStringIndex = next[SubStringIndex];
        }
    }
    // 最後匹配成功直接返回位置
    if (SubStringIndex == SubStringLen)
    {
        return MainStringIndex - SubStringIndex;
    }
    return -1;
}

// 記憶體特徵碼搜尋
ULONG ScanMemorySignatureCode(DWORD Pid, DWORD beginAddr, DWORD endAddr, char *ShellCode, DWORD ShellCodeLen)
{
    char *read = new char[ShellCodeLen];

    // 開啟程式
    HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, Pid);
    int next[100] = { 0 };

    // 開始搜尋記憶體特徵
    for (int x = 0; x < endAddr; x++)
    {
        DWORD addr = beginAddr + x;

        // 每次讀入ShellCodeLen位元組特徵
        ReadProcessMemory(process, (LPVOID)addr, read, ShellCodeLen, 0);

        // 在Str字串中找Search子串,找到後返回位置
        int ret = KMPSearchString(read, ShellCode, next);

        if (ret != -1)
        {
            return addr;
        }
    }
    return 0;
}

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

    // 開始搜尋特徵碼
    char ScanOpCode[3] = { 0x56, 0x57, 0x33 };

    // 依次傳入開始地址,結束地址,特徵碼,以及特徵碼長度
    ULONG Address = ScanMemorySignatureCode(Pid, 0x401000, 0x7FFFFFFF, ScanOpCode, 3);

    printf("[*] 找到記憶體地址 = 0x%x \n", Address);

    system("pause");
    return 0;
}

編譯並執行上述程式碼片段,讀者應該能看出與暴力列舉並無任何區別,其輸出效果圖如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/892aee6f.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!

相關文章