有关神武3手游协议分析的一些简单思路
1 常规思维
市面上大部分的手游,都是基于cocos2dx。神武3同样如此,不过有一点却很奇怪,该游戏没有采用Lua脚本引擎,而是采用了python脚本。众所周知,cocos2dx有对Lua的支持,没有对python的支持,这就意味着,多益已经将cocos2dx提供的C++api用python进行了封装,并且已经非常成熟稳定了。
当然,这对于我们来说并不重要。我们首先应该要做的是找到构造登录包的地方,最直观的就是从登录按钮那里入手,登录按钮必然包含了点击事件,那么问题就来了,这个登录按钮是继承于安卓原生Button,还是自己独立创建的新控件?
如果是原生的,只需要找到它的监听事件,用Xposed Hook所有的OnClick()函数,并打印出函数所在的类,即可找到点击事件。
如果不是原生的,那么这个监听器就是自己设计的,原理应该是当按下屏幕的时候记录下来坐标,同时记录下来抬起动作的坐标,如果两个坐标都在控件范围内,则产生一个点击事件。
接下来开始反编译apk,先静态分析一波……
2 静态分析apk
反编译的apk中搜索不到和登录相关的关键词,猜测要么做了混淆,要么放在了so里面。
于是用ida查看几个可疑的so,在libdygame.so中找到了关键字符串,并且还发现了大量的python字符串。意味着有部分代码是写在python脚本中。
根据以上分析,我们已经知道,登录的事件函数有可能存在三个地方。
- Java层,onclick点击事件中。
- Native层,自己实现了Button,并且将事件函数放入了native中。
- Python脚本,通过cocos2dx的api获取点击事件,然后调用事件函数。
3 验证假设一
在apk中搜索不到onclick函数,也就是说在Java层中,没有点击事件,所以假设一不成立。
4 验证假设二
自定义button中肯定有一个按下和抬起的动作,还有一个检测触控点移动范围的函数。Java层提供控件的UI,所有的事件都回调Native层的函数,那么native层中有暴露接口给Java层调用,结合之前分析的libdygame.so,可以看到,有几个可疑的接口,touchonend()、touchonstart()、onkeyup()、onkeydown()等等。
于是我们使用frida,Hook这几个相关函数,touchonstart(),touchonend()连个函数有被调用,而这两个函数都传入了坐标参数。然后接着看上层的引用,进入GLSurfaceView中,它继承于SurfaceView,说明是一个view控件。到这里就差不多弄明白了,登录Button是通过GLSurfaceView实现的,其点击事件处理过程交给了Native层。
通过以上分析,我们可以得知假设二成立。
5 验证假设三
因为python脚本实际上是调用cocos2dx接口,而cocos2dx在libdygame.so中,也就是说python是在native层的,所以我们无法确定点击事件有没有传递给python。
这个假设可以暂时待定,继续分析假设二中的native层。
6 ida动态调试libdygame.so
既然已经知道了登录的逻辑代码在Native层,那么我们就开始动态调试,先在所有可疑的函数前下断点。因为登录过程是需要网络发包的,登录包必然要经过send函数,所以也要对其下断点。我们的目的是寻找登录相关的代码,包括了登录数据包的生成部分。下面开始进行调试。
7 一次错误的尝试
只有send函数断下来了,那么之前的猜想可能存在误差。只能确定调用了send发包函数,而send函数是libc库函数,完全不知道问题出在那里啊,基本上算是没有思路了。根据之前的三种假设,也只剩下假设三存在一定的可能性,前路一片黑暗啊,革命尚未成功,同志仍需努力…
8 柳暗花明又一村
经过一天的休整,继续围绕send函数做突破,我们需要的send最上层的函数,packet的组包函数,那我能不能把send调用的堆栈打印出来呢?有了堆栈就能知道packet buff经过了哪些函数,最上层的有可能就是packet的组包函数。
这里使用Frida打印函数的堆栈send(Thread.backtrace(this.context, Backtracer.FUZZY).map(DebugSymbol.fromAddress).join("\n"));
。从输出结果可以看到,出现了一些列的PyEval_CallObjectWithKeywords函数调用,最上层为系统函数,下面就是python的一系列调用。现在基本可以肯定了,登录函数和组包函数全部都在python代码中。
|
|
adb看一下apk的包目录,找到一个data.fls可疑文件,而这个文件在反编译的apk中是没有的,说明是从服务器下载过来,有可能是资源文件,但更大的可能性是加密后的pyc文件集合。
9 解密data.fls文件
我们知道python是一门解释型的语言,将py文件编译成pyc字节码文件,然后将pyc字节码文件加载到python虚拟机中。换一种思想理解,在python中所有的一切都是对象,pyc文件也是一个对象,所以,加载到内存中,以一个对象的形式存在着。python又是动态执行的,通过import导入模块,那么加载pyc的机制一定在import机制里面,结合Google搜索查找import的相关资料,在python源码中可以很容易的定位到最底层的pyc加载函数,分析该函数发现有两个方法将pyc转换成object对象,从字符串加载j_PyMarshal_ReadObjectFromString
和从文件加载j_PyMarshal_ReadObjectFromFile
。那么,在这里解密data.fls文件可以从两个方向入手,第一个方向是找到加载data.fls文件的函数,分析该函数是如何解密data.fls文件的,解密出来的肯定是n多个不同路径的pyc文件组合;第二个方向就是从import导入机制入手,由于data.fls解密后的内容在内存中,python虚拟机必然是从字符串加载成object,只需要拦截j_PyMarshal_ReadObjectFromString
函数传入的参数字符串(pyc),就能还原出完整的data.fls数据。
- 方向1,分析加载data.fls的函数
已完成,以后再发表。
- 方向2,分析j_PyMarshal_ReadObjectFromString函数
每次执行import操作,都会判断模块是否已经导入,如果没有导入,则执行该函数,所以对该函数进行hook操作,第二个参数String则是我们需要的pyc。请看以下源码
|
|
js代码hook指定函数
|
|
通过以上步骤成功的导出了pyc文件,接下来我们需要将pyc文件转换成py文件,用到开源的程序uncompyle2,具体细节不再赘述。
10 分析导出的py源码
从始至终我们的目的都是为了分析登录协议。一步步分析下来,猜测到登录相关函数在python代码中,所以接下来我们开始分析dump下来的源码,在这里使用sublime进行分析,用sublime打开文件夹,在文件夹中全局搜索login、username、password等待关键字。很容易的定位到了login函数。
接下来就是按部就班的分析协议了,具体细节请参考项目demo,其中有详细的注释。
11 总结
最难的部分已经解决了,分析协议格式已经不存在难度了。所以具体的细节就不写出来了。在这次逆向中,对手游的框架有一个全面的认识。