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

2022-04-09  本文已影响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
上一篇下一篇

猜你喜欢

热点阅读