hook 技术简析

最近在研究插件化开发,插件化开发的基础就是hook技术,现在市面上存在的各种插件化框架,其基础原理都是使用hook技术

1.什么是hook技术(网上查到了一个解释,觉得挺好贴在这里):

Hook 英文翻译过来就是「钩子」的意思,那我们在什么时候使用这个「钩子」呢?在 Android 操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而「钩子」的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件,换句话说hook就是在程序的执行过程中拦截程序运行的本来流程,然后再其流程中添加我们自己的代码逻辑,其实在很多优秀的开源框架中,都是运用了类似的技术来实现,比如我们常用的retrofit,其实在将我们定义的请求接口转化成不同的serviceMethod时,就是使用到了动态代理,动态代理就是hook技术常用的技巧之一,又比如流行的卡顿监听工具BlockCanary,其原理就是在handler的dispatchMessage执行前后添加相应的处理逻辑,其实这就是hook技术的实现;Hook 的这个本领,使它能够将自身的代码插入被勾住(Hook)的程序中,成为目标进程的一个部分。API Hook 技术是一种用于改变 API 执行结果的技术,能够将系统的 API 函数执行重定向。在 Android 系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行互不干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是 Hook 的出现给我们开拓了解决此类问题的道路。当然,根据 Hook 对象与 Hook 后处理的事件方式不同,Hook 还分为不同的种类,比如消息 Hook、API Hook 等。

首先我们得找到被Hook的对象,我称之为Hook点;什么样的对象比较好Hook呢?自然是容易找到的对象。什么样的对象容易找到?静态变量和单例;在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。我们根据这个原则找到所谓的Hook点

2.hook的实现方式(通过set方法的方式实现,有可以使用的系统api)

如果我们可以调用某些类的共有方法来改变属性的值,从而可以在系统的原有的操作流程中添加我们新的处理逻辑;如:现在我们有这样一个需求,监听ui卡顿,我们知道android系统是通过消息机制进行UI更新,事件分发的,如果在主线程handler的dispatchMessage方法进行了耗时操作,就会发生UI卡顿,那么我们监听ui卡顿,其实只需要监听dispatchMessage执行时间就可以了,通过分析系统源码我们知道,dispatchMessage是在Looper类中调研(关于hander的原理,在这里不做阐述),我们可以简单的看看Looper.loop()方法执行的是什么操作呢,只看关键代码:

public static void loop() {

....................................................................

final Printer logging = me.mLogging;

if (logging !=null) {

logging.println(">>>>> Dispatching to " + msg.target +" " +

msg.callback +": " + msg.what);

}

....................................................................

try {

msg.target.dispatchMessage(msg);

    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() :0;

}finally {

if (traceTag !=0) {

Trace.traceEnd(traceTag);

    }

}

....................................................................

if (logging !=null) {

logging.println("<<<<< Finished to " + msg.target +" " + msg.callback);

}

中间的代码我们不去关心,我们只知道在dispatchMessage方法的执行前后都会调用,两次logging.println方法我们只需要在计算两次打印的时间间隔是否大于某一个阀值来确定是否存在ui卡顿(这也是BlockCanary的核心原理)实现如下:

private void initHandlerCheck(){

Looper.getMainLooper().setMessageLogging(new Printer() {

@Override

        public void println(String x) {

if (x.startsWith(START)) {

LogMonitor.getInstance().startMonitor();

            }

if (x.startsWith(END)) {

LogMonitor.getInstance().removeMonitor();

            }}});}

LogMonitor实现类如下:

public class LogMonitor {

private static LogMonitorsInstance =new LogMonitor();

    private HandlerThreadmHandlerThread =new HandlerThread("log");

    private HandlermHandler;

    public static final StringTAG ="LogMonitor";

    private LogMonitor() {

mHandlerThread.start();

        mHandler =new Handler(mHandlerThread.getLooper());

    }

private static RunnablemRunnable =new Runnable() {

@Override

        public void run() {

StringBuilder sb =new StringBuilder();

            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();

            for (StackTraceElement s : stackTrace) {

sb.append(s.toString() +"\n");

            }

Log.e(TAG, sb.toString());

        }

};

    public static LogMonitorgetInstance() {

return sInstance;

    }

public void startMonitor() {

mHandler.postDelayed(mRunnable, 1000);

    }

public void removeMonitor() {

mHandler.removeCallbacks(mRunnable);

    }

}

通过以上的代码,我们就可以把出现了耗时超过1000毫秒的操作,出现错误的地方打印在控制台上,这也是hook的一种实现形式;

2.hook的实现方式(通过反射的方式实现,静态代理)

比如我们有一个这样的需求,对于页面的所有按钮,我们想实现相同的业务处理流程,假如我们希望用户激活某种权限后按钮才能够点击, 否则就给出相应的提示,实现这个需求有很多种方式,如(1)在每个button的点击事件设置的地方都加上相应的判断(这肯定是不合适的,业务多的情况容易加漏,也是容易出错的),(2)所有的点击事件继承一个公用的点击事件类,但是,如果你并不是在项目的最开始介入,而是中途被分配来维护这个项目,这种方式可能也会涉及到大量的代码修改,那么还有没有其他的实现方式呢,hook的思想给我们提供了实现思路,通过分析view的源码(button也是一个view)我们知道所有的点击事件都是存储在ListenerInfo中,源码如下:

/**

* Register a callback to be invoked when this view is clicked. If this view is not

* clickable, it becomes clickable.

* @param l The callback that will run

* @see #setClickable(boolean)

*/

public void setOnClickListener(@Nullable OnClickListener l) {

if (!isClickable()) {

setClickable(true);

    }

getListenerInfo().mOnClickListener = l;

}

那问题就简单了,我们只需要找到一种方式,改变原来的mOnClickListener的值,就完美的达到了,我们的目的,那如果做到呢,反射就隆重登场了,如下代码:

public static void hookOnClickListener(View view)throws Exception {

// 反射得到 ListenerInfo 对象

    Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");

    getListenerInfo.setAccessible(true);

    Object listenerInfo = getListenerInfo.invoke(view);

    //得到原始的 OnClickListener事件方法

    Class listenerInfoClz = Class.forName("android.view.View$ListenerInfo");

    Field mOnClickListener = listenerInfoClz.getDeclaredField("mOnClickListener");

    mOnClickListener.setAccessible(true);

    View.OnClickListener originOnClickListener = (View.OnClickListener) mOnClickListener.get(listenerInfo);

    //用 Hook代理类 替换原始的 OnClickListener

   View.OnClickListener hookedOnClickListener =new HookedClickListenerProxy(originOnClickListener,false);

    mOnClickListener.set(listenerInfo, hookedOnClickListener);

}

HookedClickListenerProxy,就是我们用来hook原来的view点击事件的对象,代码如下:

public class HookedClickListenerProxyimplements View.OnClickListener {

public boolean isActive =false;

    private View.OnClickListenerorigin;

    public HookedClickListenerProxy(View.OnClickListener origin,boolean isActive) {

this.origin = origin;

        this.isActive = isActive;

  }

@Override

    public void onClick(View v) {

if(!isActive){

Toast.makeText(v.getContext(), "这个按钮还不能够点击", Toast.LENGTH_SHORT).show(); }

if (origin !=null) {

origin.onClick(v); }}}


4.hook的实现方式(通过动态代理的方式)

假如你收到了这样的一个需求,由于好多时候,用户都会复制我们客户端内部的一些文案去分享,我们这个时候想在复制的内容前面,默认添加上“=====斑马信用,天天向上====”

这个文案以扩大我们产品的影响力(纯粹为了演示瞎想的需求哈)如何实现呢,我们依然可以利用hook思想来实现。我们知道粘贴板服务是由ClipBoardService控制的,这个时候你依然

需要去分析它的源码(这块的源码花费了很长时间去理解),我们的目的依然是替换掉粘贴板服务的操作(我们肯定不能替换掉粘贴板服务,因为那是属于另外的一个进程),这个时候,

我们需要分析ServiceManager这个类大概的源码如下:

public final class ServiceManager {

private static final StringTAG ="ServiceManager";

private static IServiceManagersServiceManager;

private static HashMapsCache =new HashMap();

private static IServiceManagergetIServiceManager() {

if (sServiceManager !=null) {

return sServiceManager;

}

// Find the service manager

sServiceManager = ServiceManagerNative.asInterface(BinderInternal.getContextObject());

return sServiceManager;

}

/**

* Returns a reference to a service with the given name.

*

* @param name the name of the service to get

* @return a reference to the service, or <code>null</code> if the service doesn't exist

*/

public static IBindergetService(String name) {

try {

IBinder service =sCache.get(name);

if (service !=null) {

return service;

}else {

return getIServiceManager().getService(name);

}

}catch (RemoteException e) {

Log.e(TAG, "error in getService", e);

}

return null;

}

}

通过分析ServiceManager的代码,我们知道如果要替换ClipBoardService操作IBinder对象,我们只要替换掉存储在sCache这个hashmap中的值就可以了,所有我们可以这个样完成;

public static void setService(String serviceName, IBinder service) {

if (c_ServiceManager ==null) {

return;

}

if (sCacheService ==null) {

try {

Field sCache =c_ServiceManager.getDeclaredField("sCache");

sCache.setAccessible(true);

sCacheService = (Map) sCache.get(null);

}catch (Exception e) {

e.printStackTrace();

}

}

sCacheService.remove(serviceName);

sCacheService.put(serviceName, service);

}

这样就完成了ClipBoardService对于的IBinder对象的替换,但是,我们替换了这个对象,并不能完成我们的需求,第二步我们需要hook住这个service,这里我们就可以使用到动态代理了,

动态代理大家肯定很熟悉,在这里就不讨论了,具体的代码如下:

public class ClipboardHook {

private static final StringTAG = ClipboardHook.class.getSimpleName();

public static void hookService() {

IBinder clipboardService = ServiceManager.getService(Context.CLIPBOARD_SERVICE);

String IClipboard ="android.content.IClipboard";

if (clipboardService !=null) {

IBinder hookClipboardService =

(IBinder) Proxy.newProxyInstance(clipboardService.getClass().getClassLoader(),

clipboardService.getClass().getInterfaces(),

new ServiceHook(clipboardService, IClipboard, true, new ClipboardHookHandler()));

ServiceManager.setService(Context.CLIPBOARD_SERVICE, hookClipboardService);

}else {

Log.e(TAG, "ClipboardService hook failed!");

}

}

public static class ClipboardHookHandlerimplements InvocationHandler {

@Override

public Objectinvoke(Object proxy, Method method, Object[] args)throws Throwable {

String methodName = method.getName();

int argsLength = args.length;

//每次从本应用复制的文本,前面都加上分享的出处,点击复制时会调用setPrimaryClip设置数据

//所以在这里hook住这个方法

if ("setPrimaryClip".equals(methodName)) {

if (argsLength >=2 && args[0]instanceof ClipData) {

ClipData data = (ClipData) args[0];

String text = data.getItemAt(0).getText().toString();

text ="=====斑马信用,天天向上====" + text;

args[0] = ClipData.newPlainText(data.getDescription().getLabel(), text);

}

}

return method.invoke(proxy, args);

}

}

}

这样就实现对ClipBoardService复制操作的hook,当然上面的代码中ServiceHook类的作用在这里就不展开谈了,如果大家感兴趣可以看看源码,大致的作用是这样的:

通过分析ServiceManager源码我们知道asInterface最终是调用queryLocalInterface这个方法返回给Service 的调用方一个IBinder对象。所以 queryLocalInterface 方法的最后返回的对象

是会被外部直接调用的对象,所以我们需要hook住这个方法产生一个被代理的IBinder对象,而对象后续的所有操作都执行都会回调到上面的ClipboardHook 的invoke方法中,而我们只是

改变setPrimaryClip(复制操作执行的函数)这一个方法的操作,我们要偷偷的再这里添加上“=====斑马信用,天天向上====”这句话。

4)结论

        在这里只是大概用几个例子描述下,我所知道的大概几种java层的完成hook的方式,其实还存在nativie层的hook,这块目前还没有研究到,hook是一种思想,是一种实现方式

也是安卓面向切面(AOP)编程的一种体现,但是它却是插件化的基础,现在市面上开源的插件化框架很多,但是,他们的基础原理都基于hook技术,只是不同的插件框架

hook的地方不一样,hook点的多少不一样。

demo源码:hook.zip

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容