Android 外掛化原理解析——Hook機制之AMS&PMS
在前面的文章中我們介紹了DroidPlugin的Hook機制,也就是代理方式和Binder Hook;外掛框架透過AOP實現了外掛使用和開發的透明性。在講述DroidPlugin如何實現四大元件的外掛化之前,有必要說明一下它對ActivityManagerServiche以及PackageManagerService的Hook方式(以下簡稱AMS,PMS)。
ActivityManagerService對於FrameWork層的重要性不言而喻,Android的四大元件無一不與它打交道:
startActivity
最終呼叫了AMS的startActivity
系列方法,實現了Activity的啟動;Activity的生命週期回撥,也在AMS中完成;startService,bindService
最終呼叫到AMS的startService和bindService方法;動態廣播的註冊和接收在
AMS
中完成(靜態廣播在PMS
中完成)getContentResolver
最終從AMS
的getContentProvider
獲取到ContentProvider
而PMS
則完成了諸如許可權校撿(checkPermission,checkUidPermission
),Apk meta資訊獲取(getApplicationInfo
等),四大元件資訊獲取(query
系列方法)等重要功能。
在上文中講述了DroidPlugin的Binder Hook機制;我們知道AMS
和PMS
就是以Binder方式提供給應用程式使用的系統服務,理論上我們也可以採用這種方式Hook掉它們。但是由於這兩者使用得如此頻繁,Framework給他們了一些“特別優待”,這也給了我們相對於Binder Hook更加穩定可靠的hook方式。
閱讀本文之前,可以先clone一份 ,參考此專案的ams-pms-hook
模組。
AMS獲取過程
前文提到Android的四大元件無一不與AMS
相關,也許讀者還有些許疑惑;這裡我就挑一個例子,依據Android原始碼來說明,一個簡單的startActivity
是如何呼叫AMS
最終透過IPC到system_server的。
不論讀者是否知道,我們使用startActivity
有兩種形式:
直接呼叫
Context
類的startActivity
方法;這種方式啟動的Activity沒有Activity棧,因此不能以standard方式啟動,必須加上FLAG_ACTIVITY_NEW_TASK
這個Flag。呼叫被
Activity
類過載過的startActivity
方法,通常在我們的Activity中直接呼叫這個方法就是這種形式;
Context.startActivity
我們檢視Context
類的startActivity
方法,發現這竟然是一個抽象類;
我們看到諸如Activity
,Service
等並沒有直接繼承Context
,而是繼承了ContextWrapper
;繼續檢視ContextWrapper
的實現:
1 2 3 4 |
@Override public void startActivity(Intent intent) { mBase.startActivity(intent); } |
WTF!! 果然人如其名,只是一個wrapper而已;這個mBase
是什麼呢?這裡我先直接告訴你,它的真正實現是ContextImpl
類;至於為什麼,有一條思路:mBase是在ContextWrapper構造的時候傳遞進來的,那麼在ContextWrapper構造的時候可以找到答案
什麼時候會構造ContextWrapper呢?它的子類Application
,Service
等被建立的時候。
可以在App的主執行緒AcitivityThread
的performLaunchActivit
方法裡面找到答案;更詳細的解析可以參考老羅的
好了,我們姑且當作已經知道Context.startActivity最終使用了ContextImpl裡面的方法,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
public void startActivity(Intent intent, Bundle options) { warnIfCallingFromSystemProcess(); if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { throw new AndroidRuntimeException( "Calling startActivity() from outside of an Activity " + " context requires the FLAG_ACTIVITY_NEW_TASK flag." + " Is this really what you want?"); } mMainThread.getInstrumentation().execStartActivity( getOuterContext(), mMainThread.getApplicationThread(), null, (Activity)null, intent, -1, options); } |
程式碼相當簡單;我們知道了兩件事:
其一,我們知道了在Service等非Activity的Context裡面啟動Activity為什麼需要新增
FLAG_ACTIVITY_NEW_TASK
;其二,真正的
startActivity
使用了Instrumentation
類的execStartActivity
方法;繼續跟蹤:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { // ... 省略無關程式碼 try { intent.migrateExtraStreamToClipData(); intent.prepareToLeaveProcess(); // ----------------look here!!!!!!!!!!!!!!!!!!! int result = ActivityManagerNative.getDefault() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, null, options); checkStartActivityResult(result, intent); } catch (RemoteException e) { } return null; } |
到這裡我們發現真正呼叫的是ActivityManagerNative
的startActivity
方法;如果你不清楚ActivityManager
,ActivityManagerService
以及ActivityManagerNative
之間的關係;建議先仔細閱讀我之前關於Binder的文章 。
Activity.startActivity
Activity類的startActivity
方法相比Context而言直觀了很多;這個startActivity
透過若干次呼叫輾轉到達startActivityForResult
這個方法,在這個方法內部有如下程式碼:
1 2 3 4 |
Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options); |
可以看到,其實透過Activity和ContextImpl類啟動Activity並無本質不同,他們都透過Instrumentation
這個輔助類呼叫到了ActivityManagerNative
的方法。
Hook AMS
OK,我們到現在知道;其實startActivity
最終透過ActivityManagerNative
這個方法遠端呼叫了AMS
的startActivity
方法。那麼這個ActivityManagerNative
是什麼呢?
ActivityManagerNative實際上就是ActivityManagerService
這個遠端物件的Binder代理物件;每次需要與AMS打交道的時候,需要藉助這個代理物件透過驅動進而完成IPC呼叫。
我們繼續看ActivityManagerNative
的getDefault()
方法做了什麼:
1 2 3 |
static public IActivityManager getDefault() { return gDefault.get(); } |
gDefault
這個靜態變數的定義如下:
1 2 3 4 5 6 7 |
private static final Singleton |
由於整個Framework與AMS打交道是如此頻繁,framework使用了一個單例把這個AMS
的代理物件儲存了起來;這樣只要需要與AMS
進行IPC呼叫,獲取這個單例即可。這是AMS
這個系統服務與其他普通服務的不同之處,也是我們不透過Binder Hook的原因——我們只需要簡單地Hook掉這個單例即可。
這裡還有一點小麻煩:Android不同版本之間對於如何儲存這個單例的代理物件是不同的;Android 2.x系統直接使用了一個簡單的靜態變數儲存,Android 4.x以上抽象出了一個Singleton類;具體的差異可以使用grepcode
進行比較:
我們以4.x以上的程式碼為例說明如何Hook掉AMS
;方法使用的動態代理,如果有不理解的,可以參考之前的系列文章
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Class> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative"); // 獲取 gDefault 這個欄位, 想辦法替換它 Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault"); gDefaultField.setAccessible(true); Object gDefault = gDefaultField.get(null); // 4.x以上的gDefault是一個 android.util.Singleton物件; 我們取出這個單例裡面的欄位 Class> singleton = Class.forName("android.util.Singleton"); Field mInstanceField = singleton.getDeclaredField("mInstance"); mInstanceField.setAccessible(true); // ActivityManagerNative 的gDefault物件裡面原始的 IActivityManager物件 Object rawIActivityManager = mInstanceField.get(gDefault); // 建立一個這個物件的代理物件, 然後替換這個欄位, 讓我們的代理物件幫忙幹活 Class> iActivityManagerInterface = Class.forName("android.app.IActivityManager"); Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class>[] { iActivityManagerInterface }, new IActivityManagerHandler(rawIActivityManager)); mInstanceField.set(gDefault, proxy); |
好了,我們hook成功之後啟動Activity看看會發生什麼:
1 2 3 4 5 6 7 8 |
D/HookHelper﹕ hey, baby; you are hook!! D/HookHelper﹕ method:activityResumed called with args:[android.os.BinderProxy@9bc71b2] D/HookHelper﹕ hey, baby; you are hook!! D/HookHelper﹕ method:activityIdle called with args:[android.os.BinderProxy@9bc71b2, null, false] D/HookHelper﹕ hey, baby; you are hook!! D/HookHelper﹕ method:startActivity called with args:[android.app.ActivityThread$ApplicationThread@17e750c, com.weishu.upf.ams_pms_hook.app, Intent { act=android.intent.action.VIEW dat= }, null, android.os.BinderProxy@9bc71b2, null, -1, 0, null, null] D/HookHelper﹕ hey, baby; you are hook!! D/HookHelper﹕ method:activityPaused called with args:[android.os.BinderProxy@9bc71b2] |
可以看到,簡單的幾行程式碼,AMS
已經被我們完全劫持了!! 至於劫持了能幹什麼,自己發揮想象吧~
DroidPlugin關於AMS
的Hook,可以檢視IActivityManagerHook
這個類,它處理了我上述所說的相容性問題,其他原理相同。另外,也許有童鞋有疑問了,你用startActivity
為例怎麼能確保Hook掉這個靜態變數之後就能保證所有使用AMS
的入口都被Hook了呢?
答曰:無他,唯手熟爾。
Android Framewrok層對於四大元件的處理,呼叫AMS
服務的時候,全部都是透過使用這種方式;若有疑問可以自行檢視原始碼。你可以從Context
類的startActivity, startService,bindService, registerBroadcastReceiver, getContentResolver 等等入口進行跟蹤,最終都會發現它們都會使用ActivityManagerNative的這個AMS
代理物件來完成對遠端AMS的訪問。
PMS獲取過程
PMS
的獲取也是透過Context完成的,具體就是getPackageManager
這個方法;我們姑且當作已經知道了Context的實現在ContextImpl類裡面,直奔ContextImpl
類的getPackageManager
方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
public PackageManager getPackageManager() { if (mPackageManager != null) { return mPackageManager; } IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { // Doesn't matter if we make more than one instance. return (mPackageManager = new ApplicationPackageManager(this, pm)); } return null; } |
可以看到,這裡幹了兩件事:
真正的
PMS
的代理物件在ActivityThread
類裡面ContextImpl
透過ApplicationPackageManager
對它還進行了一層包裝
我們繼續檢視ActivityThread
類的getPackageManager
方法,原始碼如下:
1 2 3 4 5 6 7 8 |
public static IPackageManager getPackageManager() { if (sPackageManager != null) { return sPackageManager; } IBinder b = ServiceManager.getService("package"); sPackageManager = IPackageManager.Stub.asInterface(b); return sPackageManager; } |
可以看到,和AMS
一樣,PMS
的Binder代理物件也是一個全域性變數存放在一個靜態欄位中;我們可以如法炮製,Hook掉PMS。
現在我們的目的很明切,如果需要Hook PMS
有兩個地方需要Hook掉:
ActivityThread
的靜態欄位sPackageManager
透過Context類的
getPackageManager
方法獲取到的ApplicationPackageManager
物件裡面的mPM
欄位。
Hook PMS
現在使用代理Hook應該是輕車熟路了吧,透過上面的分析,我們Hook兩個地方;程式碼信手拈來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 獲取全域性的ActivityThread物件 Class> activityThreadClass = Class.forName("android.app.ActivityThread"); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); Object currentActivityThread = currentActivityThreadMethod.invoke(null); // 獲取ActivityThread裡面原始的 sPackageManager Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager"); sPackageManagerField.setAccessible(true); Object sPackageManager = sPackageManagerField.get(currentActivityThread); // 準備好代理物件, 用來替換原始的物件 Class> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager"); Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(), new Class>[] { iPackageManagerInterface }, new HookHandler(sPackageManager)); // 1. 替換掉ActivityThread裡面的 sPackageManager 欄位 sPackageManagerField.set(currentActivityThread, proxy); // 2. 替換 ApplicationPackageManager裡面的 mPM物件 PackageManager pm = context.getPackageManager(); Field mPmField = pm.getClass().getDeclaredField("mPM"); mPmField.setAccessible(true); mPmField.set(pm, proxy); |
好了,Hook完畢我們驗證以下結論;呼叫一下PMS
的getInstalledApplications
方法,列印日誌如下:
1 2 |
03-07 15:07:27.187 8306-8306/com.weishu.upf.ams_pms_hook.app D/IActivityManagerHandler﹕ hey, baby; you are hook!! 03-07 15:07:27.187 8306-8306/com.weishu.upf.ams_pms_hook.app D/IActivityManagerHandler﹕ method:getInstalledApplications called with args:[0, 0] |
OK,我們又成功劫持了PackageManager
!!DroidPlugin 處理PMS的程式碼可以在IPackageManagerHook
檢視。
在結束講解PackageManager的Hook之前,我們需要說明一點;那就是Context
的實現類裡面沒有使用靜態全域性變數來儲存PMS
的代理物件,而是每擁有一個Context
的例項就持有了一個PMS
代理物件的引用;所以這裡有個很蛋疼的事情,那就是我們如果想要完全Hook住PMS
,需要精確控制整個程式內部建立的Context
物件;所幸,外掛框架中,外掛的Activity,Service,ContentProvider,Broadcast等所有使用到Context的地方,都是由框架控制建立的;因此我們要小心翼翼地替換掉所有這些物件持有的PMS
代理物件。
我前面也提到過,靜態變數和單例都是良好的Hook點,這裡很好地反證了這句話:想要Hook掉一個例項變數該是多麼麻煩!
小結
寫到這裡,關於DroidPlugin的Hook技術的講解已經完結了;我相信讀者或多或少地認識到,其實Hook並不是一項神秘的技術;一個乾淨,透明的框架少不了AOP,而AOP也少不了Hook。
我所講解的Hook僅僅使用反射和動態代理技術,更加強大的Hook機制可以進行位元組碼編織,比如J2EE廣泛使用了cglib和asm進行AOP程式設計;而Android上現有的外掛框架還是載入編譯時程式碼,採用動態生成類的技術理論上也是可行的;之前有一篇文章,就講述了這種方式;現在全球的網際網路公司不排除有用這種技術實現外掛框架的可能 ;我相信不遠的未來,這種技術也會在Android上大放異彩。
瞭解完Hook技術之後,接下來的系列文章會講述DroidPlugin對Android四大元件在外掛系統上的處理,外掛框架對於這一部分的實現是DroidPlugin的精髓,Hook只不過是工具而已。學習這部分內容需要對於Activity,Service,Broadcast以及ContentProvider的工作機制有一定的瞭解,因此我也會在必要的時候穿插講解一些Android Framework的知識;我相信這一定會對讀者大有裨益。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3244/viewspace-2814852/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Android外掛化原理解析——Hook機制之Binder HookAndroidHook
- PostgreSQL外掛hook機制SQLHook
- Android外掛化原理(一)Activity外掛化Android
- Activity外掛化原理第二種方案:Hook IActivityManagerHook
- Activity外掛化原理第一種方案:Hook InstrumentationHook
- Taro cli流程和外掛化機制實現原理
- 全新的android外掛機制 - DroidPluginAndroidPlugin
- webpack外掛機制之TapableWeb
- 如何使用外掛化機制優雅的封裝你的請求hook封裝Hook
- Android 外掛化原理入門筆記Android筆記
- Android全面解析之Context機制AndroidContext
- Android全面解析之Window機制Android
- Android外掛化原理分析(基於Neptune框架)Android框架
- 從ClassLoader到Android外掛化以及熱更新原理Android
- [Android]用架構師角度看外掛化(2)-Replugin 唯一hook點Android架構PluginHook
- Android外掛化快速入門與例項解析(VirtualApk)AndroidAPK
- 外掛化實現Android多主題功能原理剖析Android
- Android外掛化開篇Android
- 淺析Android外掛化Android
- 探尋 webpack 外掛機制Web
- Android Handler訊息傳遞機制:圖文解析工作原理Android
- [Android]AAB外掛化架構Android架構
- [Android元件化]AAB外掛化架構Android元件化架構
- Android AccessibilityService機制原始碼解析Android原始碼
- webpack-外掛機制雜記Web
- 深入淺出Service外掛化原理
- Android元件化專案自動初始化外掛Initer原理解析Android元件化
- Android外掛化的一種方案Android
- Android10_原理機制系列_事件傳遞機制Android事件
- Android全面解析之由淺及深Handler訊息機制Android
- [Android] 元件化 & 模組化 & 外掛化演進Android元件化
- Libco Hook 機制淺析Hook
- MySQL索引機制(詳細+原理+解析)MySql索引
- vscode 外掛視覺化製作和管理腳手架及原理解析VSCode視覺化
- SOFATracer 外掛埋點機制詳解
- MyBatis(八):MyBatis外掛機制詳解MyBatis
- Android的Handler訊息機制 解析Android
- Android RollBack機制實現原理剖析Android