Android Hook告訴你 如何啟動未註冊的Activity

語言: CN / TW / HK

前言

Android Hook 外掛化其實已經不是什麼新鮮的技術了,不知你有沒有想過,支付寶中那麼多小軟體:淘票票 ,火車票等軟體,難道是支付寶這個軟體自己編寫的嗎?那不得寫個十年,軟體達到幾十G,但是並沒有,玩遊戲時那麼多的面板包肯定時使用者使用哪個就下載哪個面板包。

一 未在配置檔案中註冊的Activity可以啟動嗎?

從0學的時候就知道Activity必須在配置檔案中註冊,否則無法啟動且報錯。但是Hook告訴你的是,未在配置檔案中註冊Activity是可以啟動的,驚不驚喜?意不意外?

通過本文你可以學到:

1.通過對startActivity方法進行Hook,實現為startActivity方法新增日誌。

1.1 通過對Instrumentation進行Hook

1.2 通過對AMN進行Hook

2.如何啟動一個未在配置檔案中註冊的Activity實現外掛化

本片文章基礎建立在 Java反射機制App啟動流程解析,建議不太瞭解的小夥伴可以先移步至這兩篇文章。

二 對startActivity方法進行Hook

通過對查閱startActivity的原始碼可以看出startActivity最終都會走到startActivityFoResult方法中

```html public void startActivityForResult(Intent intent, int requestCode, Bundle options) { if(this.mParent == null) { ActivityResult ar = this.mInstrumentation.execStartActivity(this, this.mMainThread.getApplicationThread(), this.mToken, this, intent, requestCode, options); if(ar != null) { this.mMainThread.sendActivityResult(this.mToken, this.mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData()); }

    if(requestCode >= 0) {
        this.mStartedActivity = true;
    }
} else if(options != null) {
    this.mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
    this.mParent.startActivityFromChild(this, intent, requestCode);
}

} ```

通過mInstrumentation.execStartActivity呼叫(ps:詳細的原始碼解析已在上篇文章中講解),再看mInstrumentation.execStartActivity方法原始碼如下:

```html public Instrumentation.ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { IApplicationThread whoThread = (IApplicationThread)contextThread; if(this.mActivityMonitors != null) { Object e = this.mSync; synchronized(this.mSync) { int N = this.mActivityMonitors.size();

        for(int i = 0; i < N; ++i) {
            Instrumentation.ActivityMonitor am = (Instrumentation.ActivityMonitor)this.mActivityMonitors.get(i);
            if(am.match(who, (Activity)null, intent)) {
                ++am.mHits;
                if(am.isBlocking()) {
                    return requestCode >= 0?am.getResult():null;
                }
                break;
            }
        }
    }
}

try {
    intent.setAllowFds(false);
    intent.migrateExtraStreamToClipData();
    int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null?target.mEmbeddedID:null, requestCode, 0, (String)null, (ParcelFileDescriptor)null, options);
    checkStartActivityResult(var16, intent);
} catch (RemoteException var14) {
    ;
}

return null;

} ```

最終會交給 int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent,...處理,所以如果我們想對startActivity方法進行Hook,可以從這兩個地方入手(其實不止這兩個地方,我們只講解著兩個地方,下面使用的反射封裝類也在上篇文章中給出)。

  • 2.1 對mInstrumentation進行Hook

在Activity.class類中定義了私有變數

html private Instrumentation mInstrumentation;

我們首先要做的就是修改這個私有變數的值,在執行方法前列印一行日誌,首先我們通過反射來獲取這一私有變數。

html Instrumentation instrumentation = (Instrumentation) Reflex.getFieldObject(Activity.class,MainActivity.this,"mInstrumentation");

我們要做的是將這個Instrumentation替換成我們自己的Instrumentation,所以下面我們新建MyInstrumentation繼承自Instrumentation,並且MyInstrumentation的execStartActivity方法不變。

```html public class MyInstrumentation extends Instrumentation {

private Instrumentation instrumentation;

public MyInstrumentation(Instrumentation instrumentation) {
    this.instrumentation = instrumentation;
}

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


    Log.d("-----","啦啦啦我是hook進來的!");
    Class[] classes = {Context.class,IBinder.class,IBinder.class,Activity.class,Intent.class,int.class,Bundle.class};
    Object[] objects = {who,contextThread,token,target,intent,requestCode,options};
    Log.d("-----","啦啦啦我是hook進來的!!");
    return (ActivityResult) Reflex.invokeInstanceMethod(instrumentation,"execStartActivity",classes,objects);

}

```

我們直接通過反射呼叫這個方法 引數就是 Class[] classes = {Context.class,IBinder.class,IBinder.class,Activity.class,Intent.class,int.class,Bundle.class}與方法名中一致

html (ActivityResult) Reflex.invokeInstanceMethod(instrumentation,"execStartActivity",classes,objects)

如果我們這裡不呼叫它本身的execStartActivity方法的話,那麼startActivity就無效了。

然後我們將自定義的替換為原來的Instrumentation

html Reflex.setFieldObject(Activity.class,this,"mInstrumentation",instrumentation1);

完整程式碼就是

html Instrumentation instrumentation = (Instrumentation) Reflex.getFieldObject(Activity.class,MainActivity.this,"mInstrumentation"); MyInstrumentation instrumentation1 = new MyInstrumentation(instrumentation); Reflex.setFieldObject(Activity.class,this,"mInstrumentation",instrumentation1);

執行日誌如下:

這個時候我們就成功的Hook了startActivity方法

2.2 對AMN進行Hook

execStartActivity方法最終會走到ActivityManagerNative.getDefault().startActivity方法

html try { intent.setAllowFds(false); intent.migrateExtraStreamToClipData(); int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null?target.mEmbeddedID:null, requestCode, 0, (String)null, (ParcelFileDescriptor)null, options); checkStartActivityResult(var16, intent); } catch (RemoteException var14) { ; }

你可能會說上面說過了啊,替換ActivityManagerNative.getDefault(),重寫startActivity方法,我們來看ActivityManagerNative.getDefault()

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

```html public final T get() { synchronized(this) { if(this.mInstance == null) { this.mInstance = this.create(); }

    return this.mInstance;
}

} ```

可以看出IActivityManager是一個介面,gDefault.get()返回的是一個泛型,上述方案我們無法入手,所以我們這裡要用動態代理方案

我們定義一個AmsHookHelperUtils類,在AmsHookHelperUtils類中處理反射程式碼

gDefault是個final靜態型別的欄位,首先我們獲取gDefault欄位

html Object gDefault = Reflex.getStaticFieldObject("android.app.ActivityManagerNative","gDefault");

gDefault是 Singleton\型別的物件,Singleton是一個單例模式

```html public abstract class Singleton { private T mInstance;

public Singleton() {
}

protected abstract T create();

public final T get() {
    synchronized(this) {
        if(this.mInstance == null) {
            this.mInstance = this.create();
        }

        return this.mInstance;
    }
}

} ```

接下里我們來取出mInstance欄位

html Object mInstance = Reflex.getFieldObject("android.util.Singleton",gDefault,"mInstance");

然後建立一個代理物件

html Class<?> classInterface = Class.forName("android.app.IActivityManager"); Object proxy = Proxy.newProxyInstance(classInterface.getClassLoader(), new Class<?>[]{classInterface},new AMNInvocationHanlder(mInstance));

我們的代理物件就是new AMNInvocationHanlder(mInstance),(ps:代理模式分為靜態代理和動態代理,如果對代理模式不瞭解可以百度一波,也可以關注我,等待我的代理模式相關文章)

```html public class AMNInvocationHanlder implements InvocationHandler {

private String actionName = "startActivity";

private Object target;

public AMNInvocationHanlder(Object target) {
    this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

    if (method.getName().equals(actionName)){
        Log.d("---","啦啦啦我是hook AMN進來的");
        return method.invoke(target,args);
    }

    return method.invoke(target,args);
}

} ```

所有的代理類都要實現InvocationHandler介面,在invoke方法中method.invoke(target,args);表示的就是 執行被代理物件所對應的方法。因為AMN Singleton做的事情比較多,所以這裡只對startActivity方法hook

html if (method.getName().equals(actionName)){ Log.d("---","啦啦啦我是hook AMN進來的"); return method.invoke(target,args); }

然後我們將gDefault欄位替換為我們的代理類

html Reflex.setFieldObject("android.util.Singleton",gDefault,"mInstance",proxy);

AmsHookHelperUtils方法整體如下:

```html public class AmsHookHelperUtils {

public static void hookAmn() throws ClassNotFoundException {
    Object gDefault = Reflex.getStaticFieldObject("android.app.ActivityManagerNative","gDefault");
    Object mInstance = Reflex.getFieldObject("android.util.Singleton",gDefault,"mInstance");

    Class<?> classInterface = Class.forName("android.app.IActivityManager");
    Object proxy = Proxy.newProxyInstance(classInterface.getClassLoader(),
            new Class<?>[]{classInterface},new AMNInvocationHanlder(mInstance));
    Reflex.setFieldObject("android.util.Singleton",gDefault,"mInstance",proxy);
}

} ```

我們呼叫AmsHookHelperUtils.hookAmn();然後啟動一個新的Activity,執行日誌如下:

這樣我們就成功Hook了AMN的getDefault方法。

2.3 如何啟動一個未註冊的Activity

如何啟動一個未註冊的Activity,首先我們瞭解Activity的啟動流程,App的啟動流程已經在上篇文章中講解了,APP啟動流程解析,還不瞭解的小夥伴,可先移步至上篇文章。假設現在MainActivity,Main2Activity,Main3Activity,其中Main3Activity未註冊,我們在MainActivity中啟動Main3Activity,當啟動Main3Activity的時候,AMS會在配置檔案中檢查,是否有Main3Activity的配置資訊如果不存在則報錯,存在則啟動Main3Activity。

所以我們可以做的是,將要啟動的Activity傳送給AMS之前,將要啟動的Activity替換未已經註冊Activity Main2Activity,這樣AMS就可以檢驗通過,當AMS要啟動目標Activity的時候再將Main2Activity替換為真正要啟動的Activity。

首先我們按照上面邏輯先對startActivity方法進行Hook,這裡採用對AMN Hook的方式。和上述程式碼一樣,不一樣的地方在於mInstance的代理類不同。

新建一個AMNInvocationHanlder1物件同樣繼承自InvocationHandler,只攔截startActivity方法。

html if (method.getName().equals(actionName)){}

在這裡我們要做的就是將要啟動的Main3Activity替換為Main2Activity,這樣能繞過AMS的檢驗,首先我們從目標方法中取出目標Activity。

html Intent intent; int index = 0; for (int i = 0;i<args.length;i++){ if (args[i] instanceof Intent){ index = i; break; } }

你可能會問你怎麼知道args中一定有intent類的引數,因為invoke方法中最終會執行

html return method.invoke(target,args);

表示會執行原本的方法,而我們來看原本的startActivity方法如下:

html int var16 = ActivityManagerNative.getDefault().startActivity(whoThread, intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null?target.mEmbeddedID:null, requestCode, 0, (String)null, (ParcelFileDescriptor)null, options);

所以我們說args中肯定有個intent型別的引數,獲取真實目標Activity之後,我們獲取目標的包名

html intent = (Intent) args[index]; String packageName = intent.getComponent().getPackageName();

新建一個Intent 將intent設定為 冒充者Main2Activity的相關資訊

html Intent newIntent = new Intent(); ComponentName componentName = new ComponentName(packageName,Main2Activity.class.getName()); newIntent.setComponent(componentName);

html args[index] = newIntent;

這樣目標Activity就被替換成了Main2Activity,不過這個冒充者還要將原本的目標攜帶過去,等待真正開啟的時候再替換回來,否則就真的啟動這個冒充者了

html newIntent.putExtra(AmsHookHelperUtils.TUREINTENT,intent);

這個時候我們呼叫這個方法什麼都不做,這個時候啟動Main3Activity

html startActivity(new Intent(this,Main3Activity.class));

顯示的其實是Main2Activity,如圖所示:

這樣說明我們的冒充者已經成功替換了真實目標,所以我們接下來要在啟動的時候,將冒充者再重新替換為目標者,ActivityThread通過mH發訊息給AMS

html synchronized(this) { Message msg = Message.obtain(); msg.what = what; msg.obj = obj; msg.arg1 = arg1; msg.arg2 = arg2; this.mH.sendMessage(msg); }

AMS收到訊息後進行處理

public void handleMessage(Message msg) {     ActivityThread.ActivityClientRecord data;     switch(msg.what) {     case 100:         Trace.traceBegin(64L, "activityStart");         data = (ActivityThread.ActivityClientRecord)msg.obj;         data.packageInfo = ActivityThread.this.getPackageInfoNoCheck(data.activityInfo.applicationInfo, data.compatInfo);         ActivityThread.this.handleLaunchActivity(data, (Intent)null);         Trace.traceEnd(64L);

mH是Handler型別的訊息處理類,所以sendMessage方法會呼叫callback,Handler訊息處理機制可看我之前一篇部落格

深入理解Android訊息機制,所以我們可以對callback欄位進行Hook。如果不明白怎麼辦?關注我!

新建hookActivityThread方法,首先我們獲取當前的ActivityThread物件

html Object currentActivityThread = Reflex.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");

然後獲取物件的mH物件

html Handler mH = (Handler) Reflex.getFieldObject(currentActivityThread, "mH");

將mH替換為我們的自己自定義的MyCallback。

html Reflex.setFieldObject(Handler.class, mH, "mCallback", new MyCallback(mH));

自定義MyCallback首先 Handler.Callback介面,重新處理handleMessage方法

```html @Override public boolean handleMessage(Message msg) {

switch (msg.what) {

    case 100:
        handleLaunchActivity(msg);
        break;
    default:
        break;

}

mBase.handleMessage(msg);
return true;

} ```

我們獲取傳遞過來的目標物件

html Object obj = msg.obj; Intent intent = (Intent) Reflex.getFieldObject(obj, "intent");

然後從目標物件中取出攜帶過來的真實物件,並將intent修改為真實目標物件的資訊,這樣就可以啟動真實的目標Activity

html Intent targetIntent = intent.getParcelableExtra(AmsHookHelperUtils.TUREINTENT); intent.setComponent(targetIntent.getComponent());

html MyCallbackt如下

```html * * Created by Huanglinqing on 2019/4/30. /

public class MyCallback implements Handler.Callback {

Handler mBase;

public MyCallback(Handler base) {
    mBase = base;
}

@Override
public boolean handleMessage(Message msg) {

    switch (msg.what) {

        case 100:
            handleLaunchActivity(msg);
            break;
        default:
            break;

    }

    mBase.handleMessage(msg);
    return true;
}

private void handleLaunchActivity(Message msg) {

    Object obj = msg.obj;
    Intent intent = (Intent) Reflex.getFieldObject(obj, "intent");
    Intent targetIntent = intent.getParcelableExtra(AmsHookHelperUtils.TUREINTENT);
    intent.setComponent(targetIntent.getComponent());
}

} ```

這個時候再啟動未註冊的Main3Activity,就可以成功啟動了

html startActivity(new Intent(this,Main3Activity.class));

這樣我們就成功的啟動了未註冊Activity