美文网首页
APK加固方式

APK加固方式

作者: Coder_Sven | 来源:发表于2020-04-26 20:46 被阅读0次

    一:代码混淆

    Proguard是一个代码优化和混淆工具。能够提供对Java类文件的压缩、优化、混淆,和预校验。压缩的步骤是检测并移除未使用的类、字段、方法和属性。优化的步骤是分析和优化方法的字节码。混淆的步骤是使用短的毫无意义的名称重命名剩余的类、字段和方法。压缩、优化、混淆使得代码更小,更高效。该加密方式只是对工程提供了最小的保护,并不是说不能逆向破解;只是说难度增加,需要耐心。混淆规则很简单,就不在这里详细说明了。

    二:Dex文件加密

    1587636690781.png

    一个打包好的APK文件源代码是放在dex文件中的,反编译dex文件可以得到源代码。所以我们需要做的是将dex文件进行加密然后生成一个壳dex给系统去加载,那么别人反编译出来也看不到真正的代码内容。

    思路:将源APK文件解压得到里面的所有dex文件,然后将这些文件加密,然后生成一个壳类型的dex文件交给系统去加载,这个文件就算被反编译也暴露不了我们真正的代码,然后在这个壳dex文件的代码中先得到加密了的Apk文件,将APK解压到system/appName文件夹下面,这个文件夹需要有root权限才能访问。解密之前所有加密了的dex文件,将解密的文件存到一个数组里面,然后将这些解密后的文件加载到系统中去

    1,将dex文件加密

    1587634835058.png

    2,生成一个壳dex文件

    1587635056750.png

    3,将它打包成一个新的APK,并将新的APK执行对齐,签名操作

    1587732689763.png

    开始编写代码

    步骤一:

    新建一个命名proxy_core的AndroidLibrary的module,主项目引用这个module,并且将主项目的Application设置为module的ProxyApplication类。我们在这个类里面做解密APK操作,并加载到系统。在后面详细分析

    步骤二:

    新建一个命名proxy_tools的JavaLibrary的module,这个module是一个工具库,我们在这个库里进行APK加密操作。

    操作一:加密源apk文件中所有的dex文件
    1587634835058.png
         File apkFile=new File("app/build/outputs/apk/debug/app-debug.apk");
            File apkTemp=new File("app/build/outputs/apk/debug/temp");
            Zip.unZip(apkFile,apkTemp);
            //只要dex文件拿出来加密
            File[] dexFiles=apkTemp.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File file, String s) {
                    return s.endsWith(".dex");
                }
            });
            //AES加密了
            AES.init(AES.DEFAULT_PWD);
            for (File dexFile : dexFiles) {
                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();
            }
    
    操作二:生成壳dex文件。

    获取proxy_core壳工程的aar文件:选中proxy_core,点击Build->Make Module 'proxy_core',就会在proxy_core下面的build目录中生成aar文件

    1587733412585.png

    解压aar文件,得到里面的classes.jar文件。然后使用androidSDK工具中的dx命令将class或者jar打包成dex文件。

    1587634962900.png
           File aarFile=new File("proxy_core/build/outputs/aar/proxy_core-debug.aar");
            File aarTemp=new File("proxy_tools/temp");
            Zip.unZip(aarFile,aarTemp);
            File classesJar=new File(aarTemp,"classes.jar");
            File classesDex=new File(aarTemp,"classes.dex");
    
            //dx --dex --output out.dex in.jar
            Process process=Runtime.getRuntime().exec("cmd /c dx --dex --output "+classesDex.getAbsolutePath() +" " + classesJar.getAbsolutePath());
            process.waitFor();
            if(process.exitValue()!=0){
                throw new RuntimeException("dex error");
            }
    

    PS:dx命令是在androidsdk的buidtools下面任意一个版本里面,比如我的是(D:\android-sdk\build-tools\26.0.3),需要设置环境变量。

    操作三:将操作二中生成的classes.dex文件放到第一步的temp文件夹下。
    1587635056750.png
     classesDex.renameTo(new File(apkTemp,"classes.dex"));
    
    操作四:将temp文件夹重新压缩成一个APK文件
      File unSignedApk=new File("app/build/outputs/apk/debug/app-unsigned.apk");
      Zip.zip(apkTemp,unSignedApk);
    
    1587732689763.png

    对新生成的APK进行对齐,然后签名

          //通过命令执行对齐
          File alignedApk=new File("app/build/outputs/apk/debug/app-unsigned-aligned.apk");
            process=Runtime.getRuntime().exec("cmd /c zipalign -v -p 4 "+unSignedApk.getAbsolutePath()
                            +" "+alignedApk.getAbsolutePath());
            process.waitFor();
            if(process.exitValue()!=0){
                throw new RuntimeException("zipalign error");
            }
    
      //通过命令执行签名
      File signedApk=new File("app/build/outputs/apk/debug/app-signed-aligned.apk");
            //自己创建签名文件
            File jks=new File("proxy_tools/proxy2.jks");
            process=Runtime.getRuntime().exec("cmd /c apksigner sign --ks "+jks.getAbsolutePath()
                                +" --ks-key-alias jett --ks-pass pass:123456 --key-pass pass:123456 --out "
                                    +signedApk.getAbsolutePath()+" "+alignedApk.getAbsolutePath());
            process.waitFor();
            if(process.exitValue()!=0){
                throw new RuntimeException("apksigner error");
            }
    

    如果runtime执行命令一直等待,可以使用cmd命令行来执行。经过上面的操作我们已经完成了APK加固,下一步就是分析如何解密Apk并且可以正常加载到系统中去。

    proxy_core(接上面步骤一)
    //代理Applicaiton
    public class ProxyApplication extends Application {
    
        //定义好解密后的文件的存放路径
        private String app_name;
        private String app_version;
    
         /**
         * ActivityThread创建Application之后调用的第一个方法
         * 可以在这个代理APPlication中进行解密dex,
         * 然后再把解密后的dex交给原来的APPlication去加载
         * @param base
         */
        @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();
                    //壳dex文件不需要解密
                    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 {
                //把解密后的文件加载到系统[见下面loadDex()]
                loadDex(dexFiles, versionDir);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        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 (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            }
        }
    }
    
    1587636757618.png

    查看源码分析一下Dex文件的加载过程。

    Android主要有两个ClassLoader,分别是PathClassLoader,DexClassLoader。PathClassLoader只会加载系统类和已经安装的APK的dex。DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。一般我们都是用这个DexClassLoader来作为动态加载的加载器。

    package dalvik.system;
    
    public class PathClassLoader extends BaseDexClassLoader {
      
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }
    
       
        public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }
    
    package dalvik.system;
    
    import java.io.File;
    
    
    public class DexClassLoader extends BaseDexClassLoader {
    
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
        }
    }
    

    他们两个本身没什么好分析的,现在我们来看他们的父类BaseDexClassLoader

        public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
        }
        
            @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            //调用的是DexPathList的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;
        }
    

    [--->DexPathList.java]

        public Class findClass(String name, List<Throwable> suppressed) {
            //dexElements  从这个数组里面拿出来所有需要加载的dex文件。然后去加载
            for (Element element : dexElements) {
                DexFile dex = element.dexFile;
    
                if (dex != null) {
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    

    我们来看看dexElements是在哪里初始化的

    final class DexPathList { 
      private Element[] dexElements;
        
      public DexPathList(ClassLoader definingContext, String dexPath,
                String librarySearchPath, File optimizedDirectory) {
                
            ...
            this.definingContext = definingContext;
    
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            // save dexPath for BaseDexClassLoader
            this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                               suppressedExceptions, definingContext);
    
            ....    
           
        }
    }
    

    (loadDex)这样我们就知道只需要反射得到数组dexElements,并且将我们需要添加的dex文件加入到这个数组中去就完成了。代码如下

       private void loadDex(List<File> dexFiles, File versionDir) throws Exception {
            //getClassLoader()  获取的是PathClassLoader对象
            //pathListField 是指PathClassLoader的父类BaseDexClassLoader的pathList字段
            Field pathListField = Utils.findField(getClassLoader(), "pathList");
            //DexPathList类对象
            Object pathList = pathListField.get(getClassLoader());
            //获取到DexPathList类的dexElements字段
            Field dexElementsField = Utils.findField(pathList, "dexElements");
            Object[] dexElements = (Object[]) dexElementsField.get(pathList);
            //反射得到初始化dexElements的方法
            Object[] addElements;
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){//7.0  makeDexElements
                Method makeDexElements = Utils.findMethod(pathList, "makeDexElements", List.class, File.class, List.class,ClassLoader.class);
                ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
                addElements = (Object[]) makeDexElements.invoke(pathList,dexFiles,versionDir,suppressedExceptions,getClassLoader());
            }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){//6.0  makePathElements
                Method makeDexElements = Utils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
                ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);
            }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){//5.0 makeDexElements
                Method makeDexElements = Utils.findMethod(pathList, "makeDexElements", ArrayList.class, File.class, ArrayList.class);
                ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
                addElements = (Object[]) makeDexElements.invoke(pathList, dexFiles, versionDir, suppressedExceptions);
            }else{
                return;
            }
    
            //合并数组
            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);
        }
    
    

    至此,我们就完成了APK的解密操作,并且也能够正常加载APK应用了。但是我们发现了一个新的问题,那就是我们只能在ProxyApplication这个应用入口做各种初始化操作,不能在主项目中定义我们自己的MyApplicaiton来进行各种初始化工作。所以我们需要想办法将我们主项目的MyApplicaiton替换成真正的applicaiton入口。我在通过源码分析Activity和Applicaiton的启动流程(7.0源码)中分析过了APPlication的加载过程,所以我们可以从源码入手,分析想要替换成真正的MyApplication需要做哪些操作。

    源码分析(结合通过源码分析Activity和Applicaiton的启动流程(7.0源码)

    [--->ActivityThread.java]

     private void handleBindApplication(AppBindData data) {
            ...
            Application app = data.info.makeApplication(data.restrictedBackupMode, null);
            [第六个要替换的]
            mInitialApplication = app;
            ...        
    }
    

    [--->ActivityThread.java]

        public Application makeApplication(boolean forceDefaultAppClass,
                Instrumentation instrumentation) {
    
            Application app = null;
    
            [第一个要替换的]
            String appClass = mApplicationInfo.className;
            if (forceDefaultAppClass || (appClass == null)) {
                appClass = "android.app.Application";
            }
            ...
          
            java.lang.ClassLoader cl = getClassLoader();
            ...
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            app = mActivityThread.mInstrumentation.newApplication(
                    cl, appClass, appContext);
            [第二个要替换的]
            appContext.setOuterContext(app);
            ... 
            [第四个要替换的]
            mActivityThread.mAllApplications.add(app);
            [第五个要替换的]
            mApplication = app;
    
            ...
    
            return app;
        }
    

    [--->ContextImpl.java ]

        static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
            if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
            return new ContextImpl(null, mainThread,
                    packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
        }
        
        
        private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
       
        //ContextImpl类对象
        mOuterContext = this;
    
        ...
        //ActivityThread类对象
        mMainThread = mainThread;
        mActivityToken = activityToken;
        mFlags = flags;
    
        if (user == null) {
            user = Process.myUserHandle();
        }
        mUser = user;
    
        //LoadedApk类对象
        mPackageInfo = packageInfo;
        ...
    
    
    }
        
    

    [--->Instrumentation.java ]

        static public Application newApplication(Class<?> clazz, Context context)
                throws InstantiationException, IllegalAccessException, 
                ClassNotFoundException {
            Application app = (Application)clazz.newInstance();
            [第三个要替换的]
            app.attach(context);
            return app;
        }
    

    [--->Application.java]

        /**
         * @hide
         */
        /* package */ final void attach(Context context) {
            attachBaseContext(context);
            mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
        }
    

    第一个替换的位置:String appClass = mApplicationInfo.className;

    //反射得到  LoadedApk.mApplicationInfo字段
    Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
    Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
    mApplicationInfoField.setAccessible(true);
     
    //LoadedApk实体类对象从通过反射ContextImpl.mPackageInfo得到
    Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
    Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
    mPackageInfoField.setAccessible(true);
    Object mPackageInfo = mPackageInfoField.get(baseContext);
    ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
    mApplicationInfo.className = app_name;        
    
    

    第二个需要替换的位置:appContext.setOuterContext(app)

    Field mOuterContext = contextImplClass.getDeclaredField
    ("mOuterContext");
    mOuterContext.setAccessible(true);
    mOuterContext.set(baseContext, application);
    

    第三个需要替换的位置:app.attach(context);

      //创建用户真实的application(MyApplication)
    Class<?> delegateClass = Class.forName(app_name);
    application = (Application) delegateClass.newInstance();
    //调用Application.attach(context)方法
            Method declaredMethod = Application.class.getDeclaredMethod("attach", Context.class);
     //设置可用
    declaredMethod.setAccessible(true);
    //得到Application.attach(Context context)传入的context对象
    Context baseContext = getBaseContext();
    declaredMethod.invoke(application,baseContext);  
    

    第四个需要替换的位置: mActivityThread.mAllApplications.add(app);

      // ActivityThread类对象通过反射ContextImpl.mMainThread得到
            Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
            mMainThreadField.setAccessible(true);
            //得到的是ActivityThread类对象
            Object mMainThread = mMainThreadField.get(baseContext);
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
            mAllApplicationsField.setAccessible(true);
            ArrayList<Application> mAllApplications =(ArrayList<Application>) mAllApplicationsField.get(mMainThread);
    //        mAllApplications.remove(this);
            mAllApplications.add(application);
    

    第五个需要替换的位置:mApplication = app;

    Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
    mApplicationField.setAccessible(true);
    mApplicationField.set(mPackageInfo, application);
    

    第六个需要替换的位置:mInitialApplication = app;

    Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
    mInitialApplicationField.setAccessible(true);
    mInitialApplicationField.set(mMainThread, application);
    

    开始替换

       /**
         * 开始替换APPlication,加载真正应用的Application
         */
        @Override
        public void onCreate() {
            super.onCreate();
            try {
                bindRealApplicatin();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
           /**
         * 这个方法主要作用是创建其他程序的Context,
         * 通过这个Context可以访问该软件包的资源,甚至可以执行其他软件包的代码。
         * 这个代码不写的话,主项目中ContentProvider使用的context就没办法换回来,还是用的ProxyApplication
         * @param packageName
         * @param flags
         * @return
         * @throws PackageManager.NameNotFoundException
         */
        @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 application;
        }
        
        private void bindRealApplicatin() throws Exception {
            if (isBindReal) {
                return;
            }
            if (TextUtils.isEmpty(app_name)) {
                return;
            }
    
            //创建用户真实的application(MyApplication)
            Class<?> delegateClass = Class.forName(app_name);
            application = (Application) delegateClass.newInstance();
            //调用Application.attach(context)方法
            Method declaredMethod = Application.class.getDeclaredMethod("attach", Context.class);
            //设置可用
            declaredMethod.setAccessible(true);
            //得到Application.attach(Context context)传入的context对象
            Context baseContext = getBaseContext();
            declaredMethod.invoke(application, baseContext);
    
            //第一处: String appClass = mApplicationInfo.className;
            Class<?> loadedApkClass = Class.forName("android.app.LoadedApk");
            Field mApplicationInfoField = loadedApkClass.getDeclaredField("mApplicationInfo");
            mApplicationInfoField.setAccessible(true);
            //LoadedApk实体类对象从通过反射ContextImpl.mPackageInfo得到
            Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
            Field mPackageInfoField = contextImplClass.getDeclaredField("mPackageInfo");
            mPackageInfoField.setAccessible(true);
            Object mPackageInfo = mPackageInfoField.get(baseContext);
            ApplicationInfo mApplicationInfo = (ApplicationInfo) mApplicationInfoField.get(mPackageInfo);
            mApplicationInfo.className = app_name;
    
            //第二处:appContext.setOuterContext(app);
            Field mOuterContext = contextImplClass.getDeclaredField("mOuterContext");
            mOuterContext.setAccessible(true);
            mOuterContext.set(baseContext, application);
    
            //第三处:mActivityThread.mAllApplications.add(app);
            // ActivityThread类对象通过反射ContextImpl.mMainThread得到
            Field mMainThreadField = contextImplClass.getDeclaredField("mMainThread");
            mMainThreadField.setAccessible(true);
            //得到的是ActivityThread类对象
            Object mMainThread = mMainThreadField.get(baseContext);
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Field mAllApplicationsField = activityThreadClass.getDeclaredField("mAllApplications");
            mAllApplicationsField.setAccessible(true);
            ArrayList<Application> mAllApplications = (ArrayList<Application>) mAllApplicationsField.get(mMainThread);
    //        mAllApplications.remove(this);
            mAllApplications.add(application);
    
            //第四处:mApplication = app;
            Field mApplicationField = loadedApkClass.getDeclaredField("mApplication");
            mApplicationField.setAccessible(true);
            mApplicationField.set(mPackageInfo, application);
    
            //第五处:mInitialApplication = app;
            Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
            mInitialApplicationField.setAccessible(true);
            mInitialApplicationField.set(mMainThread, application);
    
            //调用Application的oncreat方法
            application.onCreate();
            isBindReal = true;
        }
    

    附上项目代码

    [https://github.com/games2sven/ReinForceApk​]

    相关文章

      网友评论

          本文标题:APK加固方式

          本文链接:https://www.haomeiwen.com/subject/znfuihtx.html