Xposed 框架 hook 簡介 原理 案例 [MD]


博文地址

我的GitHub 我的博客 我的微信 我的郵箱
baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

Xposed

由於Android 5.0以上采用ART,而5.0以下默認采用Dalvik,所以是有兩個版本的Xposed,一些下載鏈接:

Xposed 是什么

Xposed 是一款可以在不修改APK的情況下影響程序運行(修改系統)的框架服務。基於它可以制作出許多功能強大的模塊,且在功能不沖突的情況下同時運作。比如:直接把APP的界面改成自己想要的樣子(比如修改文字、背景),去掉界面里不喜歡的東西(比如廣告、彈窗),自動搶紅包,消息防撤回,步數修改、修改定位等等,簡直酷得不行,網上有很多插件作者開發出來的優秀插件。

Xposed 理論上能夠hook到系統任意一個Java進程,可以說Xposed真的可以為所欲為,任何事情都可以做!

重要提醒:使用Xposed是需要Root權限的! 
PS:由於Xposed是從底層hook,所以需要root權限,並且每次更新都要重新啟動設備

背景介紹

Xposed並不是什么新東西了,好幾年前就有了,以前看到搞機(基)的人都覺得很牛逼哄哄,刷系統,root下,改下系統UI,用各種各樣的插件模塊改什么什么,屌得不行。

真正開始學的時候,其實Xposed並沒有想象中那么復雜,原理和相關的API都很簡單,難的是逆向,怎么去實現你要Hook的功能:反編譯,調試輸出,堆棧跟蹤,抓包等等,在這個過程中你需要去分析很多很多東西,猜測調試,有時候折騰幾天可能毫無進展,不過也會收獲更多,比如你自己開發APP的時候也會慢慢開始考慮安全相關的東西。

The most important thing is finding good methods to hook

插件用起來是挺爽的,不過呢,因為Xposed擁有最高權限,如果不法分子在插件里植入了惡意代碼,比如登錄劫持,偷偷采集你的賬號密碼發送到他們的手里,如果涉及到了金錢,就很恐怖啦,所以在使用Xposed插件的時候,盡量選那些開源的,並進行代碼review,看是否存在惡意代碼,再進行安裝體驗。

Xposed 的原理

白話總結

Android基於Linux,第一個啟動的進程自然是init進程,該進程會啟動所有Android進程的父進程——Zygote(孵化)進程,該進程的啟動配置在/init.rc腳本中,而Zygote進程對應的執行文件是/system/bin/app_process,該文件完成類庫的加載以及一些函數的調用工作。在Zygote進程創建后,再fork出SystemServer進程和其他進程。而Xposed Framework呢,就是用自己實現的app_process替換掉了系統原本提供的app_process,加載一個額外的jar包,然后入口從原來的com.android.internal.osZygoteInit.main()被替換成了de.robv.android.xposed.XposedBridge.main(),然后創建的Zygote進程就變成Hook的Zygote進程了,而后面Fork出來的進程也是被Hook過的。這個Jar包在/data/data/de.rbov.android.xposed.installer/bin/XposedBridge.jar

官方教程:How Xposed works

Before beginning with your modification, you should get a rough idea how Xposed works (you might skip this section though if you feel too bored).

在開始修改之前,你應該大致了解Xposed如何工作(如果你覺得太無聊,你可以跳過這一部分)

There is a process that is called "Zygote". This is the heart of the Android runtime. Every application is started as a copy ("fork") of it. This process is started by an /init.rc script when the phone is booted. The process start is done with /system/bin/app_process, which loads the needed classes and invokes the initialization methods.

有一個叫做“Zygote”的進程。這是Android運行時的核心。每個應用程序都作為它的副本(“fork”)啟動。啟動手機時,此進程由/init.rc腳本啟動。進程啟動是使用/system/bin/app_process完成的,它會加載所需的類並調用初始化方法。

This is where Xposed comes into play. When you install the framework, an extended app_process executable is copied to /system/bin. This extended startup process adds an additional jar to the classpath and calls methods from there at certain places. For instance, just after the VM has been created, even before the main method of Zygote has been called. And inside that method, we are part of Zygote and can act in its context.

這就是Xposed發揮作用的地方。安裝框架時,會將擴展的app_process可執行文件復制到/system/bin。這個擴展的啟動過程在類路徑中添加了一個額外的jar,並在某些地方從那里調用方法。例如,就在創建VM之后,甚至在調用Zygote的main方法之前。在該方法中,我們是Zygote的一部分,可以在其context下行動。

The jar is located at /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar and its source code can be found here. Looking at the class XposedBridge, you can see the main method. This is what I wrote about above, this gets called in the very beginning of the process. Some initializations are done there and also the modules are loaded (I will come back to module loading later).

jar位於,其源代碼可在此處找到。查看類XposedBridge,您可以看到main方法。這就是我上面寫的內容,這個在進程一開始就被調用了。在那里進行了一些初始化,並且還加載了模塊(稍后我將回到模塊加載)。

Method hooking/replacing

What really creates the power of Xposed is the possibility to "hook" method calls. When you do a modification by decompiling an APK, you can insert/change commands directly wherever you want. However, you will need to recompile/sign the APK afterwards and you can only distribute the whole package.

我們創造Xposed真正具有的力量是"hook"方法調用具有無限可能性。通過反編譯APK進行修改時,可以直接在任意位置插入/更改命令。但是,您需要在之后重新編譯/簽署APK,並且您只能分發整個包。

With the hooks you can place with Xposed, you can't modify the code inside methods (it would be impossible to define clearly what kind of changes you want to do in which place). Instead, you can inject your own code before and after methods, which are the smallest unit in Java that can be addressed clearly.

使用可以放置在Xposed的hooks,你並不能修改方法內的代碼(因為你無法清楚地定義你想在哪個地方做什么樣的改變)。相反,您可以在方法之前和方法之后注入自己的代碼,這是Java中可以清楚解決的最小單元。

XposedBridge has a private, native method hookMethodNative. This method is implemented in the extended app_process as well. It will change the method type to "native" and link the method implementation to its own native, generic method. That means that every time the hooked method is called, the generic method will be called instead without the caller knowing about it.

XposedBridge有一個私有的本地方法hookMethodNative。此方法也在擴展的app_process中實現。它會將方法類型更改為“native”,並將方法實現link到其自己的native、generic(泛型)方法。這意味着每次調用hooked方法時,都會調用generic方法,而不會讓調用者知道它。

In this method, the method handleHookedMethod in XposedBridge is called, passing over the arguments to the method call, the this reference etc. And this method then takes care of invoking callbacks that have been registered for this method call. Those can change the arguments for the call, change instance/static variables, invoke other methods, do something with the result... or skip anything of that. It is very flexible.

在此方法中,XposedBridge中的handleHookedMethod方法會被調用,將參數傳遞給方法調用,this引用等。然后,此方法負責調用已為此方法調用注冊的回調。這些可以更改調用的參數,更改實例/靜態變量,調用其他方法,對結果執行某些操作...或者跳過任何內容。它非常靈活。

Ok, enough theory.

好的,上面的理論已經足夠了。

官方簡介

Xposed is a framework for modules that can change the behavior of the system and apps without touching any APKs.

Xposed是一個可以在不觸及任何APK的情況下改變系統和應用程序的行為的模塊框架。

That's great because it means that modules can work for different versions and even ROMs without any changes (as long as the original code was not changed too much). It's also easy to undo. As all changes are done in the memory, you just need to deactivate the module and reboot to get your original system back.

這很好,因為它意味着模塊可以在不做任何更改的情況下為不同的版本甚至ROM工作(只要原始代碼沒有太多改變)。 撤消也很容易。 由於所有更改都在內存中完成,您只需要停用模塊並重新啟動即可恢復原始系統。

There are many other advantages, but here is just one more: Multiple modules can do changes to the same part of the system or app. With modified APKs, you to decide for one. No way to combine them, unless the author builds multiple APKs with different combinations.

還有許多其他優點,但這里只有一個:多個模塊可以對系統或應用程序的同一部分進行更改。 使用經過修改的APK,您可以選擇一個。 除非作者使用不同的組合構建多個APK,否則無法組合它們。

Note that this only works with root access on Android 4.0.3 up to Android 4.4.

請注意,這僅適用於Android 4.0.3到Android 4.4 的具有root訪問權限的設備。

最新版本支持到 8.1(27)

參考這里

目前最新版本來到3.1.5,支持Android7.0、7.1、Android8.x框架安裝。

新版本的主要的功能更新是它帶來了適配於當前設備的Xposed框架直接下載安裝的選項,當然也包括卸載選項,這樣就不用自己再去針對CPU和系統版本手動篩選需要刷入的框架版本了,減少了出錯的幾率,方便新手用戶。

新版本的xposed框架主程序增加一些檢查步驟,能夠在出錯的時候給出更多的提示,所以強烈推薦更新,並且作者也鼓勵大家更新到新版本,各方面都會比舊版本要好一些。

第一個 xposed 項目

參考了 官方教程 和 WrBug的簡書 和 慢啄網的文章 和 coder-pig的文章

簡單來說就是,需要以下幾個基本步驟:

  • 1、安裝framework,重啟
  • 2、安裝XposedInstaller,重啟
  • 3、添加依賴,添加三個meta-data
  • 4、編寫Hook邏輯,配置完整類名
  • 5、安裝APP,重啟

模擬器系統環境配置

下面是模擬器下的配置,和實體機的重要區別為

  • 模擬器為 x86 架構,實體機一般為 ARM 結構,其所需安裝的 Xposed framework 是不一樣的
  • 模擬器一般具有 root 權限,實體機一般沒有 root 權限,且很難使用工具對手機 root,這是一個很棘手的問題
  • 在模擬器上你可以肆無忌憚的隨意折騰,但是在實體機上就要顧忌很多,因為使用 Xposed 有可能讓你的手機變磚
  • 模擬器上安裝第三方插件不用擔心隱式泄露、資金安全,但是在實體機上你可就要悠着點了

1、根據Android設備系統版本到 framework官網 下載對應的框架,選擇.zip結尾的文件,例如 sdk22 系統 x86 環境最新版本下載地址為 xposed-v89-sdk22-x86.zip,下載完成后運行模擬器,將 zip 包拖到模擬器界面即可刷入,完成后重啟模擬器

2、到 installer官網 下載安裝XposedInstaller應用(可能比較慢),例如最新的 XposedInstaller_3.1.5.apk,也可百度搜索后下載安裝。安裝完畢后打開此app,會提示你沒有激活,再次重啟后進入剛剛安裝的app,會提示已激活,即安裝成功。

我們可以通過Xposed installer右上角菜單中的"軟重啟"來重啟設備,當然此APP還提供了一切常用的其他功能。

實體機系統環境配置

重要提示:刷機前為了以防萬一,最好先備份數據,以免刷入后手機無法啟動等問題。

基本條件:手機已經root並且已經刷入第三方recovery 
步驟和上面基本一樣,首先下載並安裝好XposedInstaller 
然后下載和手機cpu對應的Framework,並放在手機存儲卡上 
然后重啟到recovery模式,刷入Framework 
再重啟即可

由於Xposed項目每次安裝都要重新啟動,在真機上是非常耗時間的,建議選擇Genymotion模擬器。

引用 API

在模塊中添加依賴:

compileOnly 'de.robv.android.xposed:api:82' //xposed依賴,注意這個版本號和framework版本號並不是一致的
compileOnly 'de.robv.android.xposed:api:82:sources' //非必須

Every Xposed modules needs to reference the API. 
The Xposed Framework API is published on Bintray/jCentermaven 地址在這里

It is very important that you use compileOnly instead of compile! The latter would include the API classes in your APK, which can cause issues especially on Android 4.x. Using compileOnly just makes the API classes usable from your module, but there will only be references to them in the APK. The actual implementation will be provided when the user installs the Xposed Framework.

使用compileOnly而不是compile是非常重要的! 后者將在您的APK中包含API類,這可能會導致問題,特別是在Android 4.x上。 使用compileOnly只是使API類可以從您的模塊中使用,但在APK中只會引用它們。當用戶安裝 Xposed Framework 時,將提供實際的實現。

Please make sure to disable Instant Run (File -> Settings -> Build, Execution, Deployment -> Instant Run), otherwise your classes aren't included directly in the APK, but loaded via a stub application which Xposed can't handle.

請確保禁用Instant Run,否則您的類不會直接包含在APK中,而是通過Xposed無法處理的stub應用程序加載。


API versions 
Generally, the API version equals the Xposed version that it was built on. However, only some framework changes actually result in API changes, as you can see in the change log. I only publish a new API version when there were API changes, and I try to keep them compatible with existing modules as good as possible. So when you build a module with API version 82, it will most likely also work withXposed version 90.

通常。但是,只有個別一些 framework 更改實際上會導致API更改,您可以在更改日志中看到。我只在API更改時發布了新的API版本,並嘗試盡可能地使它們與現有模塊兼容。因此,當您使用API版本82構建模塊時,它很可能也適用於Xposed版本90。

I always recommend that end-users use the latest Xposed version, so there's nothing wrong with using the highest API version that's available. You should usually set the xposedminversion in your AndroidManifest.xml to the API version that you use. If you depend on a framework change that didn't cause an API change (e.g. because a certain bug has been fixed), feel free to set you xposedminversion to the least Xposed version that your module requires.

我總是建議終端用戶使用最新的Xposed版本,因此使用可用的最高API版本沒有任何問題。您通常應該將AndroidManifest.xml中的xposedminversion設置為您使用的API版本。如果您依賴於不會導致API更改的framework更改(例如,因為已修復某個錯誤),請隨意將xposedminversion設置為模塊所需的最少Xposed版本。

If you want to support ROMs before Lollipop, you can only use API version 53, as the latest Xposed version for Android 4.x was 54. Note that the sources jar provided for this version doesn't match the actual implementation, it only makes the documentation available.

如果你想在Lollipop之前支持ROM,你只能使用API版本53,因為Android 4.x的最新Xposed版本是54.請注意,為此版本提供的源jar與實際實現不匹配,它只會使可用的文件。

配置 meta-data

在AndroidManifest.xml中添加如下配置:

<meta-data
    android:name="xposedmodule"
    android:value="true"/>
<meta-data
    android:name="xposeddescription"
    android:value="這是對你使用xposed能完成功能的簡要描述"/>
<meta-data
    android:name="xposedminversion"
    android:value="89"/>

xposedmodule:是否是一個xpose模塊(是否啟用) 
xposeddescription:a very short description of your module 
xposedminversion:the API version from the previous step,注意這里的版本號要和安裝的framework版本號一致

配置完成后,安裝到模擬器,狀態欄彈出如下提示: 

我們點擊軟重啟即可。

需求分析

例如我們有這么一個簡單的Activity

public class XposedActivity extends Activity {
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        textView = new TextView(this);
        setContentView(textView);
    }
}

下面通過 xposed 給 textView 設置文本 Hello Xposed。

思路:Xposed hook onCreate方法,在該方法執行完后獲取TextView的實例,通過setText方法設置文本。

A module can have a few entry points. Which one(s) you choose depends on the what you want to modify. You can have Xposed call a function in your module when the Android system boots, when a new app is about to be loaded, when the resources for an app are initialised and so on.

一個模塊可以有幾個入口點。 您選擇哪一個取決於您要修改的內容。 當Android系統啟動,即將加載新應用程序,應用程序的資源等初始化時,您可以讓Xposed調用模塊中的方法。

Keep in mind that you can "only" hook methods. So you have to find a place where you can insert some code to do the magic either before, after or replacing a method. You should hook methods that are as specific as possible, not ones that are called thousands of times to avoid performance issues and unintended side-effects.

請記住,您“僅”可以hook方法。因此,您必須找到一個位置,以便您可以在方法之前、之后或替換方法時在其中插入一些代碼執行magic。您應該hook盡可能具體的方法,而不是那些被調用數千次的方法,以避免性能問題和意外的副作用。

編寫 Xposed 代碼

Using reflection to find and hook a method

新建一個類XposedInit實現IXposedHookLoadPackage

public class XposedInit implements IXposedHookLoadPackage {

    private static String HOOK_PACKAGE_NAME = "com.my.bqt";
    private static String HOOK_CLASS_NAME = "com.my.bqt.xposed.XposedActivity";

    @Override
    public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) {
        Log.i("bqt", "【handleLoadPackage】" + lpparam.packageName);//任何一個app啟動時都會調用
        if (lpparam.packageName.equals(HOOK_PACKAGE_NAME)) { //匹配指定的包名
            //參數:String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback
            XposedHelpers.findAndHookMethod(lpparam.packageName, lpparam.classLoader, "onCreate", Bundle.class, new XC_MethodHook() {
                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    Log.i("bqt", "【afterHookedMethod】" + param.method.getName()); //當Hook成功后回調
                    Class c = lpparam.classLoader.loadClass(HOOK_CLASS_NAME);//不能通過Class.forName()來獲取Class,在跨應用時會失效
                    Field field = c.getDeclaredField("textView");
                    field.setAccessible(true);
                    TextView textView = (TextView) field.get(param.thisObject);//param.thisObject為執行該方法的對象,在這里指Activity
                    textView.setText("Hello Xposed");
                    //可以調用param.setResult()設置方法的返回值!
                }
            });
        }
    }
}

官方文檔相關介紹

XposedHelpers.findAndHookMethod is a helper function. This method looks up the class_A using the ClassLoader_B for the package_C. Then it looks for the method_D in it. If there were any parameters_E to this method, you would have to list the types (classes) of these parameters afterwards. As the last argument, you need to provide an implementation of the XC_MethodHook_F class. For smaller modifications, you can use a anonymous class. If you have much code, it's better to create a normal class and only create the instance here. The helper will then do everything necessary to hook the method as described above.

XposedHelpers.findAndHookMethod是一個helper 函數。 這個方法使用classLoader_Bpackage_C查找class_A。 然后它在其中查找method_D。 如果此方法有任何parameters_E,則必須在之后列出這些參數的類型(classes)。 作為最后一個參數,您需要提供XC_MethodHook_F類的實現。 對於較小的修改,您可以使用匿名類。 如果你有很多代碼,最好創建一個普通的類,只在這里創建實例。 然后,幫助程序將執行hook方法所需的所有操作,如上所述。

There are two methods in XC_MethodHook that you can override. You can override both or even none, but the latter makes absolutely no sense These methods are beforeHookedMethod and afterHookedMethod. It's not too hard to guess that the are executed before/after the original method.

XC_MethodHook中有兩種方法可以覆蓋。 你可以覆蓋兩者,甚至沒有,但如果兩個方法都不重寫那沒任何意義。 這些方法是beforeHookedMethodafterHookedMethod。 猜測在原始方法之前/之后執行並不是很難。

You can use the "before" method to evaluate/manipulate the parameters of the method call (via param.args) and even prevent the call to the original method (sending your own result). The "after" method can be used to do something based on the result of the original method. You can also manipulate the result at this point. And of course, you can add your own code which should be executed exactly before/after the method call.

您可以使用“before”方法評估/操作方法調用的參數(通過param.args),甚至阻止調用原始方法(發送您自己的結果)。 “after”方法可用於根據原始方法的結果執行某些操作。 您也可以在此處操縱結果。 當然,您可以添加自己的代碼,這些代碼應該在方法調用之前/之后執行。

If you want to replace a method completely, have a look at the subclass XC_MethodReplacementinstead, where you just need to override replaceHookedMethod.

如果你想完全替換方法,使請用子類XC_MethodReplacement代替,你只需要覆蓋replaceHookedMethod

XposedBridge keeps a list of registered callbacks for each hooked method. Those with highest priority (as defined in hookMethod) are called first. The original method has always the lowest priority. So if you have hooked a method with callbacks A (prio high) and B (prio default), then whenever the hooked method is called, the control flow will be this: A.before -> B.before -> original method -> B.after -> A.after. So A could influence the arguments B gets to see, which could further change them before passing them on. The result of the original method can be processed by B first, but A has the final word what the original caller gets.

XposedBridge為每個掛鈎方法保留一個已注冊的回調列表。 優先級最高的那些(在hookMethod中定義)首先被調用。 原始方法始終具有最低優先級。 因此,如果您使用回調A(高優先級)和B(默認優先級)hook了一個方法,那么每當調用hooked方法時,控制流將是:A.before -> B.before -> original method -> B.after -> A.after。 所以A可以影響B看到的參數,這可以在傳遞它們之前進一步改變它們。原始方法的結果由B首先處理,但A具有原始調用者所能獲得結果的最終話語權。

為什么不能用 Class.forName 方法

在上面的案例中,我們沒有用Class.forName()來獲取class,是什么原因呢?我們先來看看Class.forName()的源碼:

public static Class<?> forName(String className) throws ClassNotFoundException {
    return forName(className, true, VMStack.getCallingClassLoader()); //注意ClassLoader的獲取方式
}
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException {
    if (loader == null) {
        loader = BootClassLoader.getInstance();
    }
    Class<?> result;
    try {
        result = classForName(name, initialize, loader);
    } catch (ClassNotFoundException e) {
        Throwable cause = e.getCause();
        if (cause instanceof LinkageError) {
            throw (LinkageError) cause;
        }
        throw e;
    }
    return result;
}
static native Class<?> classForName(String className, boolean shouldInitialize, ClassLoader classLoader) throws ClassNotFoundException;

在一個參數的方法中,ClassLoader是通過VMStack.getCallingClassLoader()獲取的。VMStack是一個虛擬機棧,在Android系統中,每個應用都有一個獨立的虛擬機,所以VMStack.getCallingClassLoader()是獲取當前應用的ClassLoader,即xposed項目的ClassLoader,所以,如果使用Class.forName("xxx.xxx.xxxActivity")獲取不同應用的類會提示找不到,這就是需要通過lpparam.classLoader.loadClass()獲取的原因。

配置完整類名

Provide a hint for XposedBridge which classes contain such entry points.

新建assets文件夾,文件夾下新建xposed_init文件(文件名固定),在文件中填寫 XposedInit 的 fully qualified class name:

com.my.bqt.xposed.XposedInit

XposedBridge會從assets目錄中的xposed_init文件中獲取入口點 
然后安裝到模擬器上,然后重啟模擬器,重啟后打開app,這時textView將顯示Hello Xposed

Save your files. Then run your project as Android application. As this is the first time you install it, you need to enable it before you can use it. Open the Xposed Installer app and make sure you have installed the framework. Then go to the "Modules" tab. You should find your app in there. Check the box to enable it. Then reboot.

保存文件。 然后將您的項目作為Android應用程序運行 由於這是您第一次安裝它,因此您需要啟用它才能使用它。 打開Xposed Installer應用程序並確保已安裝框架。 然后轉到“模塊”選項卡。 你應該在那里找到你的應用程序。 選中此框以啟用它。 然后重啟。

一個基本的、也是Xposed最主要的功能已經演示完成了。

Xposed Framework API

Xposed Framework API

類結構: 
  

IXposedMod

//Marker interface for Xposed modules. Cannot be implemented directly
interface IXposedMod {}

IXposedMod 接口有四個子接口,我們可以使用的有三個,常用的也就兩個,具體如下:

  • IXposedHookLoadPackage:Get notified when an app ("Android package") is loaded.
  • IXposedHookInitPackageResources:Get notified when the resources for an app are initialized.
  • IXposedHookZygoteInit:Hook the initialization of Zygote process(es), from which all the apps are forked.
  • IXposedHookCmdInit :【不可用】Hook the initialization of Java-based command-line tools (like pm).

IXposedHookZygoteInit:在Zygote啟動時調用,用於系統服務的Hook回調方法initZygote()

IXposedHookLoadPackage 和 XC_LoadPackage.LoadPackageParam

  • IXposedHookLoadPackage是在加載包時開始hook。
  • handleLoadPackage方法會在執行Application.onCreate()方法前調用,並且攜帶一個XC_LoadPackage.LoadPackageParam類型的參數返回過來。
  • LoadPackageParam中包含了hook到的應用的一些信息,包括應用包名、應用加載后的進程名、應用的classloader以及一個android.content.pm.ApplicationInfo對象。

IXposedHookLoadPackage的定義

//Get notified when an app ("Android package") is loaded.
//This is especially useful to hook some app-specific methods.
//This interface should be implemented by the module's main class. Xposed will take care of registering it as a callback automatically.
public interface IXposedHookLoadPackage extends IXposedMod {
    //This method is called when an app is loaded. It's called very early, even before Application#onCreate is called.
    //Modules can set up their app-specific hooks here.
    //@param lpparam Information about the app.
    //@throws Throwable Everything the callback throws is caught and logged.
    void handleLoadPackage(LoadPackageParam lpparam) throws Throwable;
}

XC_LoadPackage.LoadPackageParam的定義

//This class is only used for internal purposes, except for the LoadPackageParam subclass.
public abstract class XC_LoadPackage extends XCallback implements IXposedHookLoadPackage {

    /Wraps information about the app being loaded.
    public static final class LoadPackageParam extends XCallback.Param {
        public String packageName; //The name of the package being loaded
        public String processName; // The process in which the package is executed
        public ClassLoader classLoader; //The ClassLoader used for this package
        public ApplicationInfo appInfo; //More information about the application being loaded
        public boolean isFirstApplication; //Set to true if this is the first (and main) application for this process
    }
}

XC_MethodHook.MethodHookParam

//Callback class for method hooks.
//Usually, anonymous subclasses of this class are created which override beforeHookedMethod and/or afterHookedMethod.
public abstract class XC_MethodHook extends XCallback {
    //Creates a new callback with default priority.
    public XC_MethodHook() {
        super();
    }

    //Creates a new callback with a specific priority.
    public XC_MethodHook(int priority) {
        super(priority);
    }

    //Called before the invocation of the method.
    //You can use MethodHookParam#setResult and MethodHookParam#setThrowable to prevent the original method from being called.
    //Note that implementations shouldn't call super(param), it's not necessary.
    //@param param Information about the method call.
    //@throws Throwable Everything the callback throws is caught and logged.
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {}

    //Called after the invocation of the method.
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {}

    //Wraps information about the method call and allows to influence it.
    public static final class MethodHookParam extends XCallback.Param {
        public Member method; //The hooked method/constructor
        public Object thisObject; //The 【this】 reference for an instance method, or null for static methods
        public Object[] args; //Arguments to the method call
        private Object result = null; //the result of the method call。 通過 get/set 方法訪問
        private Throwable throwable = null; //the exception thrown of the method call, 通過 get/set/has 方法訪問
        boolean returnEarly = false;

        //Returns the result of the method call, or throws the Throwable caused by it.
        public Object getResultOrThrowable() throws Throwable {
            if (throwable != null) throw throwable;
            return result;
        }
    }

    //An object with which the method/constructor can be unhooked.
    public class Unhook implements IXUnhook<XC_MethodHook> {
        private final Member hookMethod; // the method/constructor that has been hooked

        @Override
        public XC_MethodHook getCallback() {
            return XC_MethodHook.this;
        }

        @Override
        public void unhook() {
            XposedBridge.unhookMethod(hookMethod, XC_MethodHook.this);
        }
    }
}

MethodHookParam中包含與調用方法有關的信息,比較關注的是這個thisObject,代表調用該方法的對象實例,如果是靜態方法的話,返回一個Null,比如調用onCreate()方法的是MainActivity,獲得的自然是MainActivity的實例。

IXposedHookInitPackageResources 和 XC_InitPackageResources.InitPackageResourcesParam

這個是在資源布局初始化時進行hook,需要實現handleInitPackageResources() 方法,在初始化時調用,resparam有兩個字段,一個是應用包名,另一個是資源相關的android.content.res.XResources,XResources繼承自Resources,里面包含了很多資源的信息。

IXposedHookInitPackageResources的定義

//Get notified when the resources for an app are initialized.
//In handleInitPackageResources(), resource replacements can be created.
//This interface should be implemented by the module's main class. Xposed will take care of registering it as a callback automatically.
public interface IXposedHookInitPackageResources extends IXposedMod {
    //This method is called when resources for an app are being initialized.
    //Modules can call special methods of the XResources class in order to replace resources.
    //@param resparam Information about the resources.
    //@throws Throwable Everything the callback throws is caught and logged.
    void handleInitPackageResources(InitPackageResourcesParam resparam) throws Throwable;
}

XC_InitPackageResources.InitPackageResourcesParam的定義

//This class is only used for internal purposes, except for the InitPackageResourcesParam subclass.
public abstract class XC_InitPackageResources extends XCallback implements IXposedHookInitPackageResources {
    //Wraps information about the resources being initialized.
    public static final class InitPackageResourcesParam extends XCallback.Param {
        public String packageName; //The name of the package for which resources are being loaded
        public XResources res; //Reference to the resources that can be used for calls to XResources#setReplacement(String, String, String, Object)
    }
}

有了這個XResource對象,就可以拿到布局資源樹了,你可以拿到遍歷,拿到某個特定控件,然后做一些騷操作。

XC_LayoutInflated.LayoutInflatedParam

//Callback for hooking layouts. Such callbacks can be passed to {@link XResources#hookLayout} and its variants.
public abstract class XC_LayoutInflated extends XCallback {
    //Creates a new callback with default priority.
    public XC_LayoutInflated() {
        super();
    }

    //Creates a new callback with a specific priority. See XCallback#priority.
    public XC_LayoutInflated(int priority) {
        super(priority);
    }

    //Wraps information about the inflated layout.
    public static final class LayoutInflatedParam extends XCallback.Param {
        public View view; //The view that has been created from the layout.
        public ResourceNames resNames; //Container with the ID and name of the underlying resource.
        public String variant; //Directory from which the layout was actually loaded (e.g. "layout-sw600dp"). 
        public XResources res; //Resources containing the layout. 
    }

    //This method is called when the hooked layout has been inflated.
    //@param liparam Information about the layout and the inflated view.
    public abstract void handleLayoutInflated(LayoutInflatedParam liparam) throws Throwable;

    //An object with which the callback can be removed.
    public class Unhook implements IXUnhook<XC_LayoutInflated> {
        private final String resDir;
        private final int id; //the resource ID of the hooked layout

        @Override
        public XC_LayoutInflated getCallback() {
            return XC_LayoutInflated.this;
        }

        @Override
        public void unhook() {
            XResources.unhookLayout(resDir, id, XC_LayoutInflated.this);
        }
    }
}

XposedBridge 和 XposedHelpers

官方文檔地址

There are many helper methods in Xposed that can make developing a module much easier.

XposedBridge

This class contains most of Xposed's central logic, such as initialization and callbacks used by the native side. It also includes methods to add new hooks.

log方法: 
The log method is an easy way of logging debug output to the standard logcat and a file called /data/xposed/debug.log. It can take the log message or a Throwable. In the latter case, it will print the stack trace.

XposedBridge.log(“日志內容”):輸入日志和寫入到/data/xposed/debug.log,Xposed Installer的日志那里可以看到!

hookAllMethods、hookAllConstructors方法: 
You can use these methods if you want to hook all methods with a specific name or all constructors in a class. This is useful if there are different variants, but you want to execute some code before/after any of them has been called. Keep in mind that other ROMs might have additional variants that will also be hooked by this. Especially, be careful about the args you get in the callback.

如果要hook具有特定名稱的所有方法或類中的所有構造函數,則可以使用這兩種方法。 如果存在不同的變量,但是您希望在調用任何代碼之前/之后執行某些代碼,這將非常有用。 請記住,其他ROM可能還有其他變量,也會被hook。 特別是要小心你在回調中得到的args

XposedHelpers

Helpers that simplify hooking and calling methods/constructors, getting and settings fields, ...

XposeHelpers提供了一些輔助方法

  • callMethod、callStaticMethod、newInstance
  • findMethod、findConstructor、findField:獲取類實例
  • findMethodExact、findConstructorExact:通過反射查找類的成員方法
  • findAndHookXXX:查找並Hook
  • getXXXField、setXXXField、getStaticXXXField、setStaticXXXField:通過反射設置或獲取對象數據成員、靜態變量的值
  • getAdditionalXXXField、setAdditionalXXXField
  • assetAsByteArray
  • getMD5Sum
  • getProcessPid

第二個 Xposed 案例

要hook的代碼

我們再創建一個Activity測試在資源布局初始化時進行hook的效果:

public class XposedActivity2 extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i("bqt", "【setContentView前】");
        setContentView(R.layout.activity_main);
        Log.i("bqt", "【setContentView后】");

        Log.i("bqt", "【inflate前】");
        getLayoutInflater().inflate(R.layout.fragment_tab, null);
        Log.i("bqt", "【inflate后】");
    }
}

具體的hook邏輯

接下來在XposenInit里面實現IXposedHookInitPackageResources接口,並且實現handleInitPackageResources方法,代碼如下:

public class XposedInit implements IXposedHookInitPackageResources {

    private static String HOOK_PACKAGE_NAME = "com.my.bqt";
    private static String HOOK_LAYOUT_NAME = "activity_main";
    private static String HOOK_LAYOUT_NAME2 = "fragment_tab";

    @Override
    public void handleInitPackageResources(XC_InitPackageResources.InitPackageResourcesParam resparam) {
        Log.i("bqt", "【handleInitPackageResources】" + resparam.packageName);//任何一個app的包資源初始化時都會調用
        if (resparam.packageName.equals(HOOK_PACKAGE_NAME)) {
            resparam.res.hookLayout(resparam.packageName, "layout", HOOK_LAYOUT_NAME, new XC_LayoutInflated() {
                @Override
                public void handleLayoutInflated(LayoutInflatedParam liparam) {
                    Log.i("bqt", "【handleLayoutInflated】" + liparam.variant + "," + liparam.resNames.fullName);
                    printView((ViewGroup) liparam.view, 1);
                    TextView textView = liparam.view.findViewById(R.id.tv_title);
                    textView.setText("Hello Xposed");
                }
            });
            resparam.res.hookLayout(resparam.packageName, "layout", HOOK_LAYOUT_NAME2, new XC_LayoutInflated() {
                @Override
                public void handleLayoutInflated(LayoutInflatedParam liparam) {
                    Log.i("bqt", "【handleLayoutInflated2】" + liparam.variant + "," + liparam.resNames.fullName);
                }
            });
        }
    }

    //遍歷資源布局樹,並打印出來
    private void printView(ViewGroup view, int deep) {
        StringBuilder builder = new StringBuilder();
        String viewDeepFormat;
        for (int i = 0; i < deep - 1; i++) {
            builder.append("\t");
        }
        viewDeepFormat = builder + "\t";
        Log.i("bqt", "【printView】" + builder.toString() + view.toString());

        int count = view.getChildCount();
        for (int i = 0; i < count; i++) {
            if (view.getChildAt(i) instanceof ViewGroup) {
                printView((ViewGroup) view.getChildAt(i), deep + 1);
            } else {
                Log.i("bqt", "【printView】" + viewDeepFormat + view.getChildAt(i).toString());
            }
        }
    }
}

日志分析

安裝重啟后,打開demo,查看打印的日志:

【handleInitPackageResources】com.my.bqt

【setContentView前】
【handleLayoutInflated】layout,com.my.bqt:activity_main/layout
【printView】android.widget.FrameLayout{2077e3cd V.E..... ......I. 0,0-0,0 #1020002 android:id/content}
【printView】    android.widget.LinearLayout{1802a482 V.E..... ......I. 0,0-0,0}
【printView】        android.widget.TextView{5cbd793 V.ED.... ......ID 0,0-0,0 #7f07009f app:id/tv_title}
【printView】        android.view.View{222bc3d0 V.ED.... ......ID 0,0-0,0}
【printView】        android.widget.FrameLayout{252befc9 V.E..... ......I. 0,0-0,0 #7f070044 app:id/id_container}
【printView】        android.view.View{353a86ce V.ED.... ......ID 0,0-0,0}
【printView】        android.widget.LinearLayout{105259ef V.E..... ......I. 0,0-0,0 #7f07005a app:id/ly_main_tab_bottom}
【printView】            android.widget.TextView{8514cfc V.ED.... ......ID 0,0-0,0 #7f07009e app:id/tv_tab_bottom_weixin}
【printView】            android.widget.TextView{16fabf85 V.ED.... ......ID 0,0-0,0 #7f07009c app:id/tv_tab_bottom_friend}
【printView】            android.widget.TextView{14fc41da V.ED.... ......ID 0,0-0,0 #7f07009b app:id/tv_tab_bottom_contact}
【printView】            android.widget.TextView{1414a60b V.ED.... ......ID 0,0-0,0 #7f07009d app:id/tv_tab_bottom_setting}
【setContentView后】

【inflate前】
【handleLayoutInflated2】layout,com.my.bqt:fragment_tab/layout
【inflate后】

Tips:

  • 日志可以看出handleInitPackageResources會在setContentViewgetLayoutInflater().inflate時調用。
  • 對setContentView有了解的都明白,setContentView也會調用inflate方法,所以,也可以看成是hook了inflate方法。
  • 在返回的數據InitPackageResourcesParam中,有一個liparam.view的字段,通過日志可以看出setContentView方法的是一個FrameLayout,下面包含了LinearLayout,這個LinearLayout也就是我們activity_main布局最外層的view,獲取到這個view以后就可以進行一系列的操作了。

2019-4-13


免責聲明!

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



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