小白也能看懂的插件化DroidPlugin原理(三)-- 如何攔截startActivity方法

  前言:在前兩篇文章中分別介紹了動態代理、反射機制和Hook機制,若是對這些還不太瞭解的童鞋建議先去參考一下前兩篇文章。通過了前面兩篇文章的鋪墊,終於能夠玩點真刀實彈的了,本篇將會經過 Hook 掉 startActivity 方法的一個小例子來介紹如何找出合適的 Hook 切入點。 開始以前咱們須要知道的一點就是,其實在 Android 裏面啓動一個 Activity 能夠經過兩種方式實現,一種是咱們經常使用的調用 Activity.startActivity 方法,一種是調用 Context.startActivity 方法,兩種方法相比之下, 第一種啓動Activity的方式更爲簡單,因此先以第一種爲例。html

  本系列文章的代碼已經上傳至github,下載地址:https://github.com/lgliuwei/DroidPluginStudy 本篇文章對應的代碼在 com.liuwei.proxy_hook.hook.activityhook 包內,下載下來對照代碼看文章效果會更好!java

1、Hook 掉 Activity 的 startActivity 的方法android

  在 Hook Activity 的 startActivity 方法以前,咱們首先明確一下咱們的目標,咱們先經過追蹤源碼找出 startActivity 調用的真正起做用的方法,而後想辦法把目標方法攔截掉,並輸出咱們的一條 Log 信息。git

  咱們先來一步步分析 startActivity 的源碼,隨手寫一個 startActivity 的示例,按住 command 鍵( windows 下按住 control )用鼠標點擊 startActivity的方法便可跳轉到方法裏面。github

  startActivity(Intent intent) 源碼以下:windows

1 public void startActivity(Intent intent) {
2         this.startActivity(intent, null);
3 }

  接着看 this.startActivity(intent, null) 方法源碼:app

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) 的代碼。ide

  startActivityForResult(Intent intent, int requestCode) 源碼以下:oop

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算是找到了答案。

2、Hook 掉 Context 的 startActivity 的方法

  文章開頭咱們就說 Android 中有個兩種啓動 Activity 的方式,一種是 Activity.startActivity 另外一種是 Context.startActivity,但須要注意的時,咱們在使用 Context.startActivity 啓動一個 Activity 的時候將 flags 指定爲 FLAG_ACTIVITY_NEW_TASK。

  在接下來的分析中須要查看 Android 源碼,先推薦兩個查看 Android 源碼的網站:

  http://androidxref.com

  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。(在這裏你們先知道這一點就行,具體的調用細節將會在下一篇博文中詳細介紹)

  源碼地址:

  http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/app/ActivityThread.java#2338

 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() 的源碼。 

  源碼地址:

  http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/app/ContextImpl.java#ContextImpl.startActivity%28android.content.Intent%29

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 替換掉。

  源碼地址:

  http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.1.1_r1/android/app/ActivityThread.java#ActivityThread.0sCurrentActivityThread

  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 的詳細流程及調用細節將會在下一篇博文介紹,敬請期待!

3、小結

  本篇文章經過攔截 Context.startActivity() 和 Activity.startActivity() 兩個方法,將上一篇文章中介紹的 Hook 技術實踐 Activity 的啓動流程之中,同時經過這兩個小例子初步瞭解了 Android 源碼以及怎麼樣去選定一個合適的 Hook 點。想要了解插件化的基本原理,熟悉 Activity 的啓動流程是必不可少的,下一篇文章將會詳細介紹 Activity 的啓動流程,感興趣的同窗能夠關注一下!

參考文章

  《Android插件化原理解析——Hook機制之動態代理》

  《Android應用程序的Activity啓動過程簡要介紹和學習計劃》

本文連接:http://www.cnblogs.com/codingblock/p/6666239.html

相關文章
相關標籤/搜索