6.1 KMP算法搜索機器碼

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 許可協議。轉載請註明出處!

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