1. 程式人生 > >Android主流HOOK框架介紹與應用--遊戲破解遊戲外掛的必殺技

Android主流HOOK框架介紹與應用--遊戲破解遊戲外掛的必殺技

概述

使用HOOK方案主要是在分析的時候會經常用到,雖然二次打包重新修改程式碼也可以做到,但是一方面效率低,另一方面如果APP有校驗的邏輯就需要進一步繞過,總體還是比較費時費力。所以,通過動態HOOK的方式可以不用直接修改APP檔案,也比較方便。下面就分別介紹下比較成熟的幾個HOOK框架及其應用:XPOSED,frida,substrate。

XPOSED

本節介紹的是XPOSED框架的使用,XPOSED的安裝器替換安卓系統的app_process檔案,從而實現對系統的接管,通過回撥模組的方式來達到不用修改APK就能改變其表現行為的目的。該框架比較成熟,對應的模組也非常多,常用的還有模擬地理位置,偽裝手機裝置資訊等,腦洞是非常之大,基本上能想到的都能做到。由於篇幅限制這裡介紹一個簡短而實用的案例,其他模組可以參考XPOSED官網的模組列表。
場景:大媽的煩惱! 週末自駕出去玩,大媽在一旁抱怨:他們單位裡要搞個活動需要員工來組織,大家都不想參加,不然搞得都沒有時間跳廣場舞了。最後他們想出了一個辦法:投骰子,誰輸了誰上。他們群裡大家都在投的火熱,就差大媽了,大媽激動的不敢投,只好在這抱怨。。。我讓大媽先憋著急,我有大寶劍可以借她一用。 某信投骰子的原理: package com.tencent.mm.sdk.platformtools public static int tx(int paramInt){ return new Random(System.currentTimeMillis()).nextInt(paramInt + 0 + 1) + 0;
} 這個函式比較有意思,如果是猜拳遊戲就隨機化0~2之間的數字,分別表示剪刀、石頭、布;如果是投骰子游戲則隨機化一個0~5之間的數字,也就是骰子的點數。因此只要HOOK這段程式碼,返回想要的點數即可,這裡使用XPOSED框架,主要程式碼:
findAndHookMethod("com.tencent.mm.sdk.platformtools.be", lpparam.classLoader, "tx", int.class, new XC_MethodHook() {
    @Override
    protected void afterHookedMethod(MethodHookParam param) {
        int gameType = ((Integer) param.args[0]).intValue();
        switch (gameType) {
            case 5:
//投骰子游戲,強制返回最大點數,基數為0,所以返回5
                param.setResult(5);
            default:
        }
    }
});
給大媽選擇了最大的點數,大媽順利逃過一劫,從此又可以愉快地跳廣場舞了。 某信的APK安裝包並沒有進行加殼保護,雖然做過程式碼混淆處理,但是還是能夠反編譯,而且即使混效果上面的程式碼邏輯也能看得懂。

frida

本節介紹的是frida框架,frida這個HOOK框架主要使用Python和javascript指令碼編寫,所以相容性和移植性都很好,可以適用於多種平臺,最重要的是不用每次都重啟手機。官網:http://www.frida.re/,裡面介紹了安裝步驟,參考Android下的例子可以很快上手:http://www.frida.re/docs/examples/android/ 場景:厲害了我的蛇! 最近比較魔性的一款手遊“貪吃蛇大作戰”,就連很少玩遊戲的朋友也開始玩起來了。自己也體驗玩了幾局,發現有些“玩家”操作都異常的快速和靈敏,總是被他們撞死。心想這些玩家不會是用了輔助工具了吧,當看到那些原地打轉速度非常快而且軌跡非常之圓,心中就更加確定真人肯定無法達到這種操作(事後分析才知道是機器人玩家)。於是打算分析下,剛好最近同事推薦了frida這個HOOK框架,正好可以拿來練練手。 遊戲初始化函式initEntity:
package com.wepie.snake.module.game;

private void initEntity()
{
    GameConfig.factor = 1.0F;
    SnakeSurfaceView.access$102(SnakeSurfaceView.this, SnakeFactory.creatSnakeSelf(LoginHelper.getLoginUser().nickname, 5, null));
    SnakeSurfaceView.access$1102(SnakeSurfaceView.this, new AiManager(SnakeSurfaceView.this.snakeInfo));
    SnakeSurfaceView.this.allSnakes.clear();
    SnakeSurfaceView.this.allSnakes.add(SnakeSurfaceView.this.snakeInfo);
    SnakeSurfaceView.this.allSnakes.addAll(SnakeSurfaceView.this.aiManager.snakeAis);
    initUtils();
}

遊戲初始化函式會呼叫函式creatSnakeSelf來建立玩家的蛇:
public static SnakeInfo creatSnakeSelf(String paramString, int paramInt, MultiNode paramMultiNode)
{
    int i = SkinConfig.getInUserSkinId();
    SkinInfo localSkinInfo = SkinManager.getInstance().getSkin(i);
    paramString = new SnakeInfo(paramString, localSkinInfo, paramMultiNode);
    Log.i("999", "----->creatSnakeSelf skin_id=" + localSkinInfo.skin_id);
    paramString.bodyInfo.initBodyPoints(getBodyPoints(paramString, 0.0F, 0.0F, paramInt));
    return paramString;
}

該函式建立蛇的顏色和身體,顏色反正隨機的就好無所謂,主要是蛇的身體,繼續向下分析可以分析出paramInt就是身體的初始長度,再回頭看下initEntity對creatSnakeSelf的呼叫可以看出:遊戲初始時蛇的長度是5。現在就好辦了,我們HOOK一下creatSnakeSelf函式的呼叫,讓初始的長度非常大就好了。 creatSnakeSelf函式的第三個引數是MultiNode型別,先記下完整的類名: com.wepie.snake.module.game.main.MultiNode HOOK後運行遊戲,蛇的身體在開始遊戲時就非常長啦。 後面再實現無敵模式,通過遊戲結束函式(onGameOver)逆向分析,尋找一個比較巧妙又比較方便HOOK的函式,最終找到了doSnakeDie這個函式:
private void doSnakeDie(final SnakeInfo snakeInfo, final SnakeInfo snakeInfo2) {
    if (snakeInfo.snakeStatus.superFrameCount <= 0 && (snakeInfo2 == null || snakeInfo2.snakeStatus.superFrameCount <= 0)) {
        this.extraFactory.addWrecks(snakeInfo);
        if (!snakeInfo.isSnakeAi) {
            ((SnakeSurfaceView.GameStatusCallback)this.gameStatusCallback).onGameOver();
        }
        else if (snakeInfo2 != null) {
            snakeInfo2.addKillNum();
            if (!snakeInfo2.isSnakeAi) {
                ((SnakeSurfaceView.GameStatusCallback)this.gameStatusCallback).onKillAi();
            }
        }
        snakeInfo.doDie();
        return;
    }
}

這個函式的意義是:第一個引數蛇去碰第二個引數蛇,如果第一個引數是我們自己的蛇,那麼就意味著撞死遊戲結束;如果第一個引數是機器人蛇,那麼死了就死了,我們還可以繼續。 因此HOOK的方法就是:攔截該函式的呼叫,判斷第一個引數是否是我們自己的蛇,如果是則讓函式直接返回。 所以,還需要在creatSnakeSelf呼叫時儲存下建立的自己的蛇物件。當然通過函式的邏輯可以看出機器人蛇與玩家的蛇不同之處就是變數isSnakeAi,如果不想儲存creatSnakeSelf建立的蛇物件,直接通過isSnakeAi來判斷也是可以的,後面一併給出相應地做法。 記下所要用到的類與函式: 類: com.wepie.snake.module.game.snake.CollisionUtil 函式: doSnakeDie 蛇型別: com.wepie.snake.module.game.snake.SnakeInfo 參考frida的Android示例程式碼,編寫指令碼:
#coding:utf-8

import frida, sys

def on_message(message, data):
    if message['type'] == 'send':
        print("[*] {0}".format(message['payload']))
    else:
        print(message)

jscode = """
Java.perform(function () {
    var curSnake = null;

    // Function to hook is defined here
    var SnakeFactory = Java.use('com.wepie.snake.module.game.snake.SnakeFactory');

    // Whenever creatSnakeSelf is called
    SnakeFactory.creatSnakeSelf.implementation = function (name, len, multiNode) {
        // Show a message to know that the function got called
        send('creatSnakeSelf');
        len = 50;

        // Call the original creatSnakeSelf
        curSnake = this.creatSnakeSelf(name, len, multiNode);

        // Log to the console that it's done
        console.log('Done: init snake len: ' + JSON.stringify(len) + " name: " + name
            + " my snake: " + curSnake.toString() + ' isSnakeAi: ' + curSnake.isSnakeAi.value);
        return curSnake;

    };

    var CollisionUtil = Java.use('com.wepie.snake.module.game.snake.CollisionUtil');
    CollisionUtil.doSnakeDie.implementation = function (snake1, snake2) {
        // Show a message to know that the function got called
        if (snake1.equals(curSnake) || snake1.isSnakeAi.value == false){
            console.log('snake1 isSnakeAi: ' + snake1.isSnakeAi.value + " snake1: " + snake1.toString());

            // Log to the console that it's done
            console.log('Done: my snake will not die!');
            return;
        }else{
            // Call the original doSnakeDie
            return this.doSnakeDie(snake1, snake2);
        }

    };

});
"""

process = frida.get_usb_device().attach('com.wepie.snake')
script = process.create_script(jscode)
script.on('message', on_message)
print('[*] Running CTF')
script.load()
sys.stdin.read()
主要功能邏輯是上面的JS程式碼,外面的Python指令碼基本都這個模式,在蛇初始化時候creatSnakeSelf儲存下自己的蛇物件,修改CollisionUtil的碰撞邏輯,當引數1的蛇是自己的蛇物件時候就返回掉(或者引數1的蛇isSnakeAi值為false直接返回也可以),就能保證不死了。 效果圖如下,隨便撞什麼蛇或者牆都不死了:
通過上面的分析也可以看出,該遊戲沒有加殼保護也沒有進行程式碼混淆,所以很容易就被秒破了。

substrate

上面介紹的兩個HOOK場景皆是在java程式碼層做的HOOK,而且通常都會有一些限制。例如,XPOSED框架的限制是必須要求連帶安裝XPOSED installer,對應的模組功能才能起效果,而frida則需要配合PC終端操作。而這裡介紹的substrate方式,主要是在JNI層做HOOK,而且實現出的應用可以單獨使用。 場景:叉叉助手的加速器! 之前分析遊戲保護的時候接觸過叉叉助手(先了解對方怎麼做,才好想出對應的保護方法),例如下面的某三國遊戲,叉叉助手提供了加速器功能。可以在遊戲介面上注入並載入一個外掛,外掛可以自由設定輔助配置,很是強大。
後來就從這個叉叉助手模擬實現了一個可以任意注入外掛的方法:
通過逆向分析叉叉助手來看它的實現原理:使用inject工具把so注入到zygote中,並利用libsubstrate.so的功能來實現對關鍵函式的HOOK。首先對使用MSJavaHookMethod對handleBindApplication進行HOOK來攔截APP的啟動,在攔截函式中判斷啟動的APP是否是要攔截的APP,如果不是則放行,如果是則通過MSJavaHookClassLoad來HOOK目標APP中Activity的類載入,並在攔截函式中再HOOK Activity的onCreate函式,在onCreate的攔截函式中動態載入外掛APK,並呼叫外掛的init函式。部分程式碼如下:
static void OnCallback_ClassOnCreate(JNIEnv *jni, jobject activity, jobject bundle)
{
    string strName;
    strName = getObjectClassName(jni, activity);
    LOGD("[%s] begin: %s::OnCreate", __FUNCTION__, strName.c_str());

    (*old_onCreate)(jni, activity, bundle);

    LOGD("[%s] dynamic load plug apk: %s", __FUNCTION__, g_cfg.strPlugApkPath.c_str());
    ……
    //呼叫外掛的靜態函式init,原型:public static void init(Activity activity, String soPath)
    jmethodID plugUIinit = jni->GetStaticMethodID(plugClass, "init", "(Landroid/app/Activity;Ljava/lang/String;)V");
    if ( plugUIinit==NULL ) {
        LOGE("[%s] not found \"init\" in plug activity: public static void init(Activity activity, String soPath)", __FUNCTION__);
    }else{
        LOGD("[%s] invoke %s::init", __FUNCTION__, g_cfg.strPlugActivity.c_str());
        jstring soPath = jni->NewStringUTF(g_cfg.strPlugSoPath.c_str());
        jni->CallStaticVoidMethod(plugClass, plugUIinit, activity, soPath);
    }

    LOGD("[%s] end", __FUNCTION__);
}
但是C++寫起來開發效率比較低,出錯了也不容易排查。上面的JNI程式碼用java程式碼寫起來就非常輕鬆:
String dexOutputDir = "/data/data/" + targetPackageName + "/cache";
DexClassLoader dexClassLoader = new DexClassLoader(plugApkPath, dexOutputDir, null, ClassLoader.getSystemClassLoader());
java.lang.Class<?> plugClass = dexClassLoader.loadClass(plugInitClass);
Method mInit = plugClass.getDeclaredMethod(Constant.METHOD_NAME_plug_init, Activity.class, String.class);
mInit.invoke(null, param.thisObject, plugSoPath);

但是並不是意味著就不用寫JNI程式碼了,有時候是必須在SO底層做HOOK的。這個應用場景主要在破解逆向分析、脫殼上比較有用,前期的JNI程式碼寫的很是痛苦,編譯一次手機需要重啟,而且一旦出錯並不是那麼容易排查,但是框架搭建好後,以後再需要只要HOOK對應的NDK層函式即可。

總結

1、XPOSED 優點: 1)、程式碼編寫方便,開發速度較快。 2)、有許多現成的模組可以用,而且很多模組也是開源的,方便學習研究。 缺點: 1)、每次編寫程式碼需要重啟手機生效。 2)、不支援native的HOOK。 3)、獨立性較差,需要依賴XPOSED installer,不易單獨分發。 2、substrate 優點: 1)、比較擅長在native層的HOOK。 2)、獨立性較好,實現的功能可以封裝在單獨APP裡分發給使用者使用,因此也是較大型外掛輔助工具的首選。 缺點: 1)、每次編寫程式碼需要重啟手機生效。 2)、開發效率較低,成本較高。 3、frida 優點: 1)、無須重啟手機和目標APP,這個可以節省很多時間,如果APP測試的點需要很複雜地搭建好環境,一旦重新啟動就意味著很麻煩地再重新搭建環境,例如賬號登入,進入特定關卡等。 2)、JS指令碼編寫,靈活方便,再也不用擔心多引數個數和型別問題了。 3)、可以直接使用或修改物件的成員變數,非常方便。 4)、配合PC終端命令列使用,指令碼編寫出錯也不會導致APP崩潰,只需修改後重新來過即可,有時會有問題,這個時候需要重啟下APP或手機即可。 缺點: 1)、JS指令碼套在python腳本里面,編寫JS指令碼時候不是很方便,容易出錯,好在即使出錯也不會導致APP崩潰掉,修改後重新來過即可。 2)、該工具配合PC終端使用,更適合專業者,不利於分發給使用者使用。 綜上的案例也可以看出遊戲保護刻不容緩,叉叉助手能做到這麼成熟的地步主要是因為現在的手遊保護力度較弱。