文章目錄
DroidPlugin原理解析
從系統設計的角度,組件和窗口的邏輯實體都存在於系統服務,比如Activity創建後,其邏輯控制主體在AMS,對於窗口,其邏輯控制主體在WMS
android將邏輯主體放置於系統服務,系統就可以對組件和窗口的生命週期,顯示狀態進行強掌控,這樣就能做到在各種狀態變更時能做到及時回調通知
所以,創建任何組件,都需要通過RPC通訊到AMS創建 — 第一個hook點
那邏輯主體確定後,AMS就需要創建進程去運行真實的Activity對象(可以認爲它是一個提線木偶)
Android進程啓動後,JAVA的入口是ActivityThread.main
ActivityThread主要幹兩件事件
- 創建IApplicationThread native binder和AMS進行通訊
- 收到AMS發來的RPC事件後,創建並保存各個組件相關的數據 ---- 第二個hook點
組件相關數據主要包括兩個
- 組件所屬包信息和對應的loadedApk - 保存於mPackages
- 將AMS中逐漸的邏輯主體對象token和真實組件對象一同保存,便於後續跟蹤操作 - 比如Activity相關的保存於mActivities,service相關保存於mServices
還有,ActivityThread的設計本身好像就支持加載多個application,多個application會被保存到mAllApplications中
插件包安裝
DroidPlugin實現了一個簡易的IPluginManagerImpl用於插件APK包的安裝和解析,當然這部分代碼是參考系統的PMS來實現的,主要職責:
- 插件APK安裝到本地目錄
- 對插件APK的組件等數據進行解析
插件包解析和加載
- 插件包的解析,就是對AndroidManifest的解析,主要通過反射系統的PackageParser來完成
- 在Activity啓動前(hook見下面介紹),會調用
PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo);
preLoadApk內部會根據targetActivityInfo包含的包名來判斷LoadedApk是
否創建,如果未創建,則會通過反射調用ActivityThread的函數來創建插件
LoadedApk並保存到ActivityThread的mPackages中,接着創建
PluginClassLoader並設置到LoadedApk對象中
- 最後通過反射調用LoadedApk的makeApplication創建插件Application對象並調用onCreate
插件Activity啓動解析
我們先來看下Android常規Activity的啓動流程
- 調用Context.startActivity -> ActivityManagerNative -> AMS, AMS通過Intent從PMS拿到ActivityInfo並創建ActivityRecord和token放入前臺ActivityStack,接着按需啓動Activity所屬進程
- 進程啓動後,馬上執行入口ActivityThread.main並調用attachApplication將啓動信息反饋到AMS,AMS通過pid找到對應的ProcessRecord並更新其數據
- 接着從前臺ActivityStack中拿到棧頂的ActivityRecord,如果其proecssrecord爲null,並且uid和processname跟新創建的ProcessRecord一致,則正式調用app.thread.scheduleLaunchActivity
- ActivityThread在scheduleLaunchActivity中創建ActivityClientRecord,用於跟AMS中的ActivityRecord對應,ActivityClientRecord最重要的兩個字段是token和activityinfo,token用於關聯ActivityRecord,activityinfo則包含activity的描述和所屬包等信息
- 在scheduleLaunchActivity內部接着發送LAUNCH_ACTIVITY message到mH這個handler,mH收到LAUNCH_ACTIVITY message後的代碼如下:
ActivityClientRecord r = (ActivityClientRecord)msg.obj; //通過activityinfo中包含的application信息創建loaedapk並保存於packageinfo r.packageInfo = getPackageInfoNoCheck( r.activityInfo.applicationInfo, r.compatInfo); handleLaunchActivity(r, null);
理解上面第1和第5步很重要,因爲DroidPlugin的Activity hook就是基於這兩個點來進行的,原理總結如下:
- DroidPlugin首先在host app的AndroidManifest預註冊一堆stub
activity,這裏只列出一部分,詳細的可查看源碼.stub.ActivityStub$P00$Standard00 .stub.ActivityStub$P00$SingleInstance00 .stub.ActivityStub$P00$SingleInstance01 .stub.ActivityStub$P00$SingleInstance02 .stub.ActivityStub$P00$SingleInstance03 .stub.ActivityStub$P00$SingleTask00 .stub.ActivityStub$P00$SingleTask01 .stub.ActivityStub$P00$SingleTask02 .stub.ActivityStub$P00$SingleTask03 .stub.ActivityStub$P00$SingleTop00 .stub.ActivityStub$P00$SingleTop01 .stub.ActivityStub$P00$SingleTop02 .stub.ActivityStub$P00$SingleTop03
- 通過動態代理和反射,hook ActivityManagerNative的接口,這個實現原理網上很多,這裏不再贅述
- hook startActivity,相關代碼在IActivityManagerHookHandle.startActivity中
ActivityInfo activityInfo = resolveActivity(intent); if (activityInfo != null && isPackagePlugin(activityInfo.packageName)) { ComponentName component = selectProxyActivity(intent); if (component != null) { Intent newIntent = new Intent(); try { ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(component.getPackageName()); setIntentClassLoader(newIntent, pluginClassLoader); } catch (Exception e) { Log.w(TAG, "Set Class Loader to new Intent fail", e); } newIntent.setComponent(component); newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent); newIntent.setFlags(intent.getFlags()); String callingPackage = (String) args[1]; if (TextUtils.equals(mHostContext.getPackageName(), callingPackage)) { newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); args[intentOfArgIndex] = newIntent; args[1] = mHostContext.getPackageName(); }
- 根據intent從DroidPlugin的packagemanager中拿到activityinfo(如果已安裝插件包中有匹配的activty)
- 還是根據intent,根據目標activity的屬性,去匹配一個最合適的stub activity,並將component信息保存到newIntent,同時將intent作爲extra保存到newintent
- 最後將args中intent替換稱newintent達到偷樑換柱的效果
經過上面的偷樑換柱後,系統實際上拿到的是newintent,進而啓動stubactivity;DroidPlugin接下去要做的就是,將stubactivity還原成真正要啓動的插件activity,這個是在上面啓動流程第5步中完成的
- 上面啓動流程第五部可以看出,ActivityThread在啓動Activity的時候,最重要的兩個參數就是ActivityClientRecord裏的兩個變量intent和activityinfo,activityinfo是用來創建packageinfo(loadedapk), intent是要在創建activity後傳入的,所以DroidPlugin必須要在創建Acivity之前,也就是handleLaunchActivity(msg)之前將這兩個變量替換成原始的插件intent,這就是DroidPlugin Hook mH的目的,下面是hook 也就是handleLaunchActivity的部分代碼
先用intent中拿出之前保存到extra的插件intent//PluginCallback.java private boolean handleLaunchActivity(Message msg) { try { Object obj = msg.obj; Intent stubIntent = (Intent) FieldUtils.readField(obj, "intent"); //ActivityInfo activityInfo = (ActivityInfo) FieldUtils.readField(obj, "activityInfo", true); stubIntent.setExtrasClassLoader(mHostContext.getClassLoader()); Intent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT); // 這裏多加一個isNotShortcutProxyActivity的判斷,因爲ShortcutProxyActivity的很特殊,啓動它的時候, // 也會帶上一個EXTRA_TARGET_INTENT的數據,就會導致這裏誤以爲是啓動插件Activity,所以這裏要先做一個判斷。 // 之前ShortcutProxyActivity錯誤複用了key,但是爲了兼容,所以這裏就先這麼判斷吧。 if (targetIntent != null && !isShortcutProxyActivity(stubIntent)) { IPackageManagerHook.fixContextPackageManager(mHostContext); ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager()); ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0); if (targetActivityInfo != null) { if (targetComponentName != null && targetComponentName.getClassName().startsWith(".")) { targetIntent.setClassName(targetComponentName.getPackageName(), targetComponentName.getPackageName() + targetComponentName.getClassName()); } ResolveInfo resolveInfo = mHostContext.getPackageManager().resolveActivity(stubIntent, 0); ActivityInfo stubActivityInfo = resolveInfo != null ? resolveInfo.activityInfo : null; if (stubActivityInfo != null) { PluginManager.getInstance().reportMyProcessName(stubActivityInfo.processName, targetActivityInfo.processName, targetActivityInfo.packageName); } PluginProcessManager.preLoadApk(mHostContext, targetActivityInfo); ClassLoader pluginClassLoader = PluginProcessManager.getPluginClassLoader(targetComponentName.getPackageName()); setIntentClassLoader(targetIntent, pluginClassLoader); setIntentClassLoader(stubIntent, pluginClassLoader); boolean success = false; try { targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo); if (stubActivityInfo != null) { targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo); } success = true; } catch (Exception e) { Log.e(TAG, "putExtra 1 fail", e); } if (!success && Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { try { ClassLoader oldParent = fixedClassLoader(pluginClassLoader); targetIntent.putExtras(targetIntent.getExtras()); targetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo); if (stubActivityInfo != null) { targetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo); } fixedClassLoader(oldParent); success = true; } catch (Exception e) { Log.e(TAG, "putExtra 2 fail", e); } } if (!success) { Intent newTargetIntent = new Intent(); newTargetIntent.setComponent(targetIntent.getComponent()); newTargetIntent.putExtra(Env.EXTRA_TARGET_INFO, targetActivityInfo); if (stubActivityInfo != null) { newTargetIntent.putExtra(Env.EXTRA_STUB_INFO, stubActivityInfo); } FieldUtils.writeDeclaredField(msg.obj, "intent", newTargetIntent); } else { FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent); } FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo); Log.i(TAG, "handleLaunchActivity OK"); } else { Log.e(TAG, "handleLaunchActivity oldInfo==null"); } } else { Log.e(TAG, "handleLaunchActivity targetIntent==null"); } } catch (Exception e) { Log.e(TAG, "handleLaunchActivity FAIL", e); } if (mCallback != null) { return mCallback.handleMessage(msg); } else { return false; } }
接着根據targetIntent獲取對應的activityinfoIntent targetIntent = stubIntent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
最後將數據寫回到ActivityClientRecord,完成最終的替換ComponentName targetComponentName = targetIntent.resolveActivity(mHostContext.getPackageManager()); ActivityInfo targetActivityInfo = PluginManager.getInstance().getActivityInfo(targetComponentName, 0);
FieldUtils.writeDeclaredField(msg.obj, "intent", targetIntent); FieldUtils.writeDeclaredField(msg.obj, "activityInfo", targetActivityInfo);
插件service啓動分析
同樣的,先來看看service的常規啓動流程
- 調用contextimpl.startService/bindService/stopService -> AMS,AMS對應創建ServiceRecord和token後,通知ActivityThread
- ActivityThread收到startService後,會創建service並保存到mService map,key爲token,接着調用oncreate
- ActivityThread接着收到handleServiceArgs, 根據token拿到service,接着調用onStartCommond並傳入intent
- ActivityThread收到bindservice後,從根據token拿到service,接着調用onbind拿到native binder,接着調用publishService將native binder傳到AMS
ActivityManagerNative.getDefault().publishService(
data.token, data.intent, binder);
Service跟Activity還是存在很大的區別的,service非常獨立,也就是說,系統創建service後,除了調用規定的那些回調,傳遞intent外,剩下就是service自己玩自己的,跟系統一毛錢關係都沒有了
Activity則不同,因爲其涉及到窗口,所以會存在大量的交互,比如WMS,IMS等
對於DroidPlugin來說,插件service的hook,則會簡單很多,只需要用一個stub service做爲代理,在stubservice內部根據傳入的intent去管理插件service對象即可:
.stub.ServiceStub$StubP00$P00
在startservice和bindservice時,只需要把目標sevice緩存stubservice,並將真實的intent作爲extra傳遞到stub service就可以了
private static ServiceInfo replaceFirstServiceIntentOfArgs(Object[] args) throws RemoteException {
int intentOfArgIndex = findFirstIntentIndexInArgs(args);
if (args != null && args.length > 1 && intentOfArgIndex >= 0) {
Intent intent = (Intent) args[intentOfArgIndex];
ServiceInfo serviceInfo = resolveService(intent);
if (serviceInfo != null && isPackagePlugin(serviceInfo.packageName)) {
ServiceInfo proxyService = selectProxyService(intent);
if (proxyService != null) {
Intent newIntent = new Intent();
//FIXBUG:https://github.com/Qihoo360/DroidPlugin/issues/122
//如果插件中有兩個Service:ServiceA和ServiceB,在bind ServiceA的時候會調用ServiceA的onBind並返回其IBinder對象,
// 但是再次bind ServiceA的時候還是會返回ServiceA的IBinder對象,這是因爲插件系統對多個Service使用了同一個StubService
// 來代理,而系統對StubService的IBinder做了緩存的問題。這裏設置一個Action則會穿透這種緩存。
newIntent.setAction(proxyService.name + new Random().nextInt());
newIntent.setClassName(proxyService.packageName, proxyService.name);
newIntent.putExtra(Env.EXTRA_TARGET_INTENT, intent);
newIntent.setFlags(intent.getFlags());
args[intentOfArgIndex] = newIntent;
return serviceInfo;
}
}
}
return null;
}
接着在stubservice會創建ServcesManager用於插件service管理,所有的stub service回調會同步到ServcesManager裏:
public int onStart(Context context, Intent intent, int flags, int startId) throws Exception {
Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ServiceInfo targetInfo = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);
if (targetInfo != null) {
Service service = mNameService.get(targetInfo.name);
if (service == null) {
handleCreateServiceOne(context, intent, targetInfo);
}
handleOnStartOne(targetIntent, flags, startId);
}
}
return -1;
}
看到沒,ServcesManager自己管理mNameService map,service信息則是通過extr中中真實的插件intent來獲得,onbind函數同樣:
public IBinder onBind(Context context, Intent intent) throws Exception {
Intent targetIntent = intent.getParcelableExtra(Env.EXTRA_TARGET_INTENT);
if (targetIntent != null) {
ServiceInfo info = PluginManager.getInstance().resolveServiceInfo(targetIntent, 0);
Service service = mNameService.get(info.name);
if (service == null) {
handleCreateServiceOne(context, intent, info);
}
return handleOnBindOne(targetIntent);
}
return null;
}
這兩個函數在mNameService未包含該service實例的時候,都會調用handleCreateServiceOne,通過反射調用ActivityThrea的方法創建service,從而達到調用oncreate的目地
插件receiver分析
在插件apk被啓動的時候,會通過分析查看apk的receiver組件信息,然後動態註冊
插件provider分析
先介紹ContentProvider的實現原理
- 本質肯定是基於binder,所以每一個ContentProvider都會實現Transport native binder
- 當我們調用getContentResolve.insert/delete等操作時,前提肯定是需要根據authority來拿到對應ContentProvider綁定的Transport對應binder proxy
- 拿到binder proxy後,數據連接建立
數據連接建立後,後續跟系統也沒一毛錢關係了,那理論上provider跟service是一樣的,只要能hook數據發送端,接收端用一個stubprovider做代理就可以搞定了
DroidPlugin定義的stubprovider
.stub.ContentProviderStub$StubP00
發送端hook,就是替換binder proxy的過程,看DroidPlugin的getContentProvider的hook代碼:
@Override
protected boolean beforeInvoke(Object receiver, Method method, Object[] args) throws Throwable {
if (args != null) {
final int index = 1;
if (args.length > index && args[index] instanceof String) {
String name = (String) args[index];
mStubProvider = null;
mTargetProvider = null;
ProviderInfo info = mHostContext.getPackageManager().resolveContentProvider(name, 0);
mTargetProvider = PluginManager.getInstance().resolveContentProvider(name, 0);
//這裏有個很坑爹的事情,就是當插件的contentprovider和host的名稱一樣,衝突的時候處理方式。
//在Android系統上,是不會出現這種事情的,因爲系統在安裝的時候做了處理。而我們目前沒做處理。so,在出現衝突時候的時候優先用host的。
if (mTargetProvider != null && info != null && TextUtils.equals(mTargetProvider.packageName, info.packageName)) {
mStubProvider = PluginManager.getInstance().selectStubProviderInfo(name);
// PluginManager.getInstance().reportMyProcessName(mStubProvider.processName, mTargetProvider.processName);
// PluginProcessManager.preLoadApk(mHostContext, mTargetProvider);
if (mStubProvider != null) {
args[index] = mStubProvider.authority;
} else {
Log.w(TAG, "getContentProvider,fake fail 1");
}
} else {
mTargetProvider = null;
Log.w(TAG, "getContentProvider,fake fail 2=%s", name);
}
}
}
return super.beforeInvoke(receiver, method, args);
}
這裏不管是什麼請求,authority都會被改成stub provider的authority,在請求結束後,在將authority關聯contentprovider對應的binder proxy設置成DroidPlugin自己的
Object provider = FieldUtils.readField(invokeResult, "provider");
if (provider != null) {
boolean localProvider = FieldUtils.readField(toObj, "provider") == null;
IContentProviderHook invocationHandler = new IContentProviderHook(mHostContext, provider, mStubProvider, mTargetProvider, localProvider);
invocationHandler.setEnable(true);
Class<?> clazz = provider.getClass();
List<Class<?>> interfaces = Utils.getAllInterfaces(clazz);
Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
Object proxyprovider = MyProxy.newProxyInstance(clazz.getClassLoader(), ifs, invocationHandler);
FieldUtils.writeField(invokeResult, "provider", proxyprovider);
FieldUtils.writeField(toObj, "provider", proxyprovider);
}
接着在IContentProviderHook對發送uri做替換
if (!mLocalProvider && mStubProvider != null) {
final int index = indexFirstUri(args);
if (index >= 0) {
Uri uri = (Uri) args[index];
String authority = uri.getAuthority();
if (!TextUtils.equals(authority, mStubProvider.authority)) {
Uri.Builder b = new Builder();
b.scheme(uri.getScheme());
b.authority(mStubProvider.authority);
b.path(uri.getPath());
b.query(uri.getQuery());
b.appendQueryParameter(Env.EXTRA_TARGET_AUTHORITY, authority);
b.fragment(uri.getFragment());
args[index] = b.build();
}
}
}
將uri的authority替換成stub provider的,將插件provider的authority保存到Env.EXTRA_TARGET_AUTHORITY這個parameter中
stubprovider實現就很簡單了,根據Env.EXTRA_TARGET_AUTHORITY的值來創建插件provider,接着做代理就好了,這裏不就貼代碼了
下面是contentprovider常規初始化流程,大家可以瞭解下
- ContextImpl.getContentResolver.insert->ApplicationContentResolver.acquireProvider->ActivityThread.acquireProvider->ActivityManagerNative.getContentProvider->AMS.getContentProvider
- 接着ActivityThread.scheduleInstallProvider->ActivityThread.installProvider
- 接着創建ContextProvider實例並獲取內部native binder
try {
final java.lang.ClassLoader cl = c.getClassLoader();
localProvider = (ContentProvider)cl.
loadClass(info.name).newInstance();
provider = localProvider.getIContentProvider();
if (provider == null) {
Slog.e(TAG, "Failed to instantiate class " +
info.name + " from sourceDir " +
info.applicationInfo.sourceDir);
return null;
}
if (DEBUG_PROVIDER) Slog.v(
TAG, "Instantiating local provider " + info.name);
// XXX Need to create the correct context for this provider.
localProvider.attachInfo(c, info);
} catch (java.lang.Exception e) {
if (!mInstrumentation.onException(null, e)) {
throw new RuntimeException(
"Unable to get provider " + info.name
+ ": " + e.toString(), e);
}
return null;
}
從代碼裏可以看出getIContentProvider返回的native binder纔是contentprovider數據傳輸的核心
- 接着調用ActivityManagerNative.publishContentProviders將新創建的provider同步到AMS
還有一點很重要,通過AMS.getContentProvider->ActivityThread.acquireProvider,由於ActivityThread處理都是發送消息到mH,所以它是異步的,AMS.getContentProvider如果立即返回,肯定是空的,所以它必須要等待後續ActivityManagerNative.publishContentProviders執行完成後才返回,看AMS.getContentProviderImpl部分代碼:
//ActivityManagerService.getContentProviderImpl
//.....前面代碼沒貼
// Wait for the provider to be published...
synchronized (cpr) {
while (cpr.provider == null) {
if (cpr.launchingApp == null) {
return null;
}
try {
if (conn != null) {
conn.waiting = true;
}
cpr.wait();
} catch (InterruptedException ex) {
} finally {
if (conn != null) {
conn.waiting = false;
}
}
}
}
return cpr != null ? cpr.newHolder(conn) : null;
插件加載獨立性
如果插件都在主進程啓動運行,可能有人會有疑問,LoadedApk會不會亂掉?答案肯定是不會的,因爲這個是DroidPlugin這個實現方案的前提,咱們看LoadedApk的生成代碼
private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
ClassLoader baseLoader, boolean securityViolation, boolean includeCode) {
synchronized (mResourcesManager) {
WeakReference<LoadedApk> ref;
if (includeCode) {
ref = mPackages.get(aInfo.packageName);
} else {
ref = mResourcePackages.get(aInfo.packageName);
}
LoadedApk packageInfo = ref != null ? ref.get() : null;
if (packageInfo == null || (packageInfo.mResources != null
&& !packageInfo.mResources.getAssets().isUpToDate())) {
if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
: "Loading resource-only package ") + aInfo.packageName
+ " (in " + (mBoundApplication != null
? mBoundApplication.processName : null)
+ ")");
packageInfo =
new LoadedApk(this, aInfo, compatInfo, this, baseLoader,
securityViolation, includeCode &&
(aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0);
if (includeCode) {
mPackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
} else {
mResourcePackages.put(aInfo.packageName,
new WeakReference<LoadedApk>(packageInfo));
}
}
return packageInfo;
}
}
ActivityThread會保存LoadeApk的map,key就是package name,所以各個插件的LoadedApk可以獨立的存在ActivityThread中
插件resource獲取
Android資源獲取依賴
- resource id,即開發中用到R..
- 還有就是context.getResource()
由於四大組件和Application這五個入口類的創建使用的是插件的class loader,那他們使用過程中用到的R.java肯定是對應插件的,這個不會有任何問題
不過context本質是ContextImpl對象實例,這個對象不是基於插件的class loader創建的,這個要注意,但是它對插件resource獨立獲取沒任何影響,因爲
- context實例跟組件和Application都是一對一創建的,這就導致它不可能跟其他插件混淆
- context.getresource本質還是使用插件package res info創建AssertManager,它跟插件也是一對一綁定的
所以,只要完成了插件LoadedApk的創建,組件運行過程中的resource就可以正常獲取
總結
DroidPlugin的設計真的很巧妙,作者能構思出這種方案,對組件的初始化肯定是非常熟悉的,這套插件化方案出來也很多年了,最近看一遍,主要還是想學習作者的實現思路,同時也加深自己對組件初始化相關代碼的理解
組件實現能被偷天換日是基於Android這麼一個設計前提,AMS只是保存組件的邏輯對象主體,ActivityThread只是基於邏輯主體token來創建本地組件對象並做後續跟蹤,這就爲修改本地組件對象提供了可能
不過這種方式對系統潛入太大了,兼容性會比較差