簡介
相信做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版本為說明,如果你安裝之後看到類似下面的,說明已經啟用成功了,
那就開始下面的使用吧使用以及需要注意的問題
● 首先是新建Android工程,然後在libs目錄新增jar檔案,如圖:
然後新增到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_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框架會自動檢測是否有新的模組,我們已經編寫好了模組了,然後在Xposed框架裡面勾上我們選擇的模組,然後重啟即可,重啟之後再次點選看看效果吧:
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已經成功更新文字了
列印如下: 可以看到跟預期的相符合,當然了,如果你有興趣,可以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中也用到了反射,關於反射,可以參考我的另一篇文章:
關於動態代理,可以參考我的另一篇文章:
這篇文章是2017年的最後一篇文章了,最後感謝大家閱讀,祝大家2018新年快樂,事業有成!