美文网首页Android 知识
Android加固技术分析与多渠道打包实践

Android加固技术分析与多渠道打包实践

作者: walter_feng | 来源:发表于2018-04-02 16:21 被阅读23次

    摘要

    由于android应用程序使用java代码开发,java编译生成的.dex文件或.class代码反编译之后很容易得到源代码。虽然已经有混淆技术可以大大提高代码反编译之后的可读性,但反编译的源码还是暴露无遗,所以出现了许多android加固方案,本文分析一种android加固方案和多渠道打包的整合。

    class文件和DEX文件

    • Android Java代码编译之后产生.class文件,然后dex工具会把 .class文件处理成 .dex文件,再把资源文件和.dex文件等打包成 .apk文件 (apk就是android package的意思)。
    • .class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中。减少了I/O操作,提高了类的查找速度。
    • Android使用dvm(Dalvik Virtual Machine)作为虚拟机,dvm执行的是.dex格式文件。jvm执行的是.class文件。
    • dvm是基于寄存器的虚拟机,而jvm执行是基于虚拟栈的虚拟机。寄存器存取速度比栈快的多,dvm可以根据硬件实现最大的优化,比较适合移动设备。

    DEX文件结构

    dex文件结构如下:
    image

    如图,整个dex文件分为三个模块

    • 文件头,文件头记录了dex文件的一些基本信息, 以及大致的数据分布. dex文件头部总长度是固定的0x70
    • 索引区,索引区中索引了整个dex中的字符串、类型、方法声明、字段以及方法的信息, 其结构体的开始位置和个数均来自dex文件头中的记录(或通过map_list也可以索引到记录)
      1. 字符串索引区,描述dex文件中所有的字符串信息
      2. 类型索引区, 描述dex文件中所有的类型, 如类类型、基本类型、返回值类型等
      3. 方法声明索引区, 描述dex文件中所有的方法声明
    • 数据区
    dex文件头结构
    字段名称 偏移值 长度 描述
    magic 0x0 0x8 dex魔数字,文件类型的标识。 固定信息: dex\n035,035是结构的版本
    checksum 0x8 0x4 去除了magic和checksum字段之外的所有内容的校验码,(alder32算法)
    signature 0xc 0x14 SHA-1签名, 去除了magic、checksum和signature字段之外的所有内容的签名
    fileSize 0x20 0x4 整个dex的文件大小
    headerSize 0x24 0x4 整个dex文件头的大小 (固定大小为0x70)
    ... ... ... ...

    上面了解了dex文件和dex文件的文件头,接下来进入主题,看一下本文所要介绍的apk的加固过程:

    APK加固过程总图解

    apk加固过程解析.png
    以上过程大致可总结为:

    需要准备的项目有两个:

    • 需要加固的Android项目,即需要保护的源代码。
    • 解密的项目,即负责解密出原apk并使用代理去启动原apk的Android项目;还包括包含加固工具的java工具类和签名的java工具类(以Library形式包含在此项目中,不参打包),负责加密和合并apk,并写入渠道信息。

    加固工具的加密过程:

    • 将原项目和解密的项目分别编译,取原项目生成的apk文件和解密项目生成的dex文件,读入字节流;
    • 使用自己定义的对称加密算法加密原apk文件的数组流,生成新的数组;
    • 将原apk文件流和解密项目dex的文件流进行拼接,生成一个新的dex文件;
    • 修改新的dex文件的文件头,使得新的拼接而成的dex文件格式合法;

    代码:

        public static String forceApk() throws Exception {
            File payloadSrcFile = new File(payloadSrcFilePath); // 需要加壳的程序
            System.out.println("input apk size:" + payloadSrcFile.length());
            File unShellDexFile = new File(unShellDexFilePath); // 解客dex
            byte[] payloadArray = encrpt(readFileBytes(payloadSrcFile));// 以二进制形式读出apk,并进行加密处理//对源Apk进行加密操作
            byte[] unShellDexArray = readFileBytes(unShellDexFile);// 以二进制形式读出dex
            int payloadLen = payloadArray.length;
            int unShellDexLen = unShellDexArray.length;
            int totalLen = payloadLen + unShellDexLen + 4;// 多出4字节是存放长度的。
            byte[] newdex = new byte[totalLen]; // 申请了新的长度
            // 添加解壳代码
            System.arraycopy(unShellDexArray, 0, newdex, 0, unShellDexLen);// 先拷贝dex内容
            // 添加加密后的解壳数据
            System.arraycopy(payloadArray, 0, newdex, unShellDexLen, payloadLen);// 再在dex内容后面拷贝apk的内容
            // 添加解壳数据长度
            System.arraycopy(intToByte(payloadLen), 0, newdex, totalLen - 4, 4);// 最后4为长度
            // 修改DEX file size文件头
            fixFileSizeHeader(newdex);
            // 修改DEX SHA1 文件头
            fixSHA1Header(newdex);
            // 修改DEX CheckSum文件头
            fixCheckSumHeader(newdex);
            File file = new File(outputDexFileName);
            if (file.delete()||!file.exists()) {
                file.createNewFile();
            }
    
            FileOutputStream localFileOutputStream = new FileOutputStream(
                    outputDexFileName);
            localFileOutputStream.write(newdex);
            localFileOutputStream.flush();
            localFileOutputStream.close();
            return replaceDex(outputDexFileName);
        }
    
        private static byte[] encrpt(byte[] srcdata) {
            for (int i = 0; i < srcdata.length; i++) {
                srcdata[i] = (byte) (0xFF ^ srcdata[i]);
            }
            return srcdata;
        }
    

    dex文件需要修改的内容:

    • 从上面列出的dex文件头各结构的含义中可以得知,拼接dex文件之后需要改变dex文件头的checksum、signature、fileSize字段以保证dex文件合法;
    • 新dex文件的大小fileSize,写在0x20 即新dex文件数组流的第32个元素,占用占四位的长度;
    • 新dex的SHA-1签名signature,写在0xc处,即0x20往前0x14个长度,所以是12-31位置;
    • 新dex的校验码checksum写在0x8处,占四位(8-11);

    代码:

    /**
    * 修改dex头 sha1值
    */
    private static void fixCheckSumHeader(byte[] dexBytes) {
            Adler32 adler = new Adler32();
            adler.update(dexBytes, 12, dexBytes.length - 12);// 从12到文件末尾计算校验码
            long value = adler.getValue();
            int va = (int) value;
            byte[] newcs = intToByte(va);
            // 高位在前,低位在后
            byte[] recs = new byte[4];
            for (int i = 0; i < 4; i++) {
                recs[i] = newcs[newcs.length - 1 - i];
                // System.out.println(Integer.toHexString(newcs[i]));
            }
            System.arraycopy(recs, 0, dexBytes, 8, 4);// 效验码赋值(8-11)
        }
    
        /**
        * 修改dex头 sha1值
        */
        private static void fixSHA1Header(byte[] dexBytes)
                throws NoSuchAlgorithmException {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            md.update(dexBytes, 32, dexBytes.length - 32);// 从32位到结束计算sha--1
            byte[] newdt = md.digest();
            System.arraycopy(newdt, 0, dexBytes, 12, 20);// 修改sha-1值(12-31)
        }
    
        /**
        * 修改dex头 file_size值
        */
        private static void fixFileSizeHeader(byte[] dexBytes) {
            // 新文件长度
            byte[] newfs = intToByte(dexBytes.length);
            // System.out.println(Integer.toHexString(dexBytes.length));
            byte[] refs = new byte[4];
            // 高位在前,低位在后
            for (int i = 0; i < 4; i++) {
                refs[i] = newfs[newfs.length - 1 - i];
            }
            System.arraycopy(refs, 0, dexBytes, 32, 4);// 修改(32-35)
        }
    
    

    以上步骤把原apk和壳文件写成了一个合法的dex文件。接下来需要签名壳项目代码生成合法的apk文件,用户才能把它正常安装到手机上去。
    (所以需要了解apk的构建流程,了解apk打包过程的童鞋可跳过)

    APK构建流程

    APP的构建流程涉及许多将项目转换成 Android 应用软件包 (APK) 的工具和流程。构建流程非常灵活,因此了解它的一些底层工作原理会对我们很有帮助。

    典型 Android 应用模块的构建流程如下:

    1

    如上图,典型 Android 应用模块的构建流程通常依循下列步骤:

    • 编译器将源代码转换成 DEX(Dalvik Executable) 文件(其中包括运行在 Android 设备上的字节码),将所有其他内容转换成已编译资源。
    • APK 打包器将 DEX 文件和已编译资源合并成单个 APK。不过,必须先将APK签名,才能将应用安装并部署到 Android 设备上。
    • APK 打包器使用调试或发布密钥库签署您的 APK:
      1. 如果您构建的是调试版本的应用(即专用于测试和分析的应用),打包器会使用调试密钥库签署您的应用。Android Studio 自动使用调试密钥库配置新项目。
      2. 如果您构建的是打算向外发布的发布版本应用,打包器会使用发布密钥库签署您的应用。要创建发布密钥库,请阅读在 Android Studio 中签署您的应用。
      3. 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,减少其在设备上运行时的内存占用。
        构建流程结束时,将获得可用来进行部署、测试的调试 APK,或者可用来发布给外部用户的release版本的APK。
    回到加固过程,把生成的dex文件替换到壳apk中去,需要先签名

    APK签名

    • 这里使用jarsigner命令签名:
        public static String sign(String apkPath) throws Exception {
            String nameFlag = apkPath.replace(".apk", "");
            String output = nameFlag + "_singed.apk";
            String shell = "jarsigner -verbose -digestalg SHA1 -sigalg MD5withRSA -keystore "
                    + keystorePath + " -signedjar "  + output  + " " + apkPath + " "+ alias + " -storepass " + storepass + " -keypass " + keypass;
            System.out.println(shell);
            callShell(shell);
            return output;
        }
    
        public static void callShell(String shellString) throws Exception {
            Process process = Runtime.getRuntime().exec(shellString);
            int exitValue = process.waitFor();
            BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }
            String result = sb.toString();
            System.out.println(result);
            if (0 != exitValue) {
                throw new Exception("call shell failed. error code is :"
                        + exitValue);
            }
        }
    

    Android多渠道打包#

    背景

    Android应用程序会发布到各个平台的应用市场上去,以便不同品牌的手机可以方便的在自己的应用市场内下载到想要的apk,但由于Android手机品牌和应用市场非常多,一般大型的app会发布到几十个甚至更多的应用市场中去。为了统计用户来源,需要分别统计这些渠道的用户量或其他属性,因此需要给apk文件加入特殊标识 以识别应用来源。
    如果按照传统打打包方式,需要修改一次AndroidManifest.xml文件的渠道号重新打包一次,往往几十个包需要几个小时甚至更久,效率及其的低下。
    为了解决这个问题,业内诞生的较早的多渠道快速打包方案有美团多渠道打包方案

    美团多渠道打包

    美团多渠道打包的思路是,先打包并签名一个没有渠道标识的apk文件,然后每打一个渠道包复制一个apk文件出来,这个apk文件的META-INF目录中添加一个使用渠道号命名的空文件即可(v1.0签名机制下 添加一个空文件不会影响apk文件的签名),apk安装后代码中读取空文件文件名就可以得到渠道信息了。这种打包方式速度非常快,900多个渠道不到一分钟就能打完。

    增加渠道标识文件:

        public static boolean changeChannel(final String zipFilename,
                                            final String channel) {
            try (FileSystem zipfs = FileUtils.createZipFileSystem(zipFilename, false)) {
                final Path root = zipfs.getPath("/META-INF/");
                ChannelFileVisitor visitor = new ChannelFileVisitor();
                Files.walkFileTree(root, visitor);
                Path existChannel = visitor.getChannelFile();
                Path newChannel = zipfs.getPath(CHANNEL_PREFIX + channel);
                if (existChannel != null) {
                    Files.move(existChannel, newChannel, StandardCopyOption.ATOMIC_MOVE);
                } else {
                    Files.createFile(newChannel);
                }
                return true;
            } catch (IOException e) {
                e.printStackTrace();
            }
            return false;
        }
    

    操作结果:

    image

    (后续在apk中读取渠道信息的代码就不展示了,有兴趣的可以查看美团多渠道打包方案

    packer-ng打包

    • 原理
      packer-ng使用了另一种思路。android使用的apk包的压缩方式是zip,与zip有相同的文件结构,在zip的核心目录Central directory file header中包含一个File comment区域,可以存放一些数据。File comment是zip文件如果可以正确的修改这个部分,就可以在不破坏压缩包、不用重新打包的的前提下快速的给apk文件写入自己想要的数据。
      Central directory file header 即zip核心目录,记录了压缩文件的目录信息,在这个数据区中每一条纪录对应在压缩源文件数据区中的一条数据。
      End of central directory record(EOCD) 目录结束标识存在于整个归档包的结尾,用于标记压缩的目录数据的结束。每个压缩文件必须有且只有一个EOCD记录。

    • zip目录结束标识结构:

    偏移值 长度 描述 说明
    0 4 End of central directory signature = 0x06054b50 核心目录结束标记(0x06054b50)
    4 2 Number of this disk 当前磁盘编号
    6 2 number of the disk with the start of the central directory 核心目录开始位置的磁盘编号
    8 2 total number of entries in the central directory on this disk 该磁盘上所记录的核心目录数量
    10 2 total number of entries in the central directory 核心目录结构总数
    12 2 Size of central directory (bytes) 核心目录的大小
    16 4 offset of start of central directory with respect to the starting disk number 核心目录开始位置相对于archive开始的位移
    20 2 .ZIP file comment length(n) 注释长度
    22 n .ZIP Comment 注释内容

    目录结束标识区域包含zip comment 区域可以写入少量信息并不会印象apk签名,所以可以将渠道数据直接写在这里。

    • 长度处理
      由于数据是不确定的,我们无法知道comment的长度,从表中可以看到zip定义comment的长度的位置在comment之前,所以无法从zip中直接获取comment的长度。这里我们需要自定义comment的长度,在自定义comment内容的后面添加一个区域储存comment的长度。

    • 将数据写入comment

    public static void writeApk(File file, String comment) {
        ZipFile zipFile = null;
        ByteArrayOutputStream outputStream = null;
        RandomAccessFile accessFile = null;
        try {
            zipFile = new ZipFile(file);
            String zipComment = zipFile.getComment();
            if (zipComment != null) {
                return;
            }
            byte[] byteComment = comment.getBytes();
            outputStream = new ByteArrayOutputStream();
            outputStream.write(byteComment);
            outputStream.write(short2Stream((short) byteComment.length));
            byte[] data = outputStream.toByteArray();
            accessFile = new RandomAccessFile(file, "rw");
            accessFile.seek(file.length() - 2);
            accessFile.write(short2Stream((short) data.length));
            accessFile.write(data);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (zipFile != null) {
                    zipFile.close();
                }
                if (outputStream != null) {
                    outputStream.close();
                }
                if (accessFile != null) {
                    accessFile.close();
                }
            } catch (Exception e) {
            }
        }
    }
    
    • 读取渠道信息

    获取apk路径,找到comment开始位置,找到我们自己写入的渠道信息的长度。读出写到comment中的信息。
    (后续读取渠道信息的代码就不展示了,感兴趣的童鞋可以去阅读ackage_Ng源码)

    至此,从apk加固到签名、写入多渠道数据的整个流程就结束了

    后续工作

    • 替换壳apk文件的icon图标为原apk的icon,修改壳apk的配置文件中的包名;
    • 搭建加固工具的UI,使其桌面化。

    相关文章

      网友评论

        本文标题:Android加固技术分析与多渠道打包实践

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