在上一章,我们讲了如何通过枚举进程来检测我们的程序是否多开.
详情请见:程序防多开之一:进程数量检测
虽然此章中讲的是直接通过内核函数NtQuerySystemInformation()对系统进程进行扫描,但也总有可能会被一些非常规手段攻破和篡改,导致无法获取真实的进程信息.
故此本章需要采取另外一种技术手段来实现检测,对上一章所讲的技术实现查漏补缺.
以后本专栏防多开系列均会在之前文章的基础上进行漏洞修复和技术补充,避免被不法手段所攻破.
当我们通过进程枚举无法获取到真实的进程信息之时,还可以尝试通过程序的窗口来实现.
在常规技术手段之中,大多都是通过取窗口标题和类名进行检测,实现防止程序被多开的目的.
例如:通过EnumWindows(),FindWindowEx()等API枚举窗口,然后再调用GetWindowText()或者GetClassName()来获取标题和类名,并且与本程序的标题类名对比,获取程序的启动数量.
我们本章的标题虽然是程序窗口检测,但是我们却是用另外一种方式和其他的API来实现的(内核函数).
详细的技术手段参考如下代码:
头文件部分(Public.h)
#pragma once
#include <iostream>
#include <Windows.h>
using namespace std;
//宏定义DLL函数获取方法
#define FUN_Get_static(dll,return_value, calling_convention, apiname, ...) \
typedef return_value(calling_convention * pf_##apiname)(__VA_ARGS__);\
static pf_##apiname pFun_##apiname =NULL;\
if (!pFun_##apiname)\
pFun_##apiname=(pf_##apiname)myGetProc(dll, #apiname);
//宏定义DLL函数指针
#define FUN_point(apiname)pFun_##apiname
//获取dll导出函数地址
static PVOID myGetProc(LPCSTR lpLibFileName, LPCSTR lpProcName)
{
HMODULE hMod = NULL;
do
{
hMod = GetModuleHandleA(lpLibFileName);
if (hMod)
break;
hMod = LoadLibraryA(lpLibFileName);
} while (FALSE);
if (!hMod)
return NULL;
PVOID pFun = GetProcAddress(hMod, lpProcName);
return pFun;
}
//获取进程的完整路径,并返回路径文本长度
static DWORD myGetProcessPath(DWORD nPID, LPWSTR lpPath, DWORD nSize)
{
if (!nPID || !lpPath)
return 0;
DWORD nRet = 0;
HANDLE handle = NULL;
__try {
do
{
wsprintfW(lpPath, L"");
handle = ::OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, nPID);
if (!handle)
break;
::QueryFullProcessImageNameW(handle, 0, lpPath, &nSize);
nRet = wcslen(lpPath);
} while (FALSE);
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
if (handle)
CloseHandle(handle);
return nRet;
}
代码解释:
与上一章相同,我将常用的宏定义以及函数迁移到了"Public.h"头文件中.
其中包含了FUN_Get_static这个DLL函数获取宏,以及myGetProc()和myGetProcessPath()这两个函数.
.cpp部分(CheckStarts.cpp)
#include <vector>
#include "Public.h"
//调用NtUserFindWindowEx来获取当前所有窗口的PID
//如果成功,函数内部会申请内存作为数据缓冲区,并返回数据指针和数据长度
//需要在合适的时候通过free()函数释放内存,避免内存溢出.
DWORD* myEnumWindowsPids(PULONG pRetLen)
{
if (pRetLen)
*pRetLen = 0;
//通过宏定义FUN_Get_static在函数内部获取静态函数指针NtUserFindWindowEx;
//此命令将会加载win32u.DLL并通过调用GetProcAddress获取函数指针.
FUN_Get_static("win32u.DLL", HWND, NTAPI, NtUserFindWindowEx,
HWND hwndParent, HWND hwndChild, PUNICODE_STRING pstrClassName, PUNICODE_STRING pstrWindowName, ULONG dwType);
//判断指针是否获取成功.
if (!FUN_point(NtUserFindWindowEx))
return NULL;
//vPIDs 用来储存找到窗口对应的PID
vector<DWORD> vPIDs;
HWND hwndParent = 0;
HWND hwndChild = 0;
while (true)
{
CHAR strText[MAX_PATH] = { 0 };
CHAR strClass[MAX_PATH] = { 0 };
HWND hWnd = FUN_point(NtUserFindWindowEx)(hwndParent, hwndChild, (PUNICODE_STRING)strClass, (PUNICODE_STRING)strText, 0);
if (!hWnd)
break;
//用来通过hWnd寻找下一个窗口句柄
hwndChild = hWnd;
//GetWindowTextA(hWnd, strText, sizeof(strText));
//GetClassNameA(hWnd, strClass, sizeof(strClass));
//获取当前窗口句柄所属进程ID
DWORD nPid = 0;
GetWindowThreadProcessId(hWnd, &nPid);
if (!nPid)
continue;
//避免重复存储PID,此处要遍历vPIDs
BOOL nIsHas = FALSE;
for (auto item : vPIDs)
{
if (item == nPid)
{
nIsHas = TRUE;
break;
}
}
//当前窗口所属进程已经保存过了
if (nIsHas)
continue;
//没有保存,则保存此PID
vPIDs.push_back(nPid);
}
//vPIDs是否为空
if (vPIDs.size() <= 0)
return NULL;
//申请内存,并且拷贝所有PID
DWORD nLen = vPIDs.size() * sizeof(DWORD);
DWORD* pPidArray = (DWORD*)malloc(nLen);
if (!pPidArray)
return NULL;
memset(pPidArray, 0, nLen);
DWORD index = 0;
for (auto item : vPIDs)
{
pPidArray[index] = item;
index++;
}
if (pRetLen)
*pRetLen = nLen;
return pPidArray;
}
//遍历进程ID数组,并且获取与当前进程路径相同的所有进程数量
LONG GetProcessCountByPids(DWORD* pPidArray, DWORD nBufLen)
{
//获取自身进程完整路径
DWORD nCurrPID = GetCurrentProcessId();
WCHAR strCurrPath[MAX_PATH] = { 0 };
DWORD nRet = myGetProcessPath(nCurrPID, strCurrPath, sizeof(strCurrPath));
if (!nRet)
{
//获取自身进程完整路径失败!
return -1;
}
LONG nCount = 0;
__try
{
//通过数据长度,解析出数组成员数
DWORD nPidConts = nBufLen / sizeof(DWORD);
//遍历PID数组
for (size_t i = 0; i < nPidConts; i++)
{
DWORD nPid = pPidArray[i];
if (nPid == 0)
continue;
//查询当下PID所在路径是否和本进程路径相同
WCHAR strPath[MAX_PATH] = { 0 };
DWORD nRet = myGetProcessPath(nPid, strPath, sizeof(strPath));
if (nRet)
{
if (wcscmp(strPath, strCurrPath) == 0)
{
nCount++;
}
}
}
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
return nCount;
}
BOOL CheckCountByWindows(LONG nMaxCount)
{
BOOL isLimit = FALSE;
ULONG nSize = 0;
DWORD* pPidArray = NULL;
__try {
do
{
//枚举当下所有进程信息
pPidArray = myEnumWindowsPids(&nSize);
if (!pPidArray)
break;
//解析进程信息数据,并获取与当前进程路径相同的所有进程数量.
LONG nCount = GetProcessCountByPids(pPidArray, nSize);
//进程数量超出最大数
if (nCount > nMaxCount)
isLimit = TRUE;
//进程数量=0或者-1均表示系统环境异常,此处直接返回TRUE.
if (nCount <= 0)
isLimit = TRUE;
} while (FALSE);
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
if (pPidArray)
free(pPidArray);
return isLimit;
}
代码解释:
在CheckCountByWindows()代码中,我们调用自定义函数myEnumWindowsPids()获取到所有拥有窗口的进程PID.
调用GetProcessCountByPids()对获取到的PID数组进行解析和判断,获取到和本程序相同路径程序的启动数量.
最后比较是否超出本进程的最大限制数量,执行多开检测.
CheckCountByProcess()与上章用到的GetProcessCountByCurrentPath()是相同的技术手段,所以本章不再赘述(您可以更换为不对比进程路径,而是其他信息,反正PID数组已经拿到了).
这里要详细说一下myEnumWindowsPids()这个函数的实现原理.
在Windows系统中,微软提供了多个获取窗口句柄的API,例如:FindWindowEx(),EnumWindows(),EnumThreadWindows(),GetForegroundWindow()等.
这是在应用层提供给程序员使用的接口,但是在内核层面,它们最终只会调用两三个内核函数来执行窗口信息的获取.
代表函数就有NtUserFindWindowEx()以及NtUserBuildHwndList().
其中,因为NtUserBuildHwndList()会因为操作系统版本的变化而发生改变,所以我们在这里并未进行使用,当然您可以根据不同版本的系统来具体实现,实现方法和上面代码大致相同,仅传递参数有所差异.
我们这里使用内核函数NtUserFindWindowEx()就能够枚举到所有系统正在运行的程序窗口句柄了.
通过循环调用NtUserFindWindowEx()来获取窗口句柄,直到返回值为NULL,在拿到窗口句柄后,并不进行任何获取窗口标题和类名的操作.
修改一个窗口的标题实在太容易了,通过标题来进行检测是比较古老的手段.
我们通过窗口句柄,传递给API函数GetWindowThreadProcessId()来实现拿到目标窗口的进程ID.
然后我们通过获取对应进程的路径,和本程序路径对比,实现多开检测.
调用方式如下代码:
#include "CheckStarts.h"
int main()
{
//限制多开数量
LONG nMaxCount = 2;
//判断进程数量是否超出限制数量
BOOL isLimit = CheckCountByWindows(nMaxCount);
if (isLimit)
MessageBoxA(0, "客户端开启数量超出限制!", "提示", 0);
system("pause");
}
运行效果如图: