论外挂与反外挂

文章作者:hellfish

道高一尺,魔高一丈

前言:
  自<<石器时代>>开始, 外挂这一名词渐渐为世人所知. 到盛大第一款网游<<传奇>> 刚开始的宣传语 <永无外挂>
基本上所有的玩家都知道了有个叫外挂的玩意. 等到了<<MU>>基本上玩游戏的都是人手一挂.
但是当时的运营商并没有怎么把反外挂放在心上-当时游戏的模式只是点卡与月卡. 只要你玩游戏,总是要付费的.
运营商需要担心的,只是外挂会造成多少用户流失而已. <<石器>>的运营商很绝--干脆自己卖外挂.
而 <<传奇>>,<<MU>>则谣传与外挂分成. 这段时间乃是外挂的黄金期.
但是从某外挂的作者被判刑后外挂似乎一下子收敛了很多. 但是实际的情况呢?

  到了如今<免费游戏>大行其道,而且虚拟物品交易越来越成熟的今天,外挂这东西一下被各运营商列为了头号打击对象.
为何运营商一下积极了那么多?
原因很简单: 假设运营商出售某虚拟道具价值10元,这道具是消耗品,而且每个玩家都需要. 如果某外挂提供一样的功能.
你说运营商要损失多少? 又假设运营商提供个途径获得某道具需要玩家花费100元.
但是为了保留不花钱的玩家好让花钱的玩家去折腾,总不可能不让游戏里出吧.
即使几率再低也挡不住机器人24小时不停的折腾然后拿到5173之类的地方挂个50元卖. 这又损失多少?
于是呼,游戏内游戏外.网上现实中. 打击外挂成了运营商们的头号大事.

  外挂怎么办? 写挂赚钱的又怎么办? [- 好吧.我承认我也是外挂作者大军中的一员. 而且很早就开始从事这一行业.  ]
似乎外挂一夜间就混不下去了... 这就错了. 在虚拟物品交易广为人知的如今. 既然游戏运营可以换种模式赚更多的钱!
外挂为什么不可以?

  如今的外挂模式是怎么样的? 以某人为例子, 某人为三个工作室专门提供外挂的更新与制作,一个游戏一个月收维护费x元.
而工作室则24小时挂机器人打宝,打钱然后出售.单单这三家工作室便有超过500台机机器在从事光荣的任务.
运营商会损失多少的利益? 而且以前那套对付外挂的办法还拿工作室没奈何.

  于是呼又诞生了一种职称: 网络游戏反外挂工程师.

  但是目前从事这一职业者多半是软件安全出身. 绝大多数没有开发过外挂. -- 你都不知道别人怎么攻击,何谈有效的防御呢?
这便是我写此文的原因.

驱动反外挂 - 看起来很美
一. 论驱动反外挂
  随着国内软件安全行业的发展,驱动这一名词逐渐被摘去神秘的光环. 而3721的出现,告诉了人们驱动这东西不仅仅是用于硬件
越来越多的人认识到驱动的巨大作用,当<<MU>>引入了 nProtect 反外挂系统后,似乎驱动反外挂成了相当理想的选择.

  但这一切,只是看起来很美. 随着越来越多的ROOTKIT出现,各大杀毒厂商逐渐的加强了这一方面的监控. 越来越多的各类监控
软件也使得驱动反外挂举步维难.

  在进入正题之前,首先要明确一点. 你的驱动将是游戏客户端的组成部分, 很多ROOTKIT上可以用的手段你不能使用.
游戏玩家并不是专业人士,他们更相信他们所选择的杀毒软件. 总不能当你的游戏运行时,杀毒软件便提示说 - 这是个ROOTKIT

  首先我们抛开驱动的兼容性不谈 - 这也没法谈, 正如你驾驶汽车,你可以保证自己不出错. 但是你能保证其他人都能吗?
说到驱动反外挂,你应该立马想到 HOOK SSDT与SSSDT 拦截API防止游戏进程被修改. 可是这真的那么有效吗?
 
  好吧,你想说阻止 OpenProcess,ReadProcessMemory,WriteProcessMemory 这三个API就好? 不 - 相信我,这只能防防菜鸟而已.
即使你不考虑兼容性把 PsLookupProcessByProcessId,ObOpenObjectByPointer,ObOpenObjectByName,KeAttachProcess 等
全部HOOK,真的就能阻止修改了吗?

  不,我们来看看下面的代码.
 

代码:
  1. Function GetInfoTable(ATableType:dword):Pointer;  
  2. var  
  3. mSize: dword;  
  4. mPtr: pointer;  
  5. St: NTStatus;  
  6. begin  
  7. Result := nil;  
  8. mSize := $4000;   
  9. repeat  
  10.    mPtr := VirtualAlloc(nil, mSize, MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE);  
  11.    if mPtr = nil then Exit;  
  12.    St := ZwQuerySystemInformation(ATableType, mPtr, mSize, nil);  
  13.    if St = STATUS_INFO_LENGTH_MISMATCH then  
  14.       begin  
  15.         VirtualFree(mPtr, 0, MEM_RELEASE);  
  16.         mSize := mSize * 2;  
  17.       end;  
  18. until St <> STATUS_INFO_LENGTH_MISMATCH;  
  19. if St = STATUS_SUCCESS  
  20.    then Result := mPtr  
  21.    else VirtualFree(mPtr, 0, MEM_RELEASE);  
  22. end;  
  23.   
  24. function iOpenProcess(ProcessId:DWORD):DWORD;  
  25. var  
  26.         HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;  
  27.         ClientID:TClientID;  
  28.         pbi:_PROCESS_BASIC_INFORMATION;  
  29.         oa:TObjectAttributes;  
  30.         hProcessCur,hProcessToDup,hProcessToRet:DWORD;  
  31.         Ret:DWORD;  
  32.         I:Integer;  
  33. begin  
  34.         SetPrivilege('SE_DEBUG',TRUE);  
  35.         Result:=0;  
  36.         FillChar(oa,SizeOf(TObjectAttributes),0);  
  37.         FillChar(ClientID,SizeOf(TClientID),0);  
  38.         oa.Length:=SizeOf(TObjectAttributes);  
  39.   
  40.         HandlesInfo:=GetInfoTable(SystemHandleInformation);  
  41.   
  42.         for I:=0 to HandlesInfo^.NumberOfHandles do  
  43.         begin  
  44.                 If (HandlesInfo^.Information.ObjectTypeNumber=5) Then //OB_TYPE_PROCESS  
  45.                         ClientID.UniqueProcess:=HandlesInfo^.Information.ProcessId;  
  46.                         If ZwDuplicateObject(hProcessToDup,HandlesInfo^.Information.Handle,GetCurrentProcess,@hProcessCur,PROCESS_ALL_ACCESS,0,$4)=STATUS_SUCCESS then  
  47.                                 If ZwQueryInformationProcess(hProcessCur,ProcessBasicInformation,@pbi,Sizeof(_PROCESS_BASIC_INFORMATION),@Ret)=STATUS_SUCCESS then  
  48.                                         If (pbi.UniqueProcessId=ProcessId) Then  
  49.                                                 If ZwDuplicateObject(hProcessToDup,HandlesInfo^.Information.Handle,GetCurrentProcess,@hProcessToRet,PROCESS_ALL_ACCESS,0,$4)=STATUS_SUCCESS then  
  50.                                                 begin  
  51.                                                         Result:=hProcessToRet;  
  52.                                                         Break;  
  53.                                                 end;  
  54.         end;  
  55.   
  56.         if hProcessCur>0 then ZwClose(hProcessCur);  
  57.         if hProcessToDup>0 then ZwClose(hProcessToDup);  
  58.         VirtualFree(HandlesInfo,0,MEM_RELEASE);  
  59.         SetPrivilege('SE_DEBUG',FALSE);  
  60. end;  

这是枚举系统中所有已知举柄达到取得进程Handle的函数. 你或许会认为,拦截ZwDuplicateObject,ZwQueryInformationProcess不就解决问题了?
这没错,你是对的.但是你不能这样做,你做的是反外挂,不是ROOTKIT, 当你尝试这样做的时候,你会发现你的杀毒软件提示你. 这是ROOTKIT的典型行为
怎么办? 难道你要象ROOTKIT那样关闭掉玩家的杀毒软件? 还是联系各大杀毒软件厂商告诉他们: 麻烦您修改你们的规则?

这仅仅是RING 3的普通运用而已, 千万不要认为做外挂的不会驱动. 相反,与游戏开发公司那点可怜的薪水比起来. 外挂的利润只会让更多的驱动开发者
加入这一行列. 即使你HOOK接管了这一切函数,不管是inline还是普通的ssdt. 下面的驱动很轻易的就能突破任意的HOOK

 

代码
  1. .....................  
  2. NTSTATUS NTAPI GetRealAddress(PIMPORT_ENTRY Import)  
  3. {  
  4.         MODULE_INFORMATION mi,idmi;  
  5.         DWORD        i,j;  
  6.         DWORD        dwKernelBase;  
  7.         NTSTATUS        status;  
  8.         PDWORD        KiServiceTable;  
  9.         UNICODE_STRING NtdllName;  
  10.   
  11. if (KeGetCurrentIrql()!=PASSIVE_LEVEL) return STATUS_PASSIVE_LEVEL_REQUIRED;  
  12.   
  13. RtlZeroMemory(&mi,sizeof(mi));  
  14. if (!NT_SUCCESS(status=MapKernelImage(&mi,&dwKernelBase))) return status;  
  15.   
  16. RtlZeroMemory(&idmi,sizeof(idmi));  
  17. RtlInitUnicodeString(&NtdllName, L"\\SystemRoot\\System32\\ntdll.dll");                  
  18. if (!NT_SUCCESS(status=MapPeImage(&idmi,&NtdllName))) return status;  
  19.   
  20. try {  
  21.                 for (i=0;Import.szName;i++){  
  22.                         Import.dwAddress=0;  
  23.                         switch (Import.dwType) {  
  24.                                 case IMPORT_BY_NAME:  
  25.                                         if (!(Import.dwAddress=GetProcRva(mi.hModule,Import.szName))) {  
  26. #ifdef DEBUG  
  27.                                                 DbgPrint("GetRealAddress(): Failed to get %s rva!\n",Import.szName);  
  28. #endif  
  29.                                         }  
  30.                                         break;  
  31.                                 case IMPORT_BY_RVA:  
  32.                                         Import.dwAddress=(DWORD)Import.szName;  
  33.                                         break;  
  34.                                 case IMPORT_BY_ADDRESS:  
  35.                                         Import.dwAddress=(DWORD)Import.szName-dwKernelBase;  
  36.                                         break;  
  37.                                 case IMPORT_BY_SERVICE_ID:  
  38.                                         // do not search this rva if it has been already found  
  39.                                         if (!KiServiceTable_RVA) {  
  40.                                                 if (!(KiServiceTable_RVA=FindKiServiceTable(mi.hModule))) {  
  41. #ifdef DEBUG  
  42.                                                         DbgPrint("GetRealAddress(): Failed to get KiServiceTable RVA!\n");  
  43. #endif  
  44.                                                         break;  
  45.                                                 }  
  46.                                         }  
  47.                                         KiServiceTable=(PDWORD)(KiServiceTable_RVA+mi.hModule);  
  48.                                         Import.dwAddress=KiServiceTable[(DWORD)Import.szName]-mi.dwImageBase;  
  49.                                         break;  
  50.                                 case IMPORT_BY_SERVICE_NAME:  
  51.                                         if (!KiServiceTable_RVA){  
  52.                                         if (!(KiServiceTable_RVA=FindKiServiceTable(mi.hModule)))        break;  
  53.                                         }  
  54.                                         Import.dwId=GetIdForName(idmi.hModule,Import.szName);  
  55.                                         KiServiceTable=(PDWORD)(KiServiceTable_RVA+mi.hModule);  
  56.                                         Import.dwAddress=KiServiceTable[Import.dwId]-mi.dwImageBase;  
  57.                                         break;  
  58.                                 default:  
  59.                                         break;  
  60.                         } //Case End  
  61.   
  62. if (Import.dwId==0){  
  63. if (!KiServiceTable_RVA)  
  64. KiServiceTable_RVA=FindKiServiceTable(mi.hModule);  
  65. KiServiceTable=(PDWORD)(KiServiceTable_RVA+mi.hModule);  
  66. for (j=0;KiServiceTable[j];j++){if (Import.dwAddress==KiServiceTable[j]-mi.dwImageBase){Import.dwId=j;break;}}          
  67. }  
  68. Import.dwAddress=dwKernelBase+Import.dwAddress;  
  69. }  
  70. }except(EXCEPTION_EXECUTE_HANDLER){  
  71. return STATUS_ADD_FUNCTION_FAILED;  
  72. }  
  73.   
  74. try {  
  75. UnmapPeImage(&mi);  
  76. UnmapPeImage(&idmi);  
  77. }except(EXCEPTION_EXECUTE_HANDLER){  
  78. return STATUS_CODE_REBUILDING_FAILED;  
  79. }  
  80.   
  81. return STATUS_SUCCESS;  
  82. }  
  83. ...........  

恩..这不是完整的代码,这理所当然,不是么?
面对任何HOOK,只需要从NT的内核文件中取出其真实的地址,很轻易的就可以饶过SSDT的HOOK,INLINE HOOK只需要恢复代码即可.
更何况你的驱动肯定会比外挂的驱动还晚加载.
即使除开上面这些不谈,你依然要面对你的驱动被PATCH,又或者被个假冒的驱动所替代. 更别说 lpk.dll usp10.dll 了.

  这时候你应该会想反驳我,看看 nPROTECT ,安博士 吧. 好的,那么我们来看看下面这段函数

 

 

代码
  1. TSTATUS ReadPhysicalMemory(char *startaddress, UINT_PTR bytestoread, void *output)  
  2. {  
  3.         HANDLE                        physmem;  
  4.         UNICODE_STRING        physmemString;  
  5.         OBJECT_ATTRIBUTES attributes;  
  6.         WCHAR                        physmemName[] = L"\\device\\physicalmemory";  
  7.         UCHAR*                        memoryview;  
  8.         NTSTATUS                ntStatus = STATUS_UNSUCCESSFUL;  
  9.   
  10.         __try  
  11.         {  
  12.                 RtlInitUnicodeString( &physmemString, physmemName );          
  13.   
  14.                 InitializeObjectAttributes( &attributes, &physmemString, OBJ_CASE_INSENSITIVE, NULL, NULL );          
  15.                 ntStatus=ZwOpenSection( &physmem, SECTION_MAP_READ, &attributes );  
  16.                 if (ntStatus==STATUS_SUCCESS)  
  17.                 {  
  18.                         //hey look, it didn't kill it  
  19.   
  20.   
  21.                         UINT_PTR length;  
  22.                         PHYSICAL_ADDRESS        viewBase;  
  23.                         UINT_PTR offset;  
  24.                         UINT_PTR toread;  
  25.   
  26.                         viewBase.QuadPart = (ULONGLONG)(startaddress);                                          
  27.                           
  28.                         length=0x2000;//pinp->bytestoread; //in case of a overlapping region  
  29.                         toread=bytestoread;  
  30.   
  31.                         memoryview=NULL;  
  32.   
  33.                         DbgPrint("ReadPhysicalMemory:viewBase.QuadPart=%x", viewBase.QuadPart);   
  34.   
  35.   
  36.                         ntStatus=ZwMapViewOfSection(  
  37.                                 physmem,  //sectionhandle  
  38.                                 NtCurrentProcess(), //processhandle (should be -1)  
  39.                                 &memoryview, //BaseAddress  
  40.                                 0L, //ZeroBits  
  41.                                 length, //CommitSize  
  42.                                 &viewBase, //SectionOffset  
  43.                                 &length, //ViewSize  
  44.                                 ViewShare,  
  45.                                 0,  
  46.                                 PAGE_READWRITE);  
  47.   
  48.                         if (ntStatus==STATUS_SUCCESS)  
  49.                         {  
  50.                                 offset=(UINT_PTR)(startaddress)-(UINT_PTR)viewBase.QuadPart;  
  51.                                 RtlCopyMemory(output,&memoryview[offset],toread);  
  52.   
  53.                                 ZwUnmapViewOfSection( NtCurrentProcess(), memoryview);  
  54.                         }  
  55.                         else  
  56.                         {  
  57.                                 DbgPrint("ReadPhysicalMemory:ntStatus=%x", ntStatus);   
  58.                         }  
  59.   
  60.                         ZwClose(physmem);  
  61.                 };  
  62.   
  63.         }  
  64.         __except(1)  
  65.         {  
  66.                 DbgPrint("Error while reading physical memory\n");  
  67.         }  
  68.   
  69.         return ntStatus;  
  70. }  

直接读取物理内存, 到目前为止,这个方法依然对 nPROTECT 保护的进程有效.

实际上反外挂的驱动能拦截的不过是API而已, 你能拦截 mov eax,[xxxxxxx] 吗?
别忘记,你在驱动中采取的手段越多,驱动的兼容性必定越差.
在家中的玩家还好说,可是面对目前主要的玩家多数在网吧上网的情况,你不的不考虑各种网吧管理软件.
这样的情况,不谈兼容性光是你的驱动到底有没有机会被加载还是个问题....
即使是在家中上网的玩家,你难道要告诉使用 Vista 或者 Windows 7 的普通用户: 请关闭你的UAC

好吧,再这样写下去简直没完没了.  综上所述, 驱动反外挂, 这只是看起来很美而已.


怎么办?

二. 如何有效的阻止外挂
  前言中提到,要有效的反外挂,必先了解外挂如何运作.  在前文中,也描述了当前外挂主要的运作模式. 现在外挂已不是要求什么三步瞬移,格位刺杀之类的特殊功能了,对于工作室.
他们的需要仅仅是稳定的机器人,如果游戏提供的话,他们常常还需要能够把挂机角色上的金钱物品邮寄或者交易给某个账号的功能. 那么制作一个这样的机器人至少需要的是什么?
1. 游戏角色的生命值,魔法值之类的数据
2. 游戏角色的物品数据
3. 游戏角色周围的怪物数据
4. 移动函数
5. 热键函数 [假如客户端接受 SendMessage 模拟键盘这样的消息,这不需要]
6. 选中怪物函数
7. 打开NPC函数
8. 打开仓库函数
9. 交易或邮寄函数
其中的 4-9 可以被一个数据包发送函数所替代,例如

 

 

C++
  1. procedure SendPack(buf:PChar;len:DWORD); stdcall;  
  2.   
  3. procedure TOSEND; stdcall;  
  4. asm  
  5. push    -1  
  6. push    SENDPACK_STAK  
  7. mov     eax, dword ptr fs:[0]  
  8. push    eax  
  9. mov     dword ptr fs:[0], esp  
  10. sub     esp,$18  
  11. push    ebx  
  12. push    esi  
  13. push    edi  
  14. mov     edi, ecx  
  15. xor     ebx, ebx  
  16. xor     eax, eax  
  17. jmp     SENDPACK_JMP  
  18. end;  
  19.   
  20. begin  
  21. asm  
  22. pushad;  
  23. mov  ecx, [CALL_BASE];  
  24. push len;  
  25. push buf;  
  26. mov  ecx, [ecx+$20];  
  27. call TOSEND;  
  28. popad;  
  29. end;  
  30. end;  
  31.   
  32. procedure SendBuyItem(ItemId,ItemPos,ItemCount:DWORD);  
  33. var  
  34. //25 00 01 00 00 00 14 00 00 00 00 00 00 00 01 00 00 00 AA 21 00 00 01 00 00 00 01 00 00 00  
  35. //25 00 01 00 00 00 ByteCount 00 00 00 00 GroupCount ItemId ItemPos ItemCount  
  36. Pack:Array [0..29] of Byte;  
  37. begin  
  38. FillChar(Pack,SizeOf(Pack),0);  
  39. Pack[0]:=$25;  
  40. Pack[2]:=$01;  
  41. Pack[6]:=$14;  
  42. Pack[14]:=$01;  
  43. CopyMemory(@Pack[18],@ItemId,4);  
  44. CopyMemory(@Pack[22],@ItemPos,4);  
  45. CopyMemory(@Pack[26],@ItemCount,4);  
  46. SendPack(@Pack[0],30);  
  47. end;  

归根结底, 要反外挂,主要防御的只有两点:
1. 防止外部修改内存
2. 防止外部调用函数

对于第一点,比如修改某个怪物的数据,使得客户端判断该怪物在游戏角色的攻击范围之内.
最佳的解决办法不是去HOOK什么内存读写函数. 而是把判断这些数据的责任交给服务器端.
可如果是引进的游戏呢? 解决办法便是CRC32或者别的什么HASH算法校验这段内存数据.

对于第二点,最简单的办法便是在函数内取得 ESP 判断函数的返回地址. 以上面的那段函数为例.
只要游戏开发商稍微更改一下他的发包函数,判断下call 的来源, 我想这已经会让外挂的作者头痛
很久.

实质上反外挂是否有效最大的前提,在于不能让反外挂机制被饶过. 仅仅是单纯的客户端保护,作用非常有限.
对于没有代码的情况下,比较简单的解决办法:
1. 客户端反外挂DLL中 HOOK connect,recv,send. 在 connect 时使其连接到反外挂服务器端.
    保留connect所使用的socket, 在recv , send 中判断该socket对数据进行二次加密/解密

2. 反外挂服务器端监控客户端的情况,如有异常则中断用户数据的转发.


怎么做?

  下面以国内完美公司的游戏,<<完美世界>>作为改造对象.以自调试做为手段
为elementclient.exe增加一个对抗外部调用的方法.

我们先来看看<<完美世界>>的发包函数

 

 

代码
  1. 005B7BD0  /$  6A FF         push -1  
  2. 005B7BD2  |.  68 68A68800   push 0088A668  
  3. 005B7BD7  |.  64:A1 0000000>mov eax, dword ptr fs:[0]  
  4. 005B7BDD  |.  50            push eax  
  5. 005B7BDE  |.  64:8925 00000>mov dword ptr fs:[0], esp  
  6. 005B7BE5  |.  83EC 18       sub esp, 18  
  7. 005B7BE8  |.  53            push ebx  
  8. 005B7BE9  |.  56            push esi  
  9. 005B7BEA  |.  57            push edi  
  10. 005B7BEB  |.  8BF9          mov edi, ecx  
  11. 005B7BED  |.  6A 07         push 7                                   ; /Arg1 = 00000007  
  12. 005B7BEF  |.  E8 FC390D00   call 0068B5F0                            ; \elementc.0068B5F0  
  13. 005B7BF4  |.  33DB          xor ebx, ebx  
  14. 005B7BF6  |.  33C0          xor eax, eax  
  15. 005B7BF8  |.  83C4 04       add esp, 4  
  16. 005B7BFB  |.  894424 18     mov dword ptr [esp+18], eax  
  17. 005B7BFF  |.  895C24 1C     mov dword ptr [esp+1C], ebx  
  18. 005B7C03  |.  895C24 20     mov dword ptr [esp+20], ebx  
  19. 005B7C07  |.  C74424 14 086>mov dword ptr [esp+14], 008A6C08  
  20. 005B7C0F  |.  C74424 0C 183>mov dword ptr [esp+C], 008B3818  
  21. 005B7C17  |.  C74424 10 220>mov dword ptr [esp+10], 22  
  22. 005B7C1F  |.  8B7424 38     mov esi, dword ptr [esp+38]  
  23. 005B7C23  |.  895C24 2C     mov dword ptr [esp+2C], ebx  
  24. 005B7C27  |.  3BF3          cmp esi, ebx  
  25. 005B7C29  |.  76 2D         jbe short 005B7C58  
  26. 005B7C2B  |.  8D46 FF       lea eax, dword ptr [esi-1]  
  27. 005B7C2E  |.  B9 02000000   mov ecx, 2  
  28. 005B7C33  |.  D1E8          shr eax, 1  
  29. 005B7C35  |.  894C24 20     mov dword ptr [esp+20], ecx  
  30. 005B7C39  |.  74 0A         je short 005B7C45  
  31. 005B7C3B  |>  D1E1          /shl ecx, 1  
  32. 005B7C3D  |.  D1E8          |shr eax, 1  
  33. 005B7C3F  |.^ 75 FA         \jnz short 005B7C3B  
  34. 005B7C41  |.  894C24 20     mov dword ptr [esp+20], ecx  
  35. 005B7C45  |>  51            push ecx                                 ; /size  
  36. 005B7C46  |.  53            push ebx                                 ; |block  
  37. 005B7C47  |.  FF15 6C548A00 call dword ptr [<&MSVCRT.realloc>]       ; \realloc  
  38. 005B7C4D  |.  83C4 08       add esp, 8  
  39. 005B7C50  |.  894424 18     mov dword ptr [esp+18], eax  
  40. 005B7C54  |.  894424 1C     mov dword ptr [esp+1C], eax  
  41. 005B7C58  |>  8B4C24 34     mov ecx, dword ptr [esp+34]  
  42. 005B7C5C  |.  56            push esi                                 ; /n  
  43. 005B7C5D  |.  51            push ecx                                 ; |src  
  44. 005B7C5E  |.  50            push eax                                 ; |dest  
  45. 005B7C5F  |.  FF15 44548A00 call dword ptr [<&MSVCRT.memmove>]       ; \memmove  
  46. 005B7C65  |.  8B5424 24     mov edx, dword ptr [esp+24]  
  47. 005B7C69  |.  83C4 0C       add esp, 0C  
  48. 005B7C6C  |.  8D4424 0C     lea eax, dword ptr [esp+C]  
  49. 005B7C70  |.  03D6          add edx, esi  
  50. 005B7C72  |.  53            push ebx  
  51. 005B7C73  |.  50            push eax  
  52. 005B7C74  |.  8BCF          mov ecx, edi  
  53. 005B7C76  |.  895424 24     mov dword ptr [esp+24], edx  
  54. 005B7C7A  |.  E8 B1DAFFFF   call 005B5730  
  55. 005B7C7F  |.  8B4C24 18     mov ecx, dword ptr [esp+18]  
  56. 005B7C83  |.  8AD8          mov bl, al  
  57. 005B7C85  |.  51            push ecx                                 ; /block  
  58. 005B7C86  |.  C74424 18 086>mov dword ptr [esp+18], 008A6C08         ; |  
  59. 005B7C8E  |.  FF15 68548A00 call dword ptr [<&MSVCRT.free>]          ; \free  
  60. 005B7C94  |.  8B4C24 28     mov ecx, dword ptr [esp+28]  
  61. 005B7C98  |.  83C4 04       add esp, 4  
  62. 005B7C9B  |.  8AC3          mov al, bl  
  63. 005B7C9D  |.  64:890D 00000>mov dword ptr fs:[0], ecx  
  64. 005B7CA4  |.  5F            pop edi  
  65. 005B7CA5  |.  5E            pop esi  
  66. 005B7CA6  |.  5B            pop ebx  
  67. 005B7CA7  |.  83C4 24       add esp, 24  
  68. 005B7CAA  \.  C2 0800       ret 8  

 

对比上面的发包CALL的例子,可以发现 005B7BD0 - 005B7BF6 均由外挂完成, 而达到绕过 005B7BEF call 0068B5F0 的目的.

那么在这里,挑选 005B7BF8  |.  83C4 04       add esp, 4 作为监控点.
这个例子中,首先我们修改 elementclient.exe 文件, 将 005B7BF8 处的代码改为 CC 90 90. 方便写我们的调试器代码

接下来我们看看我们将用于防止外部调用的调试器部分

代码
  1. program AntiCall;  
  2. {$APPTYPE CONSOLE}  
  3. uses Windows,Sysutils;  
  4. Const  
  5. WINDOW_TITLE='Element Client';  
  6. WINDOW_CLASS='ElementClient Window';  
  7. THREAD_ALL_ACCESS=STANDARD_RIGHTS_REQUIRED or SYNCHRONIZE or $3FF;  
  8. CHECKPOINT_ADDR=$005B7BF8;  
  9.   
  10. function OpenThread(dwDesiredAccess:DWORD;bInheritHandle:Boolean;dwThreadId:DWORD):THANDLE; stdcall external kernel32 name 'OpenThread';  
  11.   
  12. procedure MainLoop;  
  13. var  
  14.   hW2iThread,hW2iProcess,dwW2iThreadId,dwW2iProcessId,hW2iWnd:DWORD;  
  15.   
  16.   DebugEv:DEBUG_EVENT;  
  17.   Regs:CONTEXT;  
  18.   dwContinueStatus,WorkBytes,dwCallRet:DWORD;  
  19.   
  20.   fp:THandle;  
  21.   hThread:DWORD;  
  22. begin  
  23.   FillChar(Regs,SizeOf(CONTEXT),0);  
  24.   dwW2iThreadId:=0;  
  25.   dwW2iProcessId:=0;  
  26.   hW2iProcess:=0;  
  27.   hW2iThread:=0;  
  28.   fp:=0;  
  29.   
  30.         hW2iWnd:=FindWindow(WINDOW_CLASS, nil);  
  31.         if( hW2iWnd>0) then dwW2iThreadId:=GetWindowThreadProcessId(hW2iWnd,@dwW2iProcessId);  
  32.   if (dwW2iProcessId>0) then hW2iProcess:=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwW2iProcessId);  
  33.   if (hW2iProcess>0) then hW2iThread:=OpenThread(THREAD_ALL_ACCESS, FALSE, dwW2iThreadId);  
  34.     
  35.   if (hW2iThread>0) then  
  36.   if DebugActiveProcess(dwW2iProcessId) then  
  37.   begin  
  38.                 while(TRUE) do  
  39.                 begin  
  40.                         if (WaitForDebugEvent(DebugEv,10)) then  
  41.                         begin  
  42.                                 dwContinueStatus:=DBG_EXCEPTION_NOT_HANDLED;  
  43.   
  44.                                 Case DebugEv.dwDebugEventCode of  
  45.   
  46.                                   EXCEPTION_DEBUG_EVENT:  
  47.                                         begin  
  48.                                                 if(DWORD(DebugEv.Exception.ExceptionRecord.ExceptionAddress)=CHECKPOINT_ADDR) then  
  49.                                                 begin  
  50.                                                         hThread:=OpenThread(THREAD_ALL_ACCESS, FALSE, DebugEv.dwThreadId);  
  51.                                                         SuspendThread(hThread);  
  52.   
  53.                                                         Regs.ContextFlags:=CONTEXT_FULL;  
  54.                                                         GetThreadContext(hThread,Regs);  
  55.   
  56.                                                         ReadProcessMemory(hW2iProcess,Pointer(Regs.esp),@dwCallRet,4,WorkBytes);  
  57.   
  58.                                                         //非常简单的判断, 应该为枚举代码段地址,或者对每个正常的调用地址作判断  
  59.                                                         if dwCallRet>$00A87000 then  
  60.                                                         begin  
  61.                                                                 WriteLn('发现外部调用!');  
  62.                                                                 // 随便做点什么吧  
  63.                                                         end else begin  
  64.                                                                 Regs.Esp:=Regs.Esp+$c; // add esp,0c  
  65.                                                                 SetThreadContext(hThread,Regs);  
  66.                                                         end;  
  67.                                                         ResumeThread(hThread);  
  68.                                                         CloseHandle(hThread);  
  69.                                                 end;  
  70.   
  71.                                                 dwContinueStatus:=DBG_CONTINUE;  
  72.                                         end;  
  73.   
  74.           EXIT_PROCESS_DEBUG_EVENT:  
  75.                                         begin  
  76.                                                 ExitProcess(0);  
  77.                                         end;  
  78.             
  79.                                 end;  
  80.   
  81.                                 ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);  
  82.                         end;  
  83.                 end;  
  84.         end  
  85.     else WriteLn('附加到进程失败!');  
  86.   
  87.   WriteLn;  
  88.         WriteLn('按下回车键退出!');  
  89.   ReadLn;  
  90.         ExitProcess(0);  
  91. end;  
  92.   
  93. begin  
  94. MainLoop;  
  95. end.  

恩...C++ 版本如下

C++代码
  1. // cl AntiCall.cpp /link user32.lib  
  2.   
  3. #include <windows.h>  
  4. #include <stdio.h>  
  5.   
  6. #define W2I_WINDOW_TITLE TEXT("Element Client")  
  7. #define W2I_WINDOW_CLASS TEXT("ElementClient Window")  
  8. #define CHECKPOINT_ADDR      0x005B7BF8  
  9.   
  10. int main(int argc, char* argv[])  
  11. {  
  12.         HANDLE hW2iThread;  
  13.         HANDLE hW2iProcess;  
  14.         DWORD dwW2iThreadId;  
  15.         DWORD dwW2iProcessId;  
  16.         HWND hW2iWnd;  
  17.         DWORD dwCallRet;  
  18.   
  19.         hW2iWnd = ::FindWindow(W2I_WINDOW_CLASS, W2I_WINDOW_TITLE);  
  20.   
  21.         if( hW2iWnd>0 && ( dwW2iThreadId = ::GetWindowThreadProcessId(hW2iWnd, &dwW2iProcessId) )  
  22.                 && dwW2iProcessId && ( hW2iProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwW2iProcessId) )   
  23.                 && ( hW2iThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, dwW2iThreadId) ) && DebugActiveProcess(dwW2iProcessId) )  
  24.         {  
  25.                 DEBUG_EVENT DebugEv;  
  26.                 DWORD dwContinueStatus;  
  27.   
  28.                 while(TRUE)  
  29.                 {  
  30.                         if(WaitForDebugEvent(&DebugEv, 10))  
  31.                         {  
  32.                                 dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;  
  33.   
  34.                                 switch(DebugEv.dwDebugEventCode)  
  35.                                 {  
  36.                                 case EXCEPTION_DEBUG_EVENT:  
  37.                                         {  
  38.                                                 if((DWORD)DebugEv.u.Exception.ExceptionRecord.ExceptionAddress==CHECKPOINT_ADDR)  
  39.                                                 {  
  40.                                                         HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, DebugEv.dwThreadId);  
  41.                                                         SuspendThread(hThread);  
  42.   
  43.                                                         CONTEXT Regs = {0};  
  44.                                                         Regs.ContextFlags = CONTEXT_FULL;  
  45.                                                         ::GetThreadContext(hThread, &Regs);  
  46.   
  47.                                                         ReadProcessMemory(hW2iProcess, (void*)Regs.Esp, &dwCallRet, 4, &len)  
  48.                                                         //非常简单的判断, 应该为枚举代码段地址,或者对每个正常的调用地址作判断  
  49.                                                         if(dwCallRet>0x00A87000)  
  50.                                                         {  
  51.                                                                 printf("发现外部调用.\n\n");  
  52.                                                                 // 随便做点什么吧  
  53.                                                         }else{  
  54.                                                                 Regs.Esp += 0xc; // add esp,0c  
  55.                                                                 ::SetThreadContext(hThread, &Regs);  
  56.                                                         }  
  57.                                                         ResumeThread(hThread);  
  58.                                                         CloseHandle(hThread);  
  59.                                                 }  
  60.   
  61.                                                 dwContinueStatus = DBG_CONTINUE;  
  62.                                                 break;  
  63.                                         }  
  64.   
  65.                                 case EXIT_PROCESS_DEBUG_EVENT:  
  66.                                         {  
  67.                                                 return 0;  
  68.                                                 break;  
  69.                                         }  
  70.                                 }  
  71.   
  72.                                 ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);  
  73.                         }  
  74.                 }  
  75.         }  
  76.         else  
  77.         {  
  78.                 printf("附加到进程失败!\n\n");  
  79.         }  
  80.         printf("按任意键退出!\n");  
  81.         getchar();  
  82.   
  83.         return 0;  
  84. }  

上面的代码仅仅是非常简单的判断call的来源.只有一个判断点,而且是个INT 3.
在实际运用中,增加多个判断点. 动态生成各种不易被发现的异常. 让反外挂与与客户更加紧密的结合.
并且采用上文提到的反外挂服务器端校验模式.例如盛大的 RODynDll.dll 以及其生成的 DynTmp0.dat


本文就此结束,虽然还有更多的方法对抗外部调用,内存修改.
但是在外挂已不再是可以随便得到样本加以分析的时候.
了解外挂制作所需再加以反制, 是每个从事游戏安全工作者应该具备的基本素质.
正如开头的那句:  道高一尺,魔高一丈.

Tags: 外挂, 反外挂, 外挂编程, 黑客编程

« 上一篇 | 下一篇 »

Trackbacks

点击获得Trackback地址,Encode: UTF-8 点击获得Trackback地址,Encode: GB2312 or GBK 点击获得Trackback地址,Encode: BIG5

发表评论

评论内容 (必填):