前言:在前兩篇文章中分別介紹了動態代理、反射機制和Hook機制,如果對這些還不太了解的童鞋建議先去參考一下前兩篇文章。經過了前面兩篇文章的鋪墊,終於可以玩點真刀實彈的了,本篇將會通過 Hook 掉 startActivity 方法的一個小例子來介紹如何找出合適的 Hook 切入點。 開始之前我們需要知道的一點就是,其實在 Android 里面啟動一個 Activity 可以通過兩種方式實現,一種是我們常用的調用 Activity.startActivity 方法,一種是調用 Context.startActivity 方法,兩種方法相比之下, 第一種啟動Activity的方式更為簡單,所以先以第一種為例。
本系列文章的代碼已經上傳至github,下載地址:https://github.com/lgliuwei/DroidPluginStudy 本篇文章對應的代碼在 com.liuwei.proxy_hook.hook.activityhook 包內,下載下來對照代碼看文章效果會更好!
一、Hook 掉 Activity 的 startActivity 的方法
在 Hook Activity 的 startActivity 方法之前,我們首先明確一下我們的目標,我們先通過追蹤源碼找出 startActivity 調用的真正起作用的方法,然后想辦法把目標方法攔截掉,並輸出我們的一條 Log 信息。
我們先來一步步分析 startActivity 的源碼,隨手寫一個 startActivity 的示例,按住 command 鍵( windows 下按住 control )用鼠標點擊 startActivity的方法即可跳轉到方法里面。
startActivity(Intent intent) 源碼如下:
1 public void startActivity(Intent intent) { 2 this.startActivity(intent, null); 3 }
接着看 this.startActivity(intent, null) 方法源碼:
1 public void startActivity(Intent intent, @Nullable Bundle options) { 2 if (options != null) { 3 startActivityForResult(intent, -1, options); 4 } else { 5 // Note we want to go through this call for compatibility with 6 // applications that may have overridden the method. 7 startActivityForResult(intent, -1); 8 } 9 }
從上一步傳入的參數 options 為 null 我們就可以知道這一步調用了 startActivityForResult(intent, -1) 的代碼。
startActivityForResult(Intent intent, int requestCode) 源碼如下:
1 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) { 2 startActivityForResult(intent, requestCode, null); 3 }
startActivityForResult(Intent intent, int requestCode, Bundle options) 源碼如下:
1 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, 2 @Nullable Bundle options) { 3 if (mParent == null) { 4 options = transferSpringboardActivityOptions(options); 5 Instrumentation.ActivityResult ar = 6 mInstrumentation.execStartActivity( 7 this, mMainThread.getApplicationThread(), mToken, this, 8 intent, requestCode, options); 9 if (ar != null) { 10 mMainThread.sendActivityResult( 11 mToken, mEmbeddedID, requestCode, ar.getResultCode(), 12 ar.getResultData()); 13 } 14 if (requestCode >= 0) { 15 // If this start is requesting a result, we can avoid making 16 // the activity visible until the result is received. Setting 17 // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the 18 // activity hidden during this time, to avoid flickering. 19 // This can only be done when a result is requested because 20 // that guarantees we will get information back when the 21 // activity is finished, no matter what happens to it. 22 mStartedActivity = true; 23 } 24 25 cancelInputsAndStartExitTransition(options); 26 // TODO Consider clearing/flushing other event sources and events for child windows. 27 } else { 28 if (options != null) { 29 mParent.startActivityFromChild(this, intent, requestCode, options); 30 } else { 31 // Note we want to go through this method for compatibility with 32 // existing applications that may have overridden it. 33 mParent.startActivityFromChild(this, intent, requestCode); 34 } 35 } 36 }
到這一步我們已經看到了關鍵點,注意上面代碼塊中紅色的代碼,其實 startActivity 真正調用的是 mInstrumentation.execStartActivity(...) 方法,mInstrumentation 是 Activity 的一個私有變量。接下來的任務將變得非常簡單,回憶一下上一篇博文《小白也能看懂插件化DroidPlugin原理(二)-- 反射機制和Hook入門》中的方案一,在替換汽車引擎時我們繼承原來的汽車引擎類創建了一個新類,然后在新引擎類中攔截了最大速度的方法,這里的思路是一樣的,我們直接新建一個繼承 Instrumentation 的新類,然后重寫 execStartActivity() 。對此有不明白的童鞋建議再看一遍上一篇博文《小白也能看懂插件化DroidPlugin原理(二)-- 反射機制和Hook入門》。代碼如下:
1 public class EvilInstrumentation extends Instrumentation { 2 private Instrumentation instrumentation; 3 public EvilInstrumentation(Instrumentation instrumentation) { 4 this.instrumentation = instrumentation; 5 } 6 public ActivityResult execStartActivity( 7 Context who, IBinder contextThread, IBinder token, Activity target, 8 Intent intent, int requestCode, Bundle options) { 9 Logger.i(EvilInstrumentation.class, "請注意! startActivity已經被hook了!"); 10 try { 11 Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity", Context.class, 12 IBinder.class, IBinder.class, Activity.class, 13 Intent.class, int.class, Bundle.class); 14 return (ActivityResult)execStartActivity.invoke(instrumentation, who, contextThread, token, target, 15 intent, requestCode, options); 16 } catch (Exception e) { 17 e.printStackTrace(); 18 } 19 20 return null; 21 } 22 }
重寫工作已經做完了,接着我們通過反射機制用新建的 EvilInstrumentation 替換掉 Activity 的 mInstrumentation 變量,具體代碼如下:
1 public static void doActivityStartHook(Activity activity){ 2 try { 3 Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation"); 4 mInstrumentationField.setAccessible(true); 5 Instrumentation originalInstrumentation = (Instrumentation)mInstrumentationField.get(activity); 6 mInstrumentationField.set(activity, new EvilInstrumentation(originalInstrumentation)); 7 } catch (Exception e) { 8 e.printStackTrace(); 9 } 10 }
這對於我們來說已經很是輕車熟路了,很快就寫完了,然后我們在 Activity 的 onCreate() 方法中需要調用一下 doActivityStartHook 即可完成對 Activity.startActivity 的 hook。MainActivity 的代碼如下:
1 public class MainActivity extends Activity { 2 private Button btn_start_by_activity; 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 super.onCreate(savedInstanceState); 6 setContentView(R.layout.activity_main); 7 // hook Activity.startActivity()的方法時不知道這行代碼為什么放在attachBaseContext里面不行? 8 // 調試發現,被hook的Instrumentation后來又會被替換掉原來的。 10 ActivityThreadHookHelper.doActivityStartHook(this); 11 btn_start_by_activity = (Button) findViewById(R.id.btn_start_by_activity); 12 btn_start_by_activity.setOnClickListener(new View.OnClickListener() { 13 @Override 14 public void onClick(View v) { 15 Intent intent = new Intent(MainActivity.this, OtherActivity.class); 16 startActivity(intent); 17 } 18 }); 19 } 20 }
程序運行之后,點擊啟動 Activity 的按鈕將輸出以下 Log:
[EvilInstrumentation] : 請注意! startActivity已經被hook了!
到此為止我們已經 hook 了 Activity 的 startActivity 方法,非常簡單,代碼量也很少,但我們也很輕易的發現這種方法需要在每一個 Activity 的 onCreate 方法里面調用一次 doActivityStartHook 方法,顯然這不是一個好的方案,所以我們在尋找 hook 點時一定要注意盡量找一些在進程中保持不變或不容易被改變的變量,就像單例和靜態變量。
問題1:在這里有一點值得一提,我們將 doActivityStartHook(...) 方法的調用如果放到 MainActivity 的 attachBaseContext(...) 方法中替換工作將不會生效,為什么?
調試發現,我們在 attachBaseContext(..) 里面執行完畢 doActivityStartHook(...) 方法后確實將 Activity 的 mInstrumentation 變量換成了我們自己的 EvilInstrumentation,但程序執行到 onCreate() 方法后就會發現這時候 mInstrumentation 變成了系統自己的 Instrumentation 對象了。這時候我們可以確信的是 mInstrumentation 變量一定是在 attachBaseContext() 之后被初始化或者賦值的。帶着這個目標我們很輕松就在 Activity 源碼的 attach() 方法中找到如下代碼:
Activity.attach() 的源碼如下(注意第8行和第26行):
1 final void attach(Context context, ActivityThread aThread, 2 Instrumentation instr, IBinder token, int ident, 3 Application application, Intent intent, ActivityInfo info, 4 CharSequence title, Activity parent, String id, 5 NonConfigurationInstances lastNonConfigurationInstances, 6 Configuration config, String referrer, IVoiceInteractor voiceInteractor, 7 Window window) { 8 attachBaseContext(context); 9 10 mFragments.attachHost(null /*parent*/); 11 12 mWindow = new PhoneWindow(this, window); 13 mWindow.setWindowControllerCallback(this); 14 mWindow.setCallback(this); 15 mWindow.setOnWindowDismissedCallback(this); 16 mWindow.getLayoutInflater().setPrivateFactory(this); 17 if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) { 18 mWindow.setSoftInputMode(info.softInputMode); 19 } 20 if (info.uiOptions != 0) { 21 mWindow.setUiOptions(info.uiOptions); 22 } 23 mUiThread = Thread.currentThread(); 24 25 mMainThread = aThread; 26 mInstrumentation = instr; 27 mToken = token; 28 mIdent = ident; 29 mApplication = application; 30 mIntent = intent; 31 mReferrer = referrer; 32 mComponent = intent.getComponent(); 33 mActivityInfo = info; 34 mTitle = title; 35 mParent = parent; 36 mEmbeddedID = id; 37 mLastNonConfigurationInstances = lastNonConfigurationInstances; 38 if (voiceInteractor != null) { 39 if (lastNonConfigurationInstances != null) { 40 mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor; 41 } else { 42 mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this, 43 Looper.myLooper()); 44 } 45 } 46 47 mWindow.setWindowManager( 48 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), 49 mToken, mComponent.flattenToString(), 50 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0); 51 if (mParent != null) { 52 mWindow.setContainer(mParent.getWindow()); 53 } 54 mWindowManager = mWindow.getWindowManager(); 55 mCurrentConfig = config; 56 }
至此,問題1算是找到了答案。
二、Hook 掉 Context 的 startActivity 的方法
文章開頭我們就說 Android 中有個兩種啟動 Activity 的方式,一種是 Activity.startActivity 另一種是 Context.startActivity,但需要注意的時,我們在使用 Context.startActivity 啟動一個 Activity 的時候將 flags 指定為 FLAG_ACTIVITY_NEW_TASK。
在接下來的分析中需要查看 Android 源碼,先推薦兩個查看 Android 源碼的網站:
http://grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/
我們試着 hook 掉 Context.startActivity 方法,我們依然隨手寫一個 Context 方式啟動 Activity 的示例,如下:
1 Intent intent = new Intent(MainActivity.this, OtherActivity.class); 2 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 3 getApplicationContext().startActivity(intent);
照着(一)中的姿勢點入 startActivity() 方法里面,由於 Context 是一個抽象類,所以我們需要找到它的實現類才能看到具體的代碼,通過查看 Android 源碼我們可以在 ActivityTread 中可知 Context 的實現類是 ContextImpl。(在這里大家先知道這一點就行,具體的調用細節將會在下一篇博文中詳細介紹)
源碼地址:
1 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { 2 ... 3 if (activity != null) { 4 Context appContext = createBaseContextForActivity(r, activity); 5 CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); 6 Configuration config = new Configuration(mCompatConfiguration); 7 ... 8 } 9 ... 10 private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) { 11 ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token); 12 appContext.setOuterContext(activity); 13 Context baseContext = appContext; 14 ... 15 }
現在我們來查看 ContextImpl.startActivity() 的源碼。
源碼地址:
1 @Override 2 public void startActivity(Intent intent) { 3 warnIfCallingFromSystemProcess(); 4 startActivity(intent, null); 5 }
再進入 startActivity(intent, null) 查看源碼如下:
1 @Override 2 public void startActivity(Intent intent, Bundle options) { 3 warnIfCallingFromSystemProcess(); 4 if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) { 5 throw new AndroidRuntimeException( 6 "Calling startActivity() from outside of an Activity " 7 + " context requires the FLAG_ACTIVITY_NEW_TASK flag." 8 + " Is this really what you want?"); 9 } 10 mMainThread.getInstrumentation().execStartActivity( 11 getOuterContext(), mMainThread.getApplicationThread(), null, 12 (Activity)null, intent, -1, options); 13 }
由上面第四行代碼可以看出在代碼中判斷了 intent 的 flag 類型,如果非 FLAG_ACTIVITY_NEW_TASK 類型就會拋出異常。接着看紅色部分的關鍵代碼,可以看出先從 ActivityTread 中獲取到了 Instrumentation 最后還是調用了 Instrumentation 的 execStartActivity(...) 方法,我們現在需要做的就是分析 ActivityTread 類,並想辦法用我們自己寫的 EvilInstrumentation 類將 ActivityTread 的 mInstrumentation 替換掉。
源碼地址:
ActivityTread 部分代碼如下:
206 private static ActivityThread sCurrentActivityThread; 207 Instrumentation mInstrumentation; ... 1597 public static ActivityThread currentActivityThread() { 1598 return sCurrentActivityThread; 1599 } ... 1797 public Instrumentation getInstrumentation() 1798 { 1799 return mInstrumentation; 1800 }
這里需要告訴大家是,ActivityTread 即代表應用的主線程,而一個應用中只有一個主線程,並且由源碼可知,ActivityTreadd 的對象又是以靜態變量的形式存在的,太好了,這正是我們要找的 Hook 點。廢話不多說了,現在我們只需利用反射通過 currentActivityThread() 方法拿到 ActivityThread 的對象,然后在將 mInstrumentation 替換成 EvilInstrumentation 即可,代碼如下:
1 public static void doContextStartHook(){ 2 try { 3 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); 4 Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread"); 5 Object activityThread = currentActivityThreadMethod.invoke(null); 6 7 Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation"); 8 mInstrumentationField.setAccessible(true); 9 Instrumentation originalInstrumentation = (Instrumentation)mInstrumentationField.get(activityThread); 10 mInstrumentationField.set(activityThread, new EvilInstrumentation(originalInstrumentation)); 11 } catch (Exception e) { 12 e.printStackTrace(); 13 } 14 }
其實代碼也不難理解,跟 Hook Activity 的 startActivity() 方法是一個思路,只是 Hook 的點不同而已。下面我們在 MainActivity 的 attachBaseContext() 方法中調用 doContextStartHook() 方法,並添加相關測試代碼,具體代碼如下:
1 public class MainActivity extends Activity { 2 private Button btn_start_by_context; 3 @Override 4 protected void onCreate(Bundle savedInstanceState) { 5 super.onCreate(savedInstanceState); 6 setContentView(R.layout.activity_main); 7 btn_start_by_context = (Button) findViewById(R.id.btn_start_by_context); 8 btn_start_by_context.setOnClickListener(new View.OnClickListener() { 9 @Override 10 public void onClick(View v) { 11 Intent intent = new Intent(MainActivity.this, OtherActivity.class); 12 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 13 getApplicationContext().startActivity(intent); 14 } 15 }); 16 } 17 @Override 18 protected void attachBaseContext(Context newBase) { 19 super.attachBaseContext(newBase); 20 ActivityThreadHookHelper.doContextStartHook(); 21 } 22 }
點擊按鈕后查看 Log 輸出如下:
[EvilInstrumentation] : 請注意! startActivity已經被hook了!
看到這樣的 Log,說明我們已經成功的 Hook 了 Context.startActivity()。而且 doContextStartHook() 方法只在程序開始的時候調用一次即可,后面在程序其他的 Activity 中調用 Context.startActivity() 時此攔截工作均可生效,這是因為 Context.startActivity() 在執行啟動 Activity 的操作時調是通過 ActivityTread 獲取到 Instrumentation,然后再調用 Instrumentation.execStartActivity() 方法,而 ActivityTread 在程序中是以單例的形式存在的,這就是原因。所以說調用 doContextStartHook() 方法最好的時機應該是放在 Application 中。
注意!前方驚現彩蛋一枚!!
將 doContextStartHook() 方法放入到了 MyApplication 的 attachBaseContext() 里面后,代碼如下:
1 public class MyApplication extends Application { 2 @Override 3 protected void attachBaseContext(Context base) { 4 super.attachBaseContext(base); 5 ActivityThreadHookHelper.doContextStartHook(); 6 } 7 }
MainActivity 的代碼如下:
1 public class MainActivity extends Activity { 2 private final static String TAG = MainActivity.class.getSimpleName(); 3 private Button btn_start_by_activity; 4 private Button btn_start_by_context; 5 @Override 6 protected void onCreate(Bundle savedInstanceState) { 7 super.onCreate(savedInstanceState); 8 setContentView(R.layout.activity_main); 9 btn_start_by_activity = (Button) findViewById(R.id.btn_start_by_activity); 10 btn_start_by_context = (Button) findViewById(R.id.btn_start_by_context); 11 ActivityThreadHookHelper.doActivityStartHook(this); 12 btn_start_by_activity.setOnClickListener(new View.OnClickListener() { 13 @Override 14 public void onClick(View v) { 15 Log.i(TAG, "onClick: Activity.startActivity()"); 16 Intent intent = new Intent(MainActivity.this, OtherActivity.class); 17 startActivity(intent); 18 } 19 }); 20 21 btn_start_by_context.setOnClickListener(new View.OnClickListener() { 22 @Override 23 public void onClick(View v) { 24 Log.i(TAG, "onClick: Context.startActivity()"); 25 Intent intent = new Intent(MainActivity.this, OtherActivity.class); 26 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 27 getApplicationContext().startActivity(intent); 28 } 29 }); 30 } 31 }
代碼如上,布局文件很簡單就不貼出來了,就是兩個按鈕,一個測試 Activity.startActivity() 方法,一個測試 Context.startActivity() 方法,然后在 MainActivity 的 onCreate() 中調用了 doActivityStartHook() 在 MyApplication 里面調用了 doContextStartHook(), 目前看來代碼很正常,符合我們上面的思路,但樓主在點擊按鈕發現 Log 輸出如下:
是的,Activity.startActivity 被 hook 的信息輸出了兩次!為什么?
我們不妨先猜想一下,一定是 Activity 的 mInstrumentation 對象在我們替換之前就已經變成了 EvilInstrumentation, 然后我們又在 Activity.onCreate 方法調用了一次 doActivityStartHook(), 相當於我們又用 EvilInstrumentation 又重寫了 EvilInstrumentation 的 startActivity() 方法,所以導致 log 信息輸出了兩次。
那問題又來了,為什么 Activity 的 mInstrumentation 對象在我們替換之前就已經變成了 EvilInstrumentation?
縱觀代碼,只有一個地方有疑點,那就是我們放到 MyApplication.attachBaseContext() 方法里面的 doContextStartHook() 起的作用!
還是先直接簡單說一下事實的真相吧,結合上文所說,一個應用內只存在一個 ActivityTread 對象,也只存在一個 Instrumentation 對象,這個 Instrumentation 是 ActivityTread 的成員變量,並在 ActivityTread 內完成初始化,在啟動一個 Activity 的流程中大概在最后的位置 ActivityTread 會回調 Activity 的 attach() 方法,並將自己的 Instrumentation 對象傳給 Activity。啟動 Activity 的詳細流程及調用細節將會在下一篇博文介紹,敬請期待!
三、小結
本篇文章通過攔截 Context.startActivity() 和 Activity.startActivity() 兩個方法,將上一篇文章中介紹的 Hook 技術實踐 Activity 的啟動流程之中,同時通過這兩個小例子初步了解了 Android 源碼以及怎么樣去選定一個合適的 Hook 點。想要了解插件化的基本原理,熟悉 Activity 的啟動流程是必不可少的,下一篇文章將會詳細介紹 Activity 的啟動流程,感興趣的同學可以關注一下!
參考文章