《地下城与勇士》(简称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++显示打开了两个客户端之后的窗口情况(有两个“地下城与勇士”的窗口同时出现): 
    最后,我建了一个小号,通过下图你就可以看出双开的情况(注意喇叭黄字): 
加不了图。。。。