app加固原理(一)

前言

apk正常打包后可以通過(guò) 反編譯工具使用 得到源碼,那這么長(zhǎng)時(shí)間的辛苦不就白費(fèi)了嗎,這就引出一個(gè)問(wèn)題了:怎么保證不讓別人不容易拿到源碼呢?

當(dāng)然得是通過(guò)加固啦,使用第三方的加固工具 (結(jié)尾給大家),但是作為一名熱愛(ài)學(xué)習(xí)的程序員,當(dāng)然得明白其中的原理才好。

app加固原理

加固原理.png
  1. 制作一個(gè)殼程序 (功能:解密和加載dex文件)
  2. 使用加密工具對(duì)原apk的dex文件進(jìn)行加密
  3. 最后重新打包、對(duì)齊、簽名

實(shí)現(xiàn)

1. 制作殼程序

  • 制作殼程序,殼程序包含兩功能解密dex文件和加載dex文件,先說(shuō)加載dex,

  • 解密dex文件:解壓apk包得到dex文件,然后把加密過(guò)的dex文件進(jìn)行解密

  • 那系統(tǒng)又是怎么加載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);
    }
}

這里調(diào)用父類(lèi)的構(gòu)造方法

Android源碼目錄\libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java

看下面這個(gè)方法

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //這里傳個(gè)名字 和 集合, 就是說(shuō)把某個(gè)類(lèi)進(jìn)行加載 
        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) {
       //通過(guò)遍歷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;
}

從這個(gè)方法中看到,dex是通過(guò)遍歷dexElements去加載的,可以通過(guò)反射dexElements拿到已經(jīng)加載的dex文件,那我們看dexElements的初始化

  //dexElements 初始化
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
                                            suppressedExceptions);


 private static Element[] makePathElements(List<File> files, File optimizedDirectory,
                                              List<IOException> suppressedExceptions) {
............................                                              
}

那我們通過(guò)反射調(diào)用這個(gè)方法把解密后的dex文件通過(guò)makePathElements方法反射 加載進(jìn)來(lái),再和原來(lái)的dex合并,那這個(gè)app就能運(yùn)行了。

3. 重新打包、對(duì)齊、簽名

  1. 重新打包
    把殼程序的dex文件和加密后的文件進(jìn)行打包
  2. 對(duì)齊

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

代碼實(shí)現(xiàn)

殼程序

  • 加密算法
    public class AES {
    
      //16字節(jié)
      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 {
              // 生成一個(gè)實(shí)現(xiàn)指定轉(zhuǎn)換的 Cipher 對(duì)象。
              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文件中每一個(gè)條目
            Enumeration<? extends ZipEntry> entries = zipFile.entries();
            //遍歷
            while (entries.hasMoreElements()) {
                ZipEntry zipEntry = entries.nextElement();
                //zip中 文件/目錄名
                String name = zipEntry.getName();
                //原來(lái)的簽名文件 不需要了
                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);
                    //創(chuàng)建目錄
                    if (!file.getParentFile().exists()) {
                        file.getParentFile().mkdirs();
                    }
                    //寫(xiě)文件
                    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();
        // 對(duì)輸出文件做CRC32校驗(yàn)
        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  遞歸子目錄時(shí)的完整目錄 如 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("/");
        }
        //添加一個(gè)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();
    }

}
  • 工具類(lèi)


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;
    }

    /**
     * 反射獲得 指定對(duì)象(當(dāng)前-》父類(lèi)-》父類(lèi)...)中的 成員屬性
     *
     * @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);
                //如果無(wú)法訪問(wèn) 設(shè)置為可訪問(wèn)
                if (!field.isAccessible()) {
                    field.setAccessible(true);
                }
                return field;
            } catch (NoSuchFieldException e) {
                //如果找不到往父類(lèi)找
                clazz = clazz.getSuperclass();
            }
        }
        throw new NoSuchFieldException("Field " + name + " not found in " + instance.getClass());
    }


    /**
     * 反射獲得 指定對(duì)象(當(dāng)前-》父類(lèi)-》父類(lèi)...)中的 函數(shù)
     *
     * @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) {
                //如果找不到往父類(lèi)找
                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 結(jié)尾的目錄 則計(jì)算該目錄下的文件的md5值

                            // 遞歸遍歷
                            traverseFolder(f, suffix);
                        }

                    } else {
                        // 得到文件的md5值
                        String string = checkMd5(f);
                        // 將每個(gè)文件的md5值相加
                        fileSum += string;
                    }
                }
            }

        } else {
            return null; // 目錄不存在
        }

        return fileSum; // 返回所有文件md5值字符串之和
    }

    /**
     * 計(jì)算文件md5值
     * 檢驗(yàn)文件生成唯一的md5值 作用:檢驗(yàn)文件是否已被修改
     *
     * @param file 需要檢驗(yàn)的文件
     * @return 該文件的md5值
     */
    private static String checkMd5(File file) {

        // 若輸入的參數(shù)不是一個(gè)文件 則拋出異常
        if (!file.isFile()) {
            throw new NumberFormatException("參數(shù)錯(cuò)誤!請(qǐng)輸入校準(zhǔn)文件。");
        }

        // 定義相關(guān)變量
        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創(chuàng)建Application之后調(diào)用的第一個(gè)方法
     * 可以在這個(gè)方法中進(jìn)行解密,同時(shí)把dex交給android去加載
     */
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        //獲取用戶填入的metadata
        getMetaData();
        //得到當(dāng)前加密了的APK文件
        File apkFile = new File(getApplicationInfo().sourceDir);
        //把a(bǔ)pk解壓   app_name+"_"+app_version目錄中的內(nèi)容需要boot權(quán)限才能用
        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<>();
        //進(jìn)行解密(最好做MD5文件校驗(yàn))
        if (!dexDir.exists() || dexDir.listFiles().length == 0) {
            //把a(bǔ)pk解壓到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);
                        //讀取文件內(nèi)容
                        byte[] bytes = Utils.getBytes(file);
                        //解密
                        byte[] decrypt = AES.decrypt(bytes);
                        //寫(xiě)到指定的目錄
                        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.把解密后的文件加載到系統(tǒng)
            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.獲取數(shù)組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);

            //合并數(shù)組
            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數(shù)組
            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();
        }
    }
}

加固工具

  • 加密算法、解壓和壓縮和工具類(lèi) 上面的一樣,這里就不貼代碼了
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文件拿出來(lái)加密
        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.對(duì)齊和簽名
//         */
//        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("執(zhí)行成功");

    }
}

GitHub代碼

第三方的加固工具

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 聲明:原創(chuàng)文章,轉(zhuǎn)載請(qǐng)備注來(lái)源:http://shuwoom.com/?p=360&preview=true 10...
    空同定翁閱讀 2,382評(píng)論 0 6
  • Base64.java public final class Base64 { static private ...
    BUG弄潮兒閱讀 937評(píng)論 0 0
  • Java字節(jié)碼詳解(二)字節(jié)碼的運(yùn)行過(guò)程 2018年10月23日 17:31:04 talex 閱讀數(shù) 677 文...
    呵呵_9e25閱讀 235評(píng)論 0 0
  • 山雞哥來(lái)啦 素萬(wàn)獨(dú)家冠名哦 看到素萬(wàn)小U和靠墊了嗎 29738213
    94f1d6bd2d12閱讀 241評(píng)論 0 0
  • 教育既要看過(guò)程更要看結(jié)果,當(dāng)溫和的過(guò)程沒(méi)有效果時(shí),我會(huì)改變策略,就像今晚,我們又回到了以前那個(gè)熟悉的不能再熟悉場(chǎng)面...
    九五自尊閱讀 217評(píng)論 0 0

友情鏈接更多精彩內(nèi)容