Apk應用安全加固所需瞭解的Application啓動流程

本文使用Android Q(API 29)版本源代碼進行講解

很多人認爲Android應用加載入口是Application的onCreate,實則不然。當點擊進入應用時,Zygote進程會fork出一個獨立進程, 通過RuntimeInit#findStaticMain找到ActivityThread#main並在ZygoteInit#main中進行調用


// ZygoteInit#main
public static void main(String argv[]) {
    ....
    Runnable caller;
    try {
       ....
       // 最終會調用到findStaticMain
       caller = zygoteServer.runSelectLoop(abiList);
    } catch (Throwable ex) {
       ....
    } finally {
       ....
    }
       ....
    if (caller != null) {
        caller.run();
    }
}

public class RuntimeInit {
    protected static Runnable findStaticMain(String className, String[] argv,
            ClassLoader classLoader) {
        Class<?> cl;
        try {
            // 使用反射拿到類實例
            cl = Class.forName(className, true, classLoader);
        } catch (ClassNotFoundException ex) {
            .....
            Method m;
            try {
                // 獲取main方法
                m = cl.getMethod("main", new Class[] { String[].class });
            } 
            .....
        }
        .....
        // 封裝返回
        return new MethodAndArgsCaller(m, argv);
    }

    static class MethodAndArgsCaller implements Runnable {
        private final Method mMethod;
        private final String[] mArgs;
        public MethodAndArgsCaller(Method method, String[] args) {
            mMethod = method;
            mArgs = args;
        }
        public void run() {
            try {
                mMethod.invoke(null, new Object[] { mArgs });
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            } catch (InvocationTargetException ex) {
                Throwable cause = ex.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                }
                throw new RuntimeException(ex);
            }
        }
    }

}

 從此Applaction才真正開始了加載流程,所以Android應用加載入口可以理解是ActivityThread#main。

ActivityThread#main


public static void main(String[] args) {
        ....
        Looper.prepareMainLooper();
        ....
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        ....
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
}

ActivityThread#main這個方法看上去非常親切,倘若C中的void main, 我們來看看這裏做了哪些事情。

首先是Looper#prepareMainLooper,我們看看其中的具體實現。


// 主線程
public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
}

// 其他線程
public static void prepare() {
        prepare(true);
}

private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
}


public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
}

我們可以清晰的看到Looper#prepare(boolean)方法是一個private類型,僅可通過prepareMainLooper與prepare兩個public的靜態方法進行調用,從參數字面意思可以看出主線程Looper是不允許退出的。 系統通過這個方法創建了一個Looper實例。

回到主幹,接下來創建了一個ActivityThread的對象實例,ActivityThread其實並非真正意義上的Thread,因爲其並沒有繼承Thread,只是給人線程的感覺,其實其仍然運行在Zygote fork出的進程中。接下來系統執行了ActivityThread#attach開始加載應用,這個後面會講,接下來系統獲取隨着AcitvityThread對象實例的成員實例Handler,將其保存到靜態成員。


final Handler getHandler() {
    return mH;
}

class ActivityThread extends ClientTransactionHandler {
    ....
    final H mH = new H(); 
    ....
}


class Hanlder() {
    /**
      * Default constructor associates this handler with the {@link Looper} for the
      * current thread.
      * If this thread does not have a looper, this handler won't be able to receive messages
      * so an exception is thrown.
      */
    public Handler() {
        this(null, false);
    } 
}

我們知道採用無參方式創建Handler實例時,Handler實例會默認綁定當前線程,如果當前線程沒有綁定Looper將不會收到任何消息,在前面我們已經爲當前線程綁定了Looper實例。

接下來調用了Looper.loop()開始進行輪詢,這裏有人可能會問爲什麼採用主線程死循環不會出現ANR問題,這裏就涉及到Linux的epoll/pipe機制了,當主線程消息隊列中沒有消息時,會主動釋放CPU資源進入休眠狀態,當有新的消息到達時在通過pipe管道喚醒主線程工作,這裏採用是epoll機制,實現IO多路複用。Android作爲基於Linux內核的開源平臺當然也支持這種機制,Handler就是採用這種機制進行實現的。所謂ANR是系統無法及時相應用戶的操作,可能由於線程堵塞造成的,這裏的Looper所採用的機制不會出現ANR的問題。

ActvityThread#attach(boolean, long)

回到主幹,我們進入ActvityThread#attach(boolean, long)看看接下來發生了哪些事情。


public final class ActivityThread extends ClientTransactionHandler {
    ....
    private static volatile ActivityThread sCurrentActivityThread;
    final ApplicationThread mAppThread = new ApplicationThread(); 
    ....
    private void attach(boolean system, long startSeq) {
        sCurrentActivityThread = this;
        mSystemThread = system;
        // 非系統線程
        if (!system) {
            ...
            final IActivityManager mgr = ActivityManager.getService();
            try {
               mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
               throw ex.rethrowFromSystemServer();
            }
            ....
        } else {
            ....
        }
        ....
    }
    public static ActivityThread currentActivityThread() {
        return sCurrentActivityThread;
    }
}

我們看到這裏首先會看到會將當前ActivityThread對象實例保存到了sCurrentActivityThread這個靜態變量中,作爲一個私有靜態變量僅可通過currentActivityThread靜態方法獲取的,這裏很關鍵,後續加固過程中可通過反射拿到這個ActivityThread對象實例對其進行篡改,接下來通過mSystemThread標記當前ActivityThread是否來自系統。Google編碼規範還是很給力的,通過變量名可以清晰知道這個變量到底是靜態成員還是普通成員。

接下來通過ActivityManager獲取到AMS(ActivityManagerService)的Binder對象,AMS運行於system_service的子線程中,此時其實完成的是建立了跨進程的通訊,我們可以進入AcitvityManagerService查看ActivityManagerService#attachApplication中具體完成了哪些事情。

ActivityManagerService#attachApplication


public class ActivityManagerService extends IActivityManager.Stub
        implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
    ....
    @Override
    public final void attachApplication(IApplicationThread thread, long startSeq) {
        synchronized (this) {
            ....
            attachApplicationLocked(thread, callingPid, callingUid, startSeq);
            ....
        }
    }
    ....
    private final boolean attachApplicationLocked(IApplicationThread thread,
            int pid, int callingUid, long startSeq) { 
        ...
        thread.bindApplication(processName, appInfo, providers,
                        instr2.mClass,
                        profilerInfo, instr2.mArguments,
                        instr2.mWatcher,
                        instr2.mUiAutomationConnection, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
                        new Configuration(app.getWindowProcessController().getConfiguration()),
                        app.compat, getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial, autofillOptions, contentCaptureOptions);
        ...
    }
}

我們可以發現最終回調了ApplicationThread#bindApplication,我們回到ApplicationThread做了哪些事情。ApplicationThread作爲一個內部類在ActivityThread類中。


public final class ActivityThread extends ClientTransactionHandler { 
    private class ApplicationThread extends IApplicationThread.Stub {
        public final void bindApplication(String processName, ApplicationInfo appInfo,List<ProviderInfo> providers, ComponentName instrumentationName
       ,ProfilerInfo profilerInfo, Bundle instrumentationArgs
       ,IInstrumentationWatcher instrumentationWatcher
       ,IUiAutomationConnection instrumentationUiConnection
       ,int debugMode,boolean enableBinderTracking, boolean trackAllocation
       ,boolean isRestrictedBackupMode,boolean persistent
       ,Configuration config,CompatibilityInfo compatInfo
       ,Map services, Bundle coreSettings
       ,String buildSerial, AutofillOptions autofillOptions
       ,ContentCaptureOptions contentCaptureOptions) {
            ....
            AppBindData data = new AppBindData();
            data.processName = processName;
            data.appInfo = appInfo;
            data.providers = providers;
            data.instrumentationName = instrumentationName;
            data.instrumentationArgs = instrumentationArgs;
            data.instrumentationWatcher = instrumentationWatcher;
            data.instrumentationUiAutomationConnection = instrumentationUiConnection;
            data.debugMode = debugMode;
            data.enableBinderTracking = enableBinderTracking;
            data.trackAllocation = trackAllocation;
            data.restrictedBackupMode = isRestrictedBackupMode;
            data.persistent = persistent;
            data.config = config;
            data.compatInfo = compatInfo;
            data.initProfilerInfo = profilerInfo;
            data.buildSerial = buildSerial;
            data.autofillOptions = autofillOptions;
            data.contentCaptureOptions = contentCaptureOptions;
            sendMessage(H.BIND_APPLICATION, data);
        }
}


看這密密麻麻的參數有些頭痛,設計者把這些參數封裝成一個AppBindData對象實例然後sendMessage,交給主線程的Handler來進行處理,這個Handler上文提過還記得嗎?我們看看這個Handler實現的handleMessage方法。

ActivityThread#handleBindApplication

public final class ActivityThread extends ClientTransactionHandler {
    class H extends Handler {
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BIND_APPLICATION:
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication");
                    AppBindData data = (AppBindData)msg.obj;
                    handleBindApplication(data);
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    break;
                ....
            } 
        }
    }

    private void handleBindApplication(AppBindData data) { 
        ....
        mBoundApplication = data;
        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
        Application app;
        ....
        try {
            ....
            app = data.info.makeApplication(data.restrictedBackupMode, null);
            mInitialApplication = app;
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);
                }
            }
            try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                ....
            }

        }finally {
            ....
        }
    }
}

我們可以在handleMessage中通過字段快速找到對應的處理方法,然後透傳至ActivityThread#handleBindApplication進行處理,首先將AppBindData對象實例保存在mBoundApplication中,這裏很關鍵,因爲AppBindData中的ApplicationData成員className保存待加載Application對象類名,加固過程需要反射進行篡改。

緊接着調用了getPackageInfoNoCheck方法,我們進入其中看看做了哪些事情。


public final class ActivityThread extends ClientTransactionHandler {
    public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,
            CompatibilityInfo compatInfo) {
        return getPackageInfo(ai, compatInfo, null, false, true, false);
    }

    private LoadedApk getPackageInfo(ApplicationInfo aInfo
         ,CompatibilityInfo compatInfo
         ,ClassLoader baseLoader, boolean securityViolation
         ,boolean includeCode, boolean registerPackage) {
            final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
            synchronized (mResourcesManager) {
                ....
                LoadedApk packageInfo = ref != null ? ref.get() : null;
                ....
                packageInfo =
                    new LoadedApk(this, aInfo, compatInfo, baseLoader,
                            securityViolation, includeCode
                            && (aInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0,         
                                registerPackage);
                ....
            if (differentUser) {
                ....
            } else if (includeCode) {
                mPackages.put(aInfo.packageName,
                        new WeakReference<LoadedApk>(packageInfo));
            } else {
                ....
            }
            return packageInfo;
        }
    }
}

還是透傳邏輯,這裏要注意得失倒數第二個參數,這裏是true,這也是mPackages對象唯一進行put的地方,其主要記錄packageName與LoadedApk的映射關係。這個地方也很重要,後續我們需要根據包名反射拿到其對應的LoadedApk對象,替換類加載器。

緊接着創建了LoadedApk對象實例,我們可以發現這個LoadedApk對象實例的mApplicationInfo其實使用的就是AppBindData#appInfo,這個也很關鍵。然後結束後將生成的LoadedApk返回。回到主幹,我們繼續往下看。

private void handleBindApplication(AppBindData data) { 
        ....
        mBoundApplication = data;
        data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
        Application app;
        ....
        try {
            ....
            app = data.info.makeApplication(data.restrictedBackupMode, null);
            mInitialApplication = app;
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    installContentProviders(app, data.providers);
                }
            }
            try {
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                ....
            }

        }finally {
            ....
        }
}

public class Instrumentation {
    ....
    public void callApplicationOnCreate(Application app) {
        app.onCreate();
    }
    ....
}

接下來使用剛生成的LoadedApk對象實例的makeApplication方法來生成Application對象實例,這裏我們後面會繼續說,緊接着將生成的Application對象實例保存到mInitialApplication中,然後調用installContentProviders方法完成contentProviders的加載過程,然後最後會通過調用callApplicationOnCreate方法完成Application對象實例的create生命週期,這裏可能會有人問了,那Application生命週期中的onAttach呢,onAttach其實隱藏在makeApplication方法內。 我們接下來看看makeApplication的具體實現。

LoadedApk#makeApplication


public final class LoadedApk {
    public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
        if (mApplication != null) {
            return mApplication;
        }

        Application app = null;

        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }

        try {
            java.lang.ClassLoader cl = getClassLoader();
            ....
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
        } catch (Exception e) {
            ....
        }
        mActivityThread.mAllApplications.add(app);
        mApplication = app;
        ....
        return app;
    }
}

public class Instrumentation {
    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        Application app = getFactory(context.getPackageName())
                .instantiateApplication(cl, className);
        app.attach(context);
        return app;
    }
}


我們發現在調用makeApplication方法時,首先會判斷mApplication是否已經被加載過,說明如果我們想重新makeApplication方法來生成其他Application對象實例,則必須對mApplication置空。接下來可以看出Application對象是根據mApplicationInfo.className反射拿到的類實例,然後通過newApplication方法來創建Application對象實例,通過查看newApplication實現可以發現還是系統還是使用反射來創建Application實例,並調用了Application的onAttach方法,然後將生成的Application對象實例存在Activity對象實例的mAllApplications中,後面加固過程我們可以處理這個列表。

由此我們可以發現,整個流程是Application.onAttach -> ContentProvider.onCreate -> Application.onCreate。

重定向Application步驟

1. 通過靜態方法ActivityThread#currentActivityThread(ActivityThread類型),即可拿到當前應用的ActivityThread實例

2. 修改currentActivityThread.mBoundApplication(AppBindData類型)內部成員appInfo(ApplicationInfo類型)的className字段

回顧一下,mBoundApplication是在ActivityThread#handleBindApplication中進行綁定,在Application.onAttach之前。

源工程的className是在殼工程AndroidManifest中記錄的

3. 修改currentActivityThread.mBoundApplication(AppBindData類型)內部成員info(ApplicationInfo類型)的mApplicationInfo(ApplicationInfo類型)的className字段

僅修改第二步即可,原因在於在構造LoadedApk對象實例時使用的是mBoundApplication(AppBindData類型)內部成員appInfo,所以他們指向的是同一個實例,不需要重複進行修改。具體參考ActivityThread#handleBindApplication的描述。

回顧一下,mBoundApplication是在ActivityThread#handleBindApplication中進行綁定,在Application.onAttach之前

4. 將currentActivityThread.mBoundApplication(AppBindData類型)內部成員info(LoadedApk類型)的mApplication設置NULL

這裏的目的是爲了後面重新調用LoadedApk#makeApplication生成源工程的Application對象實例。

5. 根據currentActivityThread.mInitialApplication(Application類型)將currentActivityThread.mAllApplications列表中的對象進行刪除

回顧一下currentActivityThread.mInitialApplication(Application類型)是在makeApplication後被賦值的。

6.  修改currentActivityThread.mBoundApplication(AppBindData類型)內部成員info(LoadedApk類型)的mClassLoader,將mClassLoader設置爲能夠加載源工程dex文件的類加載器,並將父加載器設置爲殼工程的,也就是當前的mClassLoader,這是利用類加載器的雙親委派機制。

這裏插一句題外話,有人會根據包名通過currentActivityThread.mPackages(ArrayMap類型),通過映射拿到LoadedApk,這也是一種獲取方式,我自測發現,其實都指向的是同一對象實例,所以修改一個地方就可以。

7. 反射調用currentActivityThread.mBoundApplication(AppBindData類型)內部成員info(LoadedApk類型)的makeApplication方法,重新生成源工程的application對象,重新設置currentActivityThread.mInitialApplication,然後手動onCreate完成Application的重定向

注意事項

其實Application啓動流程是十分複雜的,本文僅以重定向Application角度來解讀加載流程,其中省略了很多東西,暫時還不能講清楚。本文其實對於Application啓動流程的解讀也存在着很多錯誤問題,希望讀者在閱讀時切記要多加一些自己的思考。

參考資料

https://www.jianshu.com/p/7687b4f6b683

https://juejin.im/entry/58a560dc61ff4b0062a61833

https://blog.csdn.net/nbalichaoq/article/details/51967753

http://www.520monkey.com/archives/553

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