1.什么是Hook
Hook英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在Android操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。
Hook的这个本领,使它能够将自身的代码「融入」被勾住(Hook)的程序的进程中,成为目标进程的一个部分。APIHook技术是一种用于改变API执行结果的技术,能够将系统的API函数执行重定向。在Android系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行互不干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是Hook的出现给我们开拓了解决此类问题的道路。当然,根据Hook对象与Hook后处理的事件方式不同,Hook还分为不同的种类,比如消息Hook、APIHook等。
使用Java反射实现APIHook通过对Android平台的虚拟机注入与Java反射的方式,来改变Android虚拟机调用函数的方式(ClassLoader),从而达到Java函数重定向的目的,这里我们将此类操作称为JavaAPIHook。
下面通过HookView的OnClickListener来说明Hook的使用方法。
首先进入View的setOnClickListener方法,我们看到OnClickListener对象被保存在了一个叫做ListenerInfo的内部类里,其中mListenerInfo是View的成员变量。ListeneInfo里面保存了View的各种监听事件,比如OnClickListener、OnLongClickListener、OnKeyListener等等。
publicvoidsetOnClickListener(
NullableOnClickListenerl){if(!isClickable()){setClickable(true);}getListenerInfo().mOnClickListener=l;}ListenerInfogetListenerInfo(){if(mListenerInfo!=null){returnmListenerInfo;}mListenerInfo=newListenerInfo();returnmListenerInfo;}我们的目标是HookOnClickListener,所以就要在给View设置监听事件后,替换OnClickListener对象,注入自定义的操作。
privatevoidhookOnClickListener(Viewview){try{//得到View的ListenerInfo对象MethodgetListenerInfo=View.class.getDeclaredMethod("getListenerInfo");getListenerInfo.setAccessible(true);ObjectlistenerInfo=getListenerInfo.invoke(view);//得到原始的OnClickListener对象Class?listenerInfoClz=Class.forName("android.view.View$ListenerInfo");FieldmOnClickListener=listenerInfoClz.getDeclaredField("mOnClickListener");mOnClickListener.setAccessible(true);View.OnClickListeneroriginOnClickListener=(View.OnClickListener)mOnClickListener.get(listenerInfo);//用自定义的OnClickListener替换原始的OnClickListenerView.OnClickListenerhookedOnClickListener=newHookedOnClickListener(originOnClickListener);mOnClickListener.set(listenerInfo,hookedOnClickListener);}catch(Exceptione){log.warn("hookclickListenerfailed!",e);}}classHookedOnClickListenerimplementsView.OnClickListener{privateView.OnClickListenerorigin;HookedOnClickListener(View.OnClickListenerorigin){this.origin=origin;}
OverridepublicvoidonClick(Viewv){Toast.makeText(MainActivity.this,"hookclick",Toast.LENGTH_SHORT).show();log.info("Beforeclick,dowhatyouwanttoto.");if(origin!=null){origin.onClick(v);}log.info("Afterclick,dowhatyouwanttoto.");}}到这里,我们成功Hook了OnClickListener,在点击之前和点击之后可以执行某些操作,达到了我们的目的。下面是调用的部分,在给Button设置OnClickListener后,执行Hook操作。点击按钮后,日志的打印结果是:Beforeclick→onClick→Afterclick。
ButtonbtnSend=(Button)findViewById(R.id.btn_send);btnSend.setOnClickListener(newView.OnClickListener(){
OverridepublicvoidonClick(Viewv){log.info("onClick");}});hookOnClickListener(btnSend);我们再来看一个很常见的例子startActivity下面我们Hook掉startActivity这个方法,使得每次调用这个方法之前输出一条日志;(当然,这个输入日志有点点弱,只是为了展示原理,如果你想可以替换参数,拦截这个startActivity过程,使得调用它导致启动某个别的Activity,指鹿为马!)我们知道对于Context.startActivity,Context的实现实际上是ContextImpl;我们看ConetxtImpl类的startActivity方法:
OverridepublicvoidstartActivity(Intentintent,Bundleoptions){warnIfCallingFromSystemProcess();if((intent.getFlags()Intent.FLAG_ACTIVITY_NEW_TASK)==0){thrownewAndroidRuntimeException("CallingstartActivity()fromoutsideofanActivity"+"contextrequirestheFLAG_ACTIVITY_NEW_TASKflag."+"Isthisreallywhatyouwant?");}mMainThread.getInstrumentation().execStartActivity(getOuterContext(),mMainThread.getApplicationThread(),null,(Activity)null,intent,-1,options);}
这里,实际上使用了ActivityThread类的mInstrumentation成员的execStartActivity方法;注意到,ActivityThread实际上是主线程,而主线程一个进程只有一个,因此这里是一个良好的Hook点。
接下来就是想要Hook掉我们的主线程对象,也就是把这个主线程对象里面的mInstrumentation给替换成我们修改过的代理对象;要替换主线程对象里面的字段,首先我们得拿到主线程对象的引用,如何获取呢?ActivityThread类里面有一个静态方法currentActivityThread可以帮助我们拿到这个对象类;但是ActivityThread是一个隐藏类,我们需要用反射去获取,代码如下:
//先获取到当前的ActivityThread对象Class?activityThreadClass=Class.forName("android.app.ActivityThread");MethodcurrentActivityThreadMethod=activityThreadClass.getDeclaredMethod("currentActivityThread");currentActivityThreadMethod.setAccessible(true);ObjectcurrentActivityThread=currentActivityThreadMethod.invoke(null);
拿到这个currentActivityThread之后,我们需要修改它的mInstrumentation这个字段为我们的代理对象,我们先实现这个代理对象,由于JDK动态代理只支持接口,而这个Instrumentation是一个类,没办法,我们只有手动写静态代理类,覆盖掉原始的方法即可。(cglib可以做到基于类的动态代理,这里先不介绍)
publicclassEvilInstrumentationextendsInstrumentation{privatestaticfinalStringTAG="EvilInstrumentation";//ActivityThread中原始的对象,保存起来InstrumentationmBase;publicEvilInstrumentation(Instrumentationbase){mBase=base;}publicActivityResultexecStartActivity(Contextwho,IBindercontextThread,IBindertoken,Activitytarget,Intentintent,intrequestCode,Bundleoptions){//Hook之前,XXX到此一游!Log.d(TAG,"\n执行了startActivity,参数如下:\n"+"who=["+who+"],"+"\ncontextThread=["+contextThread+"],\ntoken=["+token+"],"+"\ntarget=["+target+"],\nintent=["+intent+"],\nrequestCode=["+requestCode+"],\noptions=["+options+"]");//开始调用原始的方法,调不调用随你,但是不调用的话,所有的startActivity都失效了.//由于这个方法是隐藏的,因此需要使用反射调用;首先找到这个方法try{MethodexecStartActivity=Instrumentation.class.getDeclaredMethod("execStartActivity",Context.class,IBinder.class,IBinder.class,Activity.class,Intent.class,int.class,Bundle.class);execStartActivity.setAccessible(true);return(ActivityResult)execStartActivity.invoke(mBase,who,contextThread,token,target,intent,requestCode,options);}catch(Exceptione){//某该死的rom修改了需要手动适配thrownewRuntimeException("donotsupport!!!plsadaptit");}}}
Ok,有了代理对象,我们要做的就是偷梁换柱!代码比较简单,采用反射直接修改:
publicstaticvoidattactContext()throwsException{//先获取到当前的ActivityThread对象Class?activityThreadClass=Class.forName("android.app.ActivityThread");FieldcurrentActivityThreadField=activityThreadClass.getDeclaredField("sCurrentActivityThread");currentActivityThreadField.setAccessible(true);ObjectcurrentActivityThread=currentActivityThreadField.get(null);//拿到原始的mInstrumentation字段FieldmInstrumentationField=activityThreadClass.getField("mInstrumentation");mInstrumentationField.setAccessible(true);InstrumentationmInstrumentation=(Instrumentation)mInstrumentationField.get(currentActivityThread);//创建代理对象InstrumentationevilInstrumentation=newEvilInstrumentation(mInstrumentation);//偷梁换柱mInstrumentationField.set(currentActivityThread,evilInstrumentation);}
好了,我们启动一个Activity测试一下,结果如下:
总结一下:
Hook过程:寻找Hook点,原则是静态变量或者单例对象,尽量Hookpublic的对象和方法。选择合适的代理方式,如果是接口可以用动态代理。偷梁换柱——用代理对象替换原始对象。Android的API版本比较多,方法和类可能不一样,所以要做好API的兼容工作。
举个例子Android10后添加了ActivityTaskManager
intresult=ActivityTaskManager.getService().startActivity(whoThread,who.getBasePackageName(),who.getAttributionTag(),intent,intent.resolveTypeIfNeeded(who.getContentResolver()),token,target!=null?target.mEmbeddedID:null,requestCode,0,null,options);
intresult=ActivityManagerNative.getDefault().startActivity(whoThread,who.getBasePackageName(),intent,intent.resolveTypeIfNeeded(who.getContentResolver()),token,target!=null?target.mEmbeddedID:null,requestCode,0,null,null,options);
2.Xposed
通过替换/system/bin/app_process程序控制Zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个Jar包,从而完成对Zygote进程及其创建的Dalvik虚拟机的劫持。Xposed在开机的时候完成对所有的HookFunction的劫持,在原Function执行的前后加上自定义代码。
现在安装Xposed比较方便,因为Xposed作者开发了一个XposedInstallerApp,下载后按照提示傻瓜式安装(前提是root手机)。其实它的安装过程是这个样子的:首先探测手机型号,然后按照手机版本下载不同的刷机包,最后把Xposed刷机包刷入手机重启就好。刷机包下载里面有所有版本的刷机包。刷机包解压打开里面的问件构成是这个样子的:
META-INF/里面有文件配置脚本flash-script.sh配置各个文件安装位置。system/bin/替换zygote进程等文件system/framework/XposedBridge.jarjar包位置system/libsystem/lib64一些so文件所在位置xposed.propxposed版本说明文件
所以安装Xposed的过程就上把上面这些文件放到手机里相同文件路径下。通过查看文件安装脚本发现:system/bin/下面的文件替换了app_process等文件,app_process就是zygote进程文件。所以Xposed通过替换zygote进程实现了控制手机上所有app进程。因为所有app进程都是由Zygotefork出来的。Xposed的基本原理是修改了ART/Davilk虚拟机,将需要hook的函数注册为Native层函数。当执行到这一函数是虚拟机会优先执行Native层函数,然后再去执行Java层函数,这样完成函数的hook。如下图:
通过读Xposed源码发现其启动过程:
手机启动时init进程会启动zygote这个进程。由于zygote进程文件app_process已被替换,所以启动的时Xposed版的zygote进程。
Xposed_zygote进程启动后会初始化一些so文件(system/libsystem/lib64),然后进入XposedBridge.jar中的XposedBridge.main中初始化jar包完成对一些关键Android系统函数的hook。
Hook则是利用修改过的虚拟机将函数注册为native函数。
然后再返回zygote中完成原本zygote需要做的工作。这只是在宏观层面稍微介绍了下Xposed,要想详细了解需要读它的源码了。