外挂学习之路(10)--- 穿透发包线程寻找call的通杀方法

来源:互联网 发布:软件著作权资助 编辑:程序博客网 时间:2024/06/13 23:21

就像我的 “外挂学习之路(1)--- bp send 回溯寻找关键call”里描述的那样,无发包线程函数的游戏找call可以用bp send方法通杀,但是遇见好点的游戏(现在游戏一般都有发包线程),这种方法就不行了。今天就来说说网上看到的一篇文章,讨论如何穿透这个发包线程的。

===================================================================================================================

原文链接:http://www.ghoffice.com/bbs/read-htm-tid-91876-page-1.html

标题:【惨淡通杀之】跳出 bp send 的死循环,浅谈结果与过程  
作者: 混沌  
联系方式: QQ**********9 (避免广告嫌疑,呵呵)  
交流群: QQ群:71367967(未满),88823177(满)  
声明:如果您看了此文,有收获,转载请写明出处,O(∩_∩)O谢谢  
 
------------------------------------------------------------淫荡的分割线--------------------------------------------------------------------  
 
相信很多热爱搞游戏的朋友,热爱搞调试的朋友,热爱搞逆向的朋友,在逆向调试网络游戏时,都会遇到过这样的经历:设bp send断点,然后在游戏中断下来了,可是Ctrl+F9,返回上一层,无一例外,最后返回到了同一个地方,并且断不一样的封包,得到的结果都是一样,结果就是无数次的看到那个堪称梦魇的地方,为什么每次结果都一样?为什么总是看到那张熟悉而又陌生的面孔?为什么就这样无限的死循环?有没有办法跳出去?去发现新大陆?  
 
答案是肯定的,有,二期很简单,可能很多人也知道,只是很多人暂时还不想放出来而已。  
 
现在,让我们一起逃离那个梦魇之地吧,远离那张魔鬼面孔。  
 
在bp send的基础上,我们断到了send相关内容,len buf flags 等等,具体的我们可以看下send()函数:  
 
---------------------------------------------  
  #include <winsock.h>  
  int PASCAL FAR send( SOCKET s, const char FAR* buf, int len, int flags);  
  s:一个用于标识已连接套接口的描述字。  
  buf:包含待发送数据的缓冲区。     
len:缓冲区中数据的长度。  
  flags:调用执行方式。  
---------------------------------------------  
而我们经常用到的无非就2个参数:len buf。  
给 len 设置条件断点,我们可以断下我们想要的那些符合长度的包;  
给 buf 设置内存断点,我们可以断下这个buf的组成过程。  
 
我想,当你看到这里的时候,你已经踩在了逃离之路的路面上了。  
没错,在数据区域,也就是左下角的内容里面给buf设置内存写入断点,就是你逃离的开始。  
 
具体你可以这样:假设我们 bp send的时候,edx值存放的是buf的地址,那么我们在左下角的命令框里面输入 db edx 回车,当然是在bp send断点被断到的时候回车,  
这样,我们就看到了这个封包的buf数据,你会发现,很多前面4个字节,或者前面2个字节相同,是很多包都会出现的,或者不一样功能的包会出现不一样的值。  
而 这几个字节就是我们逃离的出口了,给这几个字节设置 内存写入 断点,然后你会发现这时候被断下来的地方跟bp send的断下的地方不一样了,这个时候你就好好判断一下这几个准备要写入的字节,一般在断下来的地方会是这样代码:mov xxxx,al 就是把eax的后面那个字节给他写进来,比如此时 eax = 00123389,那么 al = 89 ,也就是说buf的第一个字节将会是89 ,如果这个字节正好是你所需要断下来封包开头的特征,那么好,跟我一起来操作:F8 单步走(一般情况此时断下的地方应该在系统领空),走几步,你就会发现走到了 游戏领空 ,如 00812345的地方,在这个地方的上一行,正好是个 00812340 ---- call xxxx ,这个时候,你就可以执行到返回(Ctrl+F9)了,然后在觉得可疑的地方下断点,这个返回的过程绝对要比bp send 断下返回的过程精彩得多,然后就是运行,去除一些一直断的断点了…… 后面怎么分析就看你想要什么功能了,此时,你已经成功逃离了梦魇之地,魔鬼面孔已不知何时消失了!  
 
总结:  
问:为什么给buf设内存断点就能跳出死循环?  
答:因为给buf设内存断点的时候,你断到了组建buf的过程,当你跳出这个过程的时候,那你就找到了组建的开始,也就是功能call执行完后,开始组包  
 
问:那为什么bp send会一直都是一样的结果?  
答:因为bp send的时候,断到的是组好包后的结果,所以就看不到过程了,看不到过程的结果就是死循环。  
 
问:我在返回到 00812340 ---- call xxxx 这些地方然后设断点行不行?  
答:可以,不过你会比较累一点而已,因为此时你断到的地方是组包的过程,你可以继续返回上面几层,比如返回到了 00712345 ---- mov xxx,xxx 之后再设断点,基本上这段代码已经不是组包的过程了。  
 
写到这里,让我想起了一句话:人生的精彩就在于奋斗过程!同志们,加油!  
 
混沌写于:2011年8月23日18:23:25  
(最近比较忙,时间有限,写得难免有错漏,欢迎指正,不胜感激)  

==================================================================================================================

实际测试这种方法的时候发现,这种方法有两个条件(也不算苛刻吧)

1.send函数的缓冲区地址必须是不变的,很多游戏不是,发一次缓冲区地址变一次,这时候这个方法就不行了。

2.有些游戏客户端和服务器交互过于频繁(客户端无任何操作的情况下),这时候这个方法就显得有点吃力,因为这个方法的核心是在发送的buff处下写入断点,从而跨越线程之间的障碍,但是必须保证的是下完断点下次写入到内存的数据正好是自己想要的那个call发出来的,这就要求操作员下完断点立即操作游戏,执行关键动作,在此之间游戏客户端和服务器之间没有任何的交互。

最后,

虽然这个方法有些局限性,相信通过改良可以是一个很好的找call通杀的方法。改良思路:跨过线程之后,分析调用过程,找到发包线程里面的明文call,在明文call处下断点(可以条件过滤去除自己不关心的调用,比如心跳调用明文call)



--------------------------------------------以下2017.02.28更新------------------------------------------------------------


现在大部分游戏都是模式2,带有发包线程的,这使得我们无法使用bp send直接找到call,理由很简单,发包线程和功能call线程不在同线程,OD调试的时候只有一个线程是激活状态,其他线程都出去挂起状态,单步跟踪无法跨越线程。


查看游戏是不是子线程发包

方法一:bp send下断点,CTRL+F9一直回溯,最终调试在程序领空进入死循环,或显示运行中,如下图.


注意看OD的运行状态为“执行到放回”,这一小段代码就是死循环的最顶部,就是整个发包线程,确定一下是不是发包线程,打开OD的线程窗口,即快捷栏中的T如下图


我们可以看到有个线程正好指向这段代码,为了循环过快一般这样的循环都有sleep或者waitforsingleobject函数。要想进一步验证可挂起该线程再操作游戏可看到游戏就像断了网一样的现象。

方法二:

和方法一类似,bp send下断点,断下之后打开线程窗口,刷新一下,看到只有一个线程是激活状态,其他都是暂停状态,标识为红色,入口为0的主线程,看看这个主线程是不是激活线程就可以了。

 

Bp send之后回溯到主线程

按照上面那位大哥所说的一步一步来,然而并没回溯到主线程,但是他的方法提供了一个很好的思路,我们再思考下独立线程发包模型,bp send之后咱来一步一步分析下汇编代码,

红标指向的call ebx就是调用send API的地方,我们bp send第一次回溯就是下面这段代码,


简单分析下代码,首先看到两个API—进入临界区和出来临界区,我们知道这是线程锁,用来稳定多线程的,防止多线程导致数据错乱,这段代码应该是从封包队列里面取出封包然后发送的,然后我们看到一个循环,这个循环是循环读取封包队列,一次追加到发送的BUF中,我们知道TCP协议是流式的,有粘包和分包的概念,所以这样做是没有问题的,一步一步分析汇编指令到的下面的结论:

1. 封包队里里面存的是指针,指针指向是需要加密的字节流

2. send的buf是从队列里面的buf拷贝出来的

那么我们就追这个队列里面的buf是哪里来的,

0FE8CC9F |.  8B4C24 10        |MOV ECX,DWORD PTR SS:[ESP+10]0FE8CCA3 |.  53               |PUSH EBX                                ;  缓冲区剩余大小0FE8CCA4 |.  51               |PUSH ECX                                ;  最终send的buf地址0FE8CCA5 |.  57               |PUSH EDI                                ;  需要加密的字节流的地址0FE8CCA6 |.  8BCE             |MOV ECX,ESI0FE8CCA8 |.  E8 F3F1FFFF      |CALL em.0FE8BEA0                        ;  组建封包缓冲区(有加密过程)
在call处下断点,连续点击F9,查看“需要加密的字节流的地址”是否变化,若不变化就好做了,直接在这个缓冲区下写入断点,如果变化,则另想他法。

经过测试我们发现,这个队列里面的buf地址一直在变,这就不好下手了,我们肯定要在这个队列上下手,这是我们穿越线程的最好桥梁(也可能是唯一桥梁),仔细观察代码,我们看到了有个地址储放了队列里面剩余包的个数,哈哈,很好的下手点,发包线程这边发一个剩余个数就减少一个,那主线程向队列里面放一个,队列里面剩余包的个数就加一个,肯定要改写这个值,就以此为下手点,在[ESI+3007C]下写入断点,不出意外的话应该有两处会断下,一出就是现在已经找到的发送封包时(子线程),另一处就是封包入队列时(主线程)。依照上述找法我们找到了封包入队列的子程序,如下:


我们回溯一下这段代码,看谁在调用他,发现只有一处,如下:


果然我们看到了我们熟悉的临界区,想想肯定要有临界区,否则谁来保护因多线程读写造成队列数据不稳定呢。

 

大致看下组建封包的缓冲区和加密的过程,有利于将来研究脱机外挂



找和使用明文包call

明文包call,顾名思义还没有经过加密处理的call,这个call一般是组建封包的,具有通用型,

一般不具有发包线程的有比较好找点,不用阅读汇编代码直接找到两个功能call,从上向下对比一下就找到了,不再赘述,我们找找具有发包线程的“明文包call”,其实这个call不是真正的明文包call,只不过他具有通用性,找打他我们就找到95%的功能call,因为所有的和游戏服务器交互的功能都要经过这个call,在这个call上面下断点回溯就可以找到了,为什么是95%呢,因为有些功能不是调用他来实现的,比如有些游戏的普通攻击是设置攻击状态的,普通攻击功能call本身并不发包,只是在客户端设置普通攻击的状态,然后用一个死循环检测这个状态。下面我们开始找,首先找到一个功能call(任意方式下找到任意功能call),然后看这个功能call如何最终调用我们的仅队列子程序,这个不难找,不再赘述,找到了如下的一个call,


这就是我们找到的“明文包call”,想找任意其他功能的话在这个call里面下断点,操作游戏,断点断下,回溯就可以了。说一下一个小窍门:如果这个call被调用太过频繁的话(我们还没有操作游戏断点就断下了),并且我们不好下条件断点,这时可以搜索搜有命令“CALL 1020B780”,找到所有调用这个call的地方(一般不是特别多,百十来个吧),给所有这些call上面下断点,运行游戏,我们还没有任何操作游戏就可能断下,把这些断点去除掉,直到我任何操作游戏不会断下,这时我们再找我们我们想要的call(如寻路,技能打怪,召唤坐骑,召唤宠物),断点断下,我们回溯就可以找到功能calll了。

最后,以上都是个人观点和看法,写错的地方请不吝赐教,谢谢

--------------------------------------------以下2017.03.24更新------------------------------------------------------------

上面那个例子相对来讲还是比较简单的,我们找到了封包队列里面封包的个数,剩下的都比较容易了,下面分析一个稍微复杂的例子《神魔大陆2》

 

初步分析:

bp send下断点,回溯,


看到是线程发包,并且这个发送数据包的地址是固定的(ecx值),向这个数据里面下内存写入断点,游戏中走两步,使其断下,如下图,可以看到断到了一个系统区域里面,


CTRL+F9回溯,来到游戏领空,可以看到刚刚的系统领空是个memove()的API,难办的是我们线程仍然在发包线程里面线程ID:6D4


同时在这个memove和send处下断点,我们注意到两处处理的内存数据是完全相同的(内容和长度),那就继续追寻数据来源eax,发现eax的值是变动的,如果强制一直跟下去毫无疑问的是肯定可以找到基地址和偏移,然而那又有卵用呢?我们想找到是功能代码,想跳转到主线程,这样找下去,意义不大吧。

 

进一步分析:

经分析,数据来源于eax(变动), eax来源于[esi(变动)+4],esi来源于[edi(不变)+BC],我们分析,功能线程写入数据,发包线程读取数据,两人都必须知道数据存放的地址,但这个地址是变动的,猜测流程应该是这样的:功能线程把数据写入到某内存地址(某),然后用一个不变地址存放这个变动的地址,发包线程去这个不变地址找到新的变动的地址的值,然后再去这个变动地址里面提取数据,这样来讲,由不变到变肯定是功能线程写入的,这样我们在esi+BC的地方下写入断点,再来看看效果。实际测试效果令人失望,线程没有切换,没啥打乱用,该咋办?迷茫

 

峰回路转:

OD下写入断点没找到想要的结果,那就用CE试试吧,


一共有三处写入,简单走两步测试一下,效果依然令人失望,线程没有切换,甚至第三个断点压根不经常断下。

 

再出奇招:

我们来到三个代码所在代码区域的顶部下端,然后在游戏里面喊话,为什么是喊话?因为喊话可以在堆栈明显的看到函数内容,用以确定明文包call。


经测试在第三个代码处出现如上截图,我们在堆栈里面看到了我们在游戏里面喊话的内容“11111111”,这个就离明文包call,不远了,我们再试试走路和选怪,发现线程不是主线程,这说明功能call不一定非得在主线程里面,我们根据这个关键地址进行回溯找到了喊话call和选怪call,其他功能call就不找了。

 

最后,虽然这个游戏仍然有很多疑问,但是以上的分析思路是值得记录和学习的。

 


1 0
原创粉丝点击