网易保护研究
目的
分析网易游戏保护,提取可借鉴优点
分析思路
我理解的功能范围:加壳、各个检测项、数据上报。
所以我的两种思路:
收集当下流行工具、攻击方式对网易游戏进行测试(缺点:最多只能察觉哪些操作会触发检测,对于非敏感的检测点(不能直接判定为作弊者的点)意义不大,且反馈只有崩溃、正常、和部分模糊的提示,主观猜测成分太大。优点:简单,可用于所有厂商的保护分析)
过保护,拿到正常的so对源码进行逆向分析(缺点:难度大,只针对网易)
分析过程
1. 根据思路一分析
安装游戏《创造与魔法》,打开提示 如下:
检测到修改器后,立刻退出。想检测是否安装了外挂辅助应用,必定需要读取安装应用权限,关闭该权限,查看是否可绕过:
获取权限失败,然后退出应用。没想到是这种强买强卖的操作。
此时手机内安装的敏感应用如下:ourplay(vpn、虚拟环境、谷歌服务一体的应用)、平行空间、virtualXposed、GG、游戏蜂窝、葫芦侠、virtualapp。
经过测试,以上被检查的应用,有且仅有:游戏蜂窝
此检测项让我发现思路一的不合理性,数据采集并不会单纯的检测某一项,一定是多项一起检测,并且网易是检测到危险应用并不会立即崩溃,大大的增加了猜测的难度。遂放弃思路一。
2. 思路二之静态分析
打开压缩包,查看lib,可以很明显的看到是mono的游戏,并且保护也很明显是libNetHTProtect:
使用ida打开libmono.so , 发现so被加壳了,全部是空函数。
linker是Android系统动态库so的加载器/链接器,并且linker本身就是一个动态链接库。当linker加载so时,会先执行so文件中的.init段代码,然后执行.init_array段中所指向的函数。当linker加载完返回到位于:art/runtime/java_vm_ext.cc的LoadNativeLibrary()函数,此函数继续检测并执行so库中的JNI_Onload()方法。
所以在整个so加载过程中函数执行的顺序如下:
.init段 -> .init_array段指向的函数 -> JNI_Onload() -> java_com_XXX
一般加壳会在so的init_array或者jni_onload处进行操作,由图可见,关键函数应该在init_array里
同时发现网易自己实现了一些系统函数:
因为按道理来讲,当linker执行完init加载so成功,进入JNI_Onload时,此时的so应该是在正常状态(已解密),此时从内存中dump下来,稍微修复一下代码就可以看到全部函数。
但实际上:
使用VA直接dump /proc/pid/maps 中 libmono 的地址,失败,原因:bad address ,部分内存无法访问。
使用VA将应用在libmono的 jniload处停住,使用GG将内存在中的libmono dump 下来。dump虽然成功了,但so依旧不太正常。证明此时GG内存读取检测、VA检测还未被拉起。多次试验,结果相同。使用readelf读取结果如下:
我怀疑是GG不够智能,换个姿势再dump一次。看linker加载过程可知,so的所有信息其实保存在一个soinfo结构的链表里。
但soinfo* do_dlopen(const char* name, int flags)已经是android5之前的事情了,现在do_dlopen返回的是soinfo的handle。
用ida打开/system/bin/linker,并未发现tatic soinfo* soinfo_from_handle(void* handle),又通过阅读源码可知,调用find_containing_library,传入一个地址,就能查找包含该地址的soinfo。
打开va运行,然后依旧失败。GG和soinfo dump 下来的东西一样,并且都不是已经解密的so,排除dump工具的问题,那么一定是时机不对。
观察log,发现dlopen打开了一个奇怪的so,拿到caller的地址,用上面的函数发现是libmono.so调用的,此时肯定恢复了函数。dump下来。需要记一下基地址,此时函数的地址是 基址+偏移
本以为网易会做很难的东西,结果就是把elf头抹去了,使用偷懒的办法把加壳的mono头粘贴到dump下来的so,稍微修复一下,就可以看到完整的代码了。
跟踪加载dll.so 的加载地址,稍微向上跟踪一下,会发现是mono中
mono_profiler_load函数会根据命令行参数加载 profiler 的初始化函数,默认的 profiler 名字是是 log
, 那么会找到 mono_profiler_startup_log
。如果找不到函数会尝试加载一个叫 mono-profiler-XXX
的动态链接库,然后尝试在动态链接库里面找一个叫 mono_profiler_startup
的初始化函数。
回到 mono_image_open_from_data_with_name 开始分析,此函数被libNetProtect hook了,最终会调用下图的函数,很明显,这个函数就是未加固的libmono的mono_image_open_from_data_with_name函数。
稍微跟踪一下此函数,并未发现有任何异常的函数,hook一下此函数,发现程序并不能正确运行,猜测该函数应该已经被hook,选择hook do_mono_image_load,把加载的dll dump下来,dnspy打开如下:
与未解密的dll进行对比:
到此就可以实现对游戏的测试、外挂开发了。
因为目的是研究保护,看了一圈,libmono.so并没发现有价值的东西,还是开始分析libNetHTProtect。搜索 SVC 0 找到自己实现的系统调用,发现了open、read、ptrace等等,hook open 函数,并且打印调用栈,信息如下:
1 | c6fa8000 c6e0a000 19E000 |
检测收集了一下内容:
/proc/self/status 读取失败会陷入死循环
/system/build.prop
/system/bin/linker
/proc/self/maps
/proc/29532/pagemap
/proc/net/arp
/proc/net/unix
/proc/29153/task/29153/status
/proc/bus/input/devices
/proc/mounts
/proc/sys/fs/inotify
常规的反调试检查调用位置很近都在偏移0x6A560附近,但此处有混淆,看起来很费力,流程图缩小如下:
每次主动退出会打开下面文件
/data/user/0/io.virtualapp.ex/virtual/data/user/0/com.hero.sm.android.hero/files/idymdyt_game_settings.xml
自定义文件都不是明文,内容后期还需要分析。
libNetProtect.so还收集了很多电池信息:
在init_array时,初始化了一个类,里面注册了很多自己实现和导入的系统函数。(openfile)
0xc70d4928处多次打开文件,两次自己实现的open打开失败,则调用系统的open函数
该应用使用了腾讯bugly,最近几次报错:
分析结果
1. 需要加强对蜂窝游戏的检测
原因: 游戏蜂窝辅助脚本丰富、上手难度几乎没有、甚至包含云挂机(付费项目未体验)。游戏蜂窝的危害远大于普通多开软件(平行空间、双开精灵等)
检测方法: 读取已安装应用,查看游戏蜂窝是否安装。(因为不在一个uid下无法通过检测VA的方式检测,未体验云挂机没办法分析如何检测)
2. 系统调用使用中断实现
原因: 通常调用的系统函数如open、read之类的都是导入libc中的函数,libc帮我们实现了从用户态到内核态,所以使用libc的open、read很容易就能被分析出来。自己实现系统调用可以增加反编译后的分析难度。
举例:
1 |
|
3. 网易dll加载过程
正常的mono dll 加载过程 :
网易的mono dll 加载过程(此点存疑,也可能是因为反编译错误):
这样做我能想到的优点:
- dll加载的时机提前
- 具有迷惑性,所有函数都是mono正常加载需要使用的函数
- 实现简单
我认为的缺点:
- 致命的profiler初始化机制,找不到参数对应的函数,会打开参数对应的so。dlopen是很敏感的操作,并且使用的还不是自己实现的dlopen,hook linker中的dlopen一眼就能看到奇怪的dll.so
- 脱壳后ida打开一目了然
1 | 2020-01-09 20:37:43.538 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: libmono.so in do_mono_image_load |
加载Assembly-CSharp.dll 的调用栈只能跟踪到
而System.Core.dll与上边的dll路径不同,
3.信息保存
保存信息文件名为:com.hero.sm.android.hero/files/idymdyt_game_settings.xml
策略:每次libNetProtect进入JNI_onload函数并调用初始化函数时,会检测com.hero.sm.android.hero/files/文件夹下是否有该文件,有则读取内容,进行操作(暂未找到上传接口),并且清空文件内容。如果没有则正常进行。当检测到风险主动退出时,会打开或创建该文件,写入内容后,退出应用。
4. 电池信息的作用
Linux标准的 Power Supply驱动程序 所使用的文件系统路径为:/sys/class/power_supply ,其中的每个子目录表示一种能源供应设备的名称。
1 | #define AC_ONLINE_PATH "/sys/class/power_supply/ac/online" AC 电源连接状态 |
电池主要作用为模拟器检测,一般模拟器的电池温度为0和电量始终为50%(不变或很少变化)
同理,通过检测android系统层特征检测模拟器的点:
- wifi,GPS,蓝牙,温度传感器的信息与真机不同
- Android模拟器不支持呼叫和接听实际来电,但可以通过控制台模拟电话呼叫(呼入和呼出);
- Android模拟器不支持USB连接。
- Android模拟器不支持音频输入(捕捉),但支持输出(重放)。
- Android模拟器不支持扩展耳机。
- Android模拟器不能确定SD卡的插入/弹出。
5. 函数表
在 libmono.so 和 libNetProtect.so的 init_array段的第一个函数中,都初始化了两个C++ 的对象,其中包含了很多导入的系统函数和自己实现的系统函数:
调用这些函数时,ida反编译如下:
这样做的优点:
- ida 反编译是 变量 + 偏移,隐藏了函数名、函数调用等信息,增加了分析的难度
- 所有系统函数(包括自己实现的)放在一个类里,便于开发和维护。