Android hook入门学习之一——最简单实例

本文详细介绍了Hook技术在Android开发中的应用,通过解决Toast显示导致的系统崩溃问题,展示了Hook技术的重要性和其实现步骤。文章深入讲解了Java反射、阅读安卓源码的能力以及Hook通用思路,并提供了一个具体的Hook实战案例。

前言:

最近在学习到张绍文讲到的一段关于解决崩溃的话是这样说的:

“ Hook 解决。这里分为 Java Hook 和 Native Hook。我以我最近解决的一个系统崩溃为例,我们发现线上出现一个
Toast的相关系统崩溃,它只出现在Android7.0的系统中,看起来是在Toast显示的时候窗口的token已
经无效了。这有可能出现在Toast需要显示时,窗口已经销毁了。

android.view.WindowManager$BadTokenException: 
    at android.view.ViewRootImpl.setView(ViewRootImpl.java)
    at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java)
    at android.view.WindowManagerImpl.addView(WindowManagerImpl.java4)
    at android.widget.Toast$TN.handleShow(Toast.java)

为什么Android8.0的系统不会有这个问题?在查看Android8.0的源码后我们发现有以下修改:
try {
  mWM.addView(mView, mParams);
  trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
  /* ignore */
}

考虑再三我们决定参考Android8.0的做法,直接catch住这个异常。我这的关键在于寻找Hook点,
这个安全算是相对简单的。Toast里面有一个变量叫mTN,它的类型为handler,我们只需要代理它
可以实现捕获。”

 

从上面这一段说让我发现作为一名Android开发工程师,学习Hook技术的重要性,于是我开始了我的Hook学习之旅。

前置技能

 

1.java反射

熟练掌握类Class,方法Method,成员Field的使用方法
源码内部,很多类和方法都是@hide的,外部直接无法访问,所以只能通过反射,去创建源码中的类,方法,或者成员.


2.阅读安卓源码的能力

hook的切入点都在源码内部,不能阅读源码,不能理清源码逻辑,则不用谈hook.其实使用 androidStudio来阅读源码有个坑,有时候会看到源码里面 "一片飘红",看似是有什么东西没有引用进来,其实是因为有部分源码没有对开发者开放,解决起来很麻烦,所以,推荐从安卓官网下载整套源码,然后使用 SourceInsight 查看源码。如果不需要跳来跳去的话,直接用 安卓源码网站 一步到位


 hook通用思路

无论多么复杂的源码,我们想要干涉其中的一些执行流程,最终的杀招只有一个: “偷梁换柱”.
而 “偷梁换柱”的思路,通常都是一个套路:

1. 根据需求确定 要hook的对象
2. 寻找要hook的对象的持有者,拿到要hook的对象
(持有:B类 的成员变量里有 一个是A的对象,那么B就是A的持有者,如下)
class B{ 
 A a;
}
class A{}

3. 定义“要hook的对象”的代理类,并且创建该类的对象
4. 使用上一步创建出来的对象,替换掉要hook的对象

案例实战

这是一个最简单的案例:
我们自己的代码里面,给一个view设置了点击事件,现在要求在不改动这个点击事件的情况下,添加额外的点击事件逻辑.

View v = findViewById(R.id.tv);
        v.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "别点啦,再点我咬你了...", Toast.LENGTH_SHORT).show();
            }
        });

这是view的点击事件,toast了一段话,现在要求,不允许改动这个OnClickListener,要在toast之前添加日志打印 Log.d(...).


乍一看,无从下手.看hook如何解决.
按照上面的思路来:

第一步:根据需求确定 要hook的对象;
我们的目的是在OnClickListener中,插入自己的逻辑.所以,确定要hook的,是v.setOnClickListener()方法的实参。


第二步:寻找要hook的对象的持有者,拿到要hook的对象
进入v.setOnClickListener源码:发现我们创建的OnClickListener对象被赋值给了getListenerInfo().mOnClickListener
public void setOnClickListener(@Nullable OnClickListener l) {
       if (!isClickable()) {
         setClickable(true);
      }
     getListenerInfo().mOnClickListener = l;
 }

继续索引:getListenerInfo() 是个什么玩意?继续追查:
ListenerInfo getListenerInfo() {
       if (mListenerInfo != null) {
           return mListenerInfo;
      }
     mListenerInfo = new ListenerInfo();
      return mListenerInfo;
 }  

结果发现这个其实是一个伪单例,一个View对象中只存在一个ListenerInfo对象.
进入ListenerInfo内部:发现OnClickListener对象 被ListenerInfo所持有.
static class ListenerInfo {
 ...
 public OnClickListener mOnClickListener;
 ...
}

到这里为止,完成第二步,找到了点击事件的实际持有者:ListenerInfo .


第三步:定义“要hook的对象”的代理类,并且创建该类的对象
我们要hook的是View.OnClickListener对象,所以,创建一个类 实现View.OnClickListener接口.

package com.example.hookdemo;

import android.util.Log;
import android.view.View;

public class OnClickListenerProxy implements View.OnClickListener {

    private View.OnClickListener mOriginListener;

    public OnClickListenerProxy(View.OnClickListener listener) {
        this.mOriginListener = listener;
    }

    @Override
    public void onClick(View v) {
        if (mOriginListener != null) {
            mOriginListener.onClick(v);
        }

        String name = (String) v.getTag(v.getId());  //接收参数
        Log.d("LOGCAT", name + "被hooked!");
    }
}

然后,new出它的对象待用。
ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);


第四步:使用上一步创建出来的对象,替换掉要hook的对象,达成 偷梁换柱的最终目的.
利用反射,将我们创建的代理点击事件对象,传给这个view
field.set(mListenerInfo, proxyOnClickListener);
这里,贴出最终代码:

package com.example.hookdemo;

import android.util.Log;
import android.view.View;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class HookUtil {

    public static void hookOnClickListener(View view) {

        try {

            //第一步:得到View的getListenerInfo方法
            Method getListenerInfo = View.class.getDeclaredMethod("getListenerInfo");
            //修改getListenerInfo为可访问(View中的getListenerInfo不是public)
            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);
            //第三步:创建代理类,代理原始类
            View.OnClickListener hookedOnClickListener = new OnClickListenerProxy(originOnClickListener);

//             或者用动态代理实现上面代码完成上面两步,直接生成代理类,需要多传一个Context参数
//            Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
//                @Override
//                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                    Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
//                    return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
//                }
//            });

            mOnClickListener.set(listenerInfo, hookedOnClickListener);

        } catch (Exception e) {
            Log.d("LOGCAT", "hook clickListener failed!", e);
        }
    }
}

最后 一步,调用。设置钩子,替代原有逻辑:

package com.example.hookdemo;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private Button btnHook;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btnHook = findViewById(R.id.btnHook);

        btnHook.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, "点击事件", Toast.LENGTH_LONG).show();
            }
        });
        btnHook.setTag(btnHook.getId(), "按钮1"); //练习带参数,也可以不带。
        HookUtil.hookOnClickListener(btnHook);

    }
}

自此,一个最简单的Hook实例就完成了。

参考:https://www.jianshu.com/p/74c12164ffca?tdsourcetag=s_pcqq_aiomsg

 

 

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值