Xpatch(免Root 實(shí)現(xiàn)App加載Xposed插件)源碼解析(一)

前言

Xpatch是一款免Root實(shí)現(xiàn)App加載Xposed插件的工具木张,可以非常方便地實(shí)現(xiàn)App的逆向破解(再也不用改smali代碼了)术吗,源碼也已經(jīng)上傳到Github上,歡迎各位Fork and Star渊抽。

本文主要介紹Xpatch的實(shí)現(xiàn)原理达传。由于其原理比較復(fù)雜,所以分三篇文章來詳細(xì)講解劳吠。

由于Xpatch處理Xposed module的方法參考了Xposed框架部分源碼引润,所以本文先介紹Xposed框架加載Xposed模塊原理,再詳細(xì)講解Xpatch如何兼容Xposed模塊痒玩。

Xposed框架加載Xposed Module的原理

Xposed是github上rovo89大神設(shè)計(jì)的一個(gè)針對Android平臺的動(dòng)態(tài)劫持項(xiàng)目淳附,其主要原理是通過替換/system/bin/app_process程序控制zygote進(jìn)程,使得app_process在啟動(dòng)過程中會(huì)加載XposedBridge.jar這個(gè)jar包蠢古,從而完成對Zygote進(jìn)程及其創(chuàng)建的app進(jìn)程的劫持奴曙。

XposedBridge.jar的入口方法是main(),其主要邏輯如下:

//de.robv.android.xposed.XposedBridge.java
    protected static void main(String[] args) {
        // Initialize the Xposed framework and modules
        try {
            if (!hadInitErrors()) {
                initXResources();

                SELinuxHelper.initOnce();
                SELinuxHelper.initForProcess(null);

                runtime = getRuntime();
                XPOSED_BRIDGE_VERSION = getXposedVersion();

                if (isZygote) {
                    XposedInit.hookResources();
                    XposedInit.initForZygote();
                }

                XposedInit.loadModules();
            } else {
                Log.e(TAG, "Not initializing Xposed because of previous errors");
            }
        } catch (Throwable t) {
            Log.e(TAG, "Errors during Xposed initialization", t);
            disableHooks = true;
        }

        // Call the original startup code
        if (isZygote) {
            ZygoteInit.main(args);
        } else {
            RuntimeInit.main(args);
        }
    }

這里最核心的一行代碼是:

XposedInit.loadModules();

在這個(gè)方法里草讶,通過讀取/data/data/de.robv.android.xposed.installer/conf/modules.list這個(gè)文件洽糟,找到需要加載的Xposed插件(APK)路徑。而這些路徑都是通過Xposed Installer這個(gè)App里的開關(guān)控制的堕战。在Xposed Installer App里坤溃,有一個(gè)已安裝的Xposed插件列表,用戶選定某個(gè)插件后嘱丢,就會(huì)將該插件APK路徑寫到modules.list文件里薪介,從而實(shí)現(xiàn)插件開關(guān)的控制。

在modules.list文件里查找到所有插件APK路徑后越驻,根據(jù)Apk的絕對路徑構(gòu)造一個(gè)PathClassLoader()汁政,然后用此Classloader加載全類名寫在資源文件assets/xposed_init里的入口類道偷,其核心邏輯代碼如下:

//de.robv.android.xposed.XposedInit.java
...
...
ClassLoader mcl = new PathClassLoader(apk, XposedBridge.BOOTCLASSLOADER);
        BufferedReader moduleClassesReader = new BufferedReader(new InputStreamReader(is));
        try {
            String moduleClassName;
            while ((moduleClassName = moduleClassesReader.readLine()) != null) {
                moduleClassName = moduleClassName.trim();
                if (moduleClassName.isEmpty() || moduleClassName.startsWith("#"))
                    continue;

                try {
                    Log.i(TAG, "  Loading class " + moduleClassName);
                    Class<?> moduleClass = mcl.loadClass(moduleClassName);

                    if (!IXposedMod.class.isAssignableFrom(moduleClass)) {
                        Log.e(TAG, "    This class doesn't implement any sub-interface of IXposedMod, skipping it");
                        continue;
                    } else if (disableResources && IXposedHookInitPackageResources.class.isAssignableFrom(moduleClass)) {
                        Log.e(TAG, "    This class requires resource-related hooks (which are disabled), skipping it.");
                        continue;
                    }
                    ...
                    ...

加載到這些類之后,將這些類使用全局變量保存起來:

if (moduleInstance instanceof IXposedHookLoadPackage)
    XposedBridge.hookLoadPackage(new IXposedHookLoadPackage.Wrapper((IXposedHookLoadPackage) moduleInstance));

    ...
    ...
    
    //保存在全局變量sLoadedPackageCallbacks里
public static void hookLoadPackage(XC_LoadPackage callback) {
        synchronized (sLoadedPackageCallbacks) {
            sLoadedPackageCallbacks.add(callback);
        }
    }

保存起來后烂完,何時(shí)執(zhí)行這些類里的入口方法呢试疙?
下面一段代碼,給出了答案:

// normal process initialization (for new Activity, Service, BroadcastReceiver etc.)
        findAndHookMethod(ActivityThread.class, "handleBindApplication", "android.app.ActivityThread.AppBindData", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) {
                ...
            
                XC_LoadPackage.LoadPackageParam lpparam = new XC_LoadPackage.LoadPackageParam(XposedBridge.sLoadedPackageCallbacks);
                lpparam.packageName = reportedPackageName;
                lpparam.processName = (String) getObjectField(param.args[0], "processName");
                lpparam.classLoader = loadedApk.getClassLoader();
                lpparam.appInfo = appInfo;
                lpparam.isFirstApplication = true;
                XC_LoadPackage.callAll(lpparam);
                ...
        });

通過上面代碼可知抠蚣,是在main入口處攔截了ActivityThreadhandleBindApplication方法祝旷,在這個(gè)方法執(zhí)行之前,加載了Xposed插件里的Hook代碼(入口代碼)嘶窄。而ActivityThreadhandleBindApplication方法的主要功能就是創(chuàng)建Application怀跛,并調(diào)用其attachBaseContextonCreate等方法柄冲。因此吻谋,在App的Application創(chuàng)建之前就實(shí)現(xiàn)了Xposed Hook。

至此现横,Xposed框架加載Xposed module的流程就非常清晰了漓拾。

免Root實(shí)現(xiàn)Xposed的探索

由于Xposed框架是修改了/system/bin/app_process程序,控制的zygote進(jìn)程的啟動(dòng)戒祠,從而在app進(jìn)程啟動(dòng)之前執(zhí)行了加載xposed模塊骇两,實(shí)現(xiàn)了App代碼的Hook。因此姜盈,只有Root的手機(jī)才能使用Xposed低千。

那有沒有辦法實(shí)現(xiàn)免Root下也能讓App加載Xposed模塊呢?

其中一種已經(jīng)被探索過的方法是使用App雙開工具馏颂,讓其他App運(yùn)行在自己的App里面示血。比如,利用大名鼎鼎的開源雙開工具VirtualApp救拉,讓其他App運(yùn)行其中难审,這樣VirtualApp就可以控制其他App的進(jìn)程啟動(dòng)了,當(dāng)然也就可以實(shí)現(xiàn)免Root加載Xposed模塊了亿絮。

不過剔宪,這樣做也有一些問題,其兼容性和穩(wěn)定性比較差壹无。而且葱绒,由于VirtualApp的開源版本已經(jīng)很少維護(hù),bug會(huì)比較多斗锭,有些App在里面啟動(dòng)非车氐恚卡頓,甚至無法啟動(dòng)

那有沒有更好的方案呢岖是?

有帮毁,那就是基于Apk二次打包的Xpatch方案实苞。

為了實(shí)現(xiàn)免Root Xposed,我們可以修改App入口代碼烈疚,在App的Application初始化時(shí)黔牵,插入我們加載Xposed模塊的代碼,并對App進(jìn)行重新打包簽名即可爷肝。

Xpatch加載Xposed模塊的方法

通過以上分析猾浦,Xposed框架執(zhí)行Xposed模塊的入口位置是通過Hook ActivityThreadhandleBindApplication方法,從而使在創(chuàng)建應(yīng)用的Application之前就執(zhí)行Xposed模塊里的入口方法(執(zhí)行Hook流程)灯抛。

由于我們是修改應(yīng)用代碼金赦,因此入口只能在應(yīng)用的Application里,可以是Application的靜態(tài)方法塊对嚼,也可以是attachBaseContext方法或onCreate方法夹抗。

那到底應(yīng)該選擇哪個(gè)作為加載Xposed模塊的入口呢?

首先纵竖,肯定是越早Hook越好漠烧,否則可能會(huì)出現(xiàn)有些方法調(diào)用之后才執(zhí)行Hook方法,導(dǎo)致方法沒有被Hook到靡砌。而且加載Xposed插件需要用到Applicatin的context參數(shù)已脓,所以筆者選了在attachBaseContext方法的第一行代碼執(zhí)行加載Xposed模塊:

    // MyApplication.java
    @Override
    protected void attachBaseContext(Context base) {
        XposedModuleEntry.init(base);
        ...
        //App其他業(yè)務(wù)代碼
        ...
        super.attachBaseContext(base);
    }

通過對一些app進(jìn)行測試,發(fā)現(xiàn)大多數(shù)應(yīng)用走這個(gè)流程都沒問題乏奥,唯一有問題的是微信,修改后的微信一啟動(dòng)就奔潰亥曹,具體原因暫時(shí)不清楚邓了。

因此,我嘗試將加載Xposed模塊的入口代碼放在Application的靜態(tài)代碼塊里媳瞪,靜態(tài)代碼塊在類創(chuàng)建的時(shí)候就會(huì)執(zhí)行骗炉,比attachBaseContext方法執(zhí)行的時(shí)機(jī)更早。

通過測試蛇受,修改后的微信的確能夠成功啟動(dòng)>淇!

    // MyApplication.java中的靜態(tài)代碼塊
    static {
        XposedModuleEntry.init();
    }

但是兢仰,在Application的靜態(tài)代碼塊中乍丈,并沒有Application的context參數(shù),而加載Xposed模塊時(shí)把将,需要傳遞參數(shù)轻专,參數(shù)中的applicationInfo和應(yīng)用的classLoader都是需要從Context中取得,沒有Context怎么辦察蹲?總不能傳個(gè)空過去吧请垛。

既然沒有Context催训,我們就自己構(gòu)造一個(gè)Context。

創(chuàng)建App Context流程

Android sdk并沒有提供應(yīng)用自己創(chuàng)建context的方法宗收,為了找到構(gòu)建一個(gè)Application Context的方法漫拭,我們先來了解android Framework里是如何創(chuàng)建Application的Context。

Application里最早出現(xiàn)Context的地方是attachBaseContext方法:

// Application的父類android.content.ContextWrapper.java
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }

這個(gè)方法唯一調(diào)用的地方是:

// android.app.Application.java
    /**
     * @hide
     */
    /* package */ final void attach(Context context) {
        attachBaseContext(context);
        mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
    }

Application里的attach方法是在new Application之后立即調(diào)用的混稽,具體是在android.app.Instrumentation.java里:

// android.app.Instrumentation.java
public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        return newApplication(cl.loadClass(className), context);
    }
    
static public Application newApplication(Class<?> clazz, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
    Application app = (Application)clazz.newInstance();
    app.attach(context);
    return app;
}

Instrumentation類的newApplication方法最終又是在android.app.LoadedApk.java類里的makeApplication方法調(diào)用的:

// android.app.LoadedApk.java
public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
            ...
            //代碼省略
            ...
            String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }
           ...
           //代碼省略
           ...
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            appContext.setOuterContext(app);
          
            ...
            //代碼省略
            ...
}

終于看到構(gòu)造context的方法了

ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);

這個(gè)方法的實(shí)現(xiàn)是:

// android.app.ContextImpl.java
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
                null);
        context.setResources(packageInfo.getResources());
        return context;
    }

createAppContext方法需要傳兩個(gè)參數(shù)采驻,一個(gè)是mActivityThread,另一個(gè)是this荚坞,也就是LoadedApk對象挑宠。mActivityThread這個(gè)對象比較容易找到,因?yàn)橐粋€(gè)進(jìn)程只有一個(gè)ActivityThread對象颓影,只用通過反射調(diào)用ActivityThread的靜態(tài)方法currentActivityThread即可:

// android.app.ActivityThread.java
public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
}

反射:

//反射調(diào)用ActivityThread.java的靜態(tài)方法currentActivityThread()
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object activityThreadObj = currentActivityThreadMethod.invoke(null);

另外一個(gè)對象LoadedApk該如何獲取呢各淀?

LoadedApk對象的獲取

上面代碼分析過,App啟動(dòng)時(shí)最先調(diào)用 ActivityTheadhandleBindApplication(AppBindData data)方法诡挂,并在其這個(gè)方法里創(chuàng)建Application碎浇,而創(chuàng)建Application的唯一方法是

// android.app.LoadedApk.java
public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation){}

查看handleBindApplication方法具體實(shí)現(xiàn)過程,發(fā)現(xiàn)makeApplication確實(shí)被調(diào)用到:

// android.app.ActivityThread.java
private void handleBindApplication(AppBindData data) {
        ...
        //其他代碼省略
        ...
    mBoundApplication = data;
    mConfiguration = new Configuration(data.config);
    mCompatConfiguration = new Configuration(data.config);
         ...
        //其他代碼省略
        ...
    data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
         ...
        //其他代碼省略
        ...
        // 創(chuàng)建Application
    Application app = data.info.makeApplication(data.restrictedBackupMode, null);
    mInitialApplication = app;
        ...
        //其他代碼省略
        ...
}

根據(jù)以上代碼可知璃俗,LoadedApk的實(shí)例就是data.info奴璃,而data.info是通過方法getPackageInfoNoCheck來獲取的,而且data.info對象存到了全局變量mBoundApplication里城豁,因此苟穆,mBoundApplication對象里的info變量就是我們要找的LoadedApk實(shí)例。

我們可以通過反射來獲取它:

// 獲取ActivityThread的mBoundApplication變量
Field boundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
boundApplicationField.setAccessible(true);
Object mBoundApplicationObj = boundApplicationField.get(activityThreadObj);   // mBoundApplication: AppBindData

// 獲取mBoundApplication的info變量(LoadedApk)
Field infoField = mBoundApplicationObj.getClass().getDeclaredField("info");   // info: LoadedApk
infoField.setAccessible(true);
Object loadedApkObj = infoField.get(mBoundApplicationObj);  // LoadedApk

獲取到ActivityThreadLoadedApk后唱星,通過反射調(diào)用ContextImpl的靜態(tài)方法createAppContext就可以構(gòu)造一個(gè)context對象:

Class contextImplClass = Class.forName("android.app.ContextImpl");
//Get createAppContext method
Method createAppContextMethod = contextImplClass.getDeclaredMethod("createAppContext", activityThreadClass, loadedApkObj.getClass());
createAppContextMethod.setAccessible(true);

// call method: ContextImpl.createAppContext()
Object context = createAppContextMethod.invoke(null, activityThreadObj, loadedApkObj);

至此雳旅,我們在Application的靜態(tài)代碼塊中成功得到Application context。

Comming soon...

下一篇Xpatch源碼解析中间聊,我們將接著這部分內(nèi)容介紹XposedModuleEntry.init();這個(gè)方法的具體實(shí)現(xiàn)邏輯攒盈,然后再介紹如何利用dex2jar工具修改Apk中dex文件。

關(guān)注我的技術(shù)公眾號獲取最新高質(zhì)量Android技術(shù)文章:Android葵花寶典
掃一掃關(guān)注

如果喜歡此文哎榴,就給點(diǎn)鼓勵(lì)唄型豁!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市尚蝌,隨后出現(xiàn)的幾起案子迎变,更是在濱河造成了極大的恐慌,老刑警劉巖飘言,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件氏豌,死亡現(xiàn)場離奇詭異,居然都是意外死亡热凹,警方通過查閱死者的電腦和手機(jī)泵喘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門泪电,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人纪铺,你說我怎么就攤上這事相速。” “怎么了鲜锚?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵突诬,是天一觀的道長。 經(jīng)常有香客問我芜繁,道長旺隙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任骏令,我火速辦了婚禮蔬捷,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘榔袋。我一直安慰自己周拐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布凰兑。 她就那樣靜靜地躺著妥粟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吏够。 梳的紋絲不亂的頭發(fā)上勾给,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音锅知,去河邊找鬼播急。 笑死,一個(gè)胖子當(dāng)著我的面吹牛喉镰,可吹牛的內(nèi)容都是我干的旅择。 我是一名探鬼主播惭笑,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼侣姆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了沉噩?” 一聲冷哼從身側(cè)響起捺宗,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎川蒙,沒想到半個(gè)月后蚜厉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡畜眨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年昼牛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了术瓮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贰健,死狀恐怖胞四,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伶椿,我是刑警寧澤辜伟,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站脊另,受9級特大地震影響导狡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜偎痛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一旱捧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧看彼,春花似錦廊佩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至茁计,卻和暖如春料皇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背星压。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工践剂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人娜膘。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓逊脯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親竣贪。 傳聞我的和親對象是個(gè)殘疾皇子军洼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容