nszdhd1's blog.

网易保护分析

2020/07/03

网易保护研究

目的

分析网易游戏保护,提取可借鉴优点

分析思路

我理解的功能范围:加壳、各个检测项、数据上报。
所以我的两种思路:

  1. 收集当下流行工具、攻击方式对网易游戏进行测试(缺点:最多只能察觉哪些操作会触发检测,对于非敏感的检测点(不能直接判定为作弊者的点)意义不大,且反馈只有崩溃、正常、和部分模糊的提示,主观猜测成分太大。优点:简单,可用于所有厂商的保护分析)

  2. 过保护,拿到正常的so对源码进行逆向分析(缺点:难度大,只针对网易)

分析过程

1. 根据思路一分析

安装游戏《创造与魔法》,打开提示 如下:

image-20191219150353652

检测到修改器后,立刻退出。想检测是否安装了外挂辅助应用,必定需要读取安装应用权限,关闭该权限,查看是否可绕过:

image-20191219150839657

获取权限失败,然后退出应用。没想到是这种强买强卖的操作。

此时手机内安装的敏感应用如下:ourplay(vpn、虚拟环境、谷歌服务一体的应用)、平行空间、virtualXposed、GG、游戏蜂窝、葫芦侠、virtualapp。

经过测试,以上被检查的应用,有且仅有:游戏蜂窝

此检测项让我发现思路一的不合理性,数据采集并不会单纯的检测某一项,一定是多项一起检测,并且网易是检测到危险应用并不会立即崩溃,大大的增加了猜测的难度。遂放弃思路一。

2. 思路二之静态分析

打开压缩包,查看lib,可以很明显的看到是mono的游戏,并且保护也很明显是libNetHTProtect:

image-20191219152308695

使用ida打开libmono.so , 发现so被加壳了,全部是空函数。

image-20191231110002483

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里

image-20191231110325677

image-20191219153709772

同时发现网易自己实现了一些系统函数:

image-20191219154301411

因为按道理来讲,当linker执行完init加载so成功,进入JNI_Onload时,此时的so应该是在正常状态(已解密),此时从内存中dump下来,稍微修复一下代码就可以看到全部函数。

但实际上:

  1. 使用VA直接dump /proc/pid/maps 中 libmono 的地址,失败,原因:bad address ,部分内存无法访问。

  2. 使用VA将应用在libmono的 jniload处停住,使用GG将内存在中的libmono dump 下来。dump虽然成功了,但so依旧不太正常。证明此时GG内存读取检测、VA检测还未被拉起。多次试验,结果相同。使用readelf读取结果如下:image-20191219171201819

  3. 我怀疑是GG不够智能,换个姿势再dump一次。看linker加载过程可知,so的所有信息其实保存在一个soinfo结构的链表里。image-20191219172155364

但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。

image-20191219201656654

打开va运行,然后依旧失败。GG和soinfo dump 下来的东西一样,并且都不是已经解密的so,排除dump工具的问题,那么一定是时机不对。

观察log,发现dlopen打开了一个奇怪的so,拿到caller的地址,用上面的函数发现是libmono.so调用的,此时肯定恢复了函数。dump下来。需要记一下基地址,此时函数的地址是 基址+偏移

image-20191224211317668

本以为网易会做很难的东西,结果就是把elf头抹去了,使用偷懒的办法把加壳的mono头粘贴到dump下来的so,稍微修复一下,就可以看到完整的代码了。

image-20191224211927793

跟踪加载dll.so 的加载地址,稍微向上跟踪一下,会发现是mono中

image-20191231160126869

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函数。

image-20200102144419934

稍微跟踪一下此函数,并未发现有任何异常的函数,hook一下此函数,发现程序并不能正确运行,猜测该函数应该已经被hook,选择hook do_mono_image_load,把加载的dll dump下来,dnspy打开如下:

image-20200106150325309

与未解密的dll进行对比:

image-20200108103051403

到此就可以实现对游戏的测试、外挂开发了。

因为目的是研究保护,看了一圈,libmono.so并没发现有价值的东西,还是开始分析libNetHTProtect。搜索 SVC 0 找到自己实现的系统调用,发现了open、read、ptrace等等,hook open 函数,并且打印调用栈,信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
c6fa8000   c6e0a000     19E000 
[libNetHTProtect.so] module->startAddress 0xc6fa8000 ,module->endAddress 0xc71e2000

2019-12-27 16:12:18.134 28422-28574/com.hero.sm.android.hero D/OOOK_LOG: libNetHTProtect open /proc/self/status
2019-12-27 16:12:18.134 28422-28574/com.hero.sm.android.hero D/OOOK_LOG: TAG:libNetHTProtect -->
# 0: 0xd256983a _Z16captureBacktracePPvj
# 1: 0xd2569b3c _Z17backtraceToLogcatPcPh
# 2: 0xd2560a46 _Z12new_openhookPvPcS_S_
# 3: 0xc70427d2 -->0x0009a7d2
# 4: 0xc6fc7d3e -->0x0001fd3e
# 5: 0xc6fc7d3e -->0x0001fd3e

2019-12-27 16:12:19.739 28422-28785/com.hero.sm.android.hero D/OOOK_LOG: libNetHTProtect open /data/user/0/io.virtualapp.ex/virtual/data/user/0/com.hero.sm.android.hero/files/gamePicServ
2019-12-27 16:12:19.744 28422-28785/com.hero.sm.android.hero D/OOOK_LOG: TAG:libNetHTProtect -->
# 0: 0xd256983a _Z16captureBacktracePPvj
# 1: 0xd2569b3c _Z17backtraceToLogcatPcPh
# 2: 0xd2560a46 _Z12new_openhookPvPcS_S_
# 3: 0xc70427d2 -->0x0009a7d2
# 4: 0xc70579da -->0x000af9da
# 5: 0xc70579da -->0x000af9da


2019-12-27 16:12:19.865 28422-28787/com.hero.sm.android.hero D/OOOK_LOG: libNetHTProtect open /proc/28787/pagemap
2019-12-27 16:12:19.866 28422-28787/com.hero.sm.android.hero D/OOOK_LOG: TAG:libNetHTProtect -->
# 0: 0xd256983a _Z16captureBacktracePPvj
# 1: 0xd2569b3c _Z17backtraceToLogcatPcPh
# 2: 0xd2560a46 _Z12new_openhookPvPcS_S_
# 3: 0xc70427d2 -->0x0009a7d2(openfile)
# 4: 0xc70d4928 -->0x0012c928
# 5: 0xc70d4928 -->0x0012c928

检测收集了一下内容:

/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附近,但此处有混淆,看起来很费力,流程图缩小如下:

image-20200108211852458

每次主动退出会打开下面文件
/data/user/0/io.virtualapp.ex/virtual/data/user/0/com.hero.sm.android.hero/files/idymdyt_game_settings.xml

image-20200108174650943

自定义文件都不是明文,内容后期还需要分析。

libNetProtect.so还收集了很多电池信息:

image-20200108204336620

在init_array时,初始化了一个类,里面注册了很多自己实现和导入的系统函数。(openfile)

image-20191228104219675

0xc70d4928处多次打开文件,两次自己实现的open打开失败,则调用系统的open函数

image-20191228104849063

该应用使用了腾讯bugly,最近几次报错:

image-20191228112212942

image-20191228112759167

分析结果

1. 需要加强对蜂窝游戏的检测

原因: 游戏蜂窝辅助脚本丰富、上手难度几乎没有、甚至包含云挂机(付费项目未体验)。游戏蜂窝的危害远大于普通多开软件(平行空间、双开精灵等)
检测方法: 读取已安装应用,查看游戏蜂窝是否安装。(因为不在一个uid下无法通过检测VA的方式检测,未体验云挂机没办法分析如何检测)

2. 系统调用使用中断实现

原因: 通常调用的系统函数如open、read之类的都是导入libc中的函数,libc帮我们实现了从用户态到内核态,所以使用libc的open、read很容易就能被分析出来。自己实现系统调用可以增加反编译后的分析难度。

举例

1
2
3
4
5
6
7
8
9
10
11
12
#include <private/bionic_asm.h>

ENTRY(__getpid)
mov ip, r7
ldr r7, =__NR_getpid
swi #0
mov r7, ip
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
END(__getpid)

3. 网易dll加载过程

正常的mono dll 加载过程 :

image-20200106151549009

网易的mono dll 加载过程(此点存疑,也可能是因为反编译错误):

image-20200106154240364

image-20200106153855163

这样做我能想到的优点:

  1. dll加载的时机提前
  2. 具有迷惑性,所有函数都是mono正常加载需要使用的函数
  3. 实现简单

我认为的缺点:

  1. 致命的profiler初始化机制,找不到参数对应的函数,会打开参数对应的so。dlopen是很敏感的操作,并且使用的还不是自己实现的dlopen,hook linker中的dlopen一眼就能看到奇怪的dll.so
  2. 脱壳后ida打开一目了然
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2020-01-09 20:37:43.538 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: libmono.so in do_mono_image_load 
2020-01-09 20:37:43.538 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: name:/data/app/com.hero.sm.android.hero-LE9REnGocufmcpQN2JDQog==/base.apk/assets/bin/Data/Managed/Assembly-CSharp.dll len:4472832 datac:MZ�
2020-01-09 20:37:43.538 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: TAG:libmono.so -->
# 0: 0xcec2acaa _Z16captureBacktracePPvj
# 1: 0xcec2afac _Z17backtraceToLogcatPcPh
# 2: 0xcec21e44 _Z22new_do_mono_image_loadiPiii
# 3: 0xc58a7d1c -->0x00190d1c

2020-01-09 20:37:43.779 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: libmono.so in do_mono_image_load
2020-01-09 20:37:43.779 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: name:/data/app/com.hero.sm.android.hero-LE9REnGocufmcpQN2JDQog==/base.apk/assets/bin/Data/Managed/System.Core.dll len:265728 datac:MZ�
2020-01-09 20:37:43.780 30033-30198/com.hero.sm.android.hero D/OOOK_LOG: TAG:libmono.so -->
# 0: 0xcec2acaa _Z16captureBacktracePPvj
# 1: 0xcec2afac _Z17backtraceToLogcatPcPh
# 2: 0xcec21e44 _Z22new_do_mono_image_loadiPiii
# 3: 0xc58a7420 -->0x00190420
# 4: 0xc58a8080 -->0x00191080(mono_image_open_full)
# 5: 0xc584b8dc -->0x001348dc
# 6: 0xc584ebcc -->0x00137bcc
# 7: 0xc584ecd8 -->0x00137cd8
# 8: 0xc584ed38 -->0x00137d38(mono_assembly_load)
# 9: 0xc584a52c -->0x0013352c
#10: 0xc585206c -->0x0013b06c
#11: 0xc5863bfc -->0x0014cbfc
#12: 0xc5863ea4 -->0x0014cea4(mono_class_get_full)
#13: 0xc58d97f4 -->0x001c27f4
#14: 0xc58d6b54 -->0x001bfb54
#15: 0xc58d6cf4 -->0x001bfcf4
#16: 0xc58d93a4 -->0x001c23a4
#17: 0xc58d99e8 -->0x001c29e8
#18: 0xc58d6b54 -->0x001bfb54(mono_metadata_parse_type_full)
#19: 0xc58d75dc -->0x001c05dc
#20: 0xc58afcd0 -->0x00198cd0
#21: 0xc585c8f4 -->0x001458f4
#22: 0xc585b19c -->0x0014419c
#23: 0xc585ecf4 -->0x00147cf4
#24: 0xc5864c74 -->0x0014dc74(mono_class_init)
#25: 0xc46c53f8 -->0xfefae3f8
#26: 0xc46b6c28 -->0xfef9fc28
#27: 0xc46b2014 -->0xfef9b014
#28: 0xc46b5698 -->0xfef9e698
#

加载Assembly-CSharp.dll 的调用栈只能跟踪到

image-20200109204217508

而System.Core.dll与上边的dll路径不同,

image-20200109204344874

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
2
3
4
5
6
7
8
9
#define AC_ONLINE_PATH "/sys/class/power_supply/ac/online" AC 电源连接状态 
#define USB_ONLINE_PATH "/sys/class/power_supply/usb/online" USB电源连接状态
#define BATTERY_STATUS_PATH "/sys/class/power_supply/battery/status"充电状态
#define BATTERY_HEALTH_PATH "/sys/class/power_supply/battery/health"电池状态
#define BATTERY_PRESENT_PATH "/sys/class/power_supply/battery/present"使用状态
#define BATTERY_CAPACITY_PATH "/sys/class/power_supply/battery/capacity"电池 level
#define BATTERY_VOLTAGE_PATH "/sys/class/power_supply/battery/batt_vol"电池电压
#define BATTERY_TEMPERATURE_PATH "/sys/class/power_supply/battery/batt_temp"电池温度
#define BATTERY_TECHNOLOGY_PATH "/sys/class/power_supply/battery/technology"电池技术 当电池状态发生变化时,driver 会更新这些文件。传送信息到java

电池主要作用为模拟器检测,一般模拟器的电池温度为0和电量始终为50%(不变或很少变化)

同理,通过检测android系统层特征检测模拟器的点:

  • wifi,GPS,蓝牙,温度传感器的信息与真机不同
  • Android模拟器不支持呼叫和接听实际来电,但可以通过控制台模拟电话呼叫(呼入和呼出);
  • Android模拟器不支持USB连接。
  • Android模拟器不支持音频输入(捕捉),但支持输出(重放)。
  • Android模拟器不支持扩展耳机。
  • Android模拟器不能确定SD卡的插入/弹出。

5. 函数表

在 libmono.so 和 libNetProtect.so的 init_array段的第一个函数中,都初始化了两个C++ 的对象,其中包含了很多导入的系统函数和自己实现的系统函数:

image-20200109152913179

image-20200109152937116

调用这些函数时,ida反编译如下:

image-20200109153616599

这样做的优点:

  1. ida 反编译是 变量 + 偏移,隐藏了函数名、函数调用等信息,增加了分析的难度
  2. 所有系统函数(包括自己实现的)放在一个类里,便于开发和维护。
CATALOG
  1. 1. 网易保护研究
    1. 1.0.1. 目的
    2. 1.0.2. 分析思路
    3. 1.0.3. 分析过程
      1. 1.0.3.1. 1. 根据思路一分析
      2. 1.0.3.2. 2. 思路二之静态分析
    4. 1.0.4. 分析结果
      1. 1.0.4.1. 1. 需要加强对蜂窝游戏的检测
      2. 1.0.4.2. 2. 系统调用使用中断实现
      3. 1.0.4.3. 3. 网易dll加载过程
      4. 1.0.4.4. 3.信息保存
      5. 1.0.4.5. 4. 电池信息的作用
      6. 1.0.4.6. 5. 函数表