Apk加固原理解析

来源:互联网 发布:linux创建c文件 编辑:程序博客网 时间:2024/05/16 13:28

一、背景
为了更好的防止Apk被反编译,将Apk加密后打入宿主Apk包中,宿主启动时,解密Apk然后启动Apk。

二、工作原理
通过反射的方法替换ClassLoader及资源路径,使得启动的都是加固的Apk。

三、启动过程分析
加固的Apk大致的启动过程是这样的:
宿主Apk启动 -> 宿主Application中解密Apk -> 替换ClassLoader -> 替换资源路径 -> 替换Application对象(若APk中存在的话)

3.1 解密Apk
此步骤只是将asset下的加密的Apk文件拷贝到sd卡上,拷贝过程中同时解密。

3.2 替换ClassLoader
ActivityThread类里有个成员变量mPackages,mPackages存储的是LoadedApk对象,LoadedApk类的成员变量mClassLoader就是apk包类加载器,也就是要替换的ClassLoader。

3.2.1 LoadedApk创建过程
程序启动的时候,ActivitThread在handleBindApplication函数中创建Application对象。下面贴出关键代码

private void handleBindApplication(AppBindData data) {    ...    // 创建LoadedApk对象    data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);    ...    //调用LoadedApk的makeApplication方法创建Application对象    Application app = data.info.            makeApplication(data.restrictedBackupMode, null);    mInitialApplication = app;    ...    // 调用Application的onCreate方法,至此Application对象创建完毕    try {        mInstrumentation.callApplicationOnCreate(app);    } catch (Exception e) {        if (!mInstrumentation.onException(app, e)) {            throw new RuntimeException(                "Unable to create application " + app.getClass().getName()                + ": " + e.toString(), e);        }    }}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) {    WeakReference<LoadedApk> ref;    if (differentUser) {        // Caching not supported across users        ref = null;    } else if (includeCode) {        ref = mPackages.get(aInfo.packageName);    } else {        ref = mResourcePackages.get(aInfo.packageName);    }    // 首先从mPackages从读取    LoadedApk packageInfo = ref != null ? ref.get() : null;    // 首次启动,mPackages中是空的    if (packageInfo == null || (packageInfo.mResources != null            && !packageInfo.mResources.getAssets().isUpToDate())){        //创建LoadedApk对象        packageInfo = new LoadedApk(this, aInfo, compatInfo,                      baseLoader,securityViolation, includeCode &&                 (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0,                 registerPackage);        ...        // 将LoadedApk对象保存到mPackages中        if (differentUser) {            // Caching not supported across users        } else if (includeCode) {             mPackages.put(aInfo.packageName,                 new WeakReference<LoadedApk>(packageInfo));        } else {            mResourcePackages.put(aInfo.packageName,                 new WeakReference<LoadedApk>(packageInfo));        }    }    return packageInfo;}

至此LoadedApk对象创建完毕。

3.2.2 ClassLoader创建过程
在LoadedApk对象创建的时候,并没有创建ClassLoader,看下LoadedApk构造函数。

public LoadedApk(ActivityThread activityThread,             ApplicationInfo aInfo,CompatibilityInfo compatInfo,            ClassLoader baseLoader,boolean securityViolation,            boolean includeCode, boolean registerPackage) {    final int myUid = Process.myUid();    aInfo = adjustNativeLibraryPaths(aInfo);    mActivityThread = activityThread;    mApplicationInfo = aInfo;    mPackageName = aInfo.packageName;    mAppDir = aInfo.sourceDir;    mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;    mSplitAppDirs = aInfo.splitSourceDirs;    mSplitResDirs = aInfo.uid == myUid ? aInfo.splitSourceDirs : aInfo.splitPublicSourceDirs;    mOverlayDirs = aInfo.resourceDirs;    mSharedLibraries = aInfo.sharedLibraryFiles;    mDataDir = aInfo.dataDir;    mDataDirFile = mDataDir != null ? new File(mDataDir) : null;    mLibDir = aInfo.nativeLibraryDir;    mBaseClassLoader = baseLoader;    mSecurityViolation = securityViolation;    mIncludeCode = includeCode;    mRegisterPackage = registerPackage;    mDisplayAdjustments.setCompatibilityInfo(compatInfo);}

那ClassLoader对象是什么时候创建的呢?从LoadedApk源码中,看到ClassLoader对象是在getClassLoader方法中创建的。

public ClassLoader getClassLoader() {    if (mIncludeCode && !mPackageName.equals("android")) {        // apk和lib路径        final List<String> zipPaths = new ArrayList<>();        final List<String> apkPaths = new ArrayList<>();        final List<String> libPaths = new ArrayList<>();        zipPaths.add(mAppDir);        if (mSplitAppDirs != null) {            Collections.addAll(zipPaths, mSplitAppDirs);        }        libPaths.add(mLibDir);        //添加各种路径apk和lib路径        ...        final String zip = TextUtils.join(File.pathSeparator, zipPaths);        ...        final String lib = TextUtils.join(File.pathSeparator, libPaths);        mClassLoader = ApplicationLoaders.getDefault().                getClassLoader(zip, lib, mBaseClassLoader);    } else {        if (mBaseClassLoader == null) {            mClassLoader = ClassLoader.getSystemClassLoader();        } else {            mClassLoader = mBaseClassLoader;        }    }    return mClassLoader;}

而getClassLoader是在什么时候被调用的呢?创建Application对象的时候,调用了LoadedApk的makeApplication方法,那在该方法中必然用到ClassLoader,看下该方法的代码。

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 {        // 获取ClassLoader对象,即在此创建了ClassLoader对象        java.lang.ClassLoader cl = getClassLoader();        if (!mPackageName.equals("android")) {            initializeJavaContextClassLoader();        }        ContextImpl appContext =            ContextImpl.createAppContext(mActivityThread, this);        //创建Application对象        app = mActivityThread.mInstrumentation.newApplication(                cl, appClass, appContext);        appContext.setOuterContext(app);    } catch (Exception e) {        if (!mActivityThread.mInstrumentation.            onException(app, e)) {            throw new RuntimeException(                "Unable to instantiate application " + appClass                + ": " + e.toString(), e);        }    }    mActivityThread.mAllApplications.add(app);    mApplication = app;    ...    return app;}

至此已经清楚LoadedApk中的mClassLoader的创建和使用。
3.2.3 替换ClassLoader
宿主Apk包的ClassLoader已创建完毕,那在何时替换ClassLoader呢?选择在Application的attachBaseContext方法中进行执行替换动作。因为attachBaseContext是自定义的Application类中覆盖的父类方法中第一个被调用的。那具体何时调用的呢?来看下源码。

//Instrumentation类中创建Application对象public Application newApplication(ClassLoader cl,         String className, Context context)        throws InstantiationException, IllegalAccessException,        ClassNotFoundException {    return newApplication(cl.loadClass(className), context);}static public Application newApplication(Class<?> clazz,         Context context) throws InstantiationException,        IllegalAccessException, ClassNotFoundException {    Application app = (Application)clazz.newInstance();    // attach Application context    app.attach(context);    return app;}

再来看下Application的attach方法。

final void attach(Context context) {    //在此处调用了attachBaseContext    attachBaseContext(context);    mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;}

至此已经清楚,在Application被创建的时候,就调用了attachBaseContext方法,完全可以在此方法中修改ClassLoader。
自定义Application类,覆盖attachBaseContext方法。

public class App extends Application {    private Object obj; // 加固包的Application类对象    static {        // 实现ClassLoader类,资源,Application替换的库        System.loadLibrary("native-lib");    }    @Override    protected void attachBaseContext(Context base) {        super.attachBaseContext(base);        String appName = getReinforceApkAppName();        // 实现ClassLoader类替换,资源加载路径替换,并创建加固包的Application对象        obj = onAppAttach(this, appName);    }    @Override    public void onCreate() {        super.onCreate();        //实现Application对象替换,将宿主的Application对象替换成加固包的Application对象        onAppCreate(this, obj, null);    }    // 以下是加载资源    protected static AssetManager mAssetManager;//资源管理器    protected Resources mResources;//资源    protected Resources.Theme mTheme;//主题    @Override    public AssetManager getAssets() {        AssetManager assetManager = mAssetManager == null ? super.getAssets() : mAssetManager;        return assetManager;    }    @Override    public Resources getResources() {        if(mAssetManager != null && mResources == null) {            Resources superRes = super.getResources();            mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());        }        return mResources == null ? super.getResources() : mResources;    }    @Override    public Resources.Theme getTheme() {        if(mResources != null && mTheme == null) {            mTheme = mResources.newTheme();            mTheme.setTo(super.getTheme());        }        return mTheme == null ? super.getTheme() : mTheme;    }    private native void onAppCreate(Application app, Object obj, String application);    private native Object onAppAttach(Application app, String application);}jobject Java_com_jd_apploader_App_onAppAttach(JNIEnv *env,         jobject thiz, jobject app, jstring appName) {    InitPackageName(env, app);    apkFilePath = GetApkFilePath(env, app);    apkLibPath = GetApkLibPath(env, app);    apkFileName = GetApkFileName(apkFilePath);    // 解密Apk    bool ret = CopyApkFile(env, apkFileName, apkFilePath, apkLibPath, app);    if(!ret) {        return NULL;    }    jstring strApkFileName = env->NewStringUTF(apkFileName);    jstring strApkFilePath = env->NewStringUTF(apkFilePath);    jstring strApkLibPath = env->NewStringUTF(apkLibPath);    /**    * 利用反射技术,找到ClassLoader对象,并替换    * 以及替换资源路径    */    // 获取ActivityThread的mPackages成员变量    jclass clsActThread = env->FindClass(        "android/app/ActivityThread");    jmethodID currentActivityThread = env->GetStaticMethodID(        clsActThread, "currentActivityThread",         "()Landroid/app/ActivityThread;");    jobject actThread = env->CallStaticObjectMethod(clsActThread,         currentActivityThread);    jfieldID mPackagesId;    if(GetOSVersion() >= 19) {        mPackagesId = env->GetFieldID(clsActThread, "mPackages",             "Landroid/util/ArrayMap;");    } else {        mPackagesId = env->GetFieldID(clsActThread, "mPackages",             "Ljava/util/HashMap;");    }    jobject mPackages = env->GetObjectField(actThread,                 mPackagesId);    // 获取LoadedApk对象    jclass mapCls = env->FindClass("java/util/Map");    jmethodID getId = env->GetMethodID(mapCls, "get",         "(Ljava/lang/Object;)Ljava/lang/Object;");    jobject  wr = env->CallObjectMethod(mPackages, getId,         jPackageName);    jclass wrclass = env->FindClass(        "java/lang/ref/WeakReference");    jmethodID methodGet = env->GetMethodID(wrclass, "get",         "()Ljava/lang/Object;");    jobject objApkLoader = env->CallObjectMethod(wr, methodGet);    // 获取LoadedApk中的ClassLoader对象    jclass clsApkLoader = env->FindClass("android/app/LoadedApk");    jfieldID fieldClassLoader = env->GetFieldID(clsApkLoader,         "mClassLoader", "Ljava/lang/ClassLoader;");    jobject classDexLoader = env->GetObjectField(objApkLoader,         fieldClassLoader);    // 创建Apk的ClassLoader,并以宿主的ClassLoader对象为父ClassLoader    jclass dexClassLoader = env->FindClass(        "dalvik/system/DexClassLoader");    jmethodID initDexLoaderMethod = env->GetMethodID(        dexClassLoader, "<init>",        "(Ljava/lang/String;Ljava/lang/String;" +        "Ljava/lang/String;Ljava/lang/ClassLoader;)V");    jobject dexLoader = env->NewObject(dexClassLoader,         initDexLoaderMethod, strApkFileName, strApkFilePath,         strApkLibPath, classDexLoader);    // 替换ClassLoader    env->SetObjectField(objApkLoader, fieldClassLoader,         dexLoader);    // 设置App类中的mAssetManager,mResources,mTheme    LoadResource(env, strApkFileName);    // 替换LoadedApk中的mResDir,mResDir是资源加载路径,在此将该路径替换到解密后的Apk路径,这样应用就可以正常加载资源    jfieldID resDirId = env->GetFieldID(clsApkLoader, "mResDir",         "Ljava/lang/String;");    env->SetObjectField(objApkLoader, resDirId, strApkFileName);    // 若apk包中存在自定义的Application,则创建该Application    jobject gameApp = NULL;    if(appName != NULL) {        gameApp = createGameApplication(env, appName, strApkFileName);    }    return gameApp;}void Java_com_jd_apploader_App_onAppCreate(JNIEnv *env,             jobject obj, jobject app, jobject gameApp,             jstring appName) {    if(gameApp == NULL) {        if(appName == NULL) {            free(apkFilePath);            free(apkLibPath);            free(apkFileName);            return;        } else {            gameApp = createGameApplication(env, appName,                env->NewStringUTF(apkFileName));        }    }    jclass clsActThread = env->FindClass(        "android/app/ActivityThread");    jmethodID curThreadMthId = env->GetStaticMethodID(        clsActThread, "currentActivityThread",         "()Landroid/app/ActivityThread;");    jobject actThread = env->CallStaticObjectMethod(clsActThread,        curThreadMthId);    // 替换ActivityThread中的mInitialApplication    jfieldID initAppId = env->GetFieldID(clsActThread,        "mInitialApplication", "Landroid/app/Application;");    jobject mInitialApplication = env->GetObjectField(actThread,        initAppId);    env->SetObjectField(actThread, initAppId, gameApp);    // 替换mAllApplications中的宿主Application对象    jfieldID allAppId = env->GetFieldID(clsActThread,        "mAllApplications", "Ljava/util/ArrayList;");    jobject mAllApplications = env->GetObjectField(actThread,         allAppId);    jclass clsArrList = env->FindClass("java/util/ArrayList");    jmethodID removeMth = env->GetMethodID(clsArrList, "remove",         "(Ljava/lang/Object;)Z");    env->CallBooleanMethod(mAllApplications, removeMth,         mInitialApplication);    // 替换mBoundApplication中LoadedApk里的mApplication对象    jfieldID boundAppId = env->GetFieldID(clsActThread,         "mBoundApplication",         "Landroid/app/ActivityThread$AppBindData;");    jobject mBoundApplication = env->GetObjectField(actThread,         boundAppId);    jclass clsAppBindData = env->FindClass(        "android/app/ActivityThread$AppBindData");    jfieldID infoId = env->GetFieldID(clsAppBindData, "info",         "Landroid/app/LoadedApk;");    jobject info = env->GetObjectField(mBoundApplication, infoId);    jclass clsLoadedApk = env->FindClass("android/app/LoadedApk");    jfieldID appId = env->GetFieldID(clsLoadedApk, "mApplication",         "Landroid/app/Application;");    env->SetObjectField(info, appId, gameApp);    jclass clsApplication = env->FindClass(        "android/app/Application");    jmethodID onCreate = env->GetMethodID(clsApplication,         "onCreate", "()V");    env->CallVoidMethod(gameApp, onCreate);}

至此所有需要替换的对象均已替换完成,应用可正常运行。

3.2.4 资源路径替换说明
在Java_com_jd_apploader_App_onAppAttach函数中,替换了LoadedApk里的mResDir资源路径,这个mResDir是干什么用的呢?从源码分析,mResDir是在LoadedApk的构造函数中被初始化的。

public LoadedApk(ActivityThread activityThread,             ApplicationInfo aInfo,    CompatibilityInfo compatInfo, ClassLoader baseLoader,    boolean securityViolation, boolean includeCode, boolean registerPackage) {    ...    mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;    ...}

从源码上看,mResDir实际上就是Apk文件路径。
mResDir是在何时被使用的呢?看到LoadedApk类的getResources方法使用到mResDir。

public Resources getResources(ActivityThread mainThread) {    if (mResources == null) {        mResources = mainThread.getTopLevelResources(mResDir,             mSplitResDirs, mOverlayDirs,        mApplicationInfo.sharedLibraryFiles,             Display.DEFAULT_DISPLAY, null, this);    }    return mResources;}

原来mResDir是用于创建Resources对象,而所有的资源都是通过Resource对象加载的。无论是Application、Activity还是View,加载资源的时候,都是调用Context的getResource方法获取Resource对象。Context类的getResource方法是抽象方法,具体实现是在ContextWrapper类中,Appliation和Activity实际上就是集成ContextWrapper类

public Resources getResources(){    return mBase.getResources();}

看到是通过mBase对象获取的Resource对象,mBase也是Context对象,这个mBase是怎么来的呢?从创建Application和Activity的源码出来看。LoadedApk类中makeApplication方法是创建Application对象的。

public Application makeApplication(boolean forceDefaultAppClass,                    Instrumentation instrumentation) {    ...    ContextImpl appContext = ContextImpl.        createAppContext(mActivityThread, this);    app = mActivityThread.mInstrumentation.newApplication(cl,        appClass, appContext);    ...}

在newApplication方法中传入了ContextImpl,前面已经分析过,最终这个对象是传到Application对象的attachBaseContext方法中,attachBaseContext方法是在ContextWrapper中实现的。

protected void attachBaseContext(Context base) {    if (mBase != null) {        throw new IllegalStateException("Base context already set");    }    mBase = base;}

由此可见,Application的mBase实际上就是ContextImpl对象。那Activity是否也是如此呢?来看下Activity的创建,Activity对象是在ActivityThread的performLaunchActivity方法中创建的。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {    ...    Activity activity = null;    // 创建Activity实例    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();    activity = mInstrumentation.newActivity(    cl, component.getClassName(), r.intent);    ...    // 创建base Context实例    Context appContext = createBaseContextForActivity(r, activity);    ...    // attach base Context    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);}

再来看下createBaseContextForActivity方法,看看究竟创建的是什么对象。

private Context createBaseContextForActivity(            ActivityClientRecord r, final Activity activity) {    ...    ContextImpl appContext = ContextImpl.createActivityContext(            this, r.packageInfo, displayId, r.overrideConfig);    appContext.setOuterContext(activity);    Context baseContext = appContext;    ...    return baseContext;}

从代码中很清楚看到创建的也是ContextImpl对象。而Activity的attach方法最终调到的也是ContextWrap的attachBaseContext方法。因此,mBase实际上就是ContextImpl对象,那么再来看下ContextImpl对象getResources方法。

public Resources getResources() {    return mResources;}

getResources方法返回的是mResources成员变量,再来看mResources是什么时候创建的。

private ContextImpl(ContextImpl container,         ActivityThread mainThread,        UserHandle user, boolean restricted,Display display,         Configuration overrideConfiguration,         int createDisplayWithId) {    ...    // 调用LoadedApk的getResource方法获取Resources对象    Resources resources = packageInfo.getResources(mainThread);    if (resources != null) {        if (displayId != Display.DEFAULT_DISPLAY            || overrideConfiguration != null            || (compatInfo != null && compatInfo.applicationScale            != resources.getCompatibilityInfo().applicationScale))         {            resources = mResourcesManager.getTopLevelResources(                packageInfo.getResDir(),                packageInfo.getSplitResDirs(),                 packageInfo.getOverlayDirs(),                packageInfo.getApplicationInfo().                    sharedLibraryFiles, displayId,                overrideConfiguration, compatInfo);        }    }    mResources = resources;    ...}

mResources是在ContextImpl构造函数中创建的,正是通过LaodedApk对象的getResources方法获取。前面已经分析过LoadedApk的getResources方法就是用mResDir所指的路径创建Resources对象。因此在Application的attachBaseContext方法中替换了mResDir,后续启动的Activity都是从加固的apk中获取资源。

四、存在的问题及解决方案

4.1 加固Apk的Activity theme问题
加固Apk无法在Menifest中为Activity指定Theme,必须在代码中的onCreate方法手动设置。因为若在Menifest中设置Theme,必须在宿主的styles.xml中声明相关的style。此时使用的style id是宿主的id值,而Activity启动的时候查找的是加固Apk中的style id值,这两个是不一样的,因此无法找到对应的style,导致Theme设置失效。
4.2 宿主无法使用资源
由于进程的Resources路径被替换成加固的Apk路径,因此宿主的资源是无法被找到的,导致宿主无法使用资源。
4.3 解决方案
以上两个问题都是资源路径被替换,宿主无法查找到自己的资源引起的。如果不替换资源,而是将两个资源路径合并就可以宿主资源无法被查找到的问题。通过代理Instrumentation类的方法,在Activity启动的时候,将加固的Apk路径添加到资源查找列表中即可。

public class CustomInstrumentation extends Instrumentation {    // 加固Apk路径    private static final String APK_PATH =             "/data/data/files/dex/loader.apk";    Instrumentation mBase;    public CustomInstrumentation(Instrumentation base) {        mBase = base;    }    @Override    public Activity newActivity(ClassLoader cl, String className,             Intent intent) throws InstantiationException,             IllegalAccessException, ClassNotFoundException {        return mBase.newActivity(cl, className, intent);    }    @Override    public Activity newActivity(Class<?> clazz, Context context,             IBinder token, Application application, Intent intent,                 ActivityInfo info, CharSequence title,                 Activity parent, String id,                 Object lastNonConfigurationInstance) throws                 InstantiationException, IllegalAccessException {        return mBase.newActivity(clazz, context, token,             application, intent, info, title, parent, id,             lastNonConfigurationInstance);    }    @Override    public void callActivityOnCreate(Activity activity,         Bundle icicle) {        // Activity被启动的是调用onCreate方法之前,将加固的apk路径添加到Activity的资源路径列表中        addAssetPath(activity);        mBase.callActivityOnCreate(activity, icicle);    }    @TargetApi(Build.VERSION_CODES.LOLLIPOP)    @Override    public void callActivityOnCreate(Activity activity,         Bundle icicle, PersistableBundle persistentState) {        addAssetPath(activity);        mBase.callActivityOnCreate(activity, icicle, 、            persistentState);    }    // 用反射的方法,添加资源路径列表    private void addAssetPath(Activity activity) {        AssetManager assetManager = activity.getAssets();        try {            Method addAssetPath = AssetManager.class.                getDeclaredMethod("addAssetPath",                 String.class);            addAssetPath.setAccessible(true);            addAssetPath.invoke(assetManager, APK_PATH);        } catch (NoSuchMethodException e) {            e.printStackTrace();        } catch (InvocationTargetException e) {            e.printStackTrace();        } catch (IllegalAccessException e) {            e.printStackTrace();        }    }}

至此加固的Apk路径已经成功添加到资源路径列表中,测试过程中发现资源id冲突的问题,及宿主的资源ID和加固Apk的资源ID相同,导致资源无法找到正确的资源。通过修改aapt源代码,在编译的时候修改宿主资源ID,这样就不会与加固的Apk资源ID冲突。
具体可参考:aapt修改资源ID

以上仅是自己研究记录,可能存在错误,尤其是C代码。
附上项目地址:Apk加固Github