美文网首页Android开发
app加固原理(一)

app加固原理(一)

作者: 剑心_路喜 | 来源:发表于2019-07-11 22:41 被阅读0次

    前言

    apk正常打包后可以通过 反编译工具使用 得到源码,那这么长时间的辛苦不就白费了吗,这就引出一个问题了:怎么保证不让别人不容易拿到源码呢?

    当然得是通过加固啦,使用第三方的加固工具 (结尾给大家),但是作为一名热爱学习的程序员,当然得明白其中的原理才好。

    app加固原理

    加固原理.png
    1. 制作一个壳程序 (功能:解密和加载dex文件)
    2. 使用加密工具对原apk的dex文件进行加密
    3. 最后重新打包、对齐、签名

    实现

    1. 制作壳程序

    • 制作壳程序,壳程序包含两功能解密dex文件和加载dex文件,先说加载dex,

    • 解密dex文件:解压apk包得到dex文件,然后把加密过的dex文件进行解密

    • 那系统又是怎么加载dex文件呢?

    Android源码目录\libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java
    
    public class DexClassLoader extends BaseDexClassLoader {
        public DexClassLoader(String dexPath, String optimizedDirectory,
                String librarySearchPath, ClassLoader parent) {
            super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
        }
    }
    

    这里调用父类的构造方法

    Android源码目录\libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java
    

    看下面这个方法

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
            List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
            //这里传个名字 和 集合, 就是说把某个类进行加载 
            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;
    }
    
    Android源码目录\libcore\dalvik\src\main\java\dalvik\system\DexPathList.java
    
    public Class findClass(String name, List<Throwable> suppressed) {
           //通过遍历dexElements去加载
           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;
    }
    

    从这个方法中看到,dex是通过遍历dexElements去加载的,可以通过反射dexElements拿到已经加载的dex文件,那我们看dexElements的初始化

      //dexElements 初始化
    this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                                suppressedExceptions);
    
    
     private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                                  List<IOException> suppressedExceptions) {
    ............................                                              
    }
    

    那我们通过反射调用这个方法把解密后的dex文件通过makePathElements方法反射 加载进来,再和原来的dex合并,那这个app就能运行了。

    3. 重新打包、对齐、签名

    1. 重新打包
      把壳程序的dex文件和加密后的文件进行打包
    2. 对齐

    zipalign -v -p 4 input_unsigned.apk output_unsigned.apk

    1. 签名

    apksigner sign --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out out.apk in.apk

    代码实现

    壳程序

    • 加密算法
      public class AES {
      
        //16字节
        public static final String DEFAULT_PWD = "abcdefghijklmnop";
        //填充方式
        private static final String algorithmStr = "AES/ECB/PKCS5Padding";
        private static Cipher encryptCipher;
        private static Cipher decryptCipher;
      
        public static void init(String password) {
            try {
                // 生成一个实现指定转换的 Cipher 对象。
                encryptCipher = Cipher.getInstance(algorithmStr);
                decryptCipher = Cipher.getInstance(algorithmStr);// algorithmStr
                byte[] keyStr = password.getBytes();
                SecretKeySpec key = new SecretKeySpec(keyStr, "AES");
                encryptCipher.init(Cipher.ENCRYPT_MODE, key);
                decryptCipher.init(Cipher.DECRYPT_MODE, key);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } catch (NoSuchPaddingException e) {
                e.printStackTrace();
            } catch (InvalidKeyException e) {
                e.printStackTrace();
            }
        }
      
        public static byte[] encrypt(byte[] content) {
            try {
                byte[] result = encryptCipher.doFinal(content);
                return result;
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            }
            return null;
        }
      
        public static byte[] decrypt(byte[] content) {
            try {
                byte[] result = decryptCipher.doFinal(content);
                return result;
            } catch (IllegalBlockSizeException e) {
                e.printStackTrace();
            } catch (BadPaddingException e) {
                e.printStackTrace();
            }
            return null;
          }
      }
      
    • 解压和压缩
    
    public class Zip {
    
        private static void deleteFile(File file){
            if (file.isDirectory()){
                File[] files = file.listFiles();
                for (File f: files) {
                    deleteFile(f);
                }
            }else{
                file.delete();
            }
        }
    
        /**
         * 解压zip文件至dir目录
         * @param zip
         * @param dir
         */
        public static void unZip(File zip, File dir) {
            try {
                deleteFile(dir);
                ZipFile zipFile = new ZipFile(zip);
                //zip文件中每一个条目
                Enumeration<? extends ZipEntry> entries = zipFile.entries();
                //遍历
                while (entries.hasMoreElements()) {
                    ZipEntry zipEntry = entries.nextElement();
                    //zip中 文件/目录名
                    String name = zipEntry.getName();
                    //原来的签名文件 不需要了
                    if (name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") || name
                            .equals("META-INF/MANIFEST.MF")) {
                        continue;
                    }
                    //空目录不管
                    if (!zipEntry.isDirectory()) {
                        File file = new File(dir, name);
                        //创建目录
                        if (!file.getParentFile().exists()) {
                            file.getParentFile().mkdirs();
                        }
                        //写文件
                        FileOutputStream fos = new FileOutputStream(file);
                        InputStream is = zipFile.getInputStream(zipEntry);
                        byte[] buffer = new byte[2048];
                        int len;
                        while ((len = is.read(buffer)) != -1) {
                            fos.write(buffer, 0, len);
                        }
                        is.close();
                        fos.close();
                    }
                }
                zipFile.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 压缩目录为zip
         * @param dir 待压缩目录
         * @param zip 输出的zip文件
         * @throws Exception
         */
        public static void zip(File dir, File zip) throws Exception {
            zip.delete();
            // 对输出文件做CRC32校验
            CheckedOutputStream cos = new CheckedOutputStream(new FileOutputStream(
                    zip), new CRC32());
            ZipOutputStream zos = new ZipOutputStream(cos);
            //压缩
            compress(dir, zos, "");
            zos.flush();
            zos.close();
        }
    
        /**
         * 添加目录/文件 至zip中
         * @param srcFile 需要添加的目录/文件
         * @param zos   zip输出流
         * @param basePath  递归子目录时的完整目录 如 lib/x86
         * @throws Exception
         */
        private static void compress(File srcFile, ZipOutputStream zos,
                                     String basePath) throws Exception {
            if (srcFile.isDirectory()) {
                File[] files = srcFile.listFiles();
                for (File file : files) {
                    // zip 递归添加目录中的文件
                    compress(file, zos, basePath + srcFile.getName() + "/");
                }
            } else {
                compressFile(srcFile, zos, basePath);
            }
        }
    
        private static void compressFile(File file, ZipOutputStream zos, String dir)
                throws Exception {
            // temp/lib/x86/libdn_ssl.so
            String fullName = dir + file.getName();
            // 需要去掉temp
            String[] fileNames = fullName.split("/");
            //正确的文件目录名 (去掉了temp)
            StringBuffer sb = new StringBuffer();
            if (fileNames.length > 1){
                for (int i = 1;i<fileNames.length;++i){
                    sb.append("/");
                    sb.append(fileNames[i]);
                }
            }else{
                sb.append("/");
            }
            //添加一个zip条目
            ZipEntry entry = new ZipEntry(sb.substring(1));
            zos.putNextEntry(entry);
            //读取条目输出到zip中
            FileInputStream fis = new FileInputStream(file);
            int len;
            byte data[] = new byte[2048];
            while ((len = fis.read(data, 0, 2048)) != -1) {
                zos.write(data, 0, len);
            }
            fis.close();
            zos.closeEntry();
        }
    
    }
    
    • 工具类
    
    
    public class Utils {
    
        /**
         * 读取文件
         *
         * @param file
         * @return
         * @throws Exception
         */
        public static byte[] getBytes(File file) throws Exception {
            RandomAccessFile r = new RandomAccessFile(file, "r");
            byte[] buffer = new byte[(int) r.length()];
            r.readFully(buffer);
            r.close();
            return buffer;
        }
    
        /**
         * 反射获得 指定对象(当前-》父类-》父类...)中的 成员属性
         *
         * @param instance
         * @param name
         * @return
         * @throws NoSuchFieldException
         */
        public static Field findField(Object instance, String name) throws NoSuchFieldException {
            Class clazz = instance.getClass();
            //反射获得
            while (clazz != null) {
                try {
                    Field field = clazz.getDeclaredField(name);
                    //如果无法访问 设置为可访问
                    if (!field.isAccessible()) {
                        field.setAccessible(true);
                    }
                    return field;
                } catch (NoSuchFieldException e) {
                    //如果找不到往父类找
                    clazz = clazz.getSuperclass();
                }
            }
            throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
        }
    
    
        /**
         * 反射获得 指定对象(当前-》父类-》父类...)中的 函数
         *
         * @param instance
         * @param name
         * @param parameterTypes
         * @return
         * @throws NoSuchMethodException
         */
        public static Method findMethod(Object instance, String name, Class... parameterTypes)
                throws NoSuchMethodException {
            Class clazz = instance.getClass();
            while (clazz != null) {
                try {
                    Method method = clazz.getDeclaredMethod(name, parameterTypes);
                    if (!method.isAccessible()) {
                        method.setAccessible(true);
                    }
                    return method;
                } catch (NoSuchMethodException e) {
                    //如果找不到往父类找
                    clazz = clazz.getSuperclass();
                }
            }
            throw new NoSuchMethodException("Method " + name + " with parameters " + Arrays.asList
                    (parameterTypes) + " not found in " + instance.getClass());
        }
    
        // 所有文件md5总和
        private static String fileSum = "";
    
        /**
         *
         * @param file
         * @param suffix
         * @return
         */
        public static String traverseFolder(File file, String suffix) {
    
            if (file == null) {
                throw new NullPointerException("遍历路径为空路径或非法路径");
            }
    
            if (file.exists()) { //判断文件或目录是否存在
    
                File[] files = file.listFiles();
    
                if (files.length == 0) { // 文件夹为空
                    return null;
                } else {
                    for (File f : files) { // 遍历文件夹
    
                        if (f.isDirectory()) { // 判断是否是目录
    
                            if ((f.getName().endsWith(suffix))) { // 只小羊.dex 结尾的目录 则计算该目录下的文件的md5值
    
                                // 递归遍历
                                traverseFolder(f, suffix);
                            }
    
                        } else {
                            // 得到文件的md5值
                            String string = checkMd5(f);
                            // 将每个文件的md5值相加
                            fileSum += string;
                        }
                    }
                }
    
            } else {
                return null; // 目录不存在
            }
    
            return fileSum; // 返回所有文件md5值字符串之和
        }
    
        /**
         * 计算文件md5值
         * 检验文件生成唯一的md5值 作用:检验文件是否已被修改
         *
         * @param file 需要检验的文件
         * @return 该文件的md5值
         */
        private static String checkMd5(File file) {
    
            // 若输入的参数不是一个文件 则抛出异常
            if (!file.isFile()) {
                throw new NumberFormatException("参数错误!请输入校准文件。");
            }
    
            // 定义相关变量
            FileInputStream fis = null;
            byte[] rb = null;
            DigestInputStream digestInputStream = null;
            try {
                fis = new FileInputStream(file);
                MessageDigest md5 = MessageDigest.getInstance("md5");
                digestInputStream = new DigestInputStream(fis, md5);
                byte[] buffer = new byte[4096];
    
                while (digestInputStream.read(buffer) > 0) ;
    
                md5 = digestInputStream.getMessageDigest();
                rb = md5.digest();
    
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            } finally {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
    
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < rb.length; i++) {
                String a = Integer.toHexString(0XFF & rb[i]);
                if (a.length() < 2) {
                    a = '0' + a;
                }
                sb.append(a);
            }
            return sb.toString(); //得到md5值
        }
    }
    
    
    • application
    
    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.listFiles().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) {
    
    
            try {
                //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);
            } 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 (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    加固工具

    • 加密算法、解压和压缩和工具类 上面的一样,这里就不贴代码了
    public class Main {
    
        public static void main(String[] args) throws Exception {
            /**
             * 1.制作只包含解密代码的dex文件
             */
            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");
            }
    
            /**
             * 2.加密APK中所有的dex文件
             */
            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();
            }
    
            /**
             * 3.把dex放入apk解压目录,重新压成apk文件
             */
            classesDex.renameTo(new File(apkTemp,"classes.dex"));
            File unSignedApk = new File("app/build/outputs/apk/debug/app-unsigned.apk");
            Zip.zip(apkTemp,unSignedApk);
    //
    //
    //        /**
    //         * 4.对齐和签名
    //         */
    //        zipalign -v -p 4 my-app-unsigned.apk my-app-unsigned-aligned.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("dex error");
    //        }
    //
    //
    ////        apksigner sign --ks my-release-key.jks --out my-app-release.apk my-app-unsigned-aligned.apk
    ////        apksigner sign  --ks jks文件地址 --ks-key-alias 别名 --ks-pass pass:jsk密码 --key-pass pass:别名密码 --out  out.apk in.apk
            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 123 --ks-pass pass:123456 --key-pass pass:123456 --out "
                    + signedApk.getAbsolutePath() + " " + alignedApk.getAbsolutePath());
            process.waitFor();
    //        if(process.exitValue()!=0){
    //            throw new RuntimeException("dex error");
    //        }
            System.out.println("执行成功");
    
        }
    }
    

    GitHub代码

    第三方的加固工具

    参考

    相关文章

      网友评论

        本文标题:app加固原理(一)

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