这是防多开系列文章发布的第五篇,前面还有四篇干货,可以实现精准检测程序开启数量.
今天这一篇讲的是如何利用共享(映射)内存实现检测.
共享内存和共享区间的相同点是:
都是系统在内核映射同一块内存地址进行读写操作.
进程与进程之间可以对同一个内存数据读写,建立数据共享.
随着全部的拥有共享权限关闭之后,系统会自动回收被共享的内存对象.
但是它们之间也存在着不同之处:
共享区间仅存在于自身程序实例,无法与其他程序数据共享.
共享内存可以与不同名甚至其他第三方进程建立数据共享.
相对比于共享区间,共享内存拥有更广泛的使用途径,例如:RPC进程通信.
一般情况下,共享内存是通过FileMapping实现的.
在Windows系统中,它拥有这几个API可以帮我们快速搭建共享内存和数据共享.
CreateFileMapping()
OpenFileMapping()
MapViewOfFile()
UnmapViewOfFile()
当然还有其他方式也可以实现,今天我们就以这几个API来举例.
具体代码:
#define SHARED_MEM_SIZE 1024 // 共享内存大小
#define SHARED_MEM_NAME "Global\\MySharedMemory" // 共享内存名称
HANDLE WriteReadFileMappingData(HANDLE hMapFile ,LPCSTR szFileName,PBYTE pData ,DWORD nSize,BOOL isWrite)
{
LPVOID pMapView=NULL;
DWORD nRet = 0;
__try
{
do
{
//判断是否需要获取hMapFile
if (hMapFile == NULL)
{
//首先尝试打开映射文件
hMapFile = OpenFileMappingA(
FILE_MAP_ALL_ACCESS, // 拥有所有访问权限
FALSE, // 不创建文件映射对象
szFileName // 共享内存的名称
);
}
if (hMapFile == NULL)
{
// 创建文件映射对象
hMapFile = CreateFileMappingA(
INVALID_HANDLE_VALUE, // 不使用文件句柄
NULL, // 默认的安全属性
PAGE_READWRITE, // 允许读写访问
0, // 映射的最大大小(以字节为单位)
SHARED_MEM_SIZE, // 映射的大小(以字节为单位)
szFileName // 名称
);
}
if (hMapFile == NULL)
break;
// 映射视图到进程的地址空间中
pMapView = MapViewOfFile(
hMapFile, // 文件映射对象的句柄
FILE_MAP_ALL_ACCESS, // 拥有所有访问权限
0, // 偏移量(以字节为单位)
0, // 映射的最大大小(以字节为单位)
0 // 映射的大小(以字节为单位)
);
if (pMapView == NULL)
break;
//读写共享内存中数据
if(isWrite)
memcpy(pMapView, pData, nSize);
else
memcpy(pData, pMapView, nSize);
nRet = 1;
} while (FALSE);
}
__except(EXCEPTION_EXECUTE_HANDLER){;}
// 解除映射
if(pMapView)
UnmapViewOfFile(pMapView);
if (nRet == 0 && hMapFile)
{
//读写共享内存失败,则关闭句柄
CloseHandle(hMapFile);
hMapFile = NULL;
}
//返回映射的文件句柄
return hMapFile;
}
代码解释:
以上实现一个对映射文件内存进行读写的示例.
我们通过OpenFileMapping()可以打开一个已经存在的映射,如果映射不存在,则使用CreateFileMapping()进行创建映射.
我们的读写权限是FILE_MAP_ALL_ACCESS,拥有完整的读写权限,在创建映射的时候,指定读写权限为PAGE_READWRITE,意为可读写.
拿到映射句柄后,使用MapViewOfFile()将其映射到当前进程的内存空间.
此时我们便可以针对被映射的内存进行读写操作了.
之后在调用UnmapViewOfFile()解除映射.
切记不要在读写成功之后CloseHandle关闭映射句柄,否则系统会认为映射对象不需要再引用,将会回收对象资源,这样的话,其他进程打开映射时会失败或是重新创建一个新的映射对象,导致无法实现进程之间的数据共享.
下面代码是实现利用映射内存对程序启动数量的检测.
//获取进程的完整路径,并返回路径文本长度
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 GetProcessCountByFileMapping(HANDLE &hMapFile)
{
hMapFile = NULL;
//获取自身进程完整路径
DWORD nCurrPID = GetCurrentProcessId();
WCHAR strCurrPath[MAX_PATH] = { 0 };
DWORD nRet = myGetProcessPath(nCurrPID, strCurrPath, sizeof(strCurrPath));
if (!nRet)
{
//获取自身进程完整路径失败!
return -1;
}
DWORD PidArray[10] = { 0 };
hMapFile = WriteReadFileMappingData(NULL,SHARED_MEM_NAME, (PBYTE)PidArray, sizeof(PidArray), FALSE);
if (!hMapFile)
{
//读取取映射内存失败!
return -1;
}
//取映射内存指针和长度
DWORD* ppPidArray = PidArray;
DWORD nBufLen = sizeof(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)
{
PidArray[nWriteIndex] = nCurrPID;
nCount++;
hMapFile = WriteReadFileMappingData(hMapFile,SHARED_MEM_NAME, (PBYTE)PidArray, sizeof(PidArray), TRUE);
if (!hMapFile)
return -1;
}
return nCount;
}
这里使用的方法和上一章节程序防多开之四:共享区间检测相似,只是我们将共享区间的数据地址更改为了通过读写共享映射内存来实现.
通过引用参数来传递我们拿到的映射句柄,以供程序结束时释放对象资源.
DWORD CheckCountFileMapping(DWORD nMaxCount,HANDLE &hMapFile)
{
hMapFile = NULL;
BOOL isLimit = FALSE;
ULONG nSize = 0;
DWORD* pPidArray = NULL;
__try {
do
{
//通过读取映射内存数据,获取当前程序的启动数量
LONG nCount = GetProcessCountByFileMapping(hMapFile);
//进程数量超出最大数
if (nCount > nMaxCount)
isLimit = TRUE;
//进程数量=0或者-1均表示系统环境异常,此处直接返回TRUE.
if (nCount <= 0)
isLimit = TRUE;
} while (FALSE);
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
return isLimit;
}
以上代码通过调用GetProcessCountByFileMapping()实现获取当前程序的启动数量,并判断是否超出最大限制数量.
下面是最后的调用:
int main()
{
//限制多开数量
LONG nMaxCount = 2;
HANDLE hMapFile = NULL;
//判断进程数量是否超出限制数量
BOOL isLimit = CheckCountFileMapping(nMaxCount, hMapFile);
if (isLimit)
MessageBoxA(0, "客户端开启数量超出限制!", "提示", 0);
system("pause");
//程序在需要结束的时候,关闭映射的文件句柄
if (hMapFile)
CloseHandle(hMapFile);
}
这里我们需要在程序结束阶段调用CloseHandle()关闭参数传递过来的映射文件句柄,释放系统资源.
运行效果如图: