之前章节中我们讲了几种针对程序多开的检测:
在Windows系统中,每个进程在创建时,都会加载系统的共享区间,在内核中的名称是ShareSection.
共享区间可以实现对同一个程序或者DLL数据共享,避免在系统加载的时候重复载入,导致资源负载浪费.
因此我们可以利用此种方式实现进程数据同步,对程序是否多开进行检测.
共享区间在程序中的实现方式:
#pragma data_seg("_gdata")
//告诉编译器,接下来的全局变量(例如 g_PidArray)应该被放置在一个名为 "_gdata" 的数据段中
DWORD g_PidArray[10] = {0};
#pragma data_seg()
//关闭这个设置,意味着在此之后定义的全局变量将回到默认的数据段
#pragma comment(linker,"/SECTION:_gdata,RWS")
//指示链接器将 "_gdata" 数据段设置为读写共享(RWS)模式
以上代码指示编译器在生产程序或者dll文件时,提供一块共享区间内存,区间内存名字为"_gdata",下面我们开始针对程序运行期间,对此区间的数据进程操作.
首先我们得有一个获取进程路径的函数,之前文章中已经贴过代码,此处再次给出:
//获取进程的完整路径,并返回路径文本长度
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;
}
以上代码的作用仅是获取程序的完整路径,我们今天依然要用到它.如果您怕上述的功能被作弊者使坏导致失效,可以利用其他方式实现,此处不再祥举例程.
现在开始实现检测代码:
//遍历共享内存区域进程数组,并且获取当前运行程序的启动总数
LONG GetProcessCountByShareMemory()
{
//获取自身进程完整路径
DWORD nCurrPID = GetCurrentProcessId();
WCHAR strCurrPath[MAX_PATH] = { 0 };
DWORD nRet = myGetProcessPath(nCurrPID, strCurrPath, sizeof(strCurrPath));
if (!nRet)
{
//获取自身进程完整路径失败!
return -1;
}
//取共享区域内存指针和长度
DWORD* ppPidArray = g_PidArray;
DWORD nBufLen = sizeof(g_PidArray);
LONG nCount = 0;
int nWriteIndex = -1;//用来标记写入共享区域内存的数组下标
//通过数据长度,解析出数组成员数
DWORD nPidConts = nBufLen / sizeof(DWORD);
__try
{
//遍历PID数组
for (size_t i = 0; i < nPidConts; i++)
{
DWORD nPid = ppPidArray[i];
//如果当前进程PID已经记录在共享内存中,则设置写入下标为最大数+1(用是否数组越界判断是否写入)
if (nPid == nCurrPID)
nWriteIndex = nPidConts + 1;
if (nPid == 0)
{
//仅修改一次写入下标为当前索引
if (nWriteIndex < 0)
nWriteIndex = i;
continue;
}
//查询当下PID所在路径是否和本进程路径相同
WCHAR strPath[MAX_PATH] = { 0 };
DWORD nRet = myGetProcessPath(nPid, strPath, sizeof(strPath));
if (!nRet)
{ //如果进程路径不相同,可能是旧进程关闭,这里记录索引,以便在数组写满后可以覆写数据
if (nWriteIndex < 0)
nWriteIndex = i;
}
if (nRet)
{
if (wcscmp(strPath, strCurrPath) == 0)
{
nCount++;
}
}
}
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
//判断是否在共享内存进程数组中写入本进程PID
if (nWriteIndex < nPidConts && nWriteIndex >= 0)
{
g_PidArray[nWriteIndex] = nCurrPID;
nCount++;
}
return nCount;
}
DWORD CheckCountShareMemory(DWORD nMaxCount)
{
BOOL isLimit = FALSE;
ULONG nSize = 0;
DWORD* pPidArray = NULL;
__try {
do
{
//在共享区间中读取数据,获取当前程序的启动数量.
LONG nCount = GetProcessCountByShareMemory();
//进程数量超出最大数
if (nCount > nMaxCount)
isLimit = TRUE;
//进程数量=0或者-1均表示系统环境异常,此处直接返回TRUE.
if (nCount <= 0)
isLimit = TRUE;
} while (FALSE);
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
return isLimit;
}
代码解释:
在函数CheckCountShareMemory()中我们实现调用GetProcessCountByShareMemory()来获取当前程序的运行数量,如果超出我们规定的最大启动数,则返回TRUE.
函数GetProcessCountByShareMemory()通过遍历共享区间内存数据g_PidArray来获取当前程序的所有PID.
在遍历期间,判断当前进程的PID是否已经共享,如果没有共享,则会将当前PID写入共享区间内存中.
同时也会判断共享区间内存中的PID是否已经失效,例如旧进程是否关闭(通过程序路径判断).
然后将所有存活的当前程序数量记录,并且返回准确的数量.
请注意:
通过此种方式实现的数据共享,仅能维持在程序是相同路径的情况下才会共享.
如果将此程序复制到其他目录或者修改名字,那么共享区间将会重新开辟.
我们这里仅使用此种方式实现对同一个进程的检测,本专栏以后将会详解如何针对修改文件名/路径躲避多开如何实现检测,此处不再详解.
最后的调用:
int main()
{
//限制多开数量
LONG nMaxCount = 2;
//判断进程数量是否超出限制数量
BOOL isLimit = CheckCountShareMemory(nMaxCount);
if (isLimit)
MessageBoxA(0, "客户端开启数量超出限制!", "提示", 0);
system("pause");
}
运行效果: