紙上得來終覺淺,絕知此事要躬行
前言
作為一個android開發者,一定都知道每個activity都需要在AndroidManifest.xml中顯示的聲明一下,否則在啟動的activity的時候就會拋出ActivityNotFoundException的異常。那么真的就沒有辦法去啟動一個沒有聲明的activity嗎?來讓我們從源碼看起。
activity啟動過程
想要知道能不能啟動一個不在manifest中注冊的Activity,我們先來看一下activity啟動的過程,下面以Android7.1.2的代碼為例。
在啟動activity時會調用startActivity,首先我們來看Activity中的startActivity看起,代碼在frameworks/base/core/java/android/app/Activity.java中。
public void startActivity(Intent intent) {
this.startActivity(intent, null);
}
之后有調用了兩個參數的startActivity(),又經過一連串的調用,調用到了startActivityForResult()
ublic void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
startActivityForResult(intent, -1);
}
}
其中真正啟動activity的方法是通過Instrumentation中的execStartActivity()方法啟動的。mInstrumentation是Activity中的一個成員變量,frameworks/base/core/java/android/app/Instrumentation.java
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
下面是整個流程中比較關鍵的方法
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
...
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
先看checkStartActivityResult()這個方法,這里會檢測activity啟動的各種狀態,其中就包括了"have you declared this activity in your AndroidManifest.xml?"沒有在AndroidManifest.xml注冊activity的異常。
public static void checkStartActivityResult(int res, Object intent) {
if (res >= ActivityManager.START_SUCCESS) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity "
+ intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException(
"FORWARD_RESULT_FLAG used while also requesting a result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException(
"PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException(
"Starting under voice control not allowed for: " + intent);
case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
throw new IllegalStateException(
"Session calling startVoiceActivity does not match active session");
case ActivityManager.START_VOICE_HIDDEN_SESSION:
throw new IllegalStateException(
"Cannot start voice activity on a hidden session");
case ActivityManager.START_CANCELED:
throw new AndroidRuntimeException("Activity could not be started for "
+ intent);
default:
throw new AndroidRuntimeException("Unknown error code "
+ res + " when starting " + intent);
}
}
其中啟動activity的方法是ActivityManagerNative.getDefault().startActivity()。ActivityManagerNative繼承了Binder,同時實現了IActivityManager接口,啟動activity用到了Android中binder機制,這里先不做具體討論,代碼在frameworks/base/core/java/android/app/ActivityManagerNative.java。先來看getDefault(),獲取的是一個IActivityManager單例。
static public IActivityManager getDefault() {
return gDefault.get();
}
再看單例里做了什么,獲取了系統的ActivityManagerService(AMS),ActivityManagerNative實際上就是ActivityManagerService(AMS)這個遠程對象的Binder代理對象
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
到這里,就是整個activity啟動的流程了,也找到了ActivityNotFoundException的原因,那么到底有沒有辦法啟動一個沒有在Android.xml中聲明的activity呢?
hook分析
還是從gDefault這個單例看起,有沒有辦法把這個單例替換掉,自己實現startActivity方法來繞過系統對activity的校驗呢?同時我們看到IActivityManager是一個接口,那么是不是有什么辦法可以動態修改這個接口的實現呢?熟悉Java的同學一定會想到動態代理。我們先來實現一個InvocationHandler,代碼如下
public class HookActivityHandler implements InvocationHandler {
private static final String TAG = "HookActivityHandler";
private Object mBase;
public HookActivityHandler(Object base) {
this.mBase = base;
}
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if ("startActivity".equalsIgnoreCase(method.getName())) {
Log.d(TAG, "invoke: startActivity");
}
return method.invoke(mBase, objects);
}
}
接著,我們需要通過反射將ActivityManagerNative中的gDefault中的gDefault換成我們自己的實現,代碼如下
public static final void hookActivityManagerService(ClassLoader classLoader) {
try {
Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
if (activityManagerNativeClass == null) {
return;
}
Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
if (gDefaultField == null) {
return;
}
gDefaultField.setAccessible(true);
Object gDefault = gDefaultField.get(null);
Class<?> singleton = Class.forName("android.util.Singleton");
if (singleton == null) {
return;
}
Field mInstanceField = singleton.getDeclaredField("mInstance");
if (mInstanceField == null) {
return;
}
mInstanceField.setAccessible(true);
Object activityManager = mInstanceField.get(gDefault);
Class<?> activityManagerInterface = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(classLoader,
new Class[] {activityManagerInterface}, new HookActivityHandler(activityManager));
mInstanceField.set(gDefault, proxy);
} catch (Exception e) {
e.printStackTrace();
}
}
然后新建一個正常activity看看能否正常調用到hook的方法中,輸出日志如下。
08-15 23:10:10.476 13208-13208/com.test.hookactivity D/HookActivityHandler: invoke: startActivity
可以看到,已經成功的將startActivity()方法hook住了,接下來就可以在startActivity的時候做些事情了。在啟動activity的時候,會對activity做校驗,那么我們能不能用一個注冊在AndroidMenifest.xml中的activity作為橋梁,在校驗的時候用這個存在的activity,校驗過后再換會真正想要的activity,來個偷梁換柱。再回到HookActivityHandler中,只看invoke方法。
在startActivity之前先將目標intent取出來并緩存起來,將intent換成已經存在的PlaceHolderActivity,這樣校驗的時候就可以繞過系統對activity的校驗。大家可以試著運行一次代碼,并不會拋出ActivityNotFoundException的異常,驗證了之前的想法。
@Override
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
if ("startActivity".equalsIgnoreCase(method.getName())) {
Log.d(TAG, "invoke: startActivity");
Intent rawIntent = null;
int index = 0;
for (int i = 0; i < objects.length; i++) {
if (objects[i] instanceof Intent) {
index = i;
break;
}
}
rawIntent = (Intent) objects[index];
Intent newIntent = new Intent();
String targetPackageName = "com.test.hookactivity";
ComponentName componentName = new ComponentName(targetPackageName,
PlaceHolderActivity.class.getCanonicalName());
newIntent.setComponent(componentName);
newIntent.putExtra("extra_target_intent", rawIntent);
objects[index] = newIntent;
return method.invoke(mBase, objects);
}
return method.invoke(mBase, objects);
}
那么繞過校驗之后怎樣恢復到我們想要的activity呢?從源碼中可以看到,在啟動activity時最后會通過ActivityThread中的handleMessage()方法,代碼如下
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
...
}
}
這里的handleLaunchActivity()就是啟動activity真正的方法,這里就是另一個hook點,在handleLaunchActivity()中將目標activity的intent替換回來,首先實現Handler.Callback,代碼如下。通過反射講message中的intent換會原來的intent。
public class ActivityThreadHandlerCallback implements Handler.Callback {
private Handler mHandler;
public ActivityThreadHandlerCallback(Handler handler) {
this.mHandler = handler;
}
@Override
public boolean handleMessage(Message message) {
int what = message.what;
switch (what) {
case 100: //這里對應的是LAUNCH_ACTIVITY
handleStartActivity(message);
break;
}
mHandler.handleMessage(message);
return true;
}
private void handleStartActivity(Message message) {
Object object = message.obj;
try {
Field intent = object.getClass().getDeclaredField("intent");
if (intent == null) {
return;
}
intent.setAccessible(true);
Intent rawIntent = (Intent) intent.get(object);
Intent targetIntent = rawIntent.getParcelableExtra("extra_target_intent");
if (targetIntent == null) {
return;
}
rawIntent.setComponent(targetIntent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
還差最后一步,就是將ActivityThread中的mCallback換成我們hook的callback,還是用老辦法——反射。在ActivityThread中handler定義的變量為mH,所以主要替換的目標是mH,代碼如下
public static final void hookActivityThreadHandler() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
if (currentActivityThreadField == null) {
return;
}
currentActivityThreadField.setAccessible(true);
Object currentActivityThread = currentActivityThreadField.get(null);
Field mHField = activityThreadClass.getDeclaredField("mH");
if (mHField == null) {
return;
}
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(currentActivityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
if (mCallbackField == null) {
return;
}
mCallbackField.setAccessible(true);
mCallbackField.set(mH, new ActivityThreadHandlerCallback(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
到此,就神不知鬼不覺的繞過系統的校驗,在啟動的時候就是真正我們想要的頁面了,而這個頁面并沒有在AndroidManifest.xml中注冊。
小結
通過以上的方法,用動態代理分別hook了ActivityManagerNative中的IActivityManager和ActivityThread中handler的callback,用這兩個hook點就成功的繞過了系統的校驗,實現了啟動沒有聲明的activity。看似簡單,但是一定要對activity啟動的流程十分熟悉,還要理解android中的binder機制,這就是android插件化框架用到的原理之一。除了動態代理,利用雙親委派機制也可以實現對相關方法的hook。
有些人也許會問,啟動一個沒有在AndroidManifest.xml中的activity有什么用。的確,正常應用開發的確沒什么用。但是如果有一天你想動態下發一個activity的時候,就有用了。應用發布的時候不可能預埋不存在的activity,當有一天因為產品的變化,需求的變更的時候就只能發版解決了。但是如果可以做到動態下發,就可以靜默對應用進行升級,而新增activity就是基礎能力之一。