《地下城与勇士》(简称DNF)我想就不用多介绍了,我也是这个游戏的一位爱好者。
熟悉DNF的人都知道,这个游戏主要就是分为刷图和PK两个部分,但是无论是哪个部分都可以利用双开工具收益,例如刷图双开还可以获得几次免费的深渊派队的机会,PK也可以用双开来刷决斗经验。
不过按理说DNF客户端并不允许大家双开,但是曾经见过网上流行一套驱动写的双开软件,是利用SSDT HOOK NtCreateMutant来实现的。最暴力但是有效的办法如下,其实就是拒绝掉所有创建的Mutant句柄:
int __stdcall HookSSDT_NtCreateMutant(int a1, int a2, int a3, int a4)
{
Old_NtCreteMutant(a1, a2, a3, a4);
return 0;
}
但问题是这个方法很暴力有危险不说,更重要的是现在已经不能这么办了。先看看DNF游戏在运行的过程中情况吧:运行游戏先是DNFChina.exe、QQLogin.exe以及TenSafe.exe,它们负责登陆以及环境初始化,还有就是所谓的可疑模块检查和完整性检测。然后登陆之后,即DNF.exe开始运行,这个时候就会加载驱动TenSafe.sys,我们的第一个难点就在这里,即DNF反外挂中隐藏的白名单。
还是老样子,TenSafe把NtOpenProcess中的ObOpenObjectByPointer的调用改了:
nt!NtOpenProcess+0x224:
805cc625 8d45dc lea eax,[ebp-24h]
805cc628 50 push eax
805cc629 ff75d4 push dword ptr [ebp-2Ch]
805cc62c e8c17a0000 call nt!PsLookupProcessByProcessId (805d40f2)
805cc631 ebde jmp nt!NtOpenProcess+0x1e7 (805cc611)
805cc633 8d45e0 lea eax,[ebp-20h]
805cc636 50 push eax
805cc637 ff75cc push dword ptr [ebp-34h]
805cc63a ff35b8495680 push dword ptr [nt!PsProcessType (805649b8)]
805cc640 56 push esi
805cc641 8d8548ffffff lea eax,[ebp-0B8h]
805cc647 50 push eax
805cc648 ff75c8 push dword ptr [ebp-38h]
805cc64b ff75dc push dword ptr [ebp-24h]
805cc64e e88d12bb28 call a917d8e0(还是老样子,把call nt!ObOpenObjectByPointer改了,不过里面的内容变化巨大。。。)
805cc653 8bf8 mov edi,eax
进入ObOpenObjectByPointer的函数之后,分为了3个板块:1、检查调用者是不是属于系统进程(即System、Csrss、smss一类的);2、检查是不是特殊的白名单进程(不过我的机器上好像没有这种情况);3、检查是不是腾讯相关软件的进程(例如QQ.exe、TXPlatform.exe)。如果说是白名单进程,TenSafe会直接放行,这样一来就给了我们一个可乘之机,找一个已知的白名单进程,并且可以注入的,我在那个表中果然找到了svchost.exe,因此这个进程就成了我们的载体(当然事实上后来才发现,对于用户名为SYSTEM的svchost进程,注入且运作才是有效的):
lkd> dd 86c6a9ac
86c6a9ac 86d94504 86dd75ac 00000100 0a0a0007
lkd> dd 86d94504-24
86d944e0 86ccf888 00000500 000fffff b494762b
PROCESS 86db0a88 SessionId: 0 Cid: 0354 Peb: 7ffdf000 ParentCid: 047c
DirBase: 07700160 ObjectTable: e2f60b00 HandleCount: 174.
Image: svchost.exe
因此,找到了载体,我们接下来就需要找对互斥句柄了,不过一些辅助工具给了我们明确的信息,其实甚至在网上都能查到,只不过别人的很多是加载驱动的版本,当然我们需要不利用加载驱动的方式而是通过用户模式来解决。要远程关掉DNF.exe进程中的互斥句柄,仅需要找对对象之后,利用Native函数ZwDuplicateObject来实现。具体的代码如下:
void RunDNFSK()
{
NTSTATUS status = 0;
DWORD buflen = 256, needlen = 0;
DWORD HandleCnt = 0;
OBJECT_ATTRIBUTES objatr;
UINT i;
PSYSTEM_HANDLE_INFORMATION pBuf = NULL;
PSYSTEM_HANDLE_TABLE_ENTRY_INFO pSysHandleInfo = NULL;
//初始化对对象属性
InitializeObjectAttributes(&objatr,0,0,0,0);
//获得句柄表
do
{
//申请查询句柄信息所需的内存
ZwAllocateVirtualMemory(NtCurrentProcess(),(PVOID*)&pBuf,0,&buflen,MEM_COMMIT,PAGE_READWRITE);
//查询系统句柄信息
status=ZwQuerySystemInformation(SystemHandleInformation,(PVOID)pBuf,buflen,&needlen);
if( status==STATUS_SUCCESS )
break; //申请成功,退出执行下步
//不成功:则释放内存,以返回的needlen可以作参考,需要申请一块足够大的内存
ZwFreeVirtualMemory(NtCurrentProcess(),(PVOID*)&pBuf,&buflen,MEM_RELEASE);
//然后把要申请的内存大小乘2,直至成功为止
buflen*=2;
pBuf=NULL;
} while(1);
//返回的缓冲区内容的第一个DWORD是总的句柄的个数
HandleCnt = pBuf->NumberOfHandles;
//跳过句柄计数,句柄信息真正的开始
pSysHandleInfo = (PSYSTEM_HANDLE_TABLE_ENTRY_INFO)( (char*)pBuf + sizeof(DWORD) );
for( i = 0; i < HandleCnt; i++,pSysHandleInfo++/*指向下一个结构*/ )
{
// 初始化句柄信息
HANDLE hOwner = NULL;
HANDLE hDupHandle = NULL;
// 以复制句柄方式打开进程:hOwner
hOwner=OpenProcess(PROCESS_DUP_HANDLE,FALSE,pSysHandleInfo->UniqueProcessId);
// 复制进程到可操作的范围:hDupHandle
ZwDuplicateObject(hOwner,(HANDLE)pSysHandleInfo->HandleValue,GetCurrentProcess(), &hDupHandle,PROCESS_ALL_ACCESS, FALSE, DUPLICATE_SAME_ACCESS);
if( hDupHandle == NULL )
{
// PRINT("PID:%d DuplicateHandleFailed\n\n",pSysHandleInfo->UniqueProcessId);
goto DuplicateHandleFailed;
}
// 句柄复制成功
if( hDupHandle )
{
// 获取句柄的基本信息:
OBJECT_BASIC_INFORMATION ObjBasicInfo = {0};
status=ZwQueryObject(hDupHandle,ObjectBasicInformation,&ObjBasicInfo, sizeof(ObjBasicInfo), NULL);
// 获取句柄的类型信息
POBJECT_TYPE_INFORMATION pTypeInfo = NULL;
pTypeInfo=(POBJECT_TYPE_INFORMATION)malloc( 2*ObjBasicInfo.TypeInfoSize );
memset( pTypeInfo, 0, 2*ObjBasicInfo.TypeInfoSize);
status=ZwQueryObject(hDupHandle,ObjectTypeInformation,pTypeInfo,2*ObjBasicInfo.TypeInfoSize, NULL);
// 类型名称判断
if( status == STATUS_SUCCESS &&
wcsncmp(pTypeInfo->TypeName.Buffer, L"Mutant", 6) == 0 ||
wcsncmp(pTypeInfo->TypeName.Buffer, L"Section", 7) == 0 )
{
// 获取句柄的名称信息
if( ObjBasicInfo.NameInfoSize )
{
POBJECT_NAME_INFORMATION pNameInfo = NULL;
pNameInfo=(POBJECT_NAME_INFORMATION)malloc( 2*ObjBasicInfo.NameInfoSize );
memset( pNameInfo, 0, 2*ObjBasicInfo.NameInfoSize);
status = ZwQueryObject(hDupHandle, ObjectNameInformation, pNameInfo, 2*ObjBasicInfo.NameInfoSize, NULL);
// 对获取的句柄值进行筛选(Start);
if(status == STATUS_SUCCESS && pNameInfo->Name.Buffer != NULL)
{
// PRINT("ObjName = %ws\n\n", pNameInfo->Name.Buffer);
}
else
{
// PRINT("\n\n");
}
// 操作数进行操作
int OperateValue = 0; // 操作数
OperateValue = FindSpecialHandle( (PUNICODE_STRING)pNameInfo );
if( OperateValue == 1 )
{
PRINT("%ws\n", pTypeInfo->TypeName.Buffer);
PRINT("PID = %d\n", pSysHandleInfo->UniqueProcessId);
PRINT("HandleValue = 0x%0.8X\n", pSysHandleInfo->HandleValue);
PRINT("ObjName = %ws\n\n", pNameInfo->Name.Buffer);
PRINT("Find The Handle Need to Close\n\n");
ZwDuplicateObject(hOwner, (HANDLE)pSysHandleInfo->HandleValue, 0, 0, 0, 0, DUPLICATE_CLOSE_SOURCE);//(关闭远程句柄)
}
// 对获取的句柄值进行筛选(End);
}
}
// 关闭操作的相关句柄
if(hDupHandle != NULL)
CloseHandle(hDupHandle);
}
// 关闭打开进程的句柄
DuplicateHandleFailed:
if(hOwner != NULL)
CloseHandle(hOwner);
}
}
我们只需要把这段代码注入到一个用户名为SYSTEM的svchost进程,然后就能利用TenSafe中自带的白名单机制,关闭远程进程(DNF.exe)的互斥句柄。但是,光是这样还不够,因为后来腾讯又增加了一种新的检测方案,既是窗口枚举,用Spy++可以看到DNF游戏的窗口句柄。
当我再开一个客户端的时候,扫描就会检测窗口句柄,一旦有“地下城与勇士”的窗口,新客户端游戏的窗口仍然不会显示出来,所以我们还需要加入其它的处理方式。
思路就是,把DNF.exe的“地下城与勇士”窗口,设置为一个已知窗口的子窗口进程(利用函数SetParent),然后再等到新游戏窗口出现后,重新将其恢复。
这样,就能再新开客户端的时候,躲避检测。因此执行流程就是:打开第一个DNF之后,远程关闭句柄,隐藏窗口,然后待打开第二个DNF之后,恢复第一个DNF的窗口即实现双开。利用SPY++显示打开了两个客户端之后的窗口情况(有两个“地下城与勇士”的窗口同时出现):
最后,我建了一个小号,通过下图你就可以看出双开的情况(注意喇叭黄字):
加不了图。。。。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!