靠谱的app加固分享(已完成)

先来看看大概流程

加固俯瞰


在这里插入图片描述

1、编写加密方法,作为工具方法用于后续的加密和解密准备。

2、编写代理Application(ProxyApplication),作为加固后的apk的伪入口。(ProxyApplication作为伪入口时,需要将加密apk进行解密并重新加载于classLoader中)

3、对需要加密的apk的AndroidManifest文件的Application:name 标签经行更改为ProxyApplication,并用标签声明真正的Application入口和版本号。

4、将1、2步的文件打包成aar包。

5、解压aar包(于aarTemp文件夹),并将解压后的jar文件,编译成dex文件(Entrance.dex)(安卓虚拟机可识别的机器码文件)。

6、解压需要加密的apk(于apkTemp文件夹),遍历解压后的文件夹,取出所有dex文件,用1步中的加密方法对所有dex文件进行加密,并替换原本没加密的dex。
*注:Entrance.dex在aarTemp内,没被加密

7、将aarTemp中的dex文件,复制到apkTemp文件中,并将apkTemp压缩成apk文件。

8、对齐 & 签名(才能正常使用)
附上相关的代码

public class Main {
    public static void main(String[] args) {
        //第四步:解压arr(包含加密解密工具和ProxyApplication.java)
        File aarFile = new File("core/build/outputs/aar/core-debug.aar");
        File aarTemp = new File("lib/temp");
        Zip.unZip(aarFile, aarTemp);
        // 生成classes.dex
        File classesJar = new File(aarTemp, "classes.jar");
        File classesDex = new File(aarTemp, "classes.dex");
        Process process = null;
        //dx --dex --output out.dex in.jar
        try {
            process = Runtime.getRuntime().exec("cmd /c  dx --dex --output " + classesDex.getAbsolutePath()
                    + " " + classesJar.getAbsolutePath());
            process.waitFor();
            if (process.exitValue() != 0) {
                System.out.println("dex error");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //第六步:解压apk
        File apkFile = new File("app/build/outputs/apk/debug/app-debug.apk");
        File apkTemp = new File("lib/Apktemp");
        Zip.unZip(apkFile, apkTemp);
        ArrayList<File> dexFiles = new ArrayList<>();
        for (File file : apkTemp.listFiles()) {
            if (file.getName().endsWith("dex")) {
                dexFiles.add(file);
            }
        }
        //加密apk里面的dex
        AES.init(AES.DEFAULT_PWD);
        for (File dexFile : dexFiles) {
            try {
                byte[] bytes = Utils.getBytes(dexFile);

                byte[] encrypt = AES.encrypt(bytes);
                FileOutputStream fos = new FileOutputStream(new File(apkTemp,
                        "secret-" + dexFile.getName()));
                fos.write(encrypt);
                fos.flush();
                fos.close();
                dexFile.delete();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        classesDex.renameTo(new File("lib/Apktemp", "classes.dex"));
        File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
        //第七步:把apkTemp压缩成unsightApk
        try {
            Zip.zip(apkTemp, unSignedApk);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //第八步:对齐 签名
        File alignedApk = new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
        try {
            process = Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 " + unSignedApk.getAbsolutePath()
                    + " " + alignedApk.getAbsolutePath());

            process.waitFor();
            if (process.exitValue() != 0) {
                System.out.println("zipalign error");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
        File jks=new File("mykeystore.jks");
        try {
            process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                    +" --ks-key-alias key0 --ks-pass pass:11111111 --key-pass pass:11111111 --out "
                    +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
            process.waitFor();
            if(process.exitValue()!=0){
                System.out.println("sign error");
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("over ");
    }
}

这里详细讲讲ProxyApplication:
探讨1:作为唯一没加密的dex文件内的ProxyApplication如何把加密的dex文件,加载到类加载器(ClassLoader)中?
ProxyApplication三部曲:
1.获得加密apk。
2.解压zip并解密dex文件。
3把新dex文件索引存在类加载器中。

上述过程中,涉及到把dex文件加载到类加载器中,下面简单理解下类加载机制。
前提:android的ClassLoader有两种类型系统类加载器和自定义加载器。
1)BootClassLoader:
安卓系统启动时候会使用BCL来预加载常用类。
2)DexClassLoader
加载dex文件和包含dex文件的压缩包
3)PathClassLoader
加载系统类和应用程序的类
4) InMemoryClassLoader
androidO新增的,用于加载内存中的dex
·
·ClassLoader是一个抽象类,定义了classloader的主要功能。BootClassLoader是它的内部类
·SecureClassLoader不是ClassLoader的实现类,拓展了ClassLoader的权限方面的功能
·BaseDexClassLoader继承ClassLoader,但是是抽象类,PathClassLoader, DexClassLoader, InMemoryClassLoader都继承它,并各自实现类功能
·双亲委托模式
(讲人话:首先判断该类是否已经加载,如无,不是从自身查找,而是委托到父加载器中找是否有加载目的Class,若无依次向父类递归,直至最顶层ClassLoader类。如果找到了,就直接返回Class,若果没找到就继续依次向下子加载器findClass…)
优点:
1.避免重复加载
2.保护安全性。
(沙雕A建一个 类名为 android.view.View的自定义类,可能造成系统原本的View不可用。但其实还有一层保护,虚拟机把两个类名一致的且被同一个类加载器加载的类,虚拟机才会认为他们是同一个类)

来一个demo打印看看应用的类加载器是什么:
在这里插入图片描述
这里可以看到PathClassLoader作为加载器。

ClassLoader的加载过程:
ClassLoader.java

  protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
        //找该类是否被加载过了
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    //先判断父类是否存在
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //如果不存在就在自层找
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //在委托流程中没找到该类,就会执行该句
                    c = findClass(name);
                }
            }
            //如果已加载就直接返回
            return c;
    }

BaseDexClassLoader.java

 @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //调用pathList的findClass
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

先看看pathList是什么对象

 /**
     * Constructs an instance.
     *
     * dexFile must be an in-memory representation of a full dexFile.
     *
     * @param dexFiles the array of in-memory dex files containing classes.
     * @param parent the parent class loader
     *
     * @hide
     */
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        //在构造器内初始化 是一个DexPathList对象
        this.pathList = new DexPathList(this, dexFiles);
    }

接下来看看DexPathList对象怎么存放已加载的class

 /**
     * Construct an instance.
     *
     * @param definingContext the context in which any as-yet unresolved
     * classes should be defined
     *
     * @param dexFiles the bytebuffers containing the dex files that we should load classes from.
     */
    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
       ...
        this.definingContext = definingContext;
        // TODO It might be useful to let in-memory dex-paths have native libraries.
        this.nativeLibraryDirectories = Collections.emptyList();
        this.systemNativeLibraryDirectories =
                splitPaths(System.getProperty("java.library.path"), true);
        this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //把所有存进来的dex文件存储在dexElements对象
        this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
    }

接下来看看dexElements 是何方神圣!?
这是dexElements的对象声明

  /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

重点来了:

 /**
     * Element of the dex/resource path. Note: should be called DexElement, but apps reflect on
     * this.
     */
    /*package*/ static class Element {
        /**
         * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
         * (only when dexFile is null).
         */
        private final File path;

        private final DexFile dexFile;

        private ClassPathURLStreamHandler urlHandler;
        private boolean initialized;

        /**
         * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
         * should be null), or a jar (in which case dexZipPath should denote the zip file).
         */
        public Element(DexFile dexFile, File dexZipPath) {
            this.dexFile = dexFile;
            this.path = dexZipPath;
        }

        public Element(DexFile dexFile) {
            this.dexFile = dexFile;
            this.path = null;
        }

        public Element(File path) {
          this.path = path;
          this.dexFile = null;
        }
        ....
     }

从上面代码可以看到Element存放了dex文件的实例,和对应路径。

回来~从BaseDexClassLoader.findClass()->DexPathList.findClass()
就看看DexPathList.findClass()的实现内容

public Class<?> findClass(String name, List<Throwable> suppressed) {
//遍历dexElements,findClass()
        for (Element element : dexElements) {
        /
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

看看element.findClass()

 public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

dexFile.loadClassBinaryName()

  public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }

    private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {

            //调用native
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

native方法往下就不再分析。从这波代码分析,找到一个重要转折点dexElements(Element数组),每当找应用程序的类时,都会遍历这个数组,找到目的的dex文件,再得到目的Class。
回到加固
由此,我们把解密的dex文件通过反射合并到这个dexElements对象(Element数组)就完事。
如下图:
在sssssssss述上图对应以下代码:

public class ProxyApplication extends Application {

    //定义好解密后的文件的存放路径
    private String app_name;
    private String app_version;

    /**
     * ActivityThread创建Application之后调用的第一个方法
     * 可以在这个方法中进行解密,同时把dex交给android去加载
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //获取用户填入的metadata
        getMetaData();

        //得到当前加密了的APK文件
        File apkFile=new File(getApplicationInfo().sourceDir);
        //把apk解压   app_name+"_"+app_version目录中的内容需要root权限才能用
        File versionDir = getDir(app_name+"_"+app_version,MODE_PRIVATE);
        File appDir=new File(versionDir,"app");
        File dexDir=new File(appDir,"dexDir");

        Log.e("ProxyApplication", "attachBaseContext:first "+apkFile.getAbsolutePath() );
        Log.e("ProxyApplication", "attachBaseContext:sec "+versionDir.getAbsolutePath() );

        //得到我们需要加载的Dex文件
        List<File> dexFiles=new ArrayList<>();
        //进行解密(最好做MD5文件校验)
        if(!dexDir.exists() || dexDir.list().length==0){
            //把apk解压到appDir
            Zip.unZip(apkFile,appDir);
            //获取目录下所有的文件
            File[] files=appDir.listFiles();
            for (File file : files) {
                String name=file.getName();
                if(name.endsWith(".dex") && !TextUtils.equals(name,"classes.dex")){
                    try{
                        AES.init(AES.DEFAULT_PWD);
                        //读取文件内容
                        byte[] bytes=Utils.getBytes(file);
                        //解密
                        byte[] decrypt=AES.decrypt(bytes);
                        //写到指定的目录
                        FileOutputStream fos=new FileOutputStream(file);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                        dexFiles.add(file);

                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }else{
            for (File file : dexDir.listFiles()) {
                dexFiles.add(file);
            }
        }

        try{
            //2.把解密后的文件加载到系统
            loadDex(dexFiles,versionDir);
        }catch (Exception e){
            e.printStackTrace();
        }


    }

    private void loadDex(List<File> dexFiles, File versionDir) throws Exception{
        //1.获取pathlist
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        Object pathList = pathListField.get(getClassLoader());
        //2.获取数组dexElements
        Field dexElementsField=Utils.findField(pathList,"dexElements");
        Object[] dexElements=(Object[])dexElementsField.get(pathList);
        //3.反射到初始化dexElements的方法
        Method makeDexElements=Utils.findMethod(pathList,"makePathElements",List.class,File.class,List.class);

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        Object[] addElements=(Object[])makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions);

        //合并数组
        Object[] newElements= (Object[])Array.newInstance(dexElements.getClass().getComponentType(),dexElements.length+addElements.length);
        System.arraycopy(dexElements,0,newElements,0,dexElements.length);
        System.arraycopy(addElements,0,newElements,dexElements.length,addElements.length);

        //替换classloader中的element数组
        dexElementsField.set(pathList,newElements);
    }

    private void getMetaData() {
        try{
            ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
                    getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData=applicationInfo.metaData;
            if(null!=metaData){
                if(metaData.containsKey("app_name")){
                    app_name=metaData.getString("app_name");
                }
                if(metaData.containsKey("app_version")){
                    app_version=metaData.getString("app_version");
                }
            }

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

(tinker热修复共同点:加入新dex去dexElements)
探讨2: 初次冷启动ProxyApplication进程时,已经将ProxyApplication作为入口,后续的冷启动如何更替为真正的MyApplication作为真正的应用入口?且ProxyApplication作为初次入口时,已经初始化了有关ProxyApplication的信息,怎么更换会真正的MyApplication?
在这里前提需要粗略了解app冷启动的流程(以在launcher上点击启动app作为起点):
在这里插入图片描述

  1. 首先launcher通过startActivityAsUser()请求打开activity,然后再ContextImpl里通过binder获取ActivityManager服务,调用到ActvityManagerService的startActivityAsUser()。

  2. 然后AMS在启动应用进程前会进行一系列的判断。如,当前这个应用进程是否已启动,若不存在就,向Zygote启动新建进程。

  3. zygote进程接收到新建进程请求后,
    1)通过classLoader实例化ActivityThread对象,
    2)binder线程池启动。

    ams在判断相关打开进程前的涉及前提判断的过程和binder启动的过程复杂,且知识点较多,但与这里加固主题的重点逻辑无必要关联,就简单带过。

  4. ActivityThread对象被初始化后,
    1)开启主线程的looper循环,
    2)请求ams进行绑定Application

回到加固主题的目标->在ProxyApplication初始化后,怎么将真正的MyApplication去替换ProxyApplication?
这里需要跟进源码,有两个主要目的
1.看看ProxyApplication是怎么初始化?
2.在初始化后,怎么作为对象在后续被进程所使用?

上源码!从ams调用ActivityThread绑定Application开始看

private void handleBindApplication(AppBindData data) {
...
		Application app;
        final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
        final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
        try {
            // If the app is being launched for full backup or restore, bring it up in
            // a restricted environment with the base application class.
            //data.info是LoadedApk类型对象,LoadedApk这个类就是APK在内存中的表示,可以得到代码的ClassLoader,资料mDataDir,功能清单ApplicationInfo等信息
            //通过LoadedApk的makeApplication()进行构建Application实例
            app = data.info.makeApplication(data.restrictedBackupMode, null);

            // Propagate autofill compat state
            app.setAutofillCompatibilityEnabled(data.autofillCompatibilityEnabled);
            //把构建出来的app赋值给mInitialApplication 。
            //由此找到替换目标 TODO TARGET :
            //  ActivityThread.java -> mInitialApplication对象
            mInitialApplication = app;
             // don't bring up providers in restricted mode; they may depend on the
            // app's custom Application class
            if (!data.restrictedBackupMode) {
                if (!ArrayUtils.isEmpty(data.providers)) {
                    //这里传入app->要跟进去这里看看怎样利用app。。
                    installContentProviders(app, data.providers);
                    // For process that contains content providers, we want to
                    // ensure that the JIT is enabled "at some point".
                    mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
                }
            }
...
  			try {
                //这里执行ProxyApplication的onCreat()
                mInstrumentation.callApplicationOnCreate(app);
            } catch (Exception e) {
                if (!mInstrumentation.onException(app, e)) {
                    throw new RuntimeException(
                      "Unable to create application " + app.getClass().getName()
                      + ": " + e.toString(), e);
                }
            }
            ...
}

接下来先看看LoadedApk的makeApplication(),做了什么

public Application makeApplication(boolean forceDefaultAppClass,
            Instrumentation instrumentation) {
...
		 Application app = null;
        //mApplicationInfo.className;得到声明文件中的Application标签的 Name 值
        String appClass = mApplicationInfo.className;
        if (forceDefaultAppClass || (appClass == null)) {
            appClass = "android.app.Application";
        }

        try {
            java.lang.ClassLoader cl = getClassLoader();
            if (!mPackageName.equals("android")) {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                        "initializeJavaContextClassLoader");
                initializeJavaContextClassLoader();
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            }
            //创建Context
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            //Instrumentation是用于管理和执行activity声明周期的工具类
            //通过mActivityThread.mInstrumentation 创建application对象
            //创建出来的是app对象实质是ProxyApplication -->目的替换所有用到app对象(ProxyApplication)的字段
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            //用了app去设置appContext.setOuterContext
            //找到替换目标 todo TARGET : 
            // 替换ContextImpl的mOuterContext对象  >> ContextImpl.java ->mOuterContext对象
            appContext.setOuterContext(app);
		} catch (Exception e) {
            if (!mActivityThread.mInstrumentation.onException(app, e)) {
                Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                throw new RuntimeException(
                    "Unable to instantiate application " + appClass
                    + ": " + e.toString(), e);
            }
        }
        //用了app加入mActivityThread.mAllApplications队列
        //找到替换目标 todo TARGET :
        //替换ActivityThread的mAllApplications队列 >>   ActivityThread ->mAllApplications
        mActivityThread.mAllApplications.add(app);
        //找到替换目标 todo TARGET :
        //替换LoadedApk的mApplication对象   >> LoadedApk ->mApplication
        mApplication = app;

...
}

接下来先看看ActivityThread的installContentProviders(),传入application对象做了什么

private void installContentProviders(
            Context context, List<ProviderInfo> providers) {
      ...
      //传入context到installProvider()
            ContentProviderHolder cph = installProvider(context, null, cpi,
                    false /*noisy*/, true /*noReleaseNeeded*/, true /*stable*/);
            if (cph != null) {
                cph.noReleaseNeeded = true;
                results.add(cph);
            }
        }
        ...
}

installProvider()

private ContentProviderHolder installProvider(context context,
            ContentProviderHolder holder, ProviderInfo info,
            boolean noisy, boolean noReleaseNeeded, boolean stable) {
...
 			Context c = null;
            ApplicationInfo ai = info.applicationInfo;
            //ai这个获取到还是ProxyApplication的资料
            //->如果context.getPackageName()
            //   获得到的不是ProxyApplication就可以改变c的赋值行为
            //这里可以进行重写context的getPackageName方法,
            //使输出PackageName为真正的MyApplication即可
            if (context.getPackageName().equals(ai.packageName)) {
                c = context;
            } else if (mInitialApplication != null &&
                    mInitialApplication.getPackageName().equals(ai.packageName)) {
                c = mInitialApplication;
            } else {
                try {
                //目的要进入这里进行构造c对象
                    c = context.createPackageContext(ai.packageName,
                            Context.CONTEXT_INCLUDE_CODE);
                } catch (PackageManager.NameNotFoundException e) {
                    // Ignore
                }
            }

...            
}

经过上述代码找到了四处需要替换的对象变量和一处重写方法。
整理:

  1. 新建一个真的的MyApplication对象,以用来替换ProxyApplication对象
  2. ActivityThread.java -> mInitialApplication对象
  3. ContextImpl.java ->mOuterContext对象
  4. ActivityThread的mAllApplications队列 >> ActivityThread ->mAllApplications
  5. LoadedApk的mApplication对象 >> LoadedApk ->mApplication
  6. context.getPackageName()重写
    ->如何新建MyApplication对象,这里可以模仿源码如何新建ProxyApplication对象步骤进行构造
    新建Application对象在Instrumentation.java实现:
 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;
    }

跟进instantiateApplication(): AppComponentFactory.java

public @NonNull Application instantiateApplication(@NonNull ClassLoader cl,
            @NonNull String className)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
            //通过classLoader,获得类,然后进行newInstance  
        return (Application) cl.loadClass(className).newInstance();
    }

所以我们可以类似方法获得Application对象

 //得到attachBaseContext(context) 传入的上下文 ContextImpl
        Context baseContext = getBaseContext();
        //创建用户真实的application (MyApplication)
        Class<?> delegateClass = Class.forName(app_name);
        realApplication = (Application) delegateClass.newInstance();
        //得到attach()方法
        Method attach = Application.class.getDeclaredMethod("attach", Context.class);
        attach.setAccessible(true);
        attach.invoke(realApplication, baseContext);

->关于对象替换可以通过反射进行实现。这里需要找到一个入口点,获取哪个对象进行着手?
在ProxyApplication.java中可以获得context对象,通过Context的实现类ContextImpl找到下列对象。
在这里插入图片描述
由此,通过context来作为入手进行反射。

附上ProxyApplication.java全代码,包含替换目标对象的代码


public class ProxyApplication extends Application {

    //定义好解密后的文件的存放路径
    private String app_name;
    private String app_version;

    /**
     * ActivityThread创建Application之后调用的第一个方法
     * 可以在这个方法中进行解密,同时把dex交给android去加载
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //获取用户填入的metadata
        getMetaData();

        //得到当前加密了的APK文件
        File apkFile = new File(getApplicationInfo().sourceDir);

        //把apk解压   app_name+"_"+app_version目录中的内容需要boot权限才能用
        File versionDir = getDir(app_name + "_" + app_version, MODE_PRIVATE);
        File appDir = new File(versionDir, "app");
        File dexDir = new File(appDir, "dexDir");


        //得到我们需要加载的Dex文件
        List<File> dexFiles = new ArrayList<>();
        //进行解密(最好做MD5文件校验)
        if (!dexDir.exists() || dexDir.list().length == 0) {
            //把apk解压到appDir
            Zip.unZip(apkFile, appDir);
            //获取目录下所有的文件
            File[] files = appDir.listFiles();
            for (File file : files) {
                String name = file.getName();
                if (name.endsWith(".dex") && !TextUtils.equals(name, "classes.dex")) {
                    try {
                        AES.init(AES.DEFAULT_PWD);
                        //读取文件内容
                        byte[] bytes = Utils.getBytes(file);
                        //解密
                        byte[] decrypt = AES.decrypt(bytes);
                        //写到指定的目录
                        FileOutputStream fos = new FileOutputStream(file);
                        fos.write(decrypt);
                        fos.flush();
                        fos.close();
                        dexFiles.add(file);

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } else {
            for (File file : dexDir.listFiles()) {
                dexFiles.add(file);
            }
        }

        try {
            //2.把解密后的文件加载到系统
            loadDex(dexFiles, versionDir);
        } catch (Exception e) {
            e.printStackTrace();
        }


    }

    private void loadDex(List<File> dexFiles, File versionDir) throws Exception {
        //1.获取pathlist
        Field pathListField = Utils.findField(getClassLoader(), "pathList");
        Object pathList = pathListField.get(getClassLoader());
        //2.获取数组dexElements
        Field dexElementsField = Utils.findField(pathList, "dexElements");
        Object[] dexElements = (Object[]) dexElementsField.get(pathList);
        //3.反射到初始化dexElements的方法
        Method makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);

        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        Object[] addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);

        //合并数组
        Object[] newElements = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(), dexElements.length + addElements.length);
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        System.arraycopy(addElements, 0, newElements, dexElements.length, addElements.length);

        //替换classloader中的element数组
        dexElementsField.set(pathList, newElements);
    }

    private void getMetaData() {
        try {
            ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(
                    getPackageName(), PackageManager.GET_META_DATA);
            Bundle metaData = applicationInfo.metaData;
            if (null != metaData) {
                if (metaData.containsKey("app_name")) {
                    app_name = metaData.getString("app_name");
                }
                if (metaData.containsKey("app_version")) {
                    app_version = metaData.getString("app_version");
                }
            }

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


    /**
     * 开始替换application
     */
    @Override
    public void onCreate() {
        Log.e("proxyApp", "onCreate: " + Log.getStackTraceString(new Throwable()));
        super.onCreate();
        try {
            bindRealApplicatin();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 让代码走入if中的第三段中
     *
     * @return
     */
    @Override
    public String getPackageName() {
        if (!TextUtils.isEmpty(app_name)) {
            return "";
        }
        return super.getPackageName();
    }

    @Override
    public Context createPackageContext(String packageName, int flags) throws PackageManager.NameNotFoundException {
        if (TextUtils.isEmpty(app_name)) {
            return super.createPackageContext(packageName, flags);
        }
        try {
            bindRealApplicatin();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return realApplication;

    }

    boolean isBindReal;
    Application realApplication;

    private void bindRealApplicatin() throws Exception {
        if (isBindReal) {
            return;
        }
        if (TextUtils.isEmpty(app_name)) {
            return;
        }
        //得到attachBaseContext(context) 传入的上下文 ContextImpl
        Context baseContext = getBaseContext();
        //创建用户真实的application (MyApplication)
        Class<?> delegateClass = Class.forName(app_name);
        realApplication = (Application) delegateClass.newInstance();
        //得到attach()方法
        Method attach = Application.class.getDeclaredMethod("attach", Context.class);
        attach.setAccessible(true);
        attach.invoke(realApplication, baseContext);

//        ContextImpl---->mOuterContext(app)   通过Application的attachBaseContext回调参数获取
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        //获取mOuterContext属性
        Field mOuterContextField = contextImplClass.getDeclaredField("mOuterContext");
        mOuterContextField.setAccessible(true);
        mOuterContextField.set(baseContext, realApplication);

//        ActivityThread--->mInitialApplication     ContextImpl的mMainThread属性
        Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
        mMainThreadField.setAccessible(true);
        Object mMainThread = mMainThreadField.get(baseContext);

        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
        mInitialApplicationField.setAccessible(true);
        mInitialApplicationField.set(mMainThread, realApplication);

//        ActivityThread--->mAllApplications(ArrayList)       ContextImpl的mMainThread属性
        Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
        mAllApplicationsField.setAccessible(true);
        ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
        mAllApplications.remove(this);
        mAllApplications.add(realApplication);

//        LoadedApk------->mApplication                      ContextImpl的mPackageInfo属性
        Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
        mPackageInfoField.setAccessible(true);
        Object mPackageInfo = mPackageInfoField.get(baseContext);

        Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
        Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
        mApplicationField.setAccessible(true);
        mApplicationField.set(mPackageInfo, realApplication);

        //修改ApplicationInfo className   LooadedApk
        Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
        mApplicationInfoField.setAccessible(true);
        ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
        mApplicationInfo.className = app_name;

        realApplication.onCreate();
        isBindReal = true;
    }
}


验收:

可以在四大组件中调用getApplication() 、getApplicationContext() 打印出来看看ProxyApplication是否已经被替换成MyApplication。

例如:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
(*注:广播接收器onReceive(Context context, Intent intent),参数里的context,是ReceiverRestrictedContext类型继承ContextWrapper,不可用于registerReceiver和bindService,详细分析可看我的另外一篇博文:[framework]了解android的各种Context

恭喜你,加固学习分享,终于看完了!再看一遍好吗, pleaseeeeeeee!!

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页