Hook技術之Hook Activity

wangchao05發表於2019-02-17

一、Hook技術概述


Hook技術的核心實際上是動態分析技術,動態分析是指在程式執行時對程式進行除錯的技術。眾所周知,Android系統的程式碼和回撥是按照一定的順序執行的,這裡舉一個簡單的例子,如圖所示。

Hook技術之Hook Activity
物件A呼叫類物件B,物件B處理後將資料回撥給物件A。接下來看看採用Hook的呼叫流程,如下圖:

Hook技術之Hook Activity
上圖中的Hook可以是一個方法或者一個物件,它就想一個鉤子一樣,始終連著AB,在AB之間互傳資訊的時候,hook會在中間做一些處理,比如修改方法的引數和返回值等,就這樣hook起到了欺上瞞下的作用,我們把hook的這種行為稱之為劫持。同理,大家知道,系統程式和應該程式之間是相互獨立的,應用程式要想直接去修改系統程式,這個是很難實現的,有了hook技術,就可以在程式之間進行行為更改了。如圖所示:

Hook技術之Hook Activity
可見,hook將自己融入到它所劫持的物件B所在的程式中,成為系統程式的一部分,這樣我們就可以通過hook來更改物件B的行為了,物件B就稱為hook點。

二、Hook Instrumentation


上面講了Hook可以劫持物件,被劫持的物件叫hook點,用代理物件來替代這個Hook點,這樣我們就可以在代理上實現自己想做的操作。這裡我們用Hook startActivity來舉例。Activity的外掛化中需要解決的一個問題就是啟動一個沒有在AndroidManifest中註冊的Activity,如果按照正常的啟動流程是會報crash的。這裡先簡要介紹一下Activity的啟動,具體的啟動方式講解還需移步專門的文獻。

2.1 Activity的Hook點

啟動Activity時應用程式會發訊息給AMS,請求AMS建立Activity,AMS在SystemServer系統程式中,其與應用程式是隔離的,AMS管理所有APP的啟動,所以我們無法在系統程式下做hook操作,應該在應用程式中。為了繞過AMS的驗證,我們需要新增一個在Manifest中註冊過的Activity,這個Activity稱為佔坑,這樣可以達到欺上瞞下的效果,當AMS驗證通過後再用外掛Activity替換佔坑去實現相應的功能。 核心功能兩點:

  • 替換外掛Activity為佔坑Activity
  • 繞過AMS驗證後需要還原外掛Activity

啟動Activity的時候會呼叫Activity的startActivity()如下:

   @Override
    public void startActivity(Intent intent) {
        this.startActivity(intent, null);
    }
複製程式碼

接著又呼叫了startActivity()

    @Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }
複製程式碼

檢視startActivityForResult方法

    public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            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());
            }
            if (requestCode >= 0) {
                // If this start is requesting a result, we can avoid making
                // the activity visible until the result is received.  Setting
                // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
                // activity hidden during this time, to avoid flickering.
                // This can only be done when a result is requested because
                // that guarantees we will get information back when the
                // activity is finished, no matter what happens to it.
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }
複製程式碼

上述方法中呼叫mInstrumentation的execStartActivity方法來啟動Activity,這個mInstrumentation是Activity的成員變數,我們就選擇Instrumentation為Hook點,用代理的Instrumentation去替換原始的Instrumentation來完成Hook,如下是代理類:

public class InstrumentationProxy extends Instrumentation {

    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;

    public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
        this.mInstrumentation = instrumentation;
        this.mPackageManager = packageManager;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {

        List<ResolveInfo> resolveInfo = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        //判斷啟動的外掛Activity是否在AndroidManifest.xml中註冊過
        if (null == resolveInfo || resolveInfo.size() == 0) {
            //儲存目標外掛
            intent.putExtra(HookHelper.REQUEST_TARGET_INTENT_NAME, intent.getComponent().getClassName());
            //設定為佔坑Activity
            intent.setClassName(who, "replugin.StubActivity");
        }

        try {
            Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class,
                    Intent.class, int.class, Bundle.class);
            return (ActivityResult) execStartActivity.invoke(mInstrumentation, who, contextThread, token, target, intent, requestCode, options);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String intentName = intent.getStringExtra(HookHelper.REQUEST_TARGET_INTENT_NAME);
        if (!TextUtils.isEmpty(intentName)) {
            return super.newActivity(cl, intentName, intent);
        }
        return super.newActivity(cl, className, intent);
    }

}
複製程式碼

InstrumentationProxy類繼承類Instrumentation,實現了類execStartActivity方法,接著通過反射去用原始Instrumentation的execStartActivity方法,這就是替換為佔坑Activity的過程。Activity的建立是在ActivityThread中,裡面有個performLaunchActivity方法;

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    ...
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    }
    ...
    activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
    ...
}
複製程式碼

這裡的newActivity就是建立Activity的過程,我們同樣的在代理類中去實現這個方法,這就是還原外掛Activity 的過程。

接下來我們看個例子: 佔位坑Activity:

public class StubActivity extends BaseActivity {
    @Override
    public int bindLayout() {
        return R.layout.activity_stub;
    }

    @Override
    public void initViews() {
    }

    @Override
    public void onClick(View v) {

    }
}
複製程式碼

這個Activity一定是需要在AndroidManifest中去註冊。 再寫一個外掛Activity

public class TargetActivity extends BaseActivity {
    @Override
    public int bindLayout() {
        return R.layout.activity_target;
    }

    @Override
    public void initViews() {

    }

    @Override
    public void onClick(View v) {

    }
}
複製程式碼

都是很簡單的Activity,TargetActivity並沒有註冊,現在我們需要啟動這個Activity。代理類上面程式碼已經貼出來了。接下來就是替換代理類,達到Hook的目的,我們在Application中做這個事情:

public class MyApplication extends Application {

    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        hookActivityThreadInstrumentation();
        
    }

    private void hookActivityThreadInstrumentation() {
        try {
            Class<?> activityThreadClass=Class.forName("android.app.ActivityThread");
            Field activityThreadField=activityThreadClass.getDeclaredField("sCurrentActivityThread");
            activityThreadField.setAccessible(true);
            //獲取ActivityThread物件sCurrentActivityThread
            Object activityThread=activityThreadField.get(null);

            Field instrumentationField=activityThreadClass.getDeclaredField("mInstrumentation");
            instrumentationField.setAccessible(true);
            //從sCurrentActivityThread中獲取成員變數mInstrumentation
            Instrumentation instrumentation= (Instrumentation) instrumentationField.get(activityThread);
            //建立代理物件InstrumentationProxy
            InstrumentationProxy proxy=new InstrumentationProxy(instrumentation,getPackageManager());
            //將sCurrentActivityThread中成員變數mInstrumentation替換成代理類InstrumentationProxy
            instrumentationField.set(activityThread,proxy);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

這樣就把原始的Instrumentation替換為代理的了,具體的操作我們在InstrumentationProxy中去做實現。接下來我們就是從主介面跳轉外掛Activity了:

public class PluginActivity extends BaseActivity {
    @Override
    public int bindLayout() {
        return R.layout.activity_stub;
    }

    @Override
    public void initViews() {
        Log.d("", "initViews: ");
        findViewById(R.id.btn_start_replugin).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(PluginActivity.this, TargetActivity.class
                ));
            }
        });
    }

    @Override
    public void onClick(View v) {

    }

    public static void startActivity(Context context) {
        Intent i = new Intent(context, PluginActivity.class);
        context.startActivity(i);
    }

}
複製程式碼

相關文章