MTP手游反外挂原理浅析

作者:    我是小三
博客:    http://www.cnblogs.com/2014asm/
由于时间和水平有限,本文会存在诸多不足,欢迎补充指正。

工具环境: windwos10、IDA 7.2、Nexux 5x、JEB 3
目录 :
一、基本情况介绍
二、框架与流程
三、功能模块分析
四、总结

一、基本情况介绍

1.1、基本功能介绍

该产品主要检测修改器、变速器、虚拟机等通用外挂,网络通信通过底层socket实现,一定程度上保证安全性。使用该产品须要开发时接入SDK,目前该产品也在《xx农约》《XX精英》等手游上使用。

1.2、接口介绍

SDK在Android系统下接入需要的相关文件有以下:

tp2.jar
libtersafe2.so

SDK对外接口函数:

1. 初始化接口 initEx
2. 用户登录接口 onUserLogin
3. 前台切换到后台接口 onAppPause
4. 后台切换到前台接口 onAppResume

主要的两个接口介绍:
int initEx(int gameId, String appKey);
gameId:由反外挂后台分配
appKey:由反外挂后台分配,与gameId对应

int onUserLogin(int accountType, int worldId, String openId, String roleId);
accountType:与运营平台相关的帐号类型
worldId:用户游戏角色的大区信息
openId:用户唯一身份标识
roleId:区分用户创建的不同角色的标识

二、框架与流程

2.1、初始化

游戏启动的第一时间调用初始化方法与注册Native方法,检测运行环境,获取设备参数,加密上报给服务器,服务下发配置脚本文件。

2.2、基本框架流程

基本框架如图1所示: 

MTP手游反外挂原理浅析

 

                    图1

三、功能模块分析

3.1、java层分析

游戏初始化时调用init方法:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    if (mTssInfoReceiver == null)
    {
        mTssInfoReceiver = new MyTssInfoReceiver();
        Log.d("MTP", "Register...");
        TP2Sdk.registTssInfoReceiver(mTssInfoReceiver);
    }
    
    //登录后第一时间调用init接口, 填写gameId 和 appKey
    //TP2Sdk.initEx(19257, "d5ab8dc7ef67ca92e41d730982c5c602");
    

 //在用户成功登录游戏后,调用TP2Sdk.onUserLogin方法    
 Button qqLogin = (Button)findViewById(R.id.btn_qq_login);    qqLogin.setOnClickListener(new View.OnClickListener()
 {
     @Override
     public void onClick(View v)
     {
         int worldId = 101;                                  //大区或服务器号
         String openId = "B73B36366565F9E02C7525516DCF31C2"; //用户id
         String roleId = "paladin";                          //角色id
         
         //登录后第一时间调用init接口, 填写gameId 和 appKey
         TP2Sdk.initEx(19257, "d5ab8dc7ef67ca92e41d730982c5c602");
         
         onQQLogin(worldId, openId, roleId);
         Toast.makeText(getApplicationContext(), "QQ登录",Toast.LENGTH_SHORT).show();
         startActivity(new Intent(MainActivity.this, GameActivity.class));
     }
 });

获取设备参数是通过java层获取传到Native层加密:

private void a() {
        v.a(this.a);
        r.a("Language:zh");
        r.a("cpu_model:" + this.a(this.a));
        r.a("simulator_name:" + s.b(this.a));
        r.a("adb:" + v.b(this.a));
        r.a("info:on initialize done");
    }
      public static void a(Context arg4) {
        r.a("apk_name:" + v.e());
        r.a("app_name:" + v.f());
        r.a("ver:" + v.g());
        r.a("vercode:" + v.h());
        r.a("model:" + s.e());
        r.a("api_level:0");
        r.a("sys_ver:" + s.h());
        r.a("sdcard:" + v.e(arg4));
        r.a("sd_package:" + v.f(arg4));
        r.a("ip_beg");
        v.k();
        r.a("ip_end");
        r.a("dev_cpuname:" + s.j());
        r.a("debugger:" + v.b());
        c v0 = new c(arg4, null);
        String v1 = v0.a();
        if(v1 != null) {
            r.a("cert:" + v1);
        }

java方法r.a()最终调用到了public static native void onruntimeinfo(byte[] arg0, int arg1)方法,在该Native方法中判断是否须要加密处理,与设备相关的基本都加密。

JNI注册10个Native方法如下:

public static native void init(TssSdkInitInfo arg0)
public static native void onruntimeinfo(byte[] arg0, int arg1)
public static native void senddatatosdk(byte[] arg0, int arg1)
public static native void senddatatosvr(byte[] arg0, int arg1)
public static native int getsdkantidata(TssIOCtlResult arg0)
public static native int hasMatchRate(int arg0)
public static native void setgamestatus(TssSdkGameStatusInfo arg0)
public static native void setsenddatatosvrcb(Object arg0)
public static native void setuserinfo(TssSdkUserInfo arg0)
public static native void setuserinfoex(TssSdkUserInfoEx arg0)

3.2、Native层分析

初始化:

当so被加载时会执行到init_array中的函数,init_array中有52个函数,其中主要的是第一个与第三个init函数,第一个init方法是初始化so的Native方法表,代码如下:

.text:E09EECC4             ; 初始化方法
.text:E09EECC4             ; Attributes: bp-based frame
.text:E09EECC4
.text:E09EECC4             InitFunc_1                              ; DATA XREF: .init_array:E0D15040↓o
.text:E09EECC4 F0 B5                       PUSH    {R4-R7,LR}
.text:E09EECC6 03 AF                       ADD     R7, SP, #0xC
.text:E09EECC8 81 B0                       SUB     SP, SP, #4
.text:E09EECCA 45 4C                       LDR     R4, =0xC7E
.text:E09EECCC 10 B4                       PUSH    {R4}
.text:E09EECCE 01 BC                       POP     {R0}
.text:E09EECD0 53 F0 5A F9                 BL      getString
.text:E09EECD4 43 4E                       LDR     R6, =(dword_E0D27794 - 0xE09EECDA)
.text:E09EECD6 7E 44                       ADD     R6, PC          ; dword_E0D27794
.text:E09EECD8 30 60                       STR     R0, [R6]
.text:E09EECDA 40 F0 D9 FB                 BL      sub_E0A2F490
.text:E09EECDE 70 60                       STR     R0, [R6,#(dword_E0D27798 - 0xE0D27794)]
.text:E09EECE0 41 48                       LDR     R0, =(init+1 - 0xE09EECE6)
.text:E09EECE2 78 44                       ADD     R0, PC          ; init
.text:E09EECE4 B0 60                       STR     R0, [R6,#(dword_E0D2779C - 0xE0D27794)]
.text:E09EECE6 10 B4                       PUSH    {R4}
.text:E09EECE8 01 BC                       POP     {R0}
.text:E09EECEA 08 30                       ADDS    R0, #8
.text:E09EECEC 53 F0 4C F9                 BL      getString
.text:E09EECF0 F0 60                       STR     R0, [R6,#(dword_E0D277A0 - 0xE0D27794)]
.text:E09EECF2 40 F0 D5 FB                 BL      sub_E0A2F4A0
.text:E09EECF6 30 61                       STR     R0, [R6,#(dword_E0D277A4 - 0xE0D27794)]
.text:E09EECF8 3C 48                       LDR     R0, =(setuserinfo+1 - 0xE09EECFE)
.text:E09EECFA 78 44                       ADD     R0, PC          ; setuserinfo
.text:E09EECFC 70 61                       STR     R0, [R6,#(dword_E0D277A8 - 0xE0D27794)]
.text:E09EECFE 10 B4                       PUSH    {R4}
.text:E09EED00 01 BC                       POP     {R0}
.text:E09EED02 17 30                       ADDS    R0, #0x17
.text:E09EED04 53 F0 40 F9                 BL      getString
.text:E09EED08 B0 61                       STR     R0, [R6,#(dword_E0D277AC - 0xE0D27794)]
.text:E09EED0A 40 F0 D1 FB                 BL      sub_E0A2F4B0
.text:E09EED0E F0 61                       STR     R0, [R6,#(dword_E0D277B0 - 0xE0D27794)]
.text:E09EED10 37 48                       LDR     R0, =(setuserinfoex+1 - 0xE09EED16)
.text:E09EED12 78 44                       ADD     R0, PC          ; setuserinfoex
.text:E09EED14 30 62                       STR     R0, [R6,#(dword_E0D277B4 - 0xE0D27794)]
.text:E09EED16 10 B4                       PUSH    {R4}
.text:E09EED18 01 BC                       POP     {R0}
.text:E09EED1A 28 30                       ADDS    R0, #0x28 ; '('
.text:E09EED1C 53 F0 34 F9                 BL      getString
.text:E09EED20 70 62                       STR     R0, [R6,#(dword_E0D277B8 - 0xE0D27794)]
.text:E09EED22 40 F0 CD FB                 BL      sub_E0A2F4C0
.text:E09EED26 B0 62                       STR     R0, [R6,#(dword_E0D277BC - 0xE0D27794)]
.text:E09EED28 32 48                       LDR     R0, =(setgamestatus+1 - 0xE09EED2E)
.text:E09EED2A 78 44                       ADD     R0, PC          ; setgamestatus
.text:E09EED2C F0 62                       STR     R0, [R6,#(dword_E0D277C0 - 0xE0D27794)]
.text:E09EED2E 10 B4                       PUSH    {R4}
.text:E09EED30 01 BC                       POP     {R0}
.text:E09EED32 39 30                       ADDS    R0, #0x39 ; '9'
.text:E09EED34 53 F0 28 F9                 BL      getString
.text:E09EED38 30 63                       STR     R0, [R6,#(dword_E0D277C4 - 0xE0D27794)]
.text:E09EED3A 40 F0 C9 FB                 BL      sub_E0A2F4D0
.text:E09EED3E 70 63                       STR     R0, [R6,#(dword_E0D277C8 - 0xE0D27794)]
.text:E09EED40 2D 48                       LDR     R0, =(getsdkantidata+1 - 0xE09EED46)
.text:E09EED42 78 44                       ADD     R0, PC          ; getsdkantidata
.text:E09EED44 B0 63                       STR     R0, [R6,#(dword_E0D277CC - 0xE0D27794)]

第三个init函数是初始化脚本对应的hander,代码如下:

int InitFunc_3()
{
  dword_E0D27BA4 = getString(7374);
  dword_E0D27BA8 = (int)get_cpu_type;
  dword_E0D27BAC = getString(7974);
  dword_E0D27BB0 = (int)BitNot;
  dword_E0D27BB4 = getString(7984);
  dword_E0D27BB8 = (int)BitAnd;
  dword_E0D27BBC = getString(7994);
  dword_E0D27BC0 = (int)BitOr;
  dword_E0D27BC4 = getString(8003);
  dword_E0D27BC8 = (int)BitXor;
  dword_E0D27BCC = getString(8013);
  dword_E0D27BD0 = (int)BitLShift;
  dword_E0D27BD4 = getString(8026);
  dword_E0D27BD8 = (int)BitRShift;
  dword_E0D27BDC = getString(8039);
  dword_E0D27BE0 = (int)hex2i;
  dword_E0D27BE4 = getString(8048);
  dword_E0D27BE8 = (int)str_num;
  dword_E0D27BEC = getString(9683);
  dword_E0D27BF0 = (int)ToString;
  dword_E0D27BF4 = getString(8853);
  dword_E0D27BF8 = (int)PointerAdd;
  dword_E0D27BFC = getString(8867);
  dword_E0D27C00 = (int)PointerSub;
  dword_E0D27C04 = getString(8881);
  dword_E0D27C08 = (int)Hex2Pointer;
  dword_E0D27C0C = getString(8896);
  dword_E0D27C10 = (int)StrPointer;
  dword_E0D27C14 = getString(7844);
  dword_E0D27C18 = (int)get_module_base;
  dword_E0D27C1C = getString(6973);
  dword_E0D27C20 = (int)log_0;
  dword_E0D27C24 = getString(6980);
  dword_E0D27C28 = (int)mtp_report;
  return InitFunc_54();
JNI_OnLoad

init_array初始化方法表,常量等工作完成后执行到JNI_OnLoad,在JNI_OnLoad中主要做两件事:

注册Native方法:
.text:E0A019EC 04 99       LDR     R1, [SP,#0x28+var_18]
.text:E0A019EE 01 96       STR     R6, [SP,#0x28+var_24]
.text:E0A019F0 02 9E       LDR     R6, [SP,#0x28+var_20]
.text:E0A019F2 B0 47       BLX     R6  ; art::CheckJNI::RegisterNatives
.text:E0A019F4 01 9E       LDR     R6, [SP,#0x28+var_24]
注册如下方法:
init
setuserinfo
setuserinfoex
setgamestatus
getsdkantidata
setsenddatatosvrcb
senddatatosdk
senddatatosvr
onruntimeinfo
hasMatchRate
读取解析脚本文件:

获取脚本路路径与读取到内存中,如果游戏是第一次运行是没有脚本文件的,须要从服务器上获取,脚本路径为/data/data/游戏包名/files/tss_tmp/comm.dat。

const char **__fastcall ReadCommdat(int a1, int a2)
{
  if ( v3 == v4 )
  {
LABEL_4:
    pthread_mutex_unlock_0(v17);
    j___aeabi_memclr4(&v21);
    v5 = 0;
    if ( GetFilePath(v18, &v21, 1024) )         // 获取脚本路径
      return v5;
    v7 = sub_E0A3E006(&v20);
    v5 = 0;
    if ( ReadFile(v7, &v21) )                   // 读取脚本文件
    {
      v8 = (_DWORD *)j_malloc(16);
      v19 = v8;
      v5 = 0;
      if ( v8 )
      {
        v9 = j_strlen(v18);
        *v8 = j_malloc(v9 + 1);
        v10 = v19;
        if ( *v19 )
        {
          j_strcpy(*v19, v18);
          v11 = sub_E0A3E21A((int)&v20);
          v12 = j_malloc(v11 + 1);
          v19[1] = v12;
          if ( v12 )
          {
            sub_E0A3E204((int)&v20);
            sub_E0A3E21A((int)&v20);
            j___aeabi_memcpy(v12);
            v13 = sub_E0A3E21A((int)&v20);
            v10[2] = v13;
            *(_BYTE *)(v10[1] + v13) = 0;
            pthread_mutex_lock_0(v17);
            sub_E0A10B7C(v16, &v19);
            pthread_mutex_unlock_0(v17);
            v5 = (const char **)v19;
            goto LABEL_14;
          }
          j_free(*v10);
          v14 = v19;
        }
        else
        {
          v14 = v19;
        }
        j_free(v14);
        goto LABEL_14;
      }
    }
  return v5;
}
解析脚本文件:

解析脚本格式代码如下:

.text:E0A3ED54
.text:E0A3ED54             GetScriptType                           ; CODE XREF: sub_E0A1A650+56↑p
.text:E0A3ED54                                                     ; .text:E0A3ED90↓p ...
.text:E0A3ED54 B0 B5                       PUSH    {R4,R5,R7,LR}
.text:E0A3ED56 44 68                       LDR     R4, [R0,#4]     ; index
.text:E0A3ED58 83 68                       LDR     R3, [R0,#8]     ; 总大小size
.text:E0A3ED5A 00 21                       MOVS    R1, #0
.text:E0A3ED5C 9C 42                       CMP     R4, R3          ; 比较是否结束
.text:E0A3ED5E 04 D2                       BCS     loc_E0A3ED6A
.text:E0A3ED60 62 1C                       ADDS    R2, R4, #1      ; index++
.text:E0A3ED62 42 60                       STR     R2, [R0,#4]     ; 存index
.text:E0A3ED64 05 68                       LDR     R5, [R0]        ; src
.text:E0A3ED66 2C 5D                       LDRB    R4, [R5,R4]     ; 取值
.text:E0A3ED68 03 E0                       B       loc_E0A3ED72    ; 取的值左移8位
.text:E0A3ED6A             ; 
.text:E0A3ED6A
.text:E0A3ED6A             loc_E0A3ED6A                            ; CODE XREF: GetScriptType+A↑j
.text:E0A3ED6A 10 B4                       PUSH    {R4}
.text:E0A3ED6C 04 BC                       POP     {R2}
.text:E0A3ED6E 02 B4                       PUSH    {R1}
.text:E0A3ED70 10 BC                       POP     {R4}
.text:E0A3ED72
.text:E0A3ED72             loc_E0A3ED72                            ; CODE XREF: GetScriptType+14↑j
.text:E0A3ED72 24 02                       LSLS    R4, R4, #8      ; 取的值左移8位
.text:E0A3ED74 9A 42                       CMP     R2, R3          ; 比较是否结束
.text:E0A3ED76 03 D2                       BCS     loc_E0A3ED80    ; 两次取的值相逻辑或
.text:E0A3ED78 51 1C                       ADDS    R1, R2, #1      ; index++
.text:E0A3ED7A 41 60                       STR     R1, [R0,#4]     ; 存index
.text:E0A3ED7C 00 68                       LDR     R0, [R0]        ; src
.text:E0A3ED7E 81 5C                       LDRB    R1, [R0,R2]     ; 取后一字节内容
.text:E0A3ED80
.text:E0A3ED80             loc_E0A3ED80                            ; CODE XREF: GetScriptType+22↑j
.text:E0A3ED80 21 43                       ORRS    R1, R4          ; 两次取的值相逻辑或
.text:E0A3ED82 02 B4                       PUSH    {R1}            ; 相当于mov r0,r1
.text:E0A3ED84 01 BC                       POP     {R0}
.text:E0A3ED86 B0 BD                       POP     {R4,R5,R7,PC}

解析出来后存储,后面运行脚本时使用:

int __fastcall GetCommDat(_DWORD *a1)
{
  _DWORD *v1; // r4
  int v2; // r6
  int v3; // r5
  int result; // r0
  int v5; // r6
  int v6; // r2
  unsigned __int8 v7; // cf
  int v8; // r6
  int dataOffsetSize; // [sp+4h] [bp-10h]

  v1 = a1;
  v2 = GetScriptType(a1) << 16;//解析脚本
  v3 = GetScriptType(v1) | v2;
  result = 0;
  if ( v3 >= 1 )
  {
    v5 = v1[1];
    if ( (unsigned int)(v5 + v3) <= v1[2] )
    {
      dataOffsetSize = v5 + v3;
      v6 = j_malloc(v3 + 1);//分析内存
      result = 0;
      if ( v6 )
      {
        v7 = __CFADD__(*v1, v5);
        v8 = v6;
        j___aeabi_memcpy(v6);//存储脚本密文
        *(_BYTE *)(v8 + v3) = 0;
        result = v8;
      }
      v1[1] = dataOffsetSize;
    }
  }
  return result;
}

3.3、onruntimeinfo方法分析

从java层传进设备相关信息,选择性加密,一般与设备相关的都做加密处理,上报给服务器时使用到,代码如下:

.text:E0A4735C 5F 48                       LDR     R0, =(dev_macaddress_ - 0xE0A47362)
.text:E0A4735E 78 44                       ADD     R0, PC          ; dev_macaddress_
.text:E0A47360 01 68                       LDR     R1, [R0]
.text:E0A47362 01 98                       LDR     R0, [SP,#4]
.text:E0A47364 3F F0 C6 FA                 BL      is_device_info_key
.text:E0A47368 01 28                       CMP     R0, #1
.text:E0A4736A 06 D1                       BNE     loc_E0A4737A
.text:E0A4736C 5C 48                       LDR     R0, =(dev_macaddress_ - 0xE0A47372)
.text:E0A4736E 78 44                       ADD     R0, PC          ; dev_macaddress_
.text:E0A47370 00 68                       LDR     R0, [R0]
.text:E0A47372 50 F2 8B FC                 BL      j_strlen
.text:E0A47376 5B 49                       LDR     R1, =0x91C
.text:E0A47378 50 E7                       B       loc_E0A4721C
.text:E0A4737A
.text:E0A4737A             loc_E0A4737A                            ; CODE XREF: Enc_Data_4+158E↑j
.text:E0A4737A 5B 48                       LDR     R0, =(dev_imsi_ - 0xE0A47380)
.text:E0A4737C 78 44                       ADD     R0, PC          ; dev_imsi_
.text:E0A4737E 01 68                       LDR     R1, [R0]
.text:E0A47380 01 98                       LDR     R0, [SP,#4]
.text:E0A47382 3F F0 B7 FA                 BL      is_device_info_key
.text:E0A47386 01 28                       CMP     R0, #1
.text:E0A47388 14 D1                       BNE     loc_E0A473B4
.text:E0A4738A 58 48                       LDR     R0, =(dev_imsi_ - 0xE0A47390)
.text:E0A4738C 78 44                       ADD     R0, PC          ; dev_imsi_
.text:E0A4738E 00 68                       LDR     R0, [R0]
.text:E0A47390 50 F2 7C FC                 BL      j_strlen
.text:E0A47394 01 99                       LDR     R1, [SP,#4]
.text:E0A47396 0A 18                       ADDS    R2, R1, R0
.text:E0A47398 00 2A                       CMP     R2, #0
.text:E0A4739A 02 D0                       BEQ     loc_E0A473A2
.text:E0A4739C 10 78                       LDRB    R0, [R2]
.text:E0A4739E 00 28                       CMP     R0, #0
.text:E0A473A0 01 D1                       BNE     loc_E0A473A6
.text:E0A473A2
.text:E0A473A2             loc_E0A473A2                            ; CODE XREF: Enc_Data_4+15BE↑j
.text:E0A473A2 53 4A                       LDR     R2, =(a000000000 - 0xE0A473A8)
.text:E0A473A4 7A 44                       ADD     R2, PC          ; "000000000"
.text:E0A473A6
.text:E0A473A6             loc_E0A473A6                            ; CODE XREF: Enc_Data_4+15C4↑j
.text:E0A473A6 4D 20 40 01                 MOVS    R0, #0x9A0
.text:E0A473AA 02 99                       LDR     R1, [SP,#8]
.text:E0A473AC 09 18                       ADDS    R1, R1, R0
.text:E0A473AE FC F7 43 FC                 BL      Enc_Data_2 //加密
.text:E0A473B2 7D E5                       B       loc_E0A46EB0

3.4、init方法分析

解密解析脚本、注册脚本对应方法表,判断设备是否root、是否为模拟器、是否正在调试:
注册脚本方法表部分代码如下:

v129 = 3;
  v130 = process_exists;
  v131 = getString(5530);
  v132 = 3;
  v133 = root_process_exists;
  v134 = getString(5553);
  v135 = 3;
  v136 = module_exists;
  v137 = getString(5570);
  v138 = 5;
  v139 = module_size_eq;
  v140 = getString(5588);
  v141 = 5;
  v142 = module_crc_eq;
  v143 = getString(5605);
  v144 = 5;
  v145 = module_first_crc_eq;
  v146 = getString(6757);
  v147 = 6;
  v148 = module_size_in_range;
  v149 = getString(5628);
  v150 = 6;
  v151 = opcode_eq;
  v152 = getString(5641);
  v153 = 6;
  v154 = opcode_not_eq;
  v155 = getString(5658);
  v156 = 7;
  v157 = opcode_eq2;
  v158 = getString(5672);
  v159 = 7;
  v160 = opcode_not_eq2;
  v161 = getString(5690);
  v162 = 1;
  v163 = timehooked;
  v164 = "sock_exists";
  v165 = 2;
  v166 = sock_exists;
  v167 = getString(5704);

根据传入的Key得到value解密脚本,代码如下:

int __fastcall GetScript(_DWORD *a1)
{
  _DWORD *v1; // r4
  int v2; // r6
  int v3; // r5
  int v4; // r6
  int v5; // r2
  int v6; // r0
  unsigned __int8 v7; // cf
  char *v8; // r2
  char *v9; // r3
  int v11; // [sp+8h] [bp-14h]
  int v12; // [sp+Ch] [bp-10h]

  v1 = a1;
  v2 = GetScriptType(a1) << 16;
  v3 = GetScriptType(v1) | v2;
  v4 = 0;
  if ( v3 )
  {
    v5 = v1[1];
    if ( (unsigned int)(v5 + v3) <= v1[2] )
    {
      v11 = v1[1];
      v12 = v5 + v3;
      v6 = j_malloc(v3 + 1);
      if ( v6 )
      {
        v7 = __CFADD__(*v1, v11);
        v4 = v6;
        j___aeabi_memcpy(v6);                   // 拷贝脚本密文
        *(_BYTE *)(v4 + v3) = 0;
        v1[1] = v12;
        Decsrc(v4, v3, v8, v9);                 // 解密脚本
      }
    }
  }
  return v4;
}

root:判断su文件是否存在,调试判断:TracePid。模拟器判断是根据脚本解析出来的特征判断,特征如下:

name=Netease|type_0=1|data_0=/system/bin/nemuVM-prop|type_1=1|data_1=/x86.prop|type_2=2|data_2=init.svc.nemu-service: |type_3=1|data_3=/data/data/com.netease.nemu_android_launcher.nemu

判断特征文件是否存在,代码如下:

signed int __fastcall access_stat(_BYTE *a1)
{
  _BYTE *v1; // r4
  signed int v2; // r5
  int v4; // [sp+4h] [bp-70h]
  v1 = a1;
  if ( !a1 || !*a1 || (v2 = 1, access_0(a1, 0)) && stat_0(v1, &v4) )
    v2 = 0;
  return v2;
}

3.5、解析脚本并执行

以下面解密后两条脚本为例:

module_exists("libhoudini_415c.so")
is_root()&&is_hidden_malware_exists("GGuardian")&&module_size_in_range("libh.so",12288,20480)

脚本语法分析代码如下:

signed int __fastcall Parsing_Words(int a1, int a2, int a3)
{
  int v3; // r4
  int v4; // r3
  int v5; // r5
  signed int result; // r0
  signed int v7; // r2
  _DWORD *v8; // r3
  int v9; // r0
  unsigned __int8 *v10; // r2
  int v11; // r0
  int v12; // r0
  _BYTE *v13; // r6
  int v14; // r6
  int v15; // r0
  int v16; // r2
  const char *v17; // r1
  int v18; // r2
  const char *v19; // r1
  _BYTE *v20; // r0
  int v21; // r0
  int v22; // r2
  int v23; // r3
  int v24; // r1
  unsigned int v25; // r0
  signed int v26; // r1
  signed int v27; // r2
  signed int v28; // r0
  unsigned int v29; // [sp+Ch] [bp-28h]
  _BYTE *v30; // [sp+10h] [bp-24h]
  unsigned int v31; // [sp+14h] [bp-20h]
  unsigned int *v32; // [sp+18h] [bp-1Ch]
  unsigned __int8 *v33; // [sp+1Ch] [bp-18h]
  unsigned __int8 *v34; // [sp+20h] [bp-14h]
  int v35; // [sp+24h] [bp-10h]

  v35 = a2;
  v3 = a1;
  v4 = *(_DWORD *)(a3 + 1020);
  v5 = 0;
  if ( v4 >= 0 )
  {
    *(_DWORD *)(a3 + 1020) = v4 - 1;
    v5 = *(_DWORD *)(a3 + 4 * v4);
  }
  if ( *(_DWORD *)(v5 + 264) != 2 )
  {
    *(_DWORD *)(a1 + 16) = 1;
    return free_0(v3, v5);
  }
  v7 = *(_DWORD *)(a2 + 1020);
  v8 = (_DWORD *)(a2 + 1020);
  if ( v7 > -1 )
  {
    v9 = v7 - 1;
    *v8 = v7 - 1;
    if ( v7 )
    {
      v10 = *(unsigned __int8 **)(a2 + 4 * v7);
      *v8 = v9 - 1;
      v11 = 4 * v9;
      if ( v10 )
      {
        v34 = *(unsigned __int8 **)(a2 + v11);
        if ( v34 )
        {
          v32 = (unsigned int *)(a2 + 1020);
          v33 = v10;
          v12 = malloc_0(v3, 272);
          if ( !v12 )
          {
LABEL_61:
            free_0(v3, v34);
            free_0(v3, v33);
            return free_0(v3, v5);
          }
          v13 = (_BYTE *)v12;
          j___aeabi_memclr4(v12);
          v13[1] = 0;
          *v13 = 48;
          v29 = sub_E0A392E8(v34);
          v31 = sub_E0A392E8(v33);
          v30 = v13;
          if ( !j_strcmp(v5, "+") )
          {
            v18 = v31 + v29;
            v19 = "%lu";
            v20 = v13;
          }
          else
          {
            if ( !j_strcmp(v5, "-") )
            {
              v18 = v29 - v31;
              v19 = "%lu";
            }
            else
            {
              v14 = v35;
              if ( !j_strcmp(v5, "*") )
              {
                v16 = v29 * v31;
                v17 = "%lu";
                goto LABEL_55;
              }
              v15 = j_strcmp(v5, "/");
              if ( v31 && !v15 )
              {
                v16 = sub_E0C9840C(v29);
                v17 = "%lu";
LABEL_55:
                j_sprintf(v30, v17, v16);
LABEL_56:
                v25 = *v32 + 1;
                if ( v25 < 0xFF )
                {
                  v26 = 0;
                  if ( *(_DWORD *)(v14 + 1024) == 1 )
                    v26 = 1;
                  *(_DWORD *)(v3 + 20) = v26;
                  *v32 = v25;
                  *(_DWORD *)(v14 + 4 * v25) = v30;
                }
                else
                {
                  *(_DWORD *)(v3 + 16) = 1;
                }
                goto LABEL_61;
              }
              v21 = j_strcmp(v5, "%");
              if ( v31 && !v21 )
              {
                sub_E0C97FAC(v29, v31, v22, v23);
                v16 = v24;
                v17 = "%lu";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, "==") )
              {
                v16 = 1;
                if ( v29 != v31 )
                  v16 = 0;
                v17 = "%d";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, "!=") )
              {
                v16 = 1;
                if ( v29 == v31 )
                  v16 = 0;
                v17 = "%d";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, ">=") )
              {
                v16 = 1;
                if ( v29 < v31 )
                  v16 = 0;
                v17 = "%d";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, "<=") )
              {
                v16 = 1;
                if ( v29 > v31 )
                  v16 = 0;
                v17 = "%d";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, ">") )
              {
                v16 = 1;
                if ( v29 <= v31 )
                  v16 = 0;
                v17 = "%d";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, "<") )
              {
                v16 = 1;
                if ( v29 >= v31 )
                  v16 = 0;
                v17 = "%d";
                goto LABEL_55;
              }
              if ( !j_strcmp(v5, "&&") )
              {
                v27 = 1;
                v28 = 1;
                if ( !v31 )
                  v28 = 0;
                if ( !v29 )
                  v27 = 0;
                v18 = v27 & v28;
                v19 = "%d";
              }
              else
              {
                if ( j_strcmp(v5, "||") )
                {
                  *(_DWORD *)(v3 + 16) = 1;
                  goto LABEL_23;
                }
                v18 = 1;
                if ( !(v31 | v29) )
                  v18 = 0;
                v19 = "%d";
              }
            }
            v20 = v30;
          }
          j_sprintf(v20, v19, v18);
LABEL_23:
          v14 = v35;
          goto LABEL_56;
        }
      }
    }
  }
  result = 1;
  *(_DWORD *)(v3 + 16) = 1;
  return result;
}

语法解析出来后,如果是一个方法,找到对应的模拟方法的Hander,(比如解密第一条脚本得到module_exists方法与参数libhoudini_415c.so,这条脚本判断是否为模拟器)计算方法名crc,通过crc查找上面表中对应的方法地址并执行。

.text:E0A376A2 99 69                       LDR     R1, [R3,#0x18]  ; jumptable 00058628 case 1
.text:E0A376A4 40 B4                       PUSH    {R6}
.text:E0A376A6 01 BC                       POP     {R0}
.text:E0A376A8 88 47                       BLX     R1  ; 执行脚本对应方法
.text:E0A376AA 01 B4                       PUSH    {R0}
.text:E0A376AC 04 BC                       POP     {R2}
.text:E0A376AE F0 49                       LDR     R1, =(aLd_0 - 0xE0A376B4)
.text:E0A376B0 79 44                       ADD     R1, PC          ; "%ld"
.text:E0A376B2 00 E3                       B       loc_E0A37CB6
module_exists方法代码如下:
signed int __fastcall module_exists(int a1, _BYTE *a2)
{
  int v2; // r6
  signed int v3; // r5
  signed int v4; // r6
  int v6; // [sp+Ch] [bp-10h]

  v2 = a1;
  if ( a2 && *a2 && (v6 = sub_E0A57BC8()) != 0 )
  {
    v3 = 0;
    do
    {
      if ( !getsopath(v6) )
        break;
      ++v3;
      v4 = 1;
      if ( j_strstr() )
        goto LABEL_9;
    }
    while ( v3 <= 9999 );
    v4 = 0;
LABEL_9:
    sub_E0A57C44(v6);
  }
  else
  {
    sub_E0A37194(v2);
    v4 = 0;
  }
  return v4;
}

其它脚本类似的流程执行,再将执行后的结果打包加密发送给服务器,包体结构如下:

struct TssSdkEncryptPkgInfo
{
    unsigned int cmd_id_; 
    const unsigned char *game_pkg_; 
    unsigned int game_pkg_len;
    unsigned char *encrypt_data_; 
    unsigned int encrypt_data_len_;
};
struct TssSdkDecryptPkgInfo
{
    const unsigned char *encrypt_data_;
    unsigned int encrypt_data_len; 
    unsigned char *game_pkg_; 
    unsigned int game_pkg_len_; 
};

3.6、开启心跳

创建线程定时发送心跳数据包,代码如下:

_DWORD *Ticker()
{
  _DWORD *v0; // r4

  v0 = (_DWORD *)dword_E0D27AD0;
  if ( !dword_E0D27AD0 )
  {
    j_pthread_once(&unk_E0D27AD8, t8_Ticker);
    v0 = (_DWORD *)dword_E0D27AD0;
    if ( !dword_E0D27AD0 )
    {
      v0 = (_DWORD *)malloc_2(96);
      *v0 = &off_E0D11488;
      v0[3] = 0;
      v0[2] = 0;
      v0[4] = 0;
      v0[5] = 300;
      v0[6] = 3600;
      v0[7] = 150;
      v0[8] = 5;
      v0[9] = 30;
      v0[10] = 0;
      sub_E0A0DA72(v0 + 11);
      j___aeabi_memclr4(v0 + 15);
      dword_E0D27AD0 = (int)v0;
    }
  }
  return v0;
}

 

四、总结

总体来说该反外挂产品架构设计方面比较灵活,游戏侧可以在服务端结合上报数据制定脚本策略进行对抗。比如:当游戏遇到一些定制的外挂时,反外挂目前特征暂未覆盖到的外挂时,可以根据该外挂的作弊原理,制作针对该外挂的定制脚本,将该脚本动态下发到游戏客户端,运行的游戏客户端解析该脚本后,对定制的外挂进行针对性的检测和打击。可做到实时更新检测算法逻辑。通信层主要使用socket加密传输,增加通信安全,不足点就是体积太大。

欢迎关注公众号
MTP手游反外挂原理浅析

上一篇:ARM相关


下一篇:常用汇编指令