规避网络游戏的外挂检测机制(zz)
规避网络游戏的外挂检测机制
译注:这是一篇发表在rootkit.com上的讨论怎么规避warden检测机制的文章,原文在这里。作者Darawk是D2的黑客。之前,暴雪通过Module32First/Module32Next对D2 1.11中的外挂进行了第一次打击,很多使用外挂的玩家尤其是netter's EasyMap的玩家损失惨重。黑客们开始思考Anti-warden的问题,这篇文章就是一些这方面的尝试。后来,在Darawk的帮助下,netter在EasyMap/EasyPlay中实现了Anti-warden。文中提出的几种方法在EasyMap/EasyPlay的几个版本中陆续都用到了,可惜这些版本后来还是被抓了。尽管如此,这里介绍的几种思路还是很有借鉴意义的,例如用调试寄存器来hook函数在WoW的一些外挂中就得到了应用。作者在附录中给出的几个源代码文件的链接地址已经失效,需要的可以从这里下载。http://sting.feng.googlepages.com/darawk_code.zip
------------------------------------------------
I-介绍
你们有些人可能已经知道,很多游戏公司(译注:就是指暴雪)最近对“黑客”采取了严厉措施。他们全方位地实现了基于用户态(userland)的检测机制,手段从非常简单到极其复杂的都有。我在过去几周里花了大量的时间,试图找到一种通用的规避检测的方法。从某种意义上来说,我们面临的处境和游戏公司很像:你只需找到他们的游戏系统的一个缺点就可以加以利用,而他们必须把系统设计得毫无漏洞;同样,在我们隐藏自身躲开他们的检测代码时,他们只要发现一处失误就可以抓住我们,而我们要把系统设计、实现得完美无缺。下面的文章是我在这方面的尝试。它肯定不是完整或者完美的,但我想这是朝着这个目标迈进的第一步。
II-已加载模块的检测
这里假设我们要往目标(译注:指游戏进程)加载一个模块(大部分外挂或者bot的做法)。我们必须对两种不同的情况加以区分,它们都可能导致恶意模块被检测出来。这两种情况是:注入时和其他时间(译注,指外挂加载时和外挂工作时)。
基本上每种DLL注入法都会在某处调用LoadLibrary。因此如果你想捕捉模块,一种简单的检测办法是截获LoadLibrary(或者LdrLoadDll,或更底层的Native API)。
这个问题有两种解决方法。第一种也是最简单的一种方法是把模块名称随机化。由于很多合法软件往系统里所有进程注入DLL(Trillian, AIM, 热键软件,等等),合理的检测系统不会使用“白名单”设计(这种检测方法确认只有那些验证过的模块可以加载,而把那些没在白名单上列出来的统统看成恶意模块),因此他们只能用黑名单,这就使得随机化模块名称是对付这类检测的完美、可行的解决方案。
我实现的另外一种方法是换一种做法来注入DLL,我称之为“手工映射”(manual mapping)。第一眼看上去,去模拟windows的PE loader是一件令人沮丧的事-你很难把每件事情都做正确。但其实它也没那么难,我的ManualMap(附录)代码就是干这个用的。我知道对ManualMap还可以做很多很多改进-事实上我自己有一个改进很大的版本-这个只是一个概念验证(proof of concept)的东西。
模块被注入到正在运行的进程后,有两种办法可以检测到它。第一种是扫描模块链表或者对那些你认为是外挂的模块调用GetModuleHandle(译注,看GetModuleHandle是否返回NULL)。对付这种检测的办法是利用CloakDll(附录)之类的工具把你的模块从链表中去掉。或者用ManualMap,这样你的模块从一开始就不会被加到链表中。我认为第二种做法好一些,但他们差不多是一样的。
第二种检测已加载模块的方法就聪明多了。它的做法是枚举系统中的所有内存页面(内存页面是1024字节对齐的,所以这其实很容易做),然后检查不良代码的特征码。应对措施是做一个改进的dll loader,把内存页面的边界到实际数据的起始偏移随机化(译注,也就是说不把DLL加载在页面边界处,而是离边界有一个随机的偏移)。然而,我觉得更好的方法是创建两个新的空白页面来包住你的模块,然后用VirtualProtect给这两页设上PAGE_GUARD标志。标有PAGE_GUARD位的内存在访问时会产生一个异常,你可以用未处理异常过滤器(unhandled exception filter)、向量化异常处理(vectored exception handling)或者KiUserExceptionDispatcher hook(我推荐后者)捕捉这些异常,这样碰到扫描时你就有机会做一些处理来避免检测代码抓到你。
III-Hooks,Patches和CRC检查
值得让人一用的外挂基本上都会以某种方式修改游戏代码。但是修改代码是非常容易被检测到的,到现在也没有人能对这个问题提出一个可行、通用的解决方案。我恐怕也不敢号称我已经完全解决了这个问题,但我发现了一个让它变得驯服点儿的办法。
我提出过一种不用修改任何目标进程代码就能hook函数的方法,唯一的缺点是你只能同时hook4个函数。我的做法是利用调试寄存器(即硬件断点),你可以在附录的CHook类中看到我的实现。
由于调试寄存器检测起来相当容易,任何人只要用CONTEXT_DEBUG_REGISTERS来调GetThreadContext就可以发现它,随便一个异常处理例程都会拿到一个包含调试寄存器的上下文结构(context structure),等等。解决办法是hook住NtGetContextThread、NtSetContextThread和KiUserExceptionDispatcher。当然啦,你得用一些patch来hook这些API。由于颇有一些防病毒/防广告/防火墙软件也hook这类函数来“增强系统的整体安全”,所以检测系统如果仅仅因为你改了这些系统dll就认为你作弊也是不大可能的。这意味着你可以安全的hook这4个函数,用不着担心被检测。
即使他们也许不会仅仅因为在系统模块中发现了这些hooks就抓你,但它们还是给检测留下了一点线索-他们可以通过定位你在系统模块中的hooks,分析patch的跳转指令看它跳到了哪里,然后对hook处理过程做一下CRC就足以把你认出来。
这又有两种解决办法。一种是利用int 3断点指令来做hook,然后捕捉异常(如果看的够仔细的话你会发现这在我的CHook类里也实现了)并把异常重定向到合适的hook处理过程-你需要用标准的jmp patch来hook KiUserExceptionDispatcher(这有点和我们的目标矛盾),或者用两种标准的SEH(结构化异常处理)形式之一。
还有一种办法,我觉得要好的多,虽然实现起来有点儿困难。其实也没什么花头,就是写一个hook处理过程的变形引擎,在真正有用的指令之间填充NOP等效指令(NOP等效指令是指mov eax, eax之类的指令。译注:push eax, pop eax这样的也是)。这样,对你的函数做CRC检查就行不通了。我现在还没有这么做,不过以后我可能会写些PoC代码(译注:PoC即Proof of Concept)。
最容易的一种做法是从shellcode那里来的。我敢肯定你们都知道,为了避开IDS签名(译注,IDS=Intrusion-detection system),shellcode通常是加密的,带一个动态解密的loader。用汇编给运行时解密代码写一个简单的变形引擎是很容易的,通过在不同的指令之间放置NOP指令,并且让密钥在每次加、解密时随机变化,你可以把创建签名的任何企图变得非常困难-即使不是不可能的话,而且不会牺牲多少性能。
调试寄存器在其他很多方面也是相当有用的,你可以用它hook内存读写和指令读取,这意味着你可以为内存修改做一个回调函数(用在内存数据监测上很理想,这意味着你再也用不着轮询了)。对调试寄存器的完整描述可以在附录中找到。
另外一点要考虑的是,他们可能都不检测你的API hooks,简单的用原始数据patch回去就能让你的hooks无效。这种方法实现起来不难,而且有足够的可移植性-因为不同的Windows版本中大多数API的开始几个字节是不会经常改变的-游戏公司是有可能这么干的。要对付这个问题,我们得意识到一点,就是在对进程映象的代码段进行写操作之前,必须调用VirtualProtect临时改变它的页面属性。因此,截获NtProtectVirtualMemory就可以防止他们轻易的覆盖掉你的辛勤劳动成果。
IV-总结
总而言之,注入一个模块并设置4个不可检测(在ring3级别上)的hooks是可能的。也许在某些地方会有漏洞,但我所说的基本原理应该是能到达这个效果的。以上是我对在用户态创建反检测系统的所有尝试。我知道你们肯定有能力在我描述的基础上加上你们自己的见解,我也真诚的希望你们会这么做。这个解决方案不像很多正在搞game hacking的人想的那样是一个秘密的安全体系,也和那些闭源软件开发商的做法不一样。游戏黑客比别人懂得更多,应该知道这一点,因为你们全都是玩逆向工程的。你们活着就是为了击败那些通过晦涩的手段来实现的系统安全。。。自己不要掉进同样的陷阱。
------------------------------------------------
I-介绍
你们有些人可能已经知道,很多游戏公司(译注:就是指暴雪)最近对“黑客”采取了严厉措施。他们全方位地实现了基于用户态(userland)的检测机制,手段从非常简单到极其复杂的都有。我在过去几周里花了大量的时间,试图找到一种通用的规避检测的方法。从某种意义上来说,我们面临的处境和游戏公司很像:你只需找到他们的游戏系统的一个缺点就可以加以利用,而他们必须把系统设计得毫无漏洞;同样,在我们隐藏自身躲开他们的检测代码时,他们只要发现一处失误就可以抓住我们,而我们要把系统设计、实现得完美无缺。下面的文章是我在这方面的尝试。它肯定不是完整或者完美的,但我想这是朝着这个目标迈进的第一步。
II-已加载模块的检测
这里假设我们要往目标(译注:指游戏进程)加载一个模块(大部分外挂或者bot的做法)。我们必须对两种不同的情况加以区分,它们都可能导致恶意模块被检测出来。这两种情况是:注入时和其他时间(译注,指外挂加载时和外挂工作时)。
基本上每种DLL注入法都会在某处调用LoadLibrary。因此如果你想捕捉模块,一种简单的检测办法是截获LoadLibrary(或者LdrLoadDll,或更底层的Native API)。
这个问题有两种解决方法。第一种也是最简单的一种方法是把模块名称随机化。由于很多合法软件往系统里所有进程注入DLL(Trillian, AIM, 热键软件,等等),合理的检测系统不会使用“白名单”设计(这种检测方法确认只有那些验证过的模块可以加载,而把那些没在白名单上列出来的统统看成恶意模块),因此他们只能用黑名单,这就使得随机化模块名称是对付这类检测的完美、可行的解决方案。
我实现的另外一种方法是换一种做法来注入DLL,我称之为“手工映射”(manual mapping)。第一眼看上去,去模拟windows的PE loader是一件令人沮丧的事-你很难把每件事情都做正确。但其实它也没那么难,我的ManualMap(附录)代码就是干这个用的。我知道对ManualMap还可以做很多很多改进-事实上我自己有一个改进很大的版本-这个只是一个概念验证(proof of concept)的东西。
模块被注入到正在运行的进程后,有两种办法可以检测到它。第一种是扫描模块链表或者对那些你认为是外挂的模块调用GetModuleHandle(译注,看GetModuleHandle是否返回NULL)。对付这种检测的办法是利用CloakDll(附录)之类的工具把你的模块从链表中去掉。或者用ManualMap,这样你的模块从一开始就不会被加到链表中。我认为第二种做法好一些,但他们差不多是一样的。
第二种检测已加载模块的方法就聪明多了。它的做法是枚举系统中的所有内存页面(内存页面是1024字节对齐的,所以这其实很容易做),然后检查不良代码的特征码。应对措施是做一个改进的dll loader,把内存页面的边界到实际数据的起始偏移随机化(译注,也就是说不把DLL加载在页面边界处,而是离边界有一个随机的偏移)。然而,我觉得更好的方法是创建两个新的空白页面来包住你的模块,然后用VirtualProtect给这两页设上PAGE_GUARD标志。标有PAGE_GUARD位的内存在访问时会产生一个异常,你可以用未处理异常过滤器(unhandled exception filter)、向量化异常处理(vectored exception handling)或者KiUserExceptionDispatcher hook(我推荐后者)捕捉这些异常,这样碰到扫描时你就有机会做一些处理来避免检测代码抓到你。
III-Hooks,Patches和CRC检查
值得让人一用的外挂基本上都会以某种方式修改游戏代码。但是修改代码是非常容易被检测到的,到现在也没有人能对这个问题提出一个可行、通用的解决方案。我恐怕也不敢号称我已经完全解决了这个问题,但我发现了一个让它变得驯服点儿的办法。
我提出过一种不用修改任何目标进程代码就能hook函数的方法,唯一的缺点是你只能同时hook4个函数。我的做法是利用调试寄存器(即硬件断点),你可以在附录的CHook类中看到我的实现。
由于调试寄存器检测起来相当容易,任何人只要用CONTEXT_DEBUG_REGISTERS来调GetThreadContext就可以发现它,随便一个异常处理例程都会拿到一个包含调试寄存器的上下文结构(context structure),等等。解决办法是hook住NtGetContextThread、NtSetContextThread和KiUserExceptionDispatcher。当然啦,你得用一些patch来hook这些API。由于颇有一些防病毒/防广告/防火墙软件也hook这类函数来“增强系统的整体安全”,所以检测系统如果仅仅因为你改了这些系统dll就认为你作弊也是不大可能的。这意味着你可以安全的hook这4个函数,用不着担心被检测。
即使他们也许不会仅仅因为在系统模块中发现了这些hooks就抓你,但它们还是给检测留下了一点线索-他们可以通过定位你在系统模块中的hooks,分析patch的跳转指令看它跳到了哪里,然后对hook处理过程做一下CRC就足以把你认出来。
这又有两种解决办法。一种是利用int 3断点指令来做hook,然后捕捉异常(如果看的够仔细的话你会发现这在我的CHook类里也实现了)并把异常重定向到合适的hook处理过程-你需要用标准的jmp patch来hook KiUserExceptionDispatcher(这有点和我们的目标矛盾),或者用两种标准的SEH(结构化异常处理)形式之一。
还有一种办法,我觉得要好的多,虽然实现起来有点儿困难。其实也没什么花头,就是写一个hook处理过程的变形引擎,在真正有用的指令之间填充NOP等效指令(NOP等效指令是指mov eax, eax之类的指令。译注:push eax, pop eax这样的也是)。这样,对你的函数做CRC检查就行不通了。我现在还没有这么做,不过以后我可能会写些PoC代码(译注:PoC即Proof of Concept)。
最容易的一种做法是从shellcode那里来的。我敢肯定你们都知道,为了避开IDS签名(译注,IDS=Intrusion-detection system),shellcode通常是加密的,带一个动态解密的loader。用汇编给运行时解密代码写一个简单的变形引擎是很容易的,通过在不同的指令之间放置NOP指令,并且让密钥在每次加、解密时随机变化,你可以把创建签名的任何企图变得非常困难-即使不是不可能的话,而且不会牺牲多少性能。
调试寄存器在其他很多方面也是相当有用的,你可以用它hook内存读写和指令读取,这意味着你可以为内存修改做一个回调函数(用在内存数据监测上很理想,这意味着你再也用不着轮询了)。对调试寄存器的完整描述可以在附录中找到。
另外一点要考虑的是,他们可能都不检测你的API hooks,简单的用原始数据patch回去就能让你的hooks无效。这种方法实现起来不难,而且有足够的可移植性-因为不同的Windows版本中大多数API的开始几个字节是不会经常改变的-游戏公司是有可能这么干的。要对付这个问题,我们得意识到一点,就是在对进程映象的代码段进行写操作之前,必须调用VirtualProtect临时改变它的页面属性。因此,截获NtProtectVirtualMemory就可以防止他们轻易的覆盖掉你的辛勤劳动成果。
IV-总结
总而言之,注入一个模块并设置4个不可检测(在ring3级别上)的hooks是可能的。也许在某些地方会有漏洞,但我所说的基本原理应该是能到达这个效果的。以上是我对在用户态创建反检测系统的所有尝试。我知道你们肯定有能力在我描述的基础上加上你们自己的见解,我也真诚的希望你们会这么做。这个解决方案不像很多正在搞game hacking的人想的那样是一个秘密的安全体系,也和那些闭源软件开发商的做法不一样。游戏黑客比别人懂得更多,应该知道这一点,因为你们全都是玩逆向工程的。你们活着就是为了击败那些通过晦涩的手段来实现的系统安全。。。自己不要掉进同样的陷阱。