KMP演算法是一種高效的字元串匹配演算法,它的核心思想是利用已經匹配成功的子串首碼的信息,避免重覆匹配,從而達到提高匹配效率的目的。KMP演算法的核心是構建模式串的首碼數組Next,Next數組的意義是:當模式串中的某個字元與主串中的某個字元失配時,Next數組記錄了模式串中應該回退到哪個位置,以便繼續匹... ...
KMP演算法是一種高效的字元串匹配演算法,它的核心思想是利用已經匹配成功的子串首碼的信息,避免重覆匹配,從而達到提高匹配效率的目的。KMP演算法的核心是構建模式串的首碼數組Next,Next數組的意義是:當模式串中的某個字元與主串中的某個字元失配時,Next數組記錄了模式串中應該回退到哪個位置,以便繼續匹配。Next數組的計算方法是找出模式串每一個首碼中最長的相等首碼和尾碼,並記錄下來它們的長度,作為Next數組中的對應值。
在字元串匹配時,KMP演算法從主串和模式串的開頭開始逐個字元比較,若發現匹配失敗,則根據Next數組中的值進行回退,從失配位置的下一位重新開始比較。這樣回退的次數比暴力匹配方式要少得多,因此匹配效率得到了大幅提升。
6.1.1 遍歷輸出進程記憶體
首先需要實現取進程PID的功能,當用戶傳入一個進程名稱時則輸出該進程的PID號,通過封裝GetPidByName
函數,該函數用於根據指定的進程名稱,獲取該進程的進程PID,以便於後續針對進程進行操作。函數參數name
為指定的進程名稱字元串。該函數通過調用CreateToolhelp32Snapshot
函數創建一個系統快照,返回系統中所有進程的快照句柄。然後使用該快照句柄,通過進程快照函數Process32First
和Process32Next
函數逐個對比進程的名稱,找到進程名稱匹配的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)
,其中m
和n
分別表示主串和模式串的長度。
#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 許可協議。轉載請註明出處!
文章出處:https://www.cnblogs.com/LyShark/p/17718405.html
本博客所有文章除特別聲明外,均採用 BY-NC-SA 許可協議。轉載請註明出處!