道高一尺,魔高一丈
前言:
自<<石器时代>>开始, 外挂这一名词渐渐为世人所知. 到盛大第一款网游<<传奇>> 刚开始的宣传语 <永无外挂>
基本上所有的玩家都知道了有个叫外挂的玩意. 等到了<<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,真的就能阻止修改了吗?
不,我们来看看下面的代码.这是枚举系统中所有已知举柄达到取得进程Handle的函数. 你或许会认为,拦截ZwDuplicateObject,ZwQueryInformationProcess不就解决问题了?复制代码
- Function GetInfoTable(ATableType:dword):Pointer;
- var
- mSize: dword;
- mPtr: pointer;
- St: NTStatus;
- begin
- Result := nil;
- mSize := $4000;
- repeat
- mPtr := VirtualAlloc(nil, mSize, MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE);
- if mPtr = nil then Exit;
- St := ZwQuerySystemInformation(ATableType, mPtr, mSize, nil);
- if St = STATUS_INFO_LENGTH_MISMATCH then
- begin
- VirtualFree(mPtr, 0, MEM_RELEASE);
- mSize := mSize * 2;
- end;
- until St <> STATUS_INFO_LENGTH_MISMATCH;
- if St = STATUS_SUCCESS
- then Result := mPtr
- else VirtualFree(mPtr, 0, MEM_RELEASE);
- end;
- function iOpenProcess(ProcessId:DWORD):DWORD;
- var
- HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
- ClientID:TClientID;
- pbi:_PROCESS_BASIC_INFORMATION;
- oa:TObjectAttributes;
- hProcessCur,hProcessToDup,hProcessToRet:DWORD;
- Ret:DWORD;
- I:Integer;
- begin
- SetPrivilege('SE_DEBUG',TRUE);
- Result:=0;
- FillChar(oa,SizeOf(TObjectAttributes),0);
- FillChar(ClientID,SizeOf(TClientID),0);
- oa.Length:=SizeOf(TObjectAttributes);
- HandlesInfo:=GetInfoTable(SystemHandleInformation);
- for I:=0 to HandlesInfo^.NumberOfHandles do
- begin
- If (HandlesInfo^.Information.ObjectTypeNumber=5) Then //OB_TYPE_PROCESS
- ClientID.UniqueProcess:=HandlesInfo^.Information.ProcessId;
- If ZwDuplicateObject(hProcessToDup,HandlesInfo^.Information.Handle,GetCurrentProcess,@hProcessCur,PROCESS_ALL_ACCESS,0,$4)=STATUS_SUCCESS then
- If ZwQueryInformationProcess(hProcessCur,ProcessBasicInformation,@pbi,Sizeof(_PROCESS_BASIC_INFORMATION),@Ret)=STATUS_SUCCESS then
- If (pbi.UniqueProcessId=ProcessId) Then
- If ZwDuplicateObject(hProcessToDup,HandlesInfo^.Information.Handle,GetCurrentProcess,@hProcessToRet,PROCESS_ALL_ACCESS,0,$4)=STATUS_SUCCESS then
- begin
- Result:=hProcessToRet;
- Break;
- end;
- end;
- if hProcessCur>0 then ZwClose(hProcessCur);
- if hProcessToDup>0 then ZwClose(hProcessToDup);
- VirtualFree(HandlesInfo,0,MEM_RELEASE);
- SetPrivilege('SE_DEBUG',FALSE);
- end;
这没错,你是对的.但是你不能这样做,你做的是反外挂,不是ROOTKIT, 当你尝试这样做的时候,你会发现你的杀毒软件提示你. 这是ROOTKIT的典型行为
怎么办? 难道你要象ROOTKIT那样关闭掉玩家的杀毒软件? 还是联系各大杀毒软件厂商告诉他们: 麻烦您修改你们的规则?
这仅仅是RING 3的普通运用而已, 千万不要认为做外挂的不会驱动. 相反,与游戏开发公司那点可怜的薪水比起来. 外挂的利润只会让更多的驱动开发者
加入这一行列. 即使你HOOK接管了这一切函数,不管是inline还是普通的ssdt. 下面的驱动很轻易的就能突破任意的HOOK.面对任何HOOK,只需要从NT的内核文件中取出其真实的地址,很轻易的就可以饶过SSDT的HOOK,INLINE HOOK只需要恢复代码即可.复制代码
- .....................
- NTSTATUS NTAPI GetRealAddress(PIMPORT_ENTRY Import)
- {
- MODULE_INFORMATION mi,idmi;
- DWORD i,j;
- DWORD dwKernelBase;
- NTSTATUS status;
- PDWORD KiServiceTable;
- UNICODE_STRING NtdllName;
- if (KeGetCurrentIrql()!=PASSIVE_LEVEL) return STATUS_PASSIVE_LEVEL_REQUIRED;
- RtlZeroMemory(&mi,sizeof(mi));
- if (!NT_SUCCESS(status=MapKernelImage(&mi,&dwKernelBase))) return status;
- RtlZeroMemory(&idmi,sizeof(idmi));
- RtlInitUnicodeString(&NtdllName, L"\\SystemRoot\\System32\\ntdll.dll");
- if (!NT_SUCCESS(status=MapPeImage(&idmi,&NtdllName))) return status;
- try {
- for (i=0;Import.szName;i++){
- Import.dwAddress=0;
- switch (Import.dwType) {
- case IMPORT_BY_NAME:
- if (!(Import.dwAddress=GetProcRva(mi.hModule,Import.szName))) {
- #ifdef DEBUG
- DbgPrint("GetRealAddress(): Failed to get %s rva!\n",Import.szName);
- #endif
- }
- break;
- case IMPORT_BY_RVA:
- Import.dwAddress=(DWORD)Import.szName;
- break;
- case IMPORT_BY_ADDRESS:
- Import.dwAddress=(DWORD)Import.szName-dwKernelBase;
- break;
- case IMPORT_BY_SERVICE_ID:
- // do not search this rva if it has been already found
- if (!KiServiceTable_RVA) {
- if (!(KiServiceTable_RVA=FindKiServiceTable(mi.hModule))) {
- #ifdef DEBUG
- DbgPrint("GetRealAddress(): Failed to get KiServiceTable RVA!\n");
- #endif
- break;
- }
- }
- KiServiceTable=(PDWORD)(KiServiceTable_RVA+mi.hModule);
- Import.dwAddress=KiServiceTable[(DWORD)Import.szName]-mi.dwImageBase;
- break;
- case IMPORT_BY_SERVICE_NAME:
- if (!KiServiceTable_RVA){
- if (!(KiServiceTable_RVA=FindKiServiceTable(mi.hModule))) break;
- }
- Import.dwId=GetIdForName(idmi.hModule,Import.szName);
- KiServiceTable=(PDWORD)(KiServiceTable_RVA+mi.hModule);
- Import.dwAddress=KiServiceTable[Import.dwId]-mi.dwImageBase;
- break;
- default:
- break;
- } //Case End
- if (Import.dwId==0){
- if (!KiServiceTable_RVA)
- KiServiceTable_RVA=FindKiServiceTable(mi.hModule);
- KiServiceTable=(PDWORD)(KiServiceTable_RVA+mi.hModule);
- for (j=0;KiServiceTable[j];j++){if (Import.dwAddress==KiServiceTable[j]-mi.dwImageBase){Import.dwId=j;break;}}
- }
- Import.dwAddress=dwKernelBase+Import.dwAddress;
- }
- }except(EXCEPTION_EXECUTE_HANDLER){
- return STATUS_ADD_FUNCTION_FAILED;
- }
- try {
- UnmapPeImage(&mi);
- UnmapPeImage(&idmi);
- }except(EXCEPTION_EXECUTE_HANDLER){
- return STATUS_CODE_REBUILDING_FAILED;
- }
- return STATUS_SUCCESS;
- }
- ...........
- 恩..这不是完整的代码,这理所当然,不是么?
更何况你的驱动肯定会比外挂的驱动还晚加载.
即使除开上面这些不谈,你依然要面对你的驱动被PATCH,又或者被个假冒的驱动所替代. 更别说 lpk.dll usp10.dll 了.
这时候你应该会想反驳我,看看 nPROTECT ,安博士 吧. 好的,那么我们来看看下面这段函数直接读取物理内存, 到目前为止,这个方法依然对 nPROTECT 保护的进程有效.复制代码
- NTSTATUS ReadPhysicalMemory(char *startaddress, UINT_PTR bytestoread, void *output)
- {
- HANDLE physmem;
- UNICODE_STRING physmemString;
- OBJECT_ATTRIBUTES attributes;
- WCHAR physmemName[] = L"\\device\\physicalmemory";
- UCHAR* memoryview;
- NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;
- __try
- {
- RtlInitUnicodeString( &physmemString, physmemName );
- InitializeObjectAttributes( &attributes, &physmemString, OBJ_CASE_INSENSITIVE, NULL, NULL );
- ntStatus=ZwOpenSection( &physmem, SECTION_MAP_READ, &attributes );
- if (ntStatus==STATUS_SUCCESS)
- {
- //hey look, it didn't kill it
- UINT_PTR length;
- PHYSICAL_ADDRESS viewBase;
- UINT_PTR offset;
- UINT_PTR toread;
- viewBase.QuadPart = (ULONGLONG)(startaddress);
- length=0x2000;//pinp->bytestoread; //in case of a overlapping region
- toread=bytestoread;
- memoryview=NULL;
- DbgPrint("ReadPhysicalMemory:viewBase.QuadPart=%x", viewBase.QuadPart);
- ntStatus=ZwMapViewOfSection(
- physmem, //sectionhandle
- NtCurrentProcess(), //processhandle (should be -1)
- &memoryview, //BaseAddress
- 0L, //ZeroBits
- length, //CommitSize
- &viewBase, //SectionOffset
- &length, //ViewSize
- ViewShare,
- 0,
- PAGE_READWRITE);
- if (ntStatus==STATUS_SUCCESS)
- {
- offset=(UINT_PTR)(startaddress)-(UINT_PTR)viewBase.QuadPart;
- RtlCopyMemory(output,&memoryview[offset],toread);
- ZwUnmapViewOfSection( NtCurrentProcess(), memoryview);
- }
- else
- {
- DbgPrint("ReadPhysicalMemory:ntStatus=%x", ntStatus);
- }
- ZwClose(physmem);
- };
- }
- __except(1)
- {
- DbgPrint("Error while reading physical memory\n");
- }
- return ntStatus;
- }
实际上反外挂的驱动能拦截的不过是API而已, 你能拦截 mov eax,[xxxxxxx] 吗?
别忘记,你在驱动中采取的手段越多,驱动的兼容性必定越差.
在家中的玩家还好说,可是面对目前主要的玩家多数在网吧上网的情况,你不的不考虑各种网吧管理软件.
这样的情况,不谈兼容性光是你的驱动到底有没有机会被加载还是个问题....
即使是在家中上网的玩家,你难道要告诉使用 Vista 或者 Windows 7 的普通用户: 请关闭你的UAC
好吧,再这样写下去简直没完没了. 综上所述, 驱动反外挂, 这只是看起来很美而已.
怎么办?
二. 如何有效的阻止外挂
前言中提到,要有效的反外挂,必先了解外挂如何运作. 在前文中,也描述了当前外挂主要的运作模式. 现在外挂已不是要求什么三步瞬移,格位刺杀之类的特殊功能了,对于工作室.
他们的需要仅仅是稳定的机器人,如果游戏提供的话,他们常常还需要能够把挂机角色上的金钱物品邮寄或者交易给某个账号的功能. 那么制作一个这样的机器人至少需要的是什么?
1. 游戏角色的生命值,魔法值之类的数据
2. 游戏角色的物品数据
3. 游戏角色周围的怪物数据
4. 移动函数
5. 热键函数 [假如客户端接受 SendMessage 模拟键盘这样的消息,这不需要]
6. 选中怪物函数
7. 打开NPC函数
8. 打开仓库函数
9. 交易或邮寄函数
其中的 4-9 可以被一个数据包发送函数所替代,例如归根结底, 要反外挂,主要防御的只有两点:复制代码
- procedure SendPack(buf:PChar;len:DWORD); stdcall;
- procedure TOSEND; stdcall;
- asm
- push -1
- push SENDPACK_STAK
- mov eax, dword ptr fs:[0]
- push eax
- mov dword ptr fs:[0], esp
- sub esp,$18
- push ebx
- push esi
- push edi
- mov edi, ecx
- xor ebx, ebx
- xor eax, eax
- jmp SENDPACK_JMP
- end;
- begin
- asm
- pushad;
- mov ecx, [CALL_BASE];
- push len;
- push buf;
- mov ecx, [ecx+$20];
- call TOSEND;
- popad;
- end;
- end;
- procedure SendBuyItem(ItemId,ItemPos,ItemCount:DWORD);
- var
- //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
- //25 00 01 00 00 00 ByteCount 00 00 00 00 GroupCount ItemId ItemPos ItemCount
- Pack:Array [0..29] of Byte;
- begin
- FillChar(Pack,SizeOf(Pack),0);
- Pack[0]:=$25;
- Pack[2]:=$01;
- Pack[6]:=$14;
- Pack[14]:=$01;
- CopyMemory(@Pack[18],@ItemId,4);
- CopyMemory(@Pack[22],@ItemPos,4);
- CopyMemory(@Pack[26],@ItemCount,4);
- SendPack(@Pack[0],30);
- end;
1. 防止外部修改内存
2. 防止外部调用函数
对于第一点,比如修改某个怪物的数据,使得客户端判断该怪物在游戏角色的攻击范围之内.
最佳的解决办法不是去HOOK什么内存读写函数. 而是把判断这些数据的责任交给服务器端.
可如果是引进的游戏呢? 解决办法便是CRC32或者别的什么HASH算法校验这段内存数据.
对于第二点,最简单的办法便是在函数内取得 ESP 判断函数的返回地址. 以上面的那段函数为例.
只要游戏开发商稍微更改一下他的发包函数,判断下call 的来源, 我想这已经会让外挂的作者头痛
很久.
实质上反外挂是否有效最大的前提,在于不能让反外挂机制被饶过. 仅仅是单纯的客户端保护,作用非常有限.
对于没有代码的情况下,比较简单的解决办法:
1. 客户端反外挂DLL中 HOOK connect,recv,send. 在 connect 时使其连接到反外挂服务器端.
保留connect所使用的socket, 在recv , send 中判断该socket对数据进行二次加密/解密
2. 反外挂服务器端监控客户端的情况,如有异常则中断用户数据的转发.
怎么做?
下面以国内完美公司的游戏,<<完美世界>>作为改造对象.以自调试做为手段
为elementclient.exe增加一个对抗外部调用的方法.
我们先来看看<<完美世界>>的发包函数对比上面的发包CALL的例子,可以发现 005B7BD0 - 005B7BF6 均由外挂完成, 而达到绕过 005B7BEF call 0068B5F0 的目的.复制代码
- 005B7BD0 /$ 6A FF push -1
- 005B7BD2 |. 68 68A68800 push 0088A668
- 005B7BD7 |. 64:A1 0000000>mov eax, dword ptr fs:[0]
- 005B7BDD |. 50 push eax
- 005B7BDE |. 64:8925 00000>mov dword ptr fs:[0], esp
- 005B7BE5 |. 83EC 18 sub esp, 18
- 005B7BE8 |. 53 push ebx
- 005B7BE9 |. 56 push esi
- 005B7BEA |. 57 push edi
- 005B7BEB |. 8BF9 mov edi, ecx
- 005B7BED |. 6A 07 push 7 ; /Arg1 = 00000007
- 005B7BEF |. E8 FC390D00 call 0068B5F0 ; \elementc.0068B5F0
- 005B7BF4 |. 33DB xor ebx, ebx
- 005B7BF6 |. 33C0 xor eax, eax
- 005B7BF8 |. 83C4 04 add esp, 4
- 005B7BFB |. 894424 18 mov dword ptr [esp+18], eax
- 005B7BFF |. 895C24 1C mov dword ptr [esp+1C], ebx
- 005B7C03 |. 895C24 20 mov dword ptr [esp+20], ebx
- 005B7C07 |. C74424 14 086>mov dword ptr [esp+14], 008A6C08
- 005B7C0F |. C74424 0C 183>mov dword ptr [esp+C], 008B3818
- 005B7C17 |. C74424 10 220>mov dword ptr [esp+10], 22
- 005B7C1F |. 8B7424 38 mov esi, dword ptr [esp+38]
- 005B7C23 |. 895C24 2C mov dword ptr [esp+2C], ebx
- 005B7C27 |. 3BF3 cmp esi, ebx
- 005B7C29 |. 76 2D jbe short 005B7C58
- 005B7C2B |. 8D46 FF lea eax, dword ptr [esi-1]
- 005B7C2E |. B9 02000000 mov ecx, 2
- 005B7C33 |. D1E8 shr eax, 1
- 005B7C35 |. 894C24 20 mov dword ptr [esp+20], ecx
- 005B7C39 |. 74 0A je short 005B7C45
- 005B7C3B |> D1E1 /shl ecx, 1
- 005B7C3D |. D1E8 |shr eax, 1
- 005B7C3F |.^ 75 FA \jnz short 005B7C3B
- 005B7C41 |. 894C24 20 mov dword ptr [esp+20], ecx
- 005B7C45 |> 51 push ecx ; /size
- 005B7C46 |. 53 push ebx ; |block
- 005B7C47 |. FF15 6C548A00 call dword ptr [<&MSVCRT.realloc>] ; \realloc
- 005B7C4D |. 83C4 08 add esp, 8
- 005B7C50 |. 894424 18 mov dword ptr [esp+18], eax
- 005B7C54 |. 894424 1C mov dword ptr [esp+1C], eax
- 005B7C58 |> 8B4C24 34 mov ecx, dword ptr [esp+34]
- 005B7C5C |. 56 push esi ; /n
- 005B7C5D |. 51 push ecx ; |src
- 005B7C5E |. 50 push eax ; |dest
- 005B7C5F |. FF15 44548A00 call dword ptr [<&MSVCRT.memmove>] ; \memmove
- 005B7C65 |. 8B5424 24 mov edx, dword ptr [esp+24]
- 005B7C69 |. 83C4 0C add esp, 0C
- 005B7C6C |. 8D4424 0C lea eax, dword ptr [esp+C]
- 005B7C70 |. 03D6 add edx, esi
- 005B7C72 |. 53 push ebx
- 005B7C73 |. 50 push eax
- 005B7C74 |. 8BCF mov ecx, edi
- 005B7C76 |. 895424 24 mov dword ptr [esp+24], edx
- 005B7C7A |. E8 B1DAFFFF call 005B5730
- 005B7C7F |. 8B4C24 18 mov ecx, dword ptr [esp+18]
- 005B7C83 |. 8AD8 mov bl, al
- 005B7C85 |. 51 push ecx ; /block
- 005B7C86 |. C74424 18 086>mov dword ptr [esp+18], 008A6C08 ; |
- 005B7C8E |. FF15 68548A00 call dword ptr [<&MSVCRT.free>] ; \free
- 005B7C94 |. 8B4C24 28 mov ecx, dword ptr [esp+28]
- 005B7C98 |. 83C4 04 add esp, 4
- 005B7C9B |. 8AC3 mov al, bl
- 005B7C9D |. 64:890D 00000>mov dword ptr fs:[0], ecx
- 005B7CA4 |. 5F pop edi
- 005B7CA5 |. 5E pop esi
- 005B7CA6 |. 5B pop ebx
- 005B7CA7 |. 83C4 24 add esp, 24
- 005B7CAA \. C2 0800 ret 8
那么在这里,挑选 005B7BF8 |. 83C4 04 add esp, 4 作为监控点.
这个例子中,首先我们修改 elementclient.exe 文件, 将 005B7BF8 处的代码改为 CC 90 90. 方便写我们的调试器代码
接下来我们看看我们将用于防止外部调用的调试器部分恩...C++ 版本如下复制代码
- program AntiCall;
- {$APPTYPE CONSOLE}
- uses Windows,Sysutils;
- Const
- WINDOW_TITLE='Element Client';
- WINDOW_CLASS='ElementClient Window';
- THREAD_ALL_ACCESS=STANDARD_RIGHTS_REQUIRED or SYNCHRONIZE or $3FF;
- CHECKPOINT_ADDR=$005B7BF8;
- function OpenThread(dwDesiredAccess:DWORD;bInheritHandle:Boolean;dwThreadId:DWORD):THANDLE; stdcall external kernel32 name 'OpenThread';
- procedure MainLoop;
- var
- hW2iThread,hW2iProcess,dwW2iThreadId,dwW2iProcessId,hW2iWnd:DWORD;
- DebugEv:DEBUG_EVENT;
- Regs:CONTEXT;
- dwContinueStatus,WorkBytes,dwCallRet:DWORD;
- fp:THandle;
- hThread:DWORD;
- begin
- FillChar(Regs,SizeOf(CONTEXT),0);
- dwW2iThreadId:=0;
- dwW2iProcessId:=0;
- hW2iProcess:=0;
- hW2iThread:=0;
- fp:=0;
- hW2iWnd:=FindWindow(WINDOW_CLASS, nil);
- if( hW2iWnd>0) then dwW2iThreadId:=GetWindowThreadProcessId(hW2iWnd,@dwW2iProcessId);
- if (dwW2iProcessId>0) then hW2iProcess:=OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwW2iProcessId);
- if (hW2iProcess>0) then hW2iThread:=OpenThread(THREAD_ALL_ACCESS, FALSE, dwW2iThreadId);
- if (hW2iThread>0) then
- if DebugActiveProcess(dwW2iProcessId) then
- begin
- while(TRUE) do
- begin
- if (WaitForDebugEvent(DebugEv,10)) then
- begin
- dwContinueStatus:=DBG_EXCEPTION_NOT_HANDLED;
- Case DebugEv.dwDebugEventCode of
- EXCEPTION_DEBUG_EVENT:
- begin
- if(DWORD(DebugEv.Exception.ExceptionRecord.ExceptionAddress)=CHECKPOINT_ADDR) then
- begin
- hThread:=OpenThread(THREAD_ALL_ACCESS, FALSE, DebugEv.dwThreadId);
- SuspendThread(hThread);
- Regs.ContextFlags:=CONTEXT_FULL;
- GetThreadContext(hThread,Regs);
- ReadProcessMemory(hW2iProcess,Pointer(Regs.esp),@dwCallRet,4,WorkBytes);
- //非常简单的判断, 应该为枚举代码段地址,或者对每个正常的调用地址作判断
- if dwCallRet>$00A87000 then
- begin
- WriteLn('发现外部调用!');
- // 随便做点什么吧
- end else begin
- Regs.Esp:=Regs.Esp+$c; // add esp,0c
- SetThreadContext(hThread,Regs);
- end;
- ResumeThread(hThread);
- CloseHandle(hThread);
- end;
- dwContinueStatus:=DBG_CONTINUE;
- end;
- EXIT_PROCESS_DEBUG_EVENT:
- begin
- ExitProcess(0);
- end;
- end;
- ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
- end;
- end;
- end
- else WriteLn('附加到进程失败!');
- WriteLn;
- WriteLn('按下回车键退出!');
- ReadLn;
- ExitProcess(0);
- end;
- begin
- MainLoop;
- end.
上面的代码仅仅是非常简单的判断call的来源.只有一个判断点,而且是个INT 3.复制代码
- // cl AntiCall.cpp /link user32.lib
- #include <windows.h>
- #include <stdio.h>
- #define W2I_WINDOW_TITLE TEXT("Element Client")
- #define W2I_WINDOW_CLASS TEXT("ElementClient Window")
- #define CHECKPOINT_ADDR 0x005B7BF8
- int main(int argc, char* argv[])
- {
- HANDLE hW2iThread;
- HANDLE hW2iProcess;
- DWORD dwW2iThreadId;
- DWORD dwW2iProcessId;
- HWND hW2iWnd;
- DWORD dwCallRet;
- hW2iWnd = ::FindWindow(W2I_WINDOW_CLASS, W2I_WINDOW_TITLE);
- if( hW2iWnd>0 && ( dwW2iThreadId = ::GetWindowThreadProcessId(hW2iWnd, &dwW2iProcessId) )
- && dwW2iProcessId && ( hW2iProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwW2iProcessId) )
- && ( hW2iThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, dwW2iThreadId) ) && DebugActiveProcess(dwW2iProcessId) )
- {
- DEBUG_EVENT DebugEv;
- DWORD dwContinueStatus;
- while(TRUE)
- {
- if(WaitForDebugEvent(&DebugEv, 10))
- {
- dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
- switch(DebugEv.dwDebugEventCode)
- {
- case EXCEPTION_DEBUG_EVENT:
- {
- if((DWORD)DebugEv.u.Exception.ExceptionRecord.ExceptionAddress==CHECKPOINT_ADDR)
- {
- HANDLE hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, DebugEv.dwThreadId);
- SuspendThread(hThread);
- CONTEXT Regs = {0};
- Regs.ContextFlags = CONTEXT_FULL;
- ::GetThreadContext(hThread, &Regs);
- ReadProcessMemory(hW2iProcess, (void*)Regs.Esp, &dwCallRet, 4, &len)
- //非常简单的判断, 应该为枚举代码段地址,或者对每个正常的调用地址作判断
- if(dwCallRet>0x00A87000)
- {
- printf("发现外部调用.\n\n");
- // 随便做点什么吧
- }else{
- Regs.Esp += 0xc; // add esp,0c
- ::SetThreadContext(hThread, &Regs);
- }
- ResumeThread(hThread);
- CloseHandle(hThread);
- }
- dwContinueStatus = DBG_CONTINUE;
- break;
- }
- case EXIT_PROCESS_DEBUG_EVENT:
- {
- return 0;
- break;
- }
- }
- ContinueDebugEvent(DebugEv.dwProcessId, DebugEv.dwThreadId, dwContinueStatus);
- }
- }
- }
- else
- {
- printf("附加到进程失败!\n\n");
- }
- printf("按任意键退出!\n");
- getchar();
- return 0;
- }
在实际运用中,增加多个判断点. 动态生成各种不易被发现的异常. 让反外挂与与客户更加紧密的结合.
并且采用上文提到的反外挂服务器端校验模式.例如盛大的 RODynDll.dll 以及其生成的 DynTmp0.dat
本文就此结束,虽然还有更多的方法对抗外部调用,内存修改.
但是在外挂已不再是可以随便得到样本加以分析的时候.
了解外挂制作所需再加以反制, 是每个从事游戏安全工作者应该具备的基本素质.
正如开头的那句: 道高一尺,魔高一丈.