前言:在前两篇文章中分别介绍了动态代理、反射机制和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://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 的详细流程及调用细节将会在下一篇博文介绍,敬请期待!
3、小结
本篇文章经过拦截 Context.startActivity() 和 Activity.startActivity() 两个方法,将上一篇文章中介绍的 Hook 技术实践 Activity 的启动流程之中,同时经过这两个小例子初步了解了 Android 源码以及怎么样去选定一个合适的 Hook 点。想要了解插件化的基本原理,熟悉 Activity 的启动流程是必不可少的,下一篇文章将会详细介绍 Activity 的启动流程,感兴趣的同窗能够关注一下!
参考文章