某tp手游反外掛原理淺析


作者:    我是小三
博客:    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:與運營平台相關的帳號類型p
worldId:用戶游戲角色的大區信息
openId:用戶唯一身份標識
roleId:區分用戶創建的不同角色的標識

二、框架與流程

2.1、初始化

游戲啟動的第一時間調用初始化方法與注冊Native方法,檢測運行環境,獲取設備參數,加密上報給服務器,服務下發配置腳本文件。

2.2、基本框架流程

基本框架如圖1所示: 

 

                    圖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加密傳輸,增加通信安全,不足點就是體積太大。

歡迎關注公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM