App加固(dex加密)

什么是Dex文件?

classes.dex是apk组成的一部分,包含了能被Dalvik/Art理解的可执行文件,类似Windows的exe文件;

APK组成:

  • 1. assets目录:存放assets目录下的文件,可以通过AssetManager对象获取

  • 2. lib目录:存放所支持的CPU架构对应的二进制文件(so文件),这些文件用来各自支持自己CPU架构的二进制接口(ABI)

  • 3. res目录:存放res目录下没有被编译到arsc文件的资源,layout,drawable,mipmap等

  • 4. META-INF目录:存放签名的目录,

  • 5. classes.dex文件:dvm的可执行文件,将R.java,java source Coed,java interface打包成dex文件

  • 6. resources.arsc文件(资源映射表):res/values目录下的所有配置内容,以及在APK res目录下文件文件的映射方式

  • 7. Manifest文件:配置文件,同项目中的manifest文件

Dex文件内容:

  • 文件头:记录了dex文件的一些基本信息, dex文件大小,dex文件头大小,sha1签名,checksum校验和,以及大致的数据分布

  • 索引区:存放数据的偏移量

  • 数据区:真实的数据存放在data数据区

APK打包流程:

1. aapt将资源文件打包成R.Java, resource.arsc ,res目录
2. aidl 将aidl 接口解析成对应的java接口
3. Javac 将源代码编译成.class字节码
4. dx.bat将class字节码转化成dvm字节码(dex文件)
5. 打包生成APK
6. JarSignerapk进行debug或release签名
7. zipalign对其操作,APK包中所有的资源文件距离文件起始偏移为4字节整数倍,这样通过内存映射访问apk文件时的速度会更快

上述的操作都是通过Android SDK自带的工具来完成;

Dex加密
下面通过一个简单的demo描述APK加固的整体流程

加密流程:
  1. 首先将未加密的APK进行解压,获取到Dex文件,然后对dex文件的每一个字节进行加密(AES),加密完成生成新的dex文件(classes2.dex)
  2. 下面创建一个dex壳,通过对arr文件(android module 的打包文件)的解压可以获取到一个classes.jar文件,再通过cmd的命令,将jar转成壳dex
  3. 将壳dex 和 加密的dex(源dex)一起打包成新的APK,然后再对APK进行签名;签名后可以正常安装,

1. 解压APK,对Dex文件加密

  • 解压apk
        File apkFile = new File("source/apk/app-debug.apk");

        // 解压apk文件到unzip目录
        File apkUnZipFile = new File("source/apk/unzip");
        Zip.unZip(apkFile,apkUnZipFile);
  • 将dex文件转为内存中的字节数组
        // 创建dexFile,将dexFile写入内存—dexBytes
        File dexFile = new File("source/apk/unzip/classes.dex");
        RandomAccessFile inputStream = new RandomAccessFile(dexFile,"r");
        byte[] dexBytes = new byte[(int) inputStream.length()];
        inputStream.readFully(dexBytes);
        inputStream.close();
  • AES加密初始化
        //  AES加密操作初始化
        Cipher encoder = Cipher.getInstance("AES/ECB/PKCS5Padding");
        Cipher decoder = Cipher.getInstance("AES/ECB/PKCS5Padding");
        String key = "abcdefghijklmnop";
        byte[] keyBytes = key.getBytes();
        SecretKeySpec secretKeySpec = new SecretKeySpec(keyBytes,"AES");
        encoder.init(Cipher.ENCRYPT_MODE,secretKeySpec);
        decoder.init(Cipher.DECRYPT_MODE,secretKeySpec);
  • 字节数组加密
        //  对dex字节数组加密
        byte[] dexBytesEncrypted = encoder.doFinal(dexBytes);
  • 加密后的字节数组转为dex文件
        //  将加密后的dex字节数组写入原来的文件,
        FileOutputStream fos = new FileOutputStream(dexFile);
        fos.write(dexBytesEncrypted);
        fos.close();

        //  将加密后的dex文件改名为classes1.dex(源)
        dexFile.renameTo(new File("source/apk/unzip/classes1.dex"));

2. 解压aar,获取壳

  • 解压aar文件,获取classes.jar
        // 将arr文件解压
        File arrFile = new File("source/aar/mylibrary-debug.aar");
        Zip.unZip(arrFile,new File("source/aar/unzip"));
arr解压
  • 通过cmd调用dx将jar转为dex
        // 通过cmd 调用 dx 将jar转为dex(壳)
        File jarFile = new File("source/aar/unzip/classes.jar");
        File desDexFile = new File("source/apk/unzip/classes.dex");
        Runtime runtime = Runtime.getRuntime();
        Process process = runtime.exec("cmd.exe /C dx --dex --output="
                + desDexFile.getAbsolutePath()
                + " "
                + jarFile.getAbsolutePath());
        process.waitFor(); // 等待子进程完成
        process.destroy();

通过RunTime启动cmd命令,jvm会创建一个子进程Process,waitFor()表示当前进程阻塞直到子进程完成;

3. 打包新APK,通过cmd调用jarsigner重新签名

  • 打包成新的apk文件
        // 壳 和 源 打包成新apk
        File unsignedApk = new File("source/result/unsigned.apk");
        Zip.zip(apkUnZipFile,unsignedApk);
  • 签名
        // 签名
        File signedApk = new File("source/result/signed.apk");
        Signature.signature(unsignedApk,signedApk);

public class Signature {
    public static void signature(File unsignedApk, File signedApk) throws InterruptedException, IOException {
        String cmd[] = {"cmd.exe", "/C ","jarsigner",  "-sigalg", "MD5withRSA",
                "-digestalg", "SHA1",
                "-keystore", "C:/Users/allen/.android/debug.keystore",
                "-storepass", "android",
                "-keypass", "android",
                "-signedjar", signedApk.getAbsolutePath(),
                unsignedApk.getAbsolutePath(),
                "androiddebugkey"};
        Process process = Runtime.getRuntime().exec(cmd);
        System.out.println("start sign");
//        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
//        String line;
//        while ((line = reader.readLine()) != null)
//            System.out.println("tasklist: " + line);
        try {
            int waitResult = process.waitFor();
            System.out.println("waitResult: " + waitResult);
        } catch (InterruptedException e) {
            e.printStackTrace();
            throw e;
        }
        System.out.println("process.exitValue() " + process.exitValue() );
        if (process.exitValue() != 0) {
            InputStream inputStream = process.getErrorStream();
            int len;
            byte[] buffer = new byte[2048];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            while((len=inputStream.read(buffer)) != -1){
                bos.write(buffer,0,len);
            }
            System.out.println(new String(bos.toByteArray(),"GBK"));
            throw new RuntimeException("签名执行失败");
        }
        System.out.println("finish signed");
        process.destroy();
    }
}
APK加密过程

壳中是未加密的module代码,可以直接运行,并且负责源dex的解密工作

Dex解密(脱壳)

脱壳实现:

脱壳解密过程一般是在壳Module的Application中进行,参考Tinker的脱壳实现:首先将apk进行解压获取到加密的classes1.dex文件,然后通过流转成Byte数组,再进行AES解密,解密后重新写回到原来的classes1.dex;至此,解密过程完成,下面需要将解密后的dex文件运行起来,

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

所有的脱壳,寻找dex中的class,classloader进行类加载,都是在attachBaseContext()中完成的(即App运行启动时)

根据指定目录获取apk,解压,寻找源dex,解密

        File apkFile = new File(getApplicationInfo().sourceDir);
        //data/data/包名/files/fake_apk/
        File unZipFile = getDir("fake_apk", MODE_PRIVATE);
        File app = new File(unZipFile, "app");  // 根据指定的目录找到apk文件
        if (!app.exists()) {
            Zip.unZip(apkFile, app);    // 解压apk
            File[] files = app.listFiles();
            for (File file : files) {
                String name = file.getName();
                if (name.equals("classes.dex")) {   //  过滤壳dex

                } else if (name.endsWith(".dex")) {   //  选择源dex 解密
                    try {
                            byte[] bytes = getBytes(file);
                            FileOutputStream fos = new FileOutputStream(file);
                            byte[] decrypt = AES.decrypt(bytes);
//                        fos.write(bytes);
                            fos.write(decrypt);  //  将解密后的字节数组写回源文件(源dex文件)
                            fos.flush();
                            fos.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                    }
                }
            }
        }
将所有的dex文件从apk取出,进行类加载
        List list = new ArrayList<>();  // 解密后的dex文件
        Log.d("FAKE", Arrays.toString(app.listFiles()));
        for (File file : app.listFiles()) {
            if (file.getName().endsWith(".dex")) {
                list.add(file);
                System.gc();
            }
        }

对源dex中加密的类进行类加载

ClassLoader原理
先看一下上文类加载的原理,在介绍加固脱壳的类加载思路:

  1. 通过反射获取classLoaderpathList(DexPathList类)
  2. 再获取pathListDexElements(element[])
  3. 传入源dex,通过反射调用DexPathList类makeDexElements创建新的Element[],
  4. 合并两个数组
  5. 反射将新的element[] setclassLoader
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354