Android 知识

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

2018-04-02  本文已影响23人  walter_feng

摘要

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

class文件和DEX文件

DEX文件结构

dex文件结构如下:
image

如图,整个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
以上过程大致可总结为:

需要准备的项目有两个:

加固工具的加密过程:

代码:

    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头 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文件替换到壳apk中去,需要先签名

APK签名

    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打包

偏移值 长度 描述 说明
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签名,所以可以将渠道数据直接写在这里。

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加固到签名、写入多渠道数据的整个流程就结束了

后续工作

上一篇下一篇

猜你喜欢

热点阅读