Windows程序多开一般处理

2,063 阅读6分钟

Windows程序多开一般处理

一般部分程序为了利益可能会防止你多开,所以我这里介绍一下自己摸索半年学会的一些知识点并结合某款市面上现在的游戏进行讲解.

我们要实现多开前,一般需要知道如何防止多开.

如何防止多开

文件共享

通过文件共享判断文件是否存在,如果存在即为多开.(弊端:可能程序crash导致文件句柄没有得到释放)

内部网络

通过建立Server socket进行IPC通信;

命名管道

通过建立NamedPipe进行IPC通信;(目前笔者接触到的程序大部分用的NamedPipe操作的)

//CreateNamedPipe函数创建一个命名管道的实例,并返回一个后续管道操作的句柄。命名管道服务器进程使用此函数来创建特定命名管道的第一个实例,并建立其基本属性或创建现有命名管道的新实例。

HANDLE CreateNamedPipe(
 
LPCTSTR 【lpName】,	//指向管道名称的指针
DWORD 【dwOpenMode】,	//管道打开模式
DWORD 【dwPipeMode】,	//管道特定模式
DWORD 【nMaxInstances】,	//最大实例数
DWORD 【nOutBufferSize】,	//输出缓冲区大小,以字节为单位
DWORD 【nInBufferSize】,	//输入缓冲区大小,以字节为单位
DWORD 【nDefaultTimeOut】,	//超时时间,以毫秒为单位
LPSECURITY_ATTRIBUTES 【lpSecurityAttributes】	//指向安全属性结构的指针
);
  • 通过互斥体检测;
//CreateMutex函数创建一个命名或未命名的互斥对象。

HANDLE CreateMutex(

LPSECURITY_ATTRIBUTES 【lpMutexAttributes】,	//指向安全属性的指针
BOOL 【bInitialOwner】,	//标志初始所有权
LPCTSTR 【lpName】	//指向mutex对象名称的指针
);
  • 通过OpenProcess;
//OpenProcess函数返回现有进程对象的句柄。

HANDLE OpenProcess

DWORD 【dwDesiredAccess】,	//访问标志
BOOL 【bInheritHandle】,	//处理继承标志
DWORD 【dwProcessId】	//进程标识符en
);
  • 通过FindWindow;
//FindWindow函数检索顶级窗口的句柄,其类名和窗口名称与指定的字符串相匹配。此函数不搜索子窗口。

HWND FindWindow(

LPCTSTR 【lpClassName】,	//指向类名的指针
LPCTSTR 【lpWindowName】	//指向窗口名称的指针
);

等等常见方式

多开方式有哪些

我们从上面知道了如何防止多开,那么我们就可以实现如何实现多开,任何东西都是相对的,而攻防也异如此.

以下代码均采用Detours库实现hook. 库连接:Detours库

OpenProcess函数处理

通过HOOK NtOpenProcess实现进程判断,一般程序调用OpenProcess判断进程ID与之前是否相同等:

bool Hook_NtOpenProcess(bool enable)
	{
		// 自定义下NtOpenProcess函数类型,包含返回值,参数类型等,  因为这个函数不是标准函数,也不是公开的函数,所以需要我们自己定义类型.设计到Nt都需要这样.
		// 网上都可以查到Nt相关函数原型.
		typedef NTSTATUS(NTAPI* NtOpenProcess_t)(PHANDLE ProcessHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId);
		// 通过ntdll找到NtOpenProcess的地址,所以这儿的_NtOpenProcess是原函数地址
		static NtOpenProcess_t _NtOpenProcess = reinterpret_cast<NtOpenProcess_t>(GetProcAddress(GetModuleHandleA("ntdll"), "NtOpenProcess"));

		NtOpenProcess_t NtOpenProcess_hook = [](PHANDLE ProcessHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId) -> NTSTATUS
		{

#ifdef IS_NEED_VM
			VMProtectBegin("Hook_NtOpenProcess");
#endif // IS_NEED_VM
			
			// 某进程只能访问自己游戏进程和NGS进程还有游戏聊天进程,否则一律拒绝,这样可以解决多开的时候第二个窗口能启动起来,因为游戏在启动的时候会去取系统进程ID,如果找到有游戏的进程,
			// 就会给它关了,然后重启.
			if (ClientId && ClientId->UniqueProcess != 游戏进程ID && ClientId->UniqueProcess != NGS进程ID && ClientId->UniqueProcess != 游戏聊天进程ID) {
#ifdef DEBUG_TYPE
				switch (DEBUG_TYPE)
				{
				case KART_NGS_LOG:
					break;
				case ALL_LOG:
					MyDbgPrintFun("Hook_NtOpenProcess STATUS_ACCESS_DENIED:%d", ClientId->UniqueProcess);
					break;
				}
#endif // DEBUG_TYPE
				
				return STATUS_ACCESS_DENIED;
			}
#ifdef DEBUG_TYPE
			switch (DEBUG_TYPE)
			{
			case KART_NGS_LOG:
				break;
			case ALL_LOG:
				MyDbgPrintFun("Hook_NtOpenProcess 放行:%d, NGS进程ID:%d, 游戏进程ID:%d, 游戏聊天进程ID:%d", ClientId->UniqueProcess, NGS进程ID, 游戏进程ID, 游戏聊天进程ID);
				break;
			}
#endif // DEBUG_TYPE

#ifdef IS_NEED_VM
			VMProtectEnd();
#endif // IS_NEED_VM
			
			// 放行,调用原函数即可
			return _NtOpenProcess(ProcessHandle, AccessMask, ObjectAttributes, ClientId);
		};
		// Hook原函数地址指向NtOpenProcess_hook函数地址
		return DetourFunction(enable, reinterpret_cast<void**>(&_NtOpenProcess), NtOpenProcess_hook);
	}

FindWindow函数处理

通过Hook FindWIndow:

// 该Hook在游戏初始化的时候会调用两次,分别进行:1.类名;2.标题 查找; 我们将返回空即过掉该检测
bool Hook_FindWindow(bool enable)
	{
		static decltype(&FindWindow) FindWindow_Old = FindWindow;

		decltype(&FindWindow) FindWindow_Hook = [](LPCWSTR lpClassName, LPCWSTR lpWindowName) -> HWND
		{

#ifdef DEBUG_TYPE
			switch (DEBUG_TYPE)
			{
			case KART_NGS_LOG:
			case ALL_LOG:
				if ((lpWindowName && wcsstr(lpWindowName, L"XXX Client")) || (lpClassName && wcsstr(lpClassName, L"XXX Client")))
					MyDbgPrintFun("Hook_FindWindow_Kart:%S,%S", lpClassName, lpWindowName);
				break;
			}
#endif // DEBUG_TYPE
			
			return 0;//不去查找了,全部找不到
		};
		return DetourFunction(enable, reinterpret_cast<void**>(&FindWindow_Old), FindWindow_Hook);
	}

CreateMutex函数处理

通过HOOK CreateMutex实现:

bool Hook_CreateMutexW(bool enable)
	{
		static decltype(&CreateMutexW) CreateMutexW_Old = CreateMutexW;
		decltype(&CreateMutexW) CreateMutexW_Hook = [](LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCWSTR lpName) -> HANDLE
		{
#ifdef DEBUG_TYPE
			switch (DEBUG_TYPE)
			{
			case KART_NGS_LOG:
				break;
			case ALL_LOG:
				MyDbgPrintFun("Hook_CreateMutexW_Kart:%S", lpName);
				break;
			}
#endif // DEBUG_TYPE

#ifdef IS_NEED_VM
			VMProtectBegin("Hook_CreateMutexW");
#endif // IS_NEED_VM
			
			wchar_t *mutextObjectName = new wchar_t[lstrlenW(lpName) + 11];
			lstrcpyW(mutextObjectName, lpName);
			if (wcsstr(mutextObjectName, L"Global\\vcpkgsrvmgr")) {//通过DebugView可以查看到该句柄,一般我们可以通过CloseHandle去关闭即可,但是由于该游戏检测了该句柄是否存在,所以我们随机命名该句柄
				++createTimes;
				if (createTimes > 2) {
					createTimes = 3;
					return FALSE;
				}
				wsprintf(mutextObjectName, TEXT("Global\\vcpkgsrvmgr_%d%d"), 游戏进程ID, 游戏线程ID);
				return CreateMutexW_Old(lpMutexAttributes, bInitialOwner, mutextObjectName);
			}

#ifdef IS_NEED_VM
			VMProtectEnd();
#endif // IS_NEED_VM
			
			return CreateMutexW_Old(lpMutexAttributes, bInitialOwner, lpName);
		};
		return DetourFunction(enable, reinterpret_cast<void**>(&CreateMutexW_Old), CreateMutexW_Hook);
	}

通过以上三点hook,我们大致可以实现多开了.但是某游戏在多开后五分钟会断开连接,经过Debug是因为多开后导致游戏保护不能跟游戏客户端建立连接从而客户端没有心跳而被服务器踢下去, 所以我们还需要进一步处理;

通过Debug及HOOK 一些API观察到该游戏与保护是通过命名管道进行通信的.所以我们需要查阅一些命名管道的知识,这里笔者不再阐述一些命名管道的基础.

命名管道处理

一般命名管道分为客户端及服务端,在这里,游戏客户端是命名管道的客户端(后叫NamedClient),保护进程是命名管道的服务端(后叫NamedServer),所以我们需要HOOK NamedServer的CreateNamedPipe. 但是要想HOOK到这个函数,就必须在NamedServer进程里面去HOOK就得在NamedClient里面去HOOK CreateProcess去拦截, 同样NamedClient也需要去HOOK 连接对应的命名管道的名字的API,这样客户端与服务端才能建立通信.

一旦我们勾到CreateNamedPipe在NamedServer里面后,具体实现如下:

bool Hook_CreateNamedPipe(bool enable)
	{
		typedef HANDLE(WINAPI* CreateNamedPipeW_t)(LPCWSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, DWORD nOutBufferSize,
			DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes);
		static CreateNamedPipeW_t CreateNamedPipeW_old = reinterpret_cast<CreateNamedPipeW_t>(GetProcAddress(GetModuleHandleA("KernelBase"), "CreateNamedPipeW"));
		CreateNamedPipeW_t CreateNamedPipeW_hook = [](LPCWSTR lpName, DWORD dwOpenMode, DWORD dwPipeMode, DWORD nMaxInstances, DWORD nOutBufferSize,
			DWORD nInBufferSize, DWORD nDefaultTimeOut, LPSECURITY_ATTRIBUTES lpSecurityAttributes)->HANDLE
		{
#ifdef IS_NEED_VM
			VMProtectBegin("Hook_CreateNamedPipe");
#endif // IS_NEED_VM
			
#ifdef DEBUG_TYPE
			switch (DEBUG_TYPE)
			{
			case KART_NGS_LOG:
			case ALL_LOG:
				MyDbgPrintFun("Hook_CreateNamedPipe_NGS:%S, nMaxInstances:%d", lpName, nMaxInstances);
				break;
			}
#endif // DEBUG_TYPE

			// 即核心思想是: 取当前NamedServer的进程ID或者NamedClient的进程ID来做为重新命名的命名管道
			if (wcsstr(lpName, L"\\\\.\\pipe\\XXXX"))
			{
				const wchar_t *theBCNumberPoint = wcsstr(lpName, L"1"); // 命名管道特征
				lpName = L"\\\\.\\pipe\\";
				int 追加游戏进程BufferSize = 1 + log10(游戏进程ID);
				追加游戏进程BufferSize += 1 + log10(游戏线程ID);
				追加游戏进程BufferSize += 13;//XXXX\\占13个字符
				追加游戏进程BufferSize += lstrlenW(theBCNumberPoint) + 1;// 将XX每个服务器的命名拷贝回来
				wchar_t *追加游戏进程ID = new wchar_t[追加游戏进程BufferSize];
				StringCchPrintfW(追加游戏进程ID, 追加游戏进程BufferSize, L"%d%dXXXX\\", 游戏进程ID, 游戏线程ID);
				StringCchCatW(追加游戏进程ID, 追加游戏进程BufferSize, theBCNumberPoint);
				wchar_t *newObjectName = new wchar_t[lstrlenW(lpName) + 追加游戏进程BufferSize + 1];
				wmemset(newObjectName, 0, lstrlenW(lpName) + 追加游戏进程BufferSize + 1);
				StringCchCopyNW(newObjectName, lstrlenW(lpName) + 追加游戏进程BufferSize + 1, lpName, lstrlenW(lpName) + 1);
				StringCchCatW(newObjectName, lstrlenW(lpName) + 追加游戏进程BufferSize + 1, 追加游戏进程ID);
#ifdef DEBUG_TYPE
				switch (DEBUG_TYPE)
				{
				case KART_NGS_LOG:
				case ALL_LOG:
					MyDbgPrintFun("Hook_CreateNamedPipe_NGS:%S,%d, dwPipeMode:%d, dwOpenMode:%d", newObjectName, lstrlenW(newObjectName), dwPipeMode, dwOpenMode);
					break;
				}
#endif // DEBUG_TYPE

				return CreateNamedPipeW_old(newObjectName, dwOpenMode, dwPipeMode, nMaxInstances, nOutBufferSize, nInBufferSize, nDefaultTimeOut, NULL);
			}
#ifdef IS_NEED_VM
			VMProtectEnd();
#endif // IS_NEED_VM
			return CreateNamedPipeW_old(lpName, dwOpenMode, dwPipeMode, nMaxInstances, nOutBufferSize, nInBufferSize, nDefaultTimeOut, lpSecurityAttributes);
		};
		return DetourFunction(enable, reinterpret_cast<void**>(&CreateNamedPipeW_old), CreateNamedPipeW_hook);
	}

开源地址:源码地址

以上内容请勿使用非法用途,造成后果与本人无关.若有侵权,请联系笔者删除.