android hook詳解

在談hook之前,我們先談下代理。代理對學編程的人來說,應該非常熟悉,畢竟代理模式還是很常用的。考慮到真有人不知道代理模式,我們先從靜態代理開始講解。

先寫個接口:

public interface Animal {
    void eat();
    int feet();
}

實現該接口:

public class Cat implements Animal{
    @Override
    public void eat() {
        System.out.print("cat eat");
    }

    @Override
    public int feet() {
        return 4;
    }
}

現在寫過靜態代理ProxyAnimal:

public class ProxyAnimal implements Animal {
    private Animal realAnim;
    
    public ProxyAnimal(Animal realAnim){
        this.realAnim = realAnim;
    }
    @Override
    public void eat() {
        realAnim.eat();
    }

    @Override
    public int feet() {
        return realAnim.feet();
    }
}

這樣很簡單,就是實現了一個靜態代理模式,如果第三方想訪問cat,你給它一個ProxyAnimal就可以了,這樣也避免暴露Cat對象,具體作用可以百度代理模式。

靜態代理模式儘管簡單,但是它有點繁瑣,假如我還有一個Shopping也需要個代理,那你還需要依葫蘆畫瓢,寫一個ShopingProxy,這樣公式化代碼太多,爲避免這種局面,出現了動態代理。無論爲多少個接口實現代理,都只需要寫一個代理類即可。如:

public class ProxyHandler implements InvocationHandler {
    private Object realObj;

    public ProxyHandler(Object realObj){
        this.realObj = realObj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        System.out.println("excute before >>>" + method.getName());
        Object obj;
        try {
            obj = method.invoke(realObj, args);
        } catch (Exception e) {
            obj = null;
        }
        System.out.println("excute after >>>" + method.getName());
        return obj;
    }
}

接着就可以在client測試如下:

public static void main(String [] args){
        Animal cat = new Cat();
        Animal proxy = (Animal)Proxy.newProxyInstance(Animal.class.getClassLoader(),cat.getClass().getInterfaces(),
                new ProxyHandler(cat));
        proxy.eat();
        Shopping shopping = new ShoppingImpl();
        Shopping shopProxy = (Shopping)Proxy.newProxyInstance(Shopping.class.getClassLoader(),
                shopping.getClass().getInterfaces(),new ProxyHandler(shopping));
        shopProxy.doShopping();

    }

這樣就避免去寫AnimalProxy和ShoppingProxy,用一個ProxyHandler就搞定。

到這基礎已經講解完畢,接着開始進入正題。

談到hook,其實說白了就是找一個hook點,hook點的選取十分重要,好壞決定難易程度。通常我們優先考慮靜態屬性或方法, & public屬性或方法,實在沒辦法纔去考慮保護型或私有型屬性&方法(畢竟這種類型需要考慮版本兼容)。

原理知道後,還是實戰來得舒暢,下面以startActivity啓動一個activity爲例,在Activity中啓動另一個Activity,首先我們需要了解activity的啓動流程,一步步跟蹤,發現最終在Activity.startActivityForResult中以如下方式啓動:

 Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }

如果你對Activity啓動熟悉的話會發現,此處的mInstrumentation就是ActivityThread通過ativity.attach傳過來的,而ActivityThread一個app只有一個,而mInstrumentation就是在ActivityThread創建後馬上創建的,此時,是不是感覺這個mInstrumentation符合hook點,ok,先hook一把

public static void hookCurrentThread(){
        try {
            Class<?> activityThreadCls = Class.forName("android.app.ActivityThread");
            //1.獲取ActivityThread對象
            //hook點,有public的方法或屬性,優先
            Method currentActThreadMethod = activityThreadCls.getDeclaredMethod("currentActivityThread");
            Object curThreadObj = currentActThreadMethod.invoke(null);
            //獲取mInstrumentation
            Field instrumentationField = curThreadObj.getClass().getDeclaredField("mInstrumentation");
            instrumentationField.setAccessible(true);
            instrumentationField.set(curThreadObj,new InstrumentProxy((Instrumentation) instrumentationField.get(curThreadObj)));
        } catch (Exception e) {
            e.printStackTrace();
        }

其中InstrumentProxy如下:

public class InstrumentProxy extends Instrumentation{
    private Instrumentation realObj;
    public InstrumentProxy(Instrumentation obj){
        this.realObj = obj;
    }
    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
       
        // 開始調用原始的方法, 調不調用隨你,但是不調用的話, 所有的startActivity都失效了.
        // 由於這個方法是隱藏的,因此需要使用反射調用;首先找到這個方法
        try {
            Method execStartActivity = 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(realObj, who,
                    contextThread, token, target, intent, requestCode, options);
        } catch (Exception e) {
            // 某該死的rom修改了  需要手動適配
            throw new RuntimeException("do not support!!! pls adapt it");
        }
    }
}

此處,先獲取ActivityThread中的mInstrumentation屬性,然後再採用靜態代理將其替換掉,這樣就hook住系統的方法,我們也就可以在InstrumentProxy中任意插樁。

倘若你沒有發現mInstrumentation符合hook點,你可以繼續跟蹤Instrumentation.execStartActivity方法,裏面有個非常明顯的hook點:

try {
1494            intent.migrateExtraStreamToClipData();
1495            intent.prepareToLeaveProcess();
1496            int result = ActivityManagerNative.getDefault()
1497                .startActivity(whoThread, who.getBasePackageName(), intent,
1498                        intent.resolveTypeIfNeeded(who.getContentResolver()),
1499                        token, target != null ? target.mEmbeddedID : null,
1500                        requestCode, 0, null, options);
1501            checkStartActivityResult(result, intent);
1502        } catch (RemoteException e) {
1503        }

對就是ActivityManagerNative.getDefault(),我們可以進入ActivityManagerNative類,發現getDefault方法實現如下:

static public IActivityManager getDefault() {
81          return gDefault.get();
82      }

其中gDefault是個static屬性,完全符合hook要求,具體hook如下:

public static void hookAMNative(){
        try {
            Class<?> actManagerNativeCls = Class.forName("android.app.ActivityManagerNative");

            //獲取gDefault
            Field gDefaultField = actManagerNativeCls.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);
            Object gDefaultObj = gDefaultField.get(null);
//            Method getField = gDefaultObj.getClass().getDeclaredMethod("get");
//            Object activityImpl = getField.invoke(null);

            Class<?> singleton = Class.forName("android.util.Singleton");
            Field mInstanceField = singleton.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);

            Object activityImpl = mInstanceField.get(gDefaultObj);

//            Method activityManagerMethod= actManagerNativeCls.getMethod("getDefault");
//            Object actManagerImpl = activityManagerMethod.invoke(null);

            Object actProxy = Proxy.newProxyInstance(activityImpl.getClass().getClassLoader(),
                    activityImpl.getClass().getInterfaces(),new ProxyHandler(activityImpl,null));
            mInstanceField.set(gDefaultObj,actProxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

其中ProxyHandler如下:

public class ProxyHandler implements InvocationHandler{
    private Object realObj;
    public ProxyHandler(Object obj,Object d){
        this.realObj = obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if("startActivity".equals(methodName)){
            android.util.Log.e("hooktest",">>>>proxyhandler>>>startActivityBefore");
            Object retur = method.invoke(realObj,args);
            android.util.Log.e("hooktest",">>>>proxyhandler>>>startActivityAfter");
            return retur;
        }

        return method.invoke(realObj,args);
    }
}

採用動態代理的方法對IActivityManager進行了接管,同樣完成了startActivity的hook。

其實不選單例、靜態屬性或共有屬性,整個private的也是可以的,還以啓動startActivity爲例,考慮到Activity是繼承ContextWrapper,而ContextWrapper中有個屬性mBase,如果我們能對mBase hook也是可以的,這樣就需要對ContextImpl來個代理就可以了,代碼可以如下:

public static void hookContextWrapper(ContextWrapper wrapper) {
        try {
            Field mBaseFiled;
            Class<?> wrapperClass = ContextWrapper.class;

            mBaseFiled = wrapperClass.getDeclaredField("mBase");
            mBaseFiled.setAccessible(true);
            mBaseFiled.set(wrapper,new HookWrapper((Context) mBaseFiled.get(wrapper)));

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

這樣儘管可以,但是啓動activity就需要注意了,只要弄走到mBase.startActivity(intent)接口才生效,如果沒走它,就hook失效囉,所以hook點選擇很關鍵,儘管都hook到了東西,但是是不是hook住了全部,還需要驗證下。

hook重在選hook點!!!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章