Android 插件化原理解析——Hook機制之AMS&PMS

在前面的文章中我們介紹了DroidPlugin的Hook機制,也就是代理方式Binder Hook;插件框架通過AOP實現了插件使用和開發的透明性。在講述DroidPlugin如何實現四大組件的插件化之前,有必要說明一下它對ActivityManagerServiche以及PackageManagerService的Hook方式(以下簡稱AMS,PMS)。

ActivityManagerService對于FrameWork層的重要性不言而喻,Android的四大組件無一不與它打交道:

  1. startActivity最終調用了AMS的startActivity系列方法,實現了Activity的啟動;Activity的生命周期回調,也在AMS中完成;
  2. startService,bindService最終調用到AMS的startService和bindService方法;
  3. 動態廣播的注冊和接收在AMS中完成(靜態廣播在PMS中完成)
  4. getContentResolver最終從AMSgetContentProvider獲取到ContentProvider

PMS則完成了諸如權限校撿(checkPermission,checkUidPermission),Apk meta信息獲取(getApplicationInfo等),四大組件信息獲取(query系列方法)等重要功能。

在上文Android插件化原理解析——Hook機制之Binder Hook中講述了DroidPlugin的Binder Hook機制;我們知道AMSPMS就是以Binder方式提供給應用程序使用的系統服務,理論上我們也可以采用這種方式Hook掉它們。但是由于這兩者使用得如此頻繁,Framework給他們了一些“特別優待”,這也給了我們相對于Binder Hook更加穩定可靠的hook方式。

閱讀本文之前,可以先clone一份 understand-plugin-framework,參考此項目的ams-pms-hook模塊。另外,插件框架原理解析系列文章見索引。

AMS獲取過程

前文提到Android的四大組件無一不與AMS相關,也許讀者還有些許疑惑;這里我就挑一個例子,依據Android源碼來說明,一個簡單的startActivity是如何調用AMS最終通過IPC到system_server的。

不論讀者是否知道,我們使用startActivity有兩種形式:

  1. 直接調用Context類的startActivity方法;這種方式啟動的Activity沒有Activity棧,因此不能以standard方式啟動,必須加上FLAG_ACTIVITY_NEW_TASK這個Flag。
  2. 調用被Activity類重載過的startActivity方法,通常在我們的Activity中直接調用這個方法就是這種形式;

Context.startActivity

我們查看Context類的startActivity方法,發現這竟然是一個抽象類;查看Context的類繼承關系圖如下:

我們看到諸如ActivityService等并沒有直接繼承Context,而是繼承了ContextWrapper;繼續查看ContextWrapper的實現:

@Override
public void startActivity(Intent intent) {
    mBase.startActivity(intent);
}

WTF!! 果然人如其名,只是一個wrapper而已;這個mBase是什么呢?這里我先直接告訴你,它的真正實現是ContextImpl類;至于為什么,有一條思路:mBase是在ContextWrapper構造的時候傳遞進來的,那么在ContextWrapper構造的時候可以找到答案
什么時候會構造ContextWrapper呢?它的子類Application,Service等被創建的時候。

因此可以在App的主線程AcitivityThreadperformLaunchActivit方法里面找到答案;更詳細的解析可以參考老羅的 Android應用程序啟動過程源代碼分析

好了,我們姑且當作已經知道Context.startActivity最終使用了ContextImpl里面的方法,代碼如下:

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);
}

代碼相當簡單;我們知道了兩件事:

  1. 其一,我們知道了為什么在Service等非Activity的Context里面啟動Activity為什么需要添加FLAG_ACTIVITY_NEW_TASK;
  2. 其二,真正的startActivity使用了Instrumentation類的execStartActivity方法;繼續跟蹤:
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;
}

到這里我們發現真正調用的是ActivityManagerNativestartActivity方法;如果你不清楚ActivityManager,ActivityManagerService以及ActivityManagerNative之間的關系;建議先仔細閱讀我之前關于Binder的文章 Binder學習指南。

Activity.startActivity

Activity類的startActivity方法相比Context而言直觀了很多;這個startActivity通過若干次調用輾轉到達startActivityForResult這個方法,在這個方法內部有如下代碼:

Instrumentation.ActivityResult ar =
    mInstrumentation.execStartActivity(
        this, mMainThread.getApplicationThread(), mToken, this,
        intent, requestCode, options);

可以看到,其實通過Activity和ContextImpl類啟動Activity并無本質不同,他們都通過Instrumentation這個輔助類調用到了ActivityManagerNative的方法。

Hook AMS

OK,我們到現在知道;其實startActivity最終通過ActivityManagerNative這個方法遠程調用了AMSstartActivity方法。那么這個ActivityManagerNative是什么呢?

ActivityManagerNative實際上就是ActivityManagerService這個遠程對象的Binder代理對象;每次需要與AMS打交道的時候,需要借助這個代理對象完成通過驅動進而完成IPC調用。

我們繼續看ActivityManagerNativegetDefault()方法做了什么:

    static public IActivityManager getDefault() {
        return gDefault.get();
    }

gDefault這個靜態變量的定義如下:

private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
    protected IActivityManager create() {
        IBinder b = ServiceManager.getService("activity
        IActivityManager am = asInterface(
        return am;
    }
};

由于整個Framework與AMS打交道是如此頻繁,framework使用了一個單例把這個AMS的代理對象保存了起來;這樣只要需要與AMS進行IPC調用,獲取這個單例即可。這是AMS這個系統服務與其他普通服務的不同之處,也是我們不通過Binder Hook的原因——我們只需要簡單地Hook掉這個單例即可。

這里還有一點小麻煩:Android不同版本之間對于如何保存這個單例的代理對象是不同的;Android 2.x系統直接使用了一個簡單的靜態變量存儲,Android 4.x以上抽象出了一個Singleton類;具體的差異可以使用grepcode進行比較:差異

我們以4.x以上的代碼為例說明如何Hook掉AMS;方法使用的動態代理,如果有不理解的,可以參考之前的系列文章Android插件化原理解析——Hook機制之動態代理

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看看會發生什么:

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=http://wwww.baidu.com/... }, 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方法:

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;
}

可以看到,這里干了兩件事:

  1. 真正的PMS的代理對象在ActivityThread類里面
  2. ContextImpl通過ApplicationPackageManager對它還進行了一層包裝

我們繼續查看ActivityThread類的getPackageManager方法,源碼如下:

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掉:

  1. ActivityThread的靜態字段sPackageManager
  2. 通過Context類的getPackageManager方法獲取到的ApplicationPackageManager對象里面的mPM字段。

Hook PMS

現在使用代理Hook應該是輕車熟路了吧,通過上面的分析,我們Hook兩個地方;代碼信手拈來:

// 獲取全局的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完畢我們驗證以下結論;調用一下PMSgetInstalledApplications方法,打印日志如下:

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?。roidPlugin 處理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動態加載黑科技 動態創建Activity模式,就講述了這種方式;現在全球的互聯網公司不排除有用這種技術實現插件框架的可能 ;我相信不遠的未來,這種技術也會在Android上大放異彩。

了解完Hook技術之后,接下來的系列文章會講述DroidPlugin對Android四大組件在插件系統上的處理,插件框架對于這一部分的實現是DroidPlugin的精髓,Hook只不過是工具而已。學習這部分內容需要對于Activity,Service,Broadcast以及ContentProvider的工作機制有一定的了解,因此我也會在必要的時候穿插講解一些Android Framework的知識;我相信這一定會對讀者大有裨益。

喜歡就點個贊吧~持續更新,請關注github項目 understand-plugin-framework和我的 博客!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,916評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,014評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,245評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,795評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,010評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,524評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,487評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,634評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,884評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,236評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,623評論 2 370

推薦閱讀更多精彩內容