在前面几章介绍了关于程序限制多开与检测多开数量的文章,可以精准检测到应用程序开启的数量.
本章介绍一种新的方式可以实现对程序多开检测.
使用隐藏文件进行检测的原理很简单:
在硬盘中创建一个文件,作为共享读写数据的介媒,使多个应用程序从中读写数据能够数据交互,并且检查程序的启动数量是否超过限制.
这种方式的好处是不用创建共享内存和映射内存,直接对文件进行读写,避开了某些作弊者通过类似于修改内存属性\解除映射\挂钩修改映射名字等手段实现的躲避检测.
正常情况下,如果在硬盘直接创建一个文件,很容易被作弊者发觉并且篡改数据.
所以我们需要用一些特殊的手段,对被创建的共享文件和数据进行保护.
首先介绍一下创建(打开)文件的API.
微软定义的API接口如下:
HANDLE CreateFileA(
[in] LPCSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
参数
[in] lpFileName
要创建或打开的文件或设备的名称
[in] dwDesiredAccess
请求对文件或设备的访问权限
[in] dwShareMode
请求的文件或设备的共享模式
[in, optional] lpSecurityAttributes
指向 SECURITY_ATTRIBUTES 结构的指针,该结构包含两个独立但相关的数据成员:可选的安全描述符,以及一个布尔值,用于确定是否可由子进程继承返回的句柄
[in] dwCreationDisposition
要对存在或不存在的文件或设备执行的操作
[in] dwFlagsAndAttributes
文件或设备属性和标志
[in, optional] hTemplateFile
具有 GENERIC_READ 访问权限的模板文件的有效句柄
在创建文件时,我们需要用到这个API的各个参数,做到以下事情.
1.文件是隐藏的,不可见的.
2.使用完毕后,文件将会删除,不遗留在硬盘之上
3.文件是共享的,其他程序可以读写程序.
4.文件属性是系统文件,不允许随意右键删除.
所以我们需要用以下方式来实现:
hFile = CreateFileA(
szFileName, // 文件名
dwDesiredAccess, // 读写访问权限
FILE_SHARE_READ | FILE_SHARE_WRITE| FILE_SHARE_DELETE, // 共享读写删除
NULL, // 安全属性指针(NULL表示使用默认安全属性)
dwCreationDisposition, // 如果文件不存在则创建它
FILE_ATTRIBUTE_HIDDEN| FILE_FLAG_DELETE_ON_CLOSE| FILE_ATTRIBUTE_SYSTEM| FILE_ATTRIBUTE_NORMAL, // 文件属性
NULL // 模板文件句柄(NULL表示不使用模板文件)
);
/*文件属性解释
* FILE_ATTRIBUTE_HIDDEN = 文件被隐藏。
* FILE_FLAG_DELETE_ON_CLOSE = 文件在其所有句柄都关闭后立即被删除,其中包括指定的句柄和任何其他程序打开或重复的句柄。
* FILE_ATTRIBUTE_SYSTEM = 该文件是操作系统的一部分或由操作系统独占使用
*/
上面的代码刚好可以实现我们的要求.
之后我们还需要做到一件事情:数据加密存放.
毕竟文件是存放在硬盘之中,如果被居心不良的人随随便便能够查询文件内容,那么我们做这一切的意义就不存在了.
所以我们在这里定义一个简单的加解密函数,用来对文件数据进行加密和解密.
//简单加解密函数,使用异或算法实现
VOID DataCryption(BYTE* pByte,DWORD nSize)
{
for (size_t i = 0; i < nSize; i++)
{
pByte[i] ^= (nSize%0xFF);
}
}
当然您可以使用其他加解密算法实现,具体根据您的需求.
现在我们开始实现对文件的加密读写.
代码:
#define FILE_HIDDEN_DELETE_NAME "./MyHiddenDeleteFile.dll" // 文件具名用.dll后缀以欺骗坏人,可以尝试其他后缀
int FileIsExist(LPCSTR szFileName)
{
DWORD fileAttributes = GetFileAttributesA(szFileName);
if (fileAttributes == INVALID_FILE_ATTRIBUTES)
{
// 文件不存在
return 0;
}
else {
// 文件存在
return 1;
}
}
VOID DataCryption(BYTE* pByte,DWORD nSize)
{
for (size_t i = 0; i < nSize; i++)
{
pByte[i] ^= (nSize%0xFF);
}
}
//读写具有隐藏并且关闭自删除属性的文件
HANDLE WriteReadFileData(HANDLE hFile, LPCSTR szFileName, PBYTE pData, DWORD nSize, BOOL isWrite)
{
DWORD nRet = 0;
__try
{
do
{
if (hFile == NULL || hFile == INVALID_HANDLE_VALUE)
{
int isExist = FileIsExist(szFileName);
DWORD dwCreationDisposition = isExist ? OPEN_EXISTING : OPEN_ALWAYS;
DWORD dwDesiredAccess = isWrite ? GENERIC_ALL : GENERIC_READ| FILE_SHARE_WRITE;
hFile = CreateFileA(
szFileName, // 文件名
dwDesiredAccess, // 读写访问权限
FILE_SHARE_READ | FILE_SHARE_WRITE| FILE_SHARE_DELETE, // 共享读写删除
NULL, // 安全属性指针(NULL表示使用默认安全属性)
dwCreationDisposition, // 如果文件不存在则创建它
FILE_ATTRIBUTE_HIDDEN| FILE_FLAG_DELETE_ON_CLOSE| FILE_ATTRIBUTE_SYSTEM| FILE_ATTRIBUTE_NORMAL, // 文件属性
NULL // 模板文件句柄(NULL表示不使用模板文件)
);
/*文件属性解释
* FILE_ATTRIBUTE_HIDDEN = 文件被隐藏。
* FILE_FLAG_DELETE_ON_CLOSE = 文件在其所有句柄都关闭后立即被删除,其中包括指定的句柄和任何其他程序打开或重复的句柄。
* FILE_ATTRIBUTE_SYSTEM = 该文件是操作系统的一部分或由操作系统独占使用
*/
}
if (hFile==NULL || hFile == INVALID_HANDLE_VALUE )
break;
//设置文件指针为文件头部,从头开始读取数据
SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
DWORD nrwBytes = 0;
//读写共享内存中数据
if (isWrite)
nRet = WriteFile(hFile, pData, nSize, &nrwBytes, NULL);
else
nRet =ReadFile(hFile, pData, nSize, &nrwBytes, NULL);
} while (FALSE);
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
if (nRet==NULL && hFile !=NULL && hFile!= INVALID_HANDLE_VALUE)
{
CloseHandle(hFile);
hFile = NULL;
}
return hFile;
}
LONG GetProcessCountByFileData(HANDLE& hFile)
{
hFile = NULL;
//获取自身进程完整路径
DWORD nCurrPID = GetCurrentProcessId();
WCHAR strCurrPath[MAX_PATH] = { 0 };
DWORD nRet = myGetProcessPath(nCurrPID, strCurrPath, sizeof(strCurrPath));
if (!nRet)
{
//获取自身进程完整路径失败!
return -1;
}
DWORD PidArray[10] = { 0 };
hFile = WriteReadFileData(NULL, FILE_HIDDEN_DELETE_NAME, (PBYTE)PidArray, sizeof(PidArray), FALSE);
if (hFile == NULL || hFile == INVALID_HANDLE_VALUE)
{
//读写文件失败!
return -1;
}
DataCryption((BYTE*)PidArray, sizeof(PidArray));
//取出读取到的文件数据指针和长度
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;
DataCryption((BYTE*)PidArray, sizeof(PidArray));
nCount++;
hFile = WriteReadFileData(hFile, FILE_HIDDEN_DELETE_NAME, (PBYTE)PidArray, sizeof(PidArray), TRUE);
if (!hFile)
return -1;
}
return nCount;
}
代码解释:
首先我们通过#define FILE_HIDDEN_DELETE_NAME "./MyHiddenDeleteFile.dll" // 文件具名用.dll后缀以欺骗坏人,可以尝试其他后缀
宏定义共享文件的名字,并且文件的后缀名为".dll",用来迷惑坏人,让他们以为这仅仅是一个动态链接库.当然您也可以使用其他后缀名.
在WriteReadFileData()中,我们首先通过FileIsExist()来判断文件是否已经存在.
之后传递具体的参数,实现对文件的创建或是打开.
这个共享的文件具备这些属性:隐藏|自删除|系统|共享|临时|数据运行期间一直存在.
我们使用SetFilePointer()在每次读写之时都将读写指针设置为文件头部,用以读写完整的共享数据.
在GetProcessCountByFileData()中,我们通过WriteReadFileData()读取共享数据.
然后对数据进行简单解密,并通过遍历数据中存放的进程数组以及myGetProcessPath()实现获取当前程序的启动数量.
当然还有将本程序的PID加密后写入文件数据的功能.
这样一个大致的加密存储以及隐藏文件自删除部分代码就完成了.
实现检测的代码:
DWORD CheckCountFileData(DWORD nMaxCount, HANDLE& hFile)
{
hFile = NULL;
BOOL isLimit = FALSE;
ULONG nSize = 0;
DWORD* pPidArray = NULL;
__try {
do
{
//通过读取隐藏文件数据,获取当前程序的启动数量
LONG nCount = GetProcessCountByFileData(hFile);
//进程数量超出最大数
if (nCount > nMaxCount)
isLimit = TRUE;
//进程数量=0或者-1均表示系统环境异常,此处直接返回TRUE.
if (nCount <= 0)
isLimit = TRUE;
} while (FALSE);
}
__except (EXCEPTION_EXECUTE_HANDLER) { ; }
return isLimit;
}
最后的调用:
int main()
{
//限制多开数量
LONG nMaxCount = 2;
//声明hFile句柄变量用来接收文件句柄,在用完之后需要关闭句柄,释放资源.
HANDLE hFile = NULL;
//判断进程数量是否超出限制数量
BOOL isLimit = CheckCountFileData(nMaxCount, hFile);
if (isLimit)
MessageBoxA(0, "客户端开启数量超出限制!", "提示", 0);
system("pause");
//程序在需要结束的时候,关闭文件句柄
if (hFile)
CloseHandle(hFile);
}
实现效果如图: