百度百科解释:hook技术是一种改变api执行结果的技术,Microsoft 自身也在Windows操作系统里面使用了这个技术,如Windows兼容模式等。 API HOOK 技术并不是计算机病毒专有技术,但是计算机病毒经常使用这个技术来达到隐藏自己的目的。
通俗的讲,hook其实就是一个钩子函数,在Android中,所有事件分发运行都有自己的一套机制,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步走下去。而钩子的意思就是在事件传送到终点前截取事件并监控事件的传输,就像钩子勾上事件一样,并且能在勾上事件时候。处理一些自己需要额外自定义的事件。
hook的这个本领,使它能够将自身的代码融入到被执行进程中,并成为其中一部分。
hook原理:hook技术无论是对于安全软件还是恶意软件都是一项非常关键的技术,其本质就是劫持函数调用,但是由于处于linux用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要的hook进程空间,修改其内存中的进程代码,替换其过程表的符号地址。
Hook技术的难点,并不在于Hook技术,初学者借助于资料“照葫芦画瓢”能够很容易就掌握Hook的基本使用方法。如何找到函数的入口点、替换函数,这就涉及了理解函数的连接与加载机制。
hook技术的三要素:
1>现有功能
2>目标功能
3>技术替换
实现hook技术的途径:
1>利用系统内部提供的接口,通过实现该接口,然后注入进系统。(并不通用)
2>动态代理。(所有场景)
好了,今天我们是想要通过hook技术实现清单免注册activity跳转。
我们知道安卓中activity的跳转是通过startActivity(intent);这个函数,所以我们可以肯定的是这个函数肯定是作为hook的切入点进行事件的拦截的,所以现在我们就来看一下startActivity();的源码,寻找hook拦截的方法。
//ContextWrapper.java
public class ContextWrapper extends Context {
Context mBase;
...省略其他代码
@Override
public void startActivity(Intent intent) {
mBase.startActivity(intent);
}
...省略其他代码
}
可以看到,startActivity()调用的是ContextWrapper里面的statactivity方法,可以看到里面有个mBase属性对象。在调用startActivity的时候会调用mBase的startActivity方法。这个mBase就是ContextImpl.java
现在来分析ContextImpl.java
我们看看这个类的startActivity的方法的实现
//ContextImpl.java
@Override
public void startActivity(Intent intent) {
warnIfCallingFromSystemProcess();
startActivity(intent, null);
}
直接看startActivity(intent, null);方法,前一个方法是用于进程检测的。
@Override
public void startActivity(Intent intent, Bundle options) {
//...省略代码 只看方法的重点
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}
可以看到此方法调用 mMainThread.getInstrumentation()获取一个对象然后调用对象的方法啊execStartActivity。其中 mMainThread对象的声明为final ActivityThread mMainThread;
mMainThread.getInstrumentation()查看实现如下,Instrumentation类提供的各种流程控制方法,它的作用可以把测试包和目标测试应用加载到同一个进程中运行,不过它不是我们今天hook技术实现的关键点,这里不做讨论。
//ActivtyThread.java
public Instrumentation getInstrumentation()
{
return mInstrumentation;
}
所以我们继续查看Instrumentation 对象的execStartActivity方法
//Instrumentation.java
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {
//...省略方法其他方法
int result = ActivityManagerNative.getDefault().startActivity(一堆参数);
//...
return null;
}
我们看到ActivityManagerNative.getDefault()获取一个对象然后调用startActivity(),
ActivityManagerNative.getDefault()方法探究
//ActivityManagerNative.java
static public IActivityManager getDefault() {
return gDefault.get();
}
gDefault是ActivityManagerNative.java属性对象,声明如下:
//ActivityManagerNative.java
private static final Singleton gDefault = new Singleton() {
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;
}
};
Singleton又是什么?这个是一个单例工具类,当你第一次调用此类的get()会回调使用则自己实现的抽象方法create()进而进行单例操作
//Singleton.java
package android.util;
public abstract class Singleton {
private T mInstance;
protected abstract T create();
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
从上面的例子可以看到,看到ActivityManagerNative.getDefault()返回了远程服务对象IActivityManager 接口。
再回头看看Instrumentation 对象的execStartActivity方法
//Instrumentation.java
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {
//...省略方法其他方法
int result = ActivityManagerNative.getDefault().startActivity(一堆参数);
//...
return null;
}
可以得出以下结论:获取一个远程服务对象接口IActivityManager(并且是单例)然后调用IActivityManager的startActivity方法。此时会回调到远程服务ActivityManagerServer的真正startActivity接口方法。里面会检查你传入的Intent对象的Activity是否在清单内,如果你的activity不在清单内,就是一个异常.但是我们不需要再看ActivityManagerServer源码,因为它是另一个app进程,所以无法干涉。如果可以干涉的话,Android系统就太不稳定了,所以我们无法hook到远程服务,但是IActivityManager我们却是可以用手脚的。因为那是服务端存在客户端一个代理接口对象,这一块需要大家简单了解下AIDL的知识。
也就是说我们可以通过hook技术对IActivityManager动一些手脚,在IActivityManager把我们intent传递给AMS检测之前,我们拦截这个函数,并且把intent设置成一个中间合法的ProxyInent对象,然后在通过AMS检测后,我们再把它替换回来我们自己非法的Intent,实现偷梁换柱。
如图:
我在网上找到一个startActivity的加载时序图,
我们仔细看上面的图IActivityManager调用的时候会把要启动的Activity的Intent去给ActivityManagerServer,假设我们此时用动态代理在IActivityManager调用远程服务之前,把在一个在清单文件注册过的Activity的Intent替换不就通过校验了。
所以hook其实我们已经找到了,就是IActivityManager里面的startActivity方法,在它把startActivity方法丢给ActivityManagerServer之前我们偷偷把非合法intent替换成合法intent,检测通过后再偷偷换回来,事情就搞定了。
具体代码如下,这里面用到java反射机制和Proxy动态代理类,
首先定义一个HoolUtil.java类
public void hookStartActivity(Context context) {
//还原 gDefault 成员变量 反射 调用一次
this.context = context;
try {
Class> ActivityManagerNativecls=Class.forName("android.app.ActivityManagerNative");
Field gDefault = ActivityManagerNativecls.getDeclaredField("gDefault");
gDefault.setAccessible(true);
//因为是静态变量 所以获取的到的是系统值 hook 伪hook
Object defaltValue=gDefault.get(null);
//mInstance对象
Class> SingletonClass=Class.forName("android.util.Singleton");
Field mInstance = SingletonClass.getDeclaredField("mInstance");
//还原 IActivityManager对象 系统对象
mInstance.setAccessible(true);
Object iActivityManagerObject=mInstance.get(defaltValue);
Class> IActivityManagerIntercept = Class.forName("android.app.IActivityManager");
startActivty startActivtyMethod = new startActivty(iActivityManagerObject);
//第二参数 是即将返回的对象 需要实现那些接口,其中这些接口包含OnClickListener,和IActivityManagerIntercept所实现的接口。
//也就是说IActivityManager和OnClickListener所实现的接口都动态替换成startActivtyMethod了
Object oldIactivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader()
, new Class[]{IActivityManagerIntercept, View.OnClickListener.class}
, startActivtyMethod);
//将系统的iActivityManager 替换成 自己通过动态代理实现的对象
//oldIactivityManager对象 实现了 IActivityManager这个接口的所有方法
mInstance.set(defaltValue, oldIactivityManager);
} catch (Exception e) {
e.printStackTrace();
}
}
class startActivty implements InvocationHandler {
private Object iActivityManagerObject;
public startActivty(Object iActivityManagerObject) {
this.iActivityManagerObject = iActivityManagerObject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.i("INFO","invoke "+method.getName());
if ("startActivity".equals(method.getName())) {
Log.i("INFO","-----------------startActivity--------------------------");
//瞒天过海
//寻找传进来的intent
Intent intent = null;
int index=0;
for (int i=0;i
这两个方法其实就实现了将ProxyActivity类推送给ActivityManagerServer去检测,当然这个ProxyActivity必须是在清单文件中注册了的activity。做完后你每次启动activity都会打开ProxyActivity,到这里你已经成功了一半,但是这并不是我们想要的,所以你还得分析源码,ActivityManagerServer检测完intent后会做什么呢?
网上盗图如下:
我们知道,安卓进程中消息通信是通过handler实现的,所有的消息都是存放在消息队列(MessageQueue)中,主线程中有一个Looper循环器,不断从对应线程MassgaeQueue中死循环拿取mesaage然后发送到对应的handler。
handler有消息的会调用如下方法:
//Handler.java
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
//如果mCallback 不为空并且处理后返回true 那么不会调用我们自己写handlerMessage处理消息。
handleMessage(msg);
}
}
我们后面会用此ActivityMH 就行再次替换Handler.Callback
在java中程序的启动是在Main方法中,其实在android中也有一个main方法,只不过系统把它隐藏了我们没看到而已,这个main方法在ActivityThread.java中,它是安卓进程启动的入口
//ActivityThread.java
public static void main(String[] args) {
//...代码省略
//Looper类一些初始化,会从ThreadLocal对象拿取当前线程的Looper
//关于ThreadLocal不做过多讲述,简单就是用线程资源拷贝。
//假设你有个变量a 你可以拷贝三份到ThreadLocal中不同的线程
Looper.prepareMainLooper();
//...代码省略
ActivityThread thread = new ActivityThread();
//开启一个子线程,又可以叫binder线程用于ams(ActivityManagerServer通AIDL来进程间通信,如果有信息那么从子线程发送message给主线程handler进行处理)
thread.attach(false);
//...代码省略
//开启一个死循环遍历messageQueue(里面存放messager),如果有就,所以前面才开启一个子线程,不然怎么跟AMS通信?
Looper.loop();
//...代码省略
}
可以看到这里面开启了一个线程,这个线程就是安卓的主线程,这个主线程在
包含在一个Looper中间,这个loop就是一个死循环不停在消息队列中取出消息或者任务交给主线程做,比如UI刷新就是主线程中一个定期定时任务,所以我一直认为其实主线程和子线程的区别就是在与它里面存在一个looper而已,让它不停工作永葆活力,之前我看到过在某个子线程中我们自定给它绑定一个looper,那么这个子线程也是可以刷新UI操作的,但是最好不要这样做,如果在没有线程锁的情况下会存在很大的安全漏洞。
话归正题:
通过IActivityManager发送startActivity方法到ActivityManagerServer中,然后我们知道知道在ActivityThread类中main方法死循环前开启了个子线程,这个线程会接收ActivityManagerServer回馈的信息,信息封装在Message中然后添加主线程MeassgeQueue中,当主线程的Loop遍历到有信息的时候交给主线程的Handler处理。
当ActivityManagerServer收到startActivity信息的信息的时候,会发送一个message给Handler处理。其中message.what=LAUNCH_ACTIVITY,LAUNCH_ACTIVITY=100
我们看看Activity中Handler:
public class AcitvityThread{
final H mH = new H();
private class H extends Handler {
//。。。代码省略
}
}
//ActivityThread.java
class H extends Handler{
//.....
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");
//从msg拿出ActivityClientRecord 对象,里面包含启动activity的Intent
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
//带着ActivityClientRecord 对象启动activity
handleLaunchActivity(r, null);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
}
}
}
大致可简单的理解从msg获取ActivityClientRecord 对象,内包含启动Intent启动意图等信息,还记得我们前面保存一个Intent到一个新的Intent上吗?这时候其实你可以从这个Intent取出来了。调用handleLaunchActivity去启动。
绕过AMS检测这只是第一步,下面我们还得通过hook技术实现将我们真实意图取出来,跳转到我们自己真正要跳转的页面。
其实就是通过反射机制取出ActivityThread中mH,并动态替换成我们自己的方法。
代码如下:
public void hookHookMh(Context context) {
this.context = context;
try {
Class> forName = Class.forName("android.app.ActivityThread");
Field currentActivityThreadField = forName.getDeclaredField("sCurrentActivityThread");
currentActivityThreadField.setAccessible(true);
//还原系统的ActivityTread mH
Object activityThreadObj=currentActivityThreadField.get(null);
Field handlerField = forName.getDeclaredField("mH");
handlerField.setAccessible(true);
//hook点找到了
Handler mH= (Handler) handlerField.get(activityThreadObj);
Field callbackField = Handler.class.getDeclaredField("mCallback");
callbackField.setAccessible(true);
callbackField.set(mH,new ActivityMH(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
class ActivityMH implements Handler.Callback{
private Handler mH;
public ActivityMH(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
//LAUNCH_ACTIVITY ==100 即将要加载一个activity了,这里是系统的规范定义的
if (msg.what == 100) {
//加工 --完 一定丢给系统真实intent -hook->proxyActivity---hook->secondeActivtiy
handleLuachActivity(msg);
}
//做了真正的跳转
mH.handleMessage(msg);
return true;
}
class ActivityMH implements Handler.Callback{
private Handler mH;
public ActivityMH(Handler mH) {
this.mH = mH;
}
@Override
public boolean handleMessage(Message msg) {
//LAUNCH_ACTIVITY ==100 即将要加载一个activity了,这里是系统的规范定义的
if (msg.what == 100) {
//加工 --完 一定丢给系统真实intent -hook->proxyActivity---hook->secondeActivtiy
handleLuachActivity(msg);
}
//做了真正的跳转
mH.handleMessage(msg);
return true;
}
private void handleLuachActivity(Message msg) {
//还原
Object obj = msg.obj;
try {
Field intentField=obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// ProxyActivity 2
Intent realyIntent = (Intent) intentField.get(obj);
// 到这里后,其实已经通过AMS检测了,这里将我们存入的oldIntent取出来,然后用它做真实跳转。
Intent oldIntent = realyIntent.getParcelableExtra("oldIntent");
// 登录 还原 把原有的意图 放到realyIntent
realyIntent.setComponent(oldIntent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
到这里我们算是把这个工具类写完了,当然我们需要定义一个Application类,在这个类里面去注册这两个hook函数,
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
HookUtil hookUtil = new HookUtil();
hookUtil.hookStartActivity(this);
hookUtil.hookHookMh(this);
}
}
这就是基本完成了hook技术实现免注册activity跳转,至于应用场景可以在插件化架构设计中用到,还比如你在工程所有activity跳转集中监测判断条件时候也可以用,加入你页面中每个activity跳转都需要监测是否登录了,没登陆就先跳转登录页面,是不是也可以在这里稍微改动就可以实现呢?
private void handleLuachActivity(Message msg) {
//还原
Object obj = msg.obj;
try {
Field intentField=obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
// ProxyActivity 2
Intent realyIntent = (Intent) intentField.get(obj);
// 到这里后,其实已经通过AMS检测了,这里将我们存入的oldIntent取出来,然后用它做真实跳转。
Intent oldIntent = realyIntent.getParcelableExtra("oldIntent");
if (oldIntent != null) {
//集中式登录
SharedPreferences share = context.getSharedPreferences("dcw", Context.MODE_PRIVATE);
//oldIntent.getComponent().getClassName().equals(SceondActivity.class.getName())
if (share.getBoolean("login",false)) {
// 登录 还原 把原有的意图 放到realyIntent
realyIntent.setComponent(oldIntent.getComponent());
}else {
ComponentName componentName = new ComponentName(context,LoginActivity.class);
realyIntent.putExtra("extraIntent", oldIntent.getComponent().getClassName());
realyIntent.setComponent(componentName);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
git地址
https://github.com/duanchangwen/dcw