美文网首页
Android 性能优化08 ---APK优化01(签名优化)

Android 性能优化08 ---APK优化01(签名优化)

作者: 沪漂意哥哥 | 来源:发表于2022-04-09 10:57 被阅读0次

    一. APK打包本质及打包流程细节分析

    image.png

    apk加载流程:
    1.build-tools:实际提供工具
    2.gradle:调用build-tools组件
    3.as
    联系:
    AS(IDE图形软件) -->gradle(工具,帮我们调用对用的android工具) -->android工具
    流程概述:
    1、打包资源文件,生成R.java文件
    2、处理aidl文件,生成相应java 文件
    3、编译工程源代码,生成相应class 文件
    4、转换所有class文件,生成classes.dex文件
    5、打包生成apk
    6、对apk文件进行签名
    7、对签名后的apk文件进行对其处理

    第一步:打包资源文件,生成R.java文件。

    【输入】Resource文件(就是工程中res中的文件)、Assets文件(相当于另外一种资源,这种资源Android系统并不像对res中的文件那样优化它)、AndroidManifest.xml文件(包名就是从这里读取的,因为生成R.java文件需要包名)、Android基础类库(Android.jar文件)
    【工具】aapt工具
    【输出】打包好的资源(bin目录中的resources.ap_文件)、R.java文件(gen目录中)

    第二步:处理aidl文件,生成相应的java文件。

    【输入】源码文件、aidl文件、framework.aidl文件
    【工具】aidl工具
    【输出】对应的.java文件

    第三步:编译工程源代码,生成下相应的class文件。

    【输入】源码文件(包括R.java和AIDL生成的.java文件)、库文件(.jar文件)
    【工具】javac工具
    【输出】.class文件

    第四步:转换所有的class文件,生成classes.dex文件。

    【输入】 .class文件(包括Aidl生成.class文件,R生成的.class文件,源文件生成的.class文件),库文件(.jar文件)
    【工具】javac工具
    【输出】.dex文件

    第五步:打包生成apk。

    【输入】打包后的资源文件、打包后类文件(.dex文件)、libs文件(包括.so文件,当然很多工程都没有这样的文件,如果你不使用C/C++开发的话)
    【工具】apkbuilder工具
    【输出】未签名的.apk文件

    第六步:对apk文件进行签名。

    【输入】未签名的.apk文件
    【工具】jarsigner
    【输出】签名的.apk文件

    第七步:对签名后的apk文件进行对齐处理。

    【输入】签名后的.apk文件
    【工具】zipalign工具
    【输出】对齐后的.apk文件


    image.png

    二. V1,V2,V3签名原理及处理思路

    APK签名原理 image.png

    三、 V1 签名方案

    1. 签名相关的文件

    apk 本质是个 zip 文件,解压缩后,在 META-INFO 文件夹中可以看到有 MANIFEST.MF、CERT.SF、CERT.RSA 三个文件。这三个文件在签名时创建,在安装时用于验证签名。下面让我们看一下这三个文件各自的作用:

    1.1 MANIFEST.MF文件

    文件的作用: 记录 apk 中每一个文件对应的摘要信息,防止某个文件被篡改。
    文件的内容: 打开 MANIFEST.MF 文件可以看到文件内容是这种格式:

    Manifest-Version: 1.0
    Built-By: Generated-by-ADT
    Created-By: Android Gradle 2.3.1
    
    Name: res/drawable-hdpi-v4/tracepoint_tip.png
    SHA1-Digest: UqNwQcd9oLGpVfILjkVOtNQmySA=
    
    Name: res/layout/activity_new_base_layout.xml
    SHA1-Digest: Uw3jXiCR9Msf9C6P0Mjcmh2/A/E=
    
    ...
    

    前三行记录了基础信息,后面每一块都对应了 apk 中一个原始文件的数据摘要,摘要算法是 SHA-1。 在 MANIFEST.MF 文件没被篡改的情况下,可以用于保证 apk 中的其他文件不被篡改。 那怎么保证 MANIFEST.MF 文件本身不被篡改呢? 就是靠下面的 CERT.SF 文件了.

    1.2 CERT.SF文件

    文件的作用: 记录 MANIFEST.MF 文件的摘要,以及 MANIFEST.MF 中,每个数据块的摘要。防止 MANIFEST.MF 被篡改。
    文件的内容: CERT.SF 的文件内容如下:

    Signature-Version: 1.0
    SHA1-Digest-Manifest: m4hofJv2im9b2HQo/h6VPKRnzqE=
    Created-By: 1.0 (Android)
    
    Name: res/drawable-hdpi-v4/tracepoint_tip.png
    SHA1-Digest: UqNwQcd9oLGpVfILjkVOtNQmySA=
    
    Name: res/layout/activity_new_base_layout.xml
    SHA1-Digest: Uw3jXiCR9Msf9C6P0Mjcmh2/A/E=
    ...
    

    CERT.SF 如果没被篡改,就能用于验证清单文件 MANIFEST.MF 是否被篡改。但又怎么验证 CERT.SF 是否被篡改呢? 靠的就是签名文件 CERT.RSA 了

    1.3 CERT.RSA文件

    文件的作用: 这个文件是为了验证 CERT.SF 文件有没有被篡改。
    文件的内容: 它包含了 「对 CERT.SF 文件的签名」以及「包含公钥的开发者证书」。

    2. V1的签名机制

    image.png

    签名的流程如下:

    1. 计算每一个原始文件的 SHA-1 摘要,写入到 MANIFEST.MF 中;
    2. 计算整个 MANIFEST.MF 文件的 SHA-1 摘要,写入到 CERT.SF 中;
    3. 计算 MANIFEST.MF 中,每一块的 SHA-1 摘要,写入到 CERT.SF 中;
    4. 计算整个 CERT.SF 文件的摘要,使用开发者私钥计算出摘要的签名;
    5. 将签名和开发者证书(X.509)写入 CERT.RSA 。

    3. V1签名是怎么校验的?

    校验的流程如下:

    1. 取出 CERT.RSA 中包含的开发者证书;
    2. 通过系统的根证书(CA证书)验证这个开发者证书是否可信;
    3. 如果开发者证书可信,用证书中的公钥解密 CERT.RSA 中包含的签名。
    4. 计算 CERT.SF 的签名;
    5. 对比 (3) 和 (4) 的签名是否一致;
    6. 如果一致,用 CERT.SF 去校验 MANIFEST.MF 是否被修改;
    7. 如果没有被修改,再用 MANIFEST.MF 中的每一块数据去校验每一个文件是否被修改。

    4. V1签名如何防止篡改

    假如攻击者修改了其中某一个文件,那么他必须修改 MANIFEST.MF 中对应文件的摘要,否则这个文件校验不通过; 接着还得修改 CERT.SF 中的摘要,否则摘要校验不过; 还得重新计算 CERT.SF 的签名,否则签名校验不通过; 但是计算签名需要私钥,私钥在开发者手中,攻击者没有私钥,所以无法签名。

    5. V1签名存在的问题

    校验速度慢:需要对 apk 中的每个文件都计算摘要并验证,如果文件很多,校验时间会很长。 完整性不够:V1 签名只会校验 Zip 文件中的部分文件,例如 META-INFO 文件夹就不会参与校验。

    四、 V2 签名方案

    V2 签名是在 Android7.0 之后引入的,它解决了 V1 签名校验时速度慢的问题,同时对完整性的校验扩展到整个安装包。

    了解 V2 签名原理之前,我们先了解一下 Zip 文件:

    1. Zip 文件

    1.1 Zip 文件的格式和解析过程
    image.png

    a.先从文件尾部查找 0x06054b50,确定 End Of Central Directory Record 区域的起始位置;
    b.解析 EoCD 区域,并获得中央目录的起始位置;
    c.根据起始位置,逐个解析文件。

    从解析过程可以看出,如果在 「文件信息部分」 和 「中央目录部分」之间插入了其他数据,是不会影响 Zip 文件的解压缩的。

    2. V2 签名数据块的格式[图片上传中...(image.png-5d6244-1649399220539-0)]

    V2 签名时,会将 签名信息块 插入到 Zip 文件的「文件信息」和「中央目录」之间,如图: image.png

    3. V2 摘要计算方式

    V2 签名摘要的计算就不是按照文件计算的了,而是按照 1MB 为单位计算: image.png

    步骤:
    a.对原始apk文件的 文件信息部分、中央目录部分、EoCD部分,按照 1MB 大小分割为多个小块(Chunks);
    b.2. 分别对每一个小块计算其摘要,类似于 V1 签名中的 MANIFEST.MF 文件;
    c.对(2)中所有摘要计算其摘要,类似于 V1 签名中的 CERT.SF 文件;

    4. V2 签名的校验

    Android 7.0 及以上在校验时,会先判断是否具有 V2 签名,如果有 V2 签名,会走 V2 签名的校验流程,不再验证V1签名了。
    如何判断是否有V2签名? 根据Zip文件格式的规则,我们可以找到中央目录区的起始位置。 读取从起始位置开始往回的16个字节,判断这16个字节的值是否为 "Apk Sig Block 42",如果是,则对应上了魔数,说明有 V2 签名。后续就是解析 V2 签名块的流程了。

    五、 V3 签名方案

    V3 签名方案的签名块格式和V2完全一样,只是 V2 的签名块信息存放在 ID = 0x7109871a 的数据块中,而 V3 的签名信息存放在 ID = 0xf05368c0 的数据块中。
    在这个新的数据块中,记录了旧的签名信息和新的签名信息,以密钥转轮的方案,做签名的替换和升级。这意味着我们可以更改 APK 的签名。

    六.如何绕过V1,V2,V3签名完成信息注入

    案例:多渠道打包
    优点:官方,可配置高
    缺点:效率太慢

    1. java工程方案

    public class Main {
    
        public static void main(String[] args) throws Exception {
            long l = System.currentTimeMillis();
            /**
             * 1、初始化:创建输出目录、读取渠道文件
             */
            File baseApk = new File("makechannel/output/app-debug.apk");
            File outDir = new File("makechannel/output");
            outDir.mkdirs();
    
            /**
             * 2、解析APK(zip文件)
             * 将整个fiel解析成一个APK对象
             */
            Apk apk = ApkParser.parser(baseApk);
    
            /**
             * 3、生成APK
             */
            File channelFile = new File("makechannel/channel.txt");
            List<String> channels = readChannelFile(channelFile);
    
            String name = baseApk.getName();
            name = name.substring(0, name.lastIndexOf("."));
            System.out.println(name);
            for (String channel : channels) {
                File file = new File(outDir, name + "-" + channel +
                        ".apk");
                ApkBuilder.generateChannel(channel, apk, file);
            }
    
            System.out.println(System.currentTimeMillis() - l);
    
        }
    
        private static List<String> readChannelFile(File channelFile) throws Exception {
            BufferedReader br = new BufferedReader(new InputStreamReader(new
                    FileInputStream(channelFile)));
            List<String> list = new ArrayList<>();
            String line;
            while ((line = br.readLine()) != null) {
                list.add(line);
            }
            return list;
        }
    }
    
    

    主工程:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    
        public void getChannel(View view) {
            String channel = ChannelHelper.getChannel(this);
            Toast.makeText(this, "当前渠道:" + channel, Toast.LENGTH_SHORT).show();
        }
    }
    
    
    public class ChannelHelper {
        private static final String TAG = "ChannelHelper";
    
        private static String channel = null;
    
    
        public static String getChannel(Context context) {
            if (channel != null) {
                return channel;
            }
            try {
                Apk apk = ApkParser.parser(context.getApplicationInfo().sourceDir);
                Log.i("test","APK is V2:"+apk.isV2());
                if (apk.isV2()) {
                    return v2Channel(apk);
                } else if (apk.isV1()) {
                    return v1Channel(apk);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
     
        private static String v1Channel(Apk apk) throws UnsupportedEncodingException {
            ByteBuffer data = apk.getEocd().getData();
            short commentlen = data.getShort(Constants.EOCD_COMMENT_LEN_OFFSET);
            if (commentlen == 0) {
                return null;
            }
            byte[] commentBytes = new byte[commentlen];
            data.position(Constants.EOCD_COMMENT_OFFSET);
            data.get(commentBytes);
            channel = new String(commentBytes, Constants.CHARSET);
            return channel;
        }
    
        private static String v2Channel(Apk apk) throws UnsupportedEncodingException {
            ByteBuffer byteBuffer = apk.getV2SignBlock().getPair().get(Constants
                    .APK_SIGNATURE_SCHEME_V2_CHANNEL_ID);
            channel = new String(byteBuffer.array(), Constants.CHARSET);
            return channel;
    
        }
    }
    
    

    结果:


    image.png

    2. 插件方案

    工程截图: image.png

    build.gradle:

    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        implementation gradleApi()
    }
    
    sourceCompatibility = "7"
    targetCompatibility = "7"
    
    tasks.withType(JavaCompile) {
        options.encoding = "UTF-8"
    }
    
    apply plugin: 'maven-publish'
    
    publishing {
        publications {
            ChannelPlugin(MavenPublication) {
                from components.java
                groupId = 'com.luisliuyi.plugin'
                artifactId = 'channel'
                version = '1.1'
            }
        }
    }
    
    

    ChannelPlugin.java

    public class ChannelPlugin implements Plugin<Project>{
        @Override
        public void apply(Project project) {
            project.getExtensions().create("channel", ChannelExtensions.class);
            project.afterEvaluate(new Action<Project>() {
                @Override
                public void execute(Project project) {
                    project.getTasks().create("assembleChannel",ChannelTask.class);
                }
            });
        }
    }
    
    

    ChannelTask.java

    public class ChannelTask extends DefaultTask {
        private ChannelExtensions channelExtensions;
    
        public ChannelTask() {
            setGroup("渠道包");
            setDescription("生成渠道包");
            channelExtensions = getProject().getExtensions().getByType(ChannelExtensions.class);
        }
    
        @TaskAction
        void run() {
            File baseApk = new File(channelExtensions.baseApk);
            File channelFile = new File(channelExtensions.channelFile);
            File outDir = new File(channelExtensions.outDir);
            outDir.mkdirs();
    
            String name = baseApk.getName();
            name = name.substring(0, name.lastIndexOf("."));
    
            try {
                List<String> channels = readChannelFile(channelFile);
                Apk apk = ApkParser.parser(baseApk);
                for (String channel : channels) {
                    File file = new File(outDir, name + "-" + channel +
                            ".apk");
                    ApkBuilder.generateChannel(channel, apk, file);
                }
    
            }  catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private static List<String> readChannelFile(File channelFile) throws Exception {
            BufferedReader br = new BufferedReader(new InputStreamReader(new
                    FileInputStream(channelFile)));
            List<String> list = new ArrayList<>();
            String line;
            while ((line = br.readLine()) != null) {
                list.add(line);
            }
            return list;
        }
    }
    
    

    七.代码地址

    https://gitee.com/luisliuyi/android-optimize-apk01.git
    

    相关文章

      网友评论

          本文标题:Android 性能优化08 ---APK优化01(签名优化)

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