当我们分析完游戏逻辑,收集了足够的游戏数据之后就可以动手开发一款专属于自己的辅助工具。而开发辅助的第一步就是先找到游戏进程,锁定游戏进程之后接下来才能在考虑辅助功能的实现是选择改数据、改代码还是CALL函数。锁定目标进程从实现上来讲可以分为三类:1、根据进程名或进程路径锁定目标进程;2、根据窗口名锁定目标进程;3、无脑全系统注入所有进程,在进程内在判断是否为目标进程。
所谓根据进程名锁定目标进程说的直白一些就是先枚举系统进程得到当前系统中运行的所有进程信息,然后对比进程名称或者进程路径来判断是不是目标进程。所以整个过程分为二步:1、枚举系统进程;2、根据进程名称或者进程路径信息判断是否为目标进程。这二个过程都有好几种实现方式,可以任意组合挑选其中一种组合即可达到目的。
在枚举系统进程的实现方式方面最主要有以下几种方式:1、使用Psapi 提供的API EnumProcesses;2、使用TlHelp32 提供的API Process32First&Process32Next;3、使用native API NtQuerySystemInformation。4、使用windows终端服务函数WTSEnumerateProcessesA。其中前2种方式是微软推荐的方式,随着操作系统不断更新这些API接口一般不会改变。而第三种则是native API,针对native API微软则不直接对外提供接口也不保证所有操作系统版本都会提供类似接口。在详细介绍前先针对重点函数使用进行介绍。
基于PSAPI实现进程查找主要是用EnumProcess来获取当前全系统的进程ID数组,然后通过进程ID或者进程路径或者进程名称信息,在通过进程路径或者名称信息与指定的目标进程的相关信息对比从而锁定目标进程。
关键函数介绍:
BOOL WINAPI EnumProcesses(
__out DWORD* pProcessIds,
__in DWORD cb,
__out DWORD* pBytesReturned
);
EnumProcesses用来一次性获取当前正在运行的进程ID数组。参数pProcessIds 是一个DWORD数组指针,指向一个用来存放进程ID的数据。cb用来告知操作系统前面传入的pProcessIds 进程ID数组buffer的内存大小。pBytesReturned用来返回存放进程数组需要的实际buffer大小。如果cb的值不足以存放所有进程ID信息则函数范围FALSE,然后实际需要内存通过pBytesReturned告知调用者。
HANDLE WINAPI OpenProcess(
__in DWORD dwDesiredAccess,
__in BOOL bInheritHandle,
__in DWORD dwProcessId
);
OpenProcess函数根据进程ID获取进程句柄。进程句柄是操作系统分配给进程对象的一个身份标识,是对进程进行操作的唯一方式,比如读写进程内存,获取进程信息等。dwDesiredAccess用来设定希望获取目标进程的哪些权限,这样返回的句柄信息就具有哪些操作权限,如果不清楚需要填写什么权限合适则可以申请所有权限PROCESS_ALL_ACCESS。bInheritHandle用来标识该句柄是否希望被子进程继承,不过不考虑子进程的继承权限则直接赋值为FALSE。dwProcessId则用来告知代打开进程的进程ID。
DWORD WINAPI GetProcessImageFileName(
__in HANDLE hProcess,
__out LPTSTR lpImageFileName,
__in DWORD nSize
);
GetProcessImageFileName函数用来获取hProcess进程的完整路径信息,存放在lpImageFileName指向的buffer中,nSize则是表明lpImageFileName的buffer大小。该函数在vista及以后的操作系统中已提供,XP系统及以前的系统则不支持该函数的调用。如果是需要在xp及以前的操作系统上使用则可用GetModuleFileNameEx函数代替。
DWORD WINAPI GetModuleFileNameEx(
__in HANDLE hProcess,
__in HMODULE hModule,
__out LPTSTR lpFilename,
__in DWORD nSize
);
GetModuleFileNameEx函数是用来获取指hProcess程hModule模块的一个完整路径,获取的路径放在lpFilename中,nSize表明lpFilename的buffer大小。如果hModule为NULL,则函数返回的是进程主模块的完整路径,这时候GetModuleFileNameEx函数和GetProcessImageFileName的作用完全一样了。
DWORD WINAPI GetModuleBaseName(
__in HANDLE hProcess,
__in HMODULE hModule,
__out LPTSTR lpBaseName,
__in DWORD nSize
);
GetModuleBaseName函数用来获取进程的基础名称,也就是短名称。其参数个数和用法基本和GetModuleFileNameEx一样这里不再赘述。
BOOL WINAPI CloseHandle(
__in HANDLE hObject
);
CloseHandle函数用来释放前面分配的各种句柄资源。一般我们使用api之前先查看MSDN里对api的描述。MSDN里一般会包含一下几项:Parameters\Return Value\Remarks\Example Code。以OpenProcess为例,在Remarks项里有这么一句话:When you are finished with the handle, be sure to close it using the CloseHandle function.这句话的意思就是说当不再使用该进程句柄时需要用CloseHandle来关闭该句柄。
相关源码分享:
//函数作用通过3中方式获取进程路径信息
VOID PrintProcessNameAndID(DWORD processID)
{
char szProcessName[1024] = { 0 };
char szOutputText[1024] = { 0 };
// Get a handle to the process.
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, processID);
if (hProcess == NULL)
{
wsprintfA(szOutputText, “open process Id %d error, error id:%d”, processID, GetLastError());
OutputDebugStringA(szOutputText);
return;// open process error
}
// Get the process name.
GetProcessImageFileName(hProcess, szProcessName, 1024);
wsprintfA(szOutputText, “get path by GetProcessImageFileName %s”, szProcessName);
OutputDebugStringA(szOutputText);
GetModuleFileNameEx(hProcess, NULL, szProcessName, 1024);
wsprintfA(szOutputText, “get path by GetModuleFileNameEx %s”, szProcessName);
OutputDebugStringA(szOutputText);
GetModuleBaseName(hProcess, NULL, szProcessName, 1024);
wsprintfA(szOutputText, “get path by GetModuleBaseName %s”, szProcessName);
OutputDebugStringA(szOutputText);
CloseHandle(hProcess);
}
//函数作用 通过EnumProcess方式查找进程为szTargetProcess的进程
DWORD FindProcessByEnumProcess(char *szTargetProcess)
{
DWORD dwTargetProcessId = 0;
DWORD pdwProcesses[1024] = { 0 };
DWORD dwNeeded = 0;
if (!EnumProcesses(pdwProcesses, sizeof(pdwProcesses), &dwNeeded))
return dwTargetProcessId;//call EnumProcesses error
DWORD dwProcessNumber = dwNeeded / sizeof(DWORD);
for (unsigned int i = 0; i < dwProcessNumber; i++)
{
if (pdwProcesses[i] != 0)
{
HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
FALSE, pdwProcesses[i]);
if (hProcess != NULL)
{
char szProcessName[1024] = { 0 };
GetModuleFileNameEx(hProcess, NULL, szProcessName, 1024);
_strlwr_s(szProcessName, sizeof(szProcessName));
if (strstr(szProcessName, szTargetProcess) != NULL)
{
dwTargetProcessId = pdwProcesses[i];
}
CloseHandle(hProcess);
hProcess = NULL;
}
}
}
return dwTargetProcessId;
}
进程快照是在tlhelp32.h文件里声明的一系列api,主要是通过CreateToolhelp32Snapshot创建一个进程快照,然后通过Process32First和Process32Next配合遍历当前系统进程。进程信息保存在一个PROCESSENTRY32结构体中。PROCESSENTRY32结构体定义如下:
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID;
DWORD th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID;
LONG pcPriClassBase;
DWORD dwFlags;
TCHAR szExeFile[MAX_PATH];
DWORD th32MemoryBase;
DWORD th32AccessKey;
} PROCESSENTRY32;
th32ProcessID为进程ID信息,szExeFile为进程路径信息。与PSAPI系列API相比快照方式枚举的进程直接就能获取路径信息所以不需要再次调用其它API获取进程路径信息。需要注意的是在使用完快照句柄之后需要调用CloseToolhelp32Snapshot来关闭快照句柄而不是CloseHandle关闭快照句柄。
关键函数介绍:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
CreateToolhelp32Snapshot函数用来创建一个快照,dwFlags用来指明快照类型。当dwFlags的值等于TH32CS_SNAPPROCESS时就是用来创建一份进程快照,th32ProcessID的值在进程快照中会被忽略。
BOOL WINAPI Process32First(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe
);
BOOL WINAPI Process32Next(
HANDLE hSnapshot,
LPPROCESSENTRY32 lppe
);
Process32First和Process32Next配合使用。hSnapshot传入进程快照句柄,lppe里则返回进程的相关信息。快照里的第一个进程信息用Process32First获取,接下来的进程信息则用Process32Next获取。当快照遍历完成之后Process32Next函数返回FALSE。
相关源码分享:
//函数作用 通过快照方式查找进程为szTargetProcess的进程
DWORD FindProcessBySnapshot(char *szTargetProcess)
{
char szOutputText[1024] = { 0 };
DWORD dwTargetProcessId = 0;
HANDLE hProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (INVALID_HANDLE_VALUE == hProcessSnapshot)
{
wsprintfA(szOutputText, “call CreateToolhelp32Snapshot error, error id:%d”, GetLastError());
OutputDebugStringA(szOutputText);
return dwTargetProcessId;
}
PROCESSENTRY32 stProcessEntry = { 0 };
stProcessEntry.dwSize = sizeof(stProcessEntry);
if (Process32First(hProcessSnapshot, &stProcessEntry) == FALSE)
{
wsprintfA(szOutputText, “call Process32First error, error id:%d”, GetLastError());
OutputDebugStringA(szOutputText);
CloseHandle(hProcessSnapshot);
return dwTargetProcessId;
}
do
{
wsprintfA(szOutputText, “process id:%d %s”, stProcessEntry.th32ProcessID, stProcessEntry.szExeFile);
OutputDebugStringA(szOutputText);
if (_stricmp(stProcessEntry.szExeFile, szTargetProcess) == 0)
{
dwTargetProcessId = stProcessEntry.th32ProcessID;
}
memset(&stProcessEntry, 0, sizeof(stProcessEntry));
stProcessEntry.dwSize = sizeof(stProcessEntry);
} while (Process32Next(hProcessSnapshot, &stProcessEntry));
CloseHandle(hProcessSnapshot);
return dwTargetProcessId;
}
NtQuerySystemInformation(ZwQuerySystemInformation)函数主要功能是获取或者设置系统信息,声明如下:
typedef NTSTATUS (__stdcall *NTQUERYSYSTEMINFORMATION)
(IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL);
从声明中可以看出,参数SystemInformationClass是一个类型信息,可以对50多种系统信息进行查看或者设置。参数SystemInformation为接收信息的缓冲区。可以通过设置SystemInformationClass值为NT_PROCESSTHREAD_INFO来获取进程信息,获取到的进程结构体如下:
typedef struct _SYSTEM_PROCESSES
{
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved1[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters;
SYSTEM_THREADS Threads[1];
}SYSTEM_PROCESSES,*PSYSTEM_PROCESSES;
这里需要注意获取到的进程名为UNICODE_STRING,其结构体定义如下:
typedef struct _LSA_UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
}LSA_UNICODE_STRING,*PLSA_UNICODE_STRING;
typedef LSA_UNICODE_STRING UNICODE_STRING, *PUNICODE_STRING;
需要将其转换为ASCII字符串,使用WideCharToMultiByte函数。
相关源码分享:
//函数作用 通过NtQuerySytemInfo方法查找进程名称为szTargetProcess的进程
int FindProcessByNtQuery(char *szTargetProcess)
{
HINSTANCE hModule = NULL;
DWORD dwTotalProcess = 0;
DWORD dwReturnLength = 0;
NTSTATUS Status = 0;
PSYSTEM_PROCESSES pstSystemProc = NULL;
pNtQuerySystemInformation pfNtQuerySystemInformation = NULL;
DWORD dwNumberBytes = MAX_INFO_BUF_LEN;
hModule = GetModuleHandleA(“ntdll.dll”);
if (NULL == hModule)
{
return -1;
}
pfNtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(hModule, (LPCSTR)“ZwQuerySystemInformation”);
if (NULL == pfNtQuerySystemInformation)
{
return -1;
}
void *lpSystemInfo = (LPVOID)malloc(dwNumberBytes);
if (!lpSystemInfo)
{
return -1;
}
Status = pfNtQuerySystemInformation(NT_PROCESSTHREAD_INFO, lpSystemInfo, dwNumberBytes, &dwReturnLength);
if (Status == STATUS_INFO_LENGTH_MISMATCH)
{
printf(“STATUS_INFO_LENGTH_MISMATCH\n”);
return -1;
}
else if (Status != STATUS_SUCCESS)
{
printf(“NtQuerySystemInformation Error: %d\n”, GetLastError());
return -1;
}
pstSystemProc = (PSYSTEM_PROCESSES)lpSystemInfo;
while (pstSystemProc->NextEntryDelta != 0)
{
char pszProcessName = (char)malloc(pstSystemProc->ProcessName.Length + 1);
if (!pszProcessName)
{
return -1;
}
WideCharToMultiByte(CP_ACP,
0,
pstSystemProc->ProcessName.Buffer,
pstSystemProc->ProcessName.Length + 1,
pszProcessName,
pstSystemProc->ProcessName.Length + 1,
NULL,
NULL);
if (!strcmp(pszProcessName, szTargetProcess))
{
return pstSystemProc->ProcessId;
}
pstSystemProc = (PSYSTEM_PROCESSES)((char *)pstSystemProc + pstSystemProc->NextEntryDelta);
}
return -1;
}
Windows终端服务相关的函数都是以WTS开头。WTSOpenServer函数可以打开一台远程机器的访问链接,WTSEnumerateProcesses函数可以枚举指定Server上的所有进程,函数声明如下:
typedef BOOL (_stdcall WTSENUMERATEPROCESSES)(
HANDLE hServer, //WTSOpenServer返回的句柄
DWORD Reserved, //保留值, 0
DWORD Version, //指定枚举要求的版本, 必须为1
PWTS_PROCESS_INFO ppProcessInfo, //这个参数是关键,存放我们要的进程名和进程id
DWORD* pCount //用来存放ppProcessInfo里WTS_PROCESS_INFO结构的数量指针);
当函数第一个参数hServer值指定为WTS_CURRENT_SERVER_HANDLE时,代表枚举本地机器上的进程。
相关源码分享:
int FindProcessByWtsEnumerate(char *szTargetProcess)
{
HINSTANCE hWtsApi32 = LoadLibraryA(“wtsapi32.dll”);
if (NULL == hWtsApi32)
{
return -1;
}
WTSENUMERATEPROCESSES pWtsEnumerateProcesses = (WTSENUMERATEPROCESSES)GetProcAddress(hWtsApi32, “WTSEnumerateProcessesA”);
if (NULL == pWtsEnumerateProcesses)
{
return -1;
}
PWTS_PROCESS_INFOA pWtspi = {0};
DWORD dwCount = 0;
if (!pWtsEnumerateProcesses(WTS_CURRENT_SERVER_HANDLE, 0, 1, &pWtspi, &dwCount))
{
return -1;
}
for (int i = 0; i < dwCount; i++)
{
if (!strcmp(pWtspi[i].pProcessName, szTargetProcess))
{
return pWtspi[i].ProcessId;
}
}
return -1;
}
操作系统提供了一个api GetWindowThreadProcessId,使用该api可以获得窗口所在线程与所在进程信息,所以如果目标进程有窗口那么就可以使用窗口的方式来查找目标进程。获得窗口句柄的方式一般用FindWindow和EnumWindows。
关键函数介绍:
HWND FindWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName
);
FindWindow函数用来查找一个类名等于lpClassName,窗口名等于lpWindowName的特定窗口。如果类名和窗口名等于NULL则该项过滤条件忽略,类名和窗口名不能同时等于NULL,也就是说至少需要一个参数有效。如果找到复合条件的窗口则返回窗口句柄,否则返回NULL。
BOOL EnumWindows(
WNDENUMPROC lpEnumFunc,
LPARAM lParam
);
EnumWindows函数用来枚举当前系统内的所有窗口,lParam 是一个开发者自定义的数值,lpEnumFunc则是一个找到窗口的回调函数。当系统发现一个窗口则调用一次回调函数lpEnumFunc,参数为找到的窗口句柄和调用EnumWindows时传入的lParam值。回调函数原型如下:
BOOL CALLBACK EnumWindowsProc(
HWND hwnd,
LPARAM lParam
);
EnumWindows函数只能用来找顶层窗口,如果要找指窗口则需要调用函数EnumChildWindows。
BOOL EnumChildWindows(
HWND hWndParent, WNDENUMPROC lpEnumFunc, LPARAM lParam
);
EnumChildWindows函数与EnumWindows参数相似,作用也相似。参数上多了一个hWndParent,用来表明要找的窗口为hWndParent窗口的子窗口。如果用EnumWindows和EnumChildWindows函数查找窗口,则需要在lpEnumFunc回调函数内调用GetWindowText来获取窗口名,调用GetClassName来获取窗口的类名。然后在通过窗口名和类名对比来确认目标窗口。
一般窗口名在窗口上可以直接看到,但是类名是隐藏的,需要用一些特定工具才可以看到,比如Spy ++等。
DWORD GetWindowThreadProcessId(
HWND hWnd,
LPDWORD lpdwProcessId
);
GetWindowThreadProcessId函数用来获得指定窗口hWnd所在的进程和线程信息,线程ID用返回值来传递,而进程ID信息则通过lpdwProcessId 来传递。
关键代码分享:
//函数作用 通过FindWindows方式查找窗口szWindowTitle所在的进程
DWORD FindProcessByFindWindow(char *szWindowTitle)
{
DWORD dwTargetProcessId = 0;
HWND hTargetWindow = FindWindow(NULL, szWindowTitle);
if (hTargetWindow == NULL)
{
return dwTargetProcessId;
}
GetWindowThreadProcessId(hTargetWindow, &dwTargetProcessId);
return dwTargetProcessId;
}
/**************************************************************/
typedef struct tagWINDOWPROCESSINFO32 {
DWORD dwWindowProcessId;
char szWindowTitle[256];
} WINDOWPROCESSINFO32;
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
{
WINDOWPROCESSINFO32 *pstWindowProcessInfo = (WINDOWPROCESSINFO32 *)lParam;
char szWindowText[1024] = { 0 };
GetWindowText(hwnd, szWindowText, 1024);
if (_stricmp(szWindowText, pstWindowProcessInfo->szWindowTitle) == 0)
{
DWORD dwProcessId = 0;
GetWindowThreadProcessId(hwnd, &dwProcessId);
pstWindowProcessInfo->dwWindowProcessId = dwProcessId;
return FALSE;
}
else
{
return TRUE;
}
}
//函数作用 通过EnumWindows方式查找窗口szWindowTitle所在的进程
DWORD FindProcessByEnumWindow(char *szWindowTitle)
{
WINDOWPROCESSINFO32 stWindowProcessInfo = { 0 };
strncpy_s(stWindowProcessInfo.szWindowTitle, szWindowTitle, sizeof(stWindowProcessInfo.szWindowTitle) - 1);
EnumWindows(EnumWindowsProc, (LPARAM)&stWindowProcessInfo);
return stWindowProcessInfo.dwWindowProcessId;
}
在众多注入方式中,有一些注入方式在注入过程中是不知道所注入的目标进程的,只要复合条件的进程都会注入。比如采用钩子注入那么自要有消息队列的线程所在进程都会注入。这种情况下我们可以通过模块信息来判定是否是目标进程,如果是目标进程则执行相应操作(例如改游戏代码,CALL游戏函数等等)。关于注入方式我们在后面的章节在详细介绍,现在主要介绍集中简单的判定目标进程的3种方式。GetModuleFileName\GetModuleFileNameEx\GetModuleHandle。其中GetModuleFileName使用方式和GetModuleFileNameEx这个函数类似,使用说明在前面已经有介绍过这里不再重复,都是先过去当前进程EXE路径然后判断是否 为指定进程的EXE。
HMODULE GetModuleHandle(
LPCTSTR lpModuleName
);
GetModuleHandle函数则是用来获的名字为lpModuleName的模块的句柄。如果找到了复合条件的模块则返回模块句柄,没找到模块则返回NULL。
关键代码分享:
//以下函数作用都是用来判断当前进程是否是进程名为szTargetProcess的进程,如果是则返回真,否则返回假。
BOOL IsTargetProcessByGetModuleHandle(char *szTargetProcess)
{
BOOL bFindProcess = FALSE;
if (GetModuleHandle(szTargetProcess) != NULL)
bFindProcess = TRUE;
return bFindProcess;
}
BOOL IsTargetProcessByGetModuleFileName(char *szTargetProcess)
{
BOOL bFindProcess = FALSE;
char szProcessPath[1024] = { 0 };
GetModuleFileName(NULL, szProcessPath, 1024);
if (strstr(szProcessPath, szTargetProcess) != NULL)
bFindProcess = TRUE;
return bFindProcess;
}
BOOL IsTargetProcessByGetModuleFileNameEx(char *szTargetProcess)
{
BOOL bFindProcess = FALSE;
char szProcessPath[1024] = { 0 };
GetModuleFileNameEx(GetCurrentProcess(), NULL, szProcessPath, 1024);
if (strstr(szProcessPath, szTargetProcess) != NULL)
bFindProcess = TRUE;
return bFindProcess;
}
BOOL IsTargetProcessByGetModuleBaseName(char *szTargetProcess)
{
BOOL bFindProcess = FALSE;
char szProcessPath[1024] = { 0 };
GetModuleBaseName(GetCurrentProcess(), NULL, szProcessPath, 1024);
if (strstr(szProcessPath, szTargetProcess) != NULL)
bFindProcess = TRUE;
return bFindProcess;
}