Xposed瞭解以及在Android中的應用

stormWen發表於2018-01-02

簡介

相信做Android開發的人都多多少少聽過Xposed這個神器吧,即使沒有用過,也多少聽過,號稱Android上最強大的神器,是由Android大神rovo89開發出來的,下面我簡單分析下Xposed以及在Android中的應用,在分析的時候會有相關涉及到的簡單原理描述。

首先,xposed是一個框架,上面有很多模組,這些模組都依賴於xposed這個框架,之所以稱xposed是第一神器,就是因為這些模組可以完成許多匪夷所思的功能,例如:修改微信的介面,自動搶紅包模組,自定義程式的文字,修改地理位置等等其他模組,這些模組通常可以完全在正常情況下難以實現的,比如搶紅包,大家都知道,自動搶紅包就是監聽了微信的收到紅包的相關介面,從而修改其中邏輯,達到自動化的效果,可以說效果槓槓的。

Xposed原理

xposed 原理就是修改系統的關鍵檔案,然後當APP呼叫系統API時,首先經過xposed,而這些基於xposed的模組就可以選擇性的在App呼叫這些api的時候做一些自己想要的事情,或者修改返回的結果,這樣app在執行的時候效果就會改變,但app本身並沒有被破壞,只是呼叫系統api的時候,Android系統的表現發生了變化,舉個例子,"奧迪"進去,"奧拓"出來,哈哈,這就是Hook,所以,說白了,Xposed就是個強大的鉤子框架,實際上Xposed作為Java層的重要Hook手段,在一些模擬的場合發揮了重要的作用,比如需要測試模擬的地理位置,就可以Hook掉相關的介面,返回我們需要的值即可,Xposed的底層原理是通過替換/system/bin/app_precesss 程式控制zygote程式,使得它在系統啟動的過程中會載入Xposed framework的一個jar檔案即XposedBridge.jar,從而完成對Zygote程式及其建立的Dalvik虛擬機器的劫持,並且能夠允許開發者獨立的替代任何class,例如framework本身,系統UI又或者隨意的一個app。

我們都知道,zygote程式是Android中所有程式的父程式,是init程式之後的第一個程式,所有的程式都是由zygote程式孵化出來,zygote程式會完成虛擬機器的初始化,庫的載入,預置類庫的載入和初始化等等操作,而以後如果需要開啟新的應用程式,zygote程式便會Fork一份出來,從而使應用程式共享了已經載入的資源等等,加快了啟動速度,也提高了效能。而Xposed由於劫持了zygote程式,就相當於劫持了所有的應用程式,從而可以實現Hook掉目標程式達到自己想要的目的,聽起來是不是很牛逼,實際上確實很牛逼,那就看看實際的效果吧。

安裝

首先,使用Xposed需要裝置是root的,因為安裝Xposed框架需要root許可權,使用過程不需要root,當然網上可以找到"免root使用Xposed之類的文章",有興趣的同學可以去研究,這裡需要注意的是:在安裝Xposed框架的時候,要選擇跟自己的手機版本相符合的,不然很容易失敗,甚至裝置變磚,特別是第三方深度定製的系統,在應用層開發都一堆相容性問題,底層的問題可能會更多,因此要注意選擇好對應的版本就好了,安裝Xposed的過程就不講了,自己參考,一般能找到對應版本的話,應該問題不難,在這裡以Google Nexus6,Android 6.0.1版本為說明,如果你安裝之後看到類似下面的,說明已經啟用成功了,

Xposed瞭解以及在Android中的應用
那就開始下面的使用吧

使用以及需要注意的問題

● 首先是新建Android工程,然後在libs目錄新增jar檔案,如圖:

Xposed瞭解以及在Android中的應用
然後新增到gradle編譯,這裡要注意的是:不能是compile files,而必須是provider files() 程式碼是:

 provided files('libs/XposedBridgeApi-82.jar')
複製程式碼

這是第一個需要注意的地方,必須是provided方式匯入,如果compile files方式匯入的話,會報找不到檔案的錯誤,這裡注意就好。

● 編寫Hook的類,需要實現IXposedHookLoadPackage,方法為:

public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam)
複製程式碼

其中XC_LoadPackage.LoadPackageParam的定義如下

public static final class LoadPackageParam extends Param {
        public ApplicationInfo appInfo;//應用程式的資訊
        public ClassLoader classLoader;//類載入器,用來載入需要Hook的類
        public boolean isFirstApplication;
        public String packageName;//包名
        public String processName;//程式名

        LoadPackageParam() {
            throw new RuntimeException("Stub!");
        }
    }
複製程式碼

一般情況下會先判斷包名,然後再進行下一步的Hook行為,比如:

if (loadPackageParam.packageName.equals("com.example.xposeproject")) {
            Class<?> clazz = loadPackageParam.classLoader.loadClass("com.example.xposeproject.ScrollingActivity");
            XposedHelpers.findAndHookMethod(clazz, "getTest", new Object[]{new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    //方法執行前呼叫
                    super.beforeHookedMethod(param);
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    //方法執行後呼叫,這裡可以修改為自己需要的結果
                    super.afterHookedMethod(param);
                    Log.d("[app]", "you are hook!");
                    param.setResult("you are Hook");
                }
            }});
        }
複製程式碼

我這裡以HookScrollingActivity的一個getTest方法為例子,ScrollingActivity的程式碼如下,會省略掉一些非關鍵資訊:

public class ScrollingActivity extends AppCompatActivity {
    private Handler mHandler=new Handler(Looper.getMainLooper());
    private TextView text;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
      //省略一些程式碼
        text=findViewById(R.id.text);
        text.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                text.setText(text.getText().toString()+":"+getTest());
            }
        });
    }

    private String getTest() {
        Log.d("[app]","沒有被劫持");
        return "沒有被劫持";
    }
}
複製程式碼

text點選的時候改變自身文字,然後我們在afterHookedMethod介面方法裡面修改了返回值

param.setResult("you are Hook")
複製程式碼

這句程式碼就是修改返回值,就是修改getTest的返回值為"you are hook"的意思,Hook方法一般用XposedHelpers.findAndHookMethod方法,引數一般有需要Hook的類,方法名,以及引數和回撥,

findAndHookMethod(Class<?> clazz, String methodName, 
Object... parameterTypesAndCallback) 
複製程式碼

從方法的原型便可以看得出來,clazz是由loadPackageParam的classLoader載入的,而方法名是需要Hook的方法,回撥函式有2個,分別是:

beforeHookedMethod(MethodHookParam param)
afterHookedMethod(MethodHookParam param)
複製程式碼

分別是方法呼叫前和方法呼叫後,MethodHookParam定義如下

public static final class MethodHookParam extends Param {
        public Object[] args = null;//引數
        public Member method;//方法名
        public Object thisObject;//被Hook類的例項,如果是Hook例項方法則不為空
        //如果Hook的是靜態方法,根據JVM呼叫方法的原理,
        //這個引數為null,下面例子會說明

        MethodHookParam() {
            throw new RuntimeException("Stub!");
        }

        public Object getResult() {
            throw new RuntimeException("Stub!");
        }

        public void setResult(Object result) {
            throw new RuntimeException("Stub!");
        }
        ...
    }
複製程式碼

我們需要修改的是改變文字,很顯然在方法的執行後返回為我們需要的,好了,Hook方法就暫時這樣

● 進行Hook檔案的配置,我們需要在assets目錄下面新建一個xposed_init檔案,寫上我們Hook檔案的路徑,如圖:

Xposed瞭解以及在Android中的應用
xposed_init寫的是檔案的路徑,這裡的路徑包括了包名+類名的

● AndroidManifest.xml檔案配置Xposed資訊,在節點之內配置下面的資訊:

        <!-- 說明是xpose模組 -->
        <meta-data
            android:name="xposedmodule"
            android:value="true" />
        <!-- 模組描述 -->
        <meta-data
            android:name="xposeddescription"
            android:value="Xpose模組描述" />
        <!-- XposedBridgeApi的最低版本號 -->
        <meta-data
            android:name="xposedminversion"
            android:value="54" />
複製程式碼

我們先來看看沒有Hook之前的介面:點選textview就可

Xposed瞭解以及在Android中的應用

一般情況下Xposed框架會自動檢測是否有新的模組,我們已經編寫好了模組了,然後在Xposed框架裡面勾上我們選擇的模組,然後重啟即可,重啟之後再次點選看看效果吧:

Xposed瞭解以及在Android中的應用

12-31 04:14:42.376 24325-24325/com.example.xposeproject D/[app]:
you are hook!
複製程式碼

通過介面和日誌,我們可以看到,Hook getText方法成功了,返回了我們需要的值,實際上我們同樣可以Hook其他的系統api介面來返回我們需要的值,更多的Hook,大家可以去多實踐,下面講講Hook第三方的應用

Xposed Hook第三方的應用

通過上面的例子,我們可以看到Xposed不僅可以Hook自身的應用,其實也可以Hook第三方的應用,像那些什麼紅包外掛,自動打卡外掛等等都是Hook了第三方的應用的,下面來Hook 另一個應用,首先新建另一個Android工程,並且加入一個自定義的application,程式碼如下:

public class UserApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d("[app]","自定義的Application");
    }
}
複製程式碼

其主頁面程式碼如下:

public class MainActivity extends AppCompatActivity {
    private static final int abc = 100;
    private static final int abcd = 200;
    private static final int abcde = 200;
    private static final int abcdef = 200;
    private TextView textView;
    private Handler mHandler = new Handler(Looper.getMainLooper());

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text);
        textView.setText("這是測試Hook第三方應用的");
        mHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent intent=new Intent("com.example.hook");
                sendBroadcast(intent);
            }
        }, 2000);
    }
}
複製程式碼

要求是在application內註冊一個廣播接收器用來接收action為"com.example.hook"的廣播,並且通過Xpose修改abc 的值,下面我們來分析一下,我們前面說了XC_LoadPackage.LoadPackageParam裡面有個packageName包名引數,我們可以通過這個過濾掉自己想要Hook的應用,在這裡的測試包名為:"com.example.xposehooktest",因此,我們也是可以這樣做的,由於要求在application注入一個廣播接收器,因此我們可以在Hook application的onCreate方法加入一個廣播接收器,如下程式碼:

private UserBrodCast mBroadCast;
 if (loadPackageParam.packageName.equals("com.example.xposehooktest")) {
            //找到application
            Class<?> clazz = loadPackageParam.classLoader.loadClass("com.example.xposehooktest.UserApplication");
            XposedHelpers.findAndHookMethod(clazz, "onCreate", new Object[]{new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    /**
                     * 這裡需要注意的是param.thisObject表示的是獲取該類的例項,
                     * 如果Hook的是例項方法,那麼param.thisObject表示該類的例項,
                     * 但如果Hook的是靜態方法的話,那麼param.thisObject是為null的
                     * 因為根據虛擬機器執行方法的原理,呼叫靜態方法不需要類的例項
                     */
                    Object object = param.thisObject;
                    Context context = null;
                    if (object instanceof Context) {
                        context = (Context) object;
                    }
                    //在這裡可以做一些特定的事情,這裡以註冊一個廣播接收器為例子
                    IntentFilter intentFilter = new IntentFilter("com.example.hook");
                    context.registerReceiver(mBroadCast=new UserBrodCast(), intentFilter);
                }
            }});
}
複製程式碼

可以看到我們在方法之後執行了註冊廣播接收器,接收器程式碼如下:

private class UserBrodCast extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.d("[app]", "這是Hook的動態註冊的廣播");
        }
    }
複製程式碼

當然這裡只是列印而已,實際上可以做更多的事情,下面我們來Hook 主頁的onCreate方法並且跟反射結合起來修改變數的值,如下程式碼:

 //這裡再以textview改變文字的例子,這裡Hook 了Activity的onCreate方法
            final Class<?> clazz2 = loadPackageParam.classLoader.loadClass("com.example.xposehooktest.MainActivity");
            XposedHelpers.findAndHookMethod(clazz2, "onCreate", new Object[]{Bundle.class,
                    new XC_MethodHook() {
                        @Override
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                            super.beforeHookedMethod(param);
                        }

                        @Override
                        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                            super.afterHookedMethod(param);
                            Field[] fields = clazz2.getDeclaredFields();
                            for (int i = 0; i < fields.length; i++) {
                                Log.d("[app]", "名稱為:" + fields[i].getName());
                            }

                            //嘗試修改final static修飾的
                            Field abcField=clazz2.getDeclaredField("abc");
                            abcField.setAccessible(true);
                            XposedHelpers.setStaticIntField(clazz2,"abc",1001);
                            Log.d("[app]","Xpose修改的值為:"+abcField.get(param.thisObject));
                            //獲取textview
                            Field field = clazz2.getDeclaredField("textView");
                            field.setAccessible(true);
                            final TextView textView = (TextView) field.get(param.thisObject);
                            Handler mHandler=new Handler(Looper.getMainLooper());
                            mHandler.postDelayed(new Runnable() {
                                @Override
                                public void run() {
                                    Log.d("[app]","動態改變文字成功");
                                    textView.setText(textView.getText().toString()+"you are Hook,This is third application");
                                }
                            },2000);
                        }
                    }});
複製程式碼

可以看到這裡獲取了所有的欄位,並且反射獲取了TextView來動態修改文字,這裡面用到了setStaticIntField方法,實際上XposedHelpers裡面還有很多類似這樣的方法,我這裡只是為了舉例子而已,實際上原理都是一樣的,有同學說了,那動態註冊的廣播如何銷燬呢,我們可以在onTrimMemory方法裡面再次Hook,然後銷燬啊,程式碼如下:

XposedHelpers.findAndHookMethod(clazz, "onTrimMemory", new Object[]{int.class,new XC_MethodHook() {
                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    super.beforeHookedMethod(param);
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                    super.afterHookedMethod(param);
                    Object object = param.thisObject;
                    Context context = null;
                    if (object instanceof Context) {
                        context = (Context) object;
                    }
                    context.unregisterReceiver(mBroadCast);
                }
            }});
複製程式碼

程式碼上就這樣了,然後重啟看看效果吧, 如圖textview已經成功更新文字了

Xposed瞭解以及在Android中的應用
列印如下:

Xposed瞭解以及在Android中的應用
可以看到跟預期的相符合,當然了,如果你有興趣,可以Hook更多的方法,做更多的事情,這裡限於篇幅,就介紹到此了。

總結

我們前面主要介紹了Xposed模組開發的基本步驟(5個主要步驟):

● 將XposedBridgeApi.jar匯入到目錄下,同時Add As Library…加入編譯

● 修改build.gradle,將compile改為provided(這個非常重要)

● 在AndroidManifest.xml中的application節點之內新增meta-data元素

● 新建主類,實現IXposedHookLoadPackage介面,重寫handleLoadPackage方法 在main目錄下新建assets資料夾,然後實現自己的目的,然後在assets中新建xposed_init檔案,寫入packagename+主類,最後重啟即可。

實際上,Xposed作為Java層Hook的重要工具,往往在一些特殊的情況下用到,除了Xposed,Java的反射和動態代理也是實現Hook的手段,反射負責找到物件,而動態代理負責替換物件,在例子中,我們在Xposed中也用到了反射,關於反射,可以參考我的另一篇文章:

Java反射以及在Android中的特殊應用

關於動態代理,可以參考我的另一篇文章:

Java代理以及在Android中的一些簡單應用

這篇文章是2017年的最後一篇文章了,最後感謝大家閱讀,祝大家2018新年快樂,事業有成!

相關文章