浅谈Hook

2,499 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情

前言

温馨提示,本文可能比较适合有一定基础的朋友阅读。为什么要提这个问题呢,因为平时多多少少也用到“我所谓的hook”的概念去解决问题,但是这个思想真的真的是hook这个概念的思想吗?我做的操作,我所称之为hook,那它真的能叫hook吗,如果不是,那又该称为什么?

什么是hook

什么是hook? hook是钩子,这我知道,就算英语不好的人拿个翻译也知道是这意思,关键它想表达什么思想,它被提出是为了解决什么问题。前端有 hook这个概念,Android也有,windows也有。很多人有告诉你具体怎么操作,但却没给你讲清楚它是什么?它是为了解决什么问题被提出的?是哪个逼先提出的?这些我都找不到。但好像是在windows里面最先提出这个概念的。

我的理解是,给一个已知的流程中添加特定的代码。“添加”这个词让人觉得怪怪的,换成“给一个已知的流程中hook特定的代码”,你别把hook想成钩子,把他当成一个特定的行为。

我举一个很邪恶的例子,经过高考,一所大学已经录取了小明,然后大帅成绩不好,落选了,但他想上这所大学,于是把自己的信息替换成小明的信息。(这是违法行为,十分不提倡,我怎么就只能想出这种例子了)

public class University {

    private Student student;

    public void examination(){

        BureauOfEducation.seach(new OnSeachListener{

            @Override
            public void onCallBack(Student xiaoming){
                this.student = xiaoming;
            }
        });

    }
}
try {
    Student dashuai = new Student();
    Class<?> cls = Class.forName("xxx.xxx.xxx.University");
    Field field = cls.getDeclaredField("student");
    field.setAccessible(true);
    field.set(university, dashuai);
}catch (Exception e){
    e.printStackTrace();
}

写的是伪代码,但应该不难看懂。这样在examination()方法调用之后再反射,就能替换对象了。你看这段代码,能被称为hook吗?因为已知流程是student为xiaoming,我们通过操作改成了dashuai。我说的是代码,这件事就不是hook了,这叫hacker。这招狸猫换太子,我玩得也不少,其中最经典的就是插件化的占坑法。

这叫hook吗,我是用反射去改变原有的执行结果,所以反射 = hook ? 那往大的想。假如你有一个应用,这个应用不是我的。我们都知道,每个线程都有一个Looper,线程会按照顺序执行Message。好,先别管我能不能实现,怎么实现。我用一些办法,在你应用运行时,把一个我的Message插入你的应用的MessageQueue,这算不算hook?我觉得算,因为在我理解,你的代码的已知流程就是按照MessageQueue的顺序去执行,而我的行为改变了这个顺序。再往更大的想,你有个app,我往你的app里面去注入代码,我把你的icon替换了或者什么的,这算不算hook?

所以又感觉“给一个已知的流程中hook特定的代码”好像又不太准确,“给一个已知的流程中hook特定的功能”如何?这里的hook是一个行为,这个行为包含插入、替换、删除等操作。

除了替换操作,还有其它操作,比如获取。我之前有碰到过这样的场景,使用GsyVideoPlaye的时候我想去设置取消音频焦点,取消是用这个方法AudioManager.abandonAudioFocus(onAudioFocusChangeListener),而这个onAudioFocusChangeListener是GSY内部私有的,未提供出来,这时候就需要用同的方式去获取(伪代码)

try {
    Class<?> cls = Class.forName("xxx.xxx.xxx.xxxx");
    Field field = cls.getDeclaredField("xxxxx");
    field.setAccessible(true);
    AudioManager.OnAudioFocusChangeListener onAudioFocusChangeListener = field.get(university, dashuai);
}catch (Exception e){
    e.printStackTrace();
}

然后AudioManager.abandonAudioFocus(onAudioFocusChangeListener)就行了。

那这算不算hook?其实这操感觉也没有影响到GSY内部的流程,我只是拿这个对象出来而言,是因为你内部没有提供get方法暴露出来,我才会通过这种方式。

怎么去hook?

怎么去hook,其实在不明确这个概念的基础上怎么去做?很难,这个是没有一个明确的答案的,全要靠你对Android的理解和平时的经验积累,去找到hook的点,这个没人能教你,即便看了别人的例子,也很学以致用,只能靠自己多看多写。

已经介绍了两种场景,这两种场景都是通过非正规的手段去影响代码,最后看一个。

在Mainifest定义了两个一样的Activity,然后外部跳转的时候,只执行了其中一个Activity的方法,我想让两个Activity都的逻辑都执行,那要怎么处理?这时有的人就想问了,怎么可能定义两个相同的Activity。在清单中,有一个直接<activity name = "xxx.A" ...... /> ,另外一个用重定向<activity-alias name = "xxx.A" ...... /> 这样的操作是可以的。

但如果我想整合两个的逻辑代码,我就需要把重定向的Activity当成一个普通的类,假如重定向的Activity是这样的。

public class Test extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        functionA();
        functionB();
    }

    @Override
    protected void onResume() {
        super.onResume();
        functionC();
        functionD();
    }
}

我得为它写个代理

public class TestProxy {

    private Test test;

    public TestProxy(){
        try {
            Class<?> cls = Class.forName("xxx.xxx.xxx.Test");
            test = (Test) cls.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void onCreate(Bundle savedInstanceState){
        try {
            if (test != null) {
                Class<?> cls = Class.forName("xxx.xxx.xxx.Test");
                Method method = cls.getDeclaredMethod("onCreate", Bundle.class);
                method.invoke(test, savedInstanceState);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void onResume() {
        try {
            if (test != null) {
                Class<?> cls = Class.forName("xxx.xxx.xxx.Test");
                Method method = cls.getDeclaredMethod("onResume");
                method.invoke(test);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

}

然后我在另一个Activity的生命周期中去调用TestProxy对应的方法,这样可以吗?其实是不行的,为什么?因为原方法中有super,这样调用会执行到它的父类Activity的方法,肯定炸。所以我们要改成这样。我们不能去调用onCreate和onResume,那样会走super,我不想走super,所以我直接代理里面的逻辑。

public class TestProxy {

    private Test test;

    public TestProxy(){
        try {
            Class<?> cls = Class.forName("xxx.xxx.xxx.Test");
            test = (Test) cls.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void onCreate(Bundle savedInstanceState){
        try {
            if (test != null) {
                Class<?> cls = Class.forName("xxx.xxx.xxx.Test");
                Method method = cls.getDeclaredMethod("functionA");
                method.invoke(test);

                Method method2 = cls.getDeclaredMethod("functionB");
                method2.invoke(test);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    protected void onResume() {
        ......
    }

}

这样就能绕过super,但实际的场景没这么简单,假如在functionA中有拿跳转的Intent的值,你怎么做?比如这样

private void functionA(){
    Intent intent = getIntent();
    ......
}

你现在怎么getIntent(),因为这里是通过TestProxy去走Test的,而不是打开Activity的方式走Test,这里我们已经把Test当成一个普通的类而不是Activity了,这里getIntent()肯定拿不到,那这么办?没关系,我们看源码(Activity的源码)

public Intent getIntent() {
    return mIntent;
}

@UnsupportedAppUsage
/*package*/ Intent mIntent;

哎哟,它这里是全局的一个mIntent,那我就能用我第一个例子的方式把我的intent给替换进去,这样getIntent()就有值了。

那这一系列的操作是否能被称为hook呢?

我继续假设,这里不止是getIntent()这么多简单,还有很多个地方有类似的操作, 难道我要一个一个地方进行处理?

那我就想用另外一种方法去解决,我把你这个类拷贝过来(假如没有混淆),然后我在这个基础上做一些操作,然后打到dex中,用插件化的思想,或者用热更的思想去让它先加载。能明白是什么意思吧,就是我利用双亲委派机制,让我这个拷贝并改代码的类先加载。那这种操作其实基本能算热更那边的了,还能称作hook吗?