ZIP文件格式及其在android系统中的应用

1 篇文章 0 订阅

zip是一种归档文件格式,zip可以把若干文件和目录下的文件进行归档,这些归档的文件可以压缩也可以不压缩,并且压缩算法也是可以选择的,目前zip最经常使用的是deflate算法,因为zip中包含若干归档的文件,每个文件都有一个元数据区描述该文件,而这个元数据区域是不能被压缩的,因此如果zip中存在大量文件时,直接存储zip格式的文件并不是很有效率,可以对一个zip格式的文件,使用gzip进行压缩,gzip是http请求中默认的压缩格式,gzip通常也是使用deflate算法,但是gzip有相对小的元数据区域.关于zip和gzip的格式,请参选相关规范.

deflate算法在android系统(或者java系统)中有API可以直接调用,其实现由底层的zlib库负责,java相关的封装为java的类InflaterDeflater实现.在c/c++环境下,可以直接使用zlib的API,只需包含zlib.h即可.通常更高的压缩比,更快的时间执行效率的算法是更优秀的压缩算法,在内存受限的设备上,内存占用也可能是考虑因素.根据压缩算法固有的属性,解压缩通常比压缩快上好几倍,所以在一个解压缩运行频率高,而压缩运行频率低的上下文环境中,取决于解压缩的时间消耗, 例如android系统中安装APK的解压缩过程.通常而言deflate算法的压缩比是可以的,但是压缩和解压缩的效率不是很高,facebook设计的新的压缩算法zstd,想比较deflate算法,压缩比略有提高,压缩和解压缩效率提高显著,因此较deflate算法更好一些.其github工程链接为https://github.com/facebook/zstd/wiki .

zip格式是个典型的分段结构,包括三部分构成,前面是文件内容段,后面两段都是元数据:
这里写图片描述
通常对于zip中包含的每个文件,视为一个entry.对于zip格式,其访问机制可以用如下伪码表示:

search the start of End of central directory 
    start=file.length-minLenEOCD
    stop=file.length-maxLenEOCD
    for(;start>=stop;start--)
        seek(start)
        int curr=read()
        if (curr==signarure(EOCD)) break 
 seek(offset of central directory)
 load central directory
 jump to file entry to access

这段伪码的思想就是先找到EOCD在文件中的偏移,EOCD中记录了central directory在文件中的偏移,从而定位到central directory起始位置,central directory包含了各个file entry的元数据和在文件中的偏移.从而可以定位到指定file entry的位置,每个file entry的起始部分是local file header,描述该文件的元数据.其中第一步中如何定位EOCD在文件中的偏移,其细节是这样的:对于EOCD段,其最后是一个comments字段,该字段为一个short,通常zip文件不会填充comments字段,伪码中minLenEOCD就是comments未填充时(长度为0字节)的EOCD长度,maxLenEOCD为comments为最大填充时(长度为65536字节)的EOCD的长度,所以在start和stop这两个文件偏移区间进行扫描,如果匹配到EOCD的签名(四字节值0x06054B50),则定位到了EOCD在文件中的起始位置.

有的zip实现会把central directory部分装载到内存,这样对file entry的定位就比较快,但是如果zip中file entry比较多,可能对内存消耗有一定影响.

android系统中的jar包和APK文件都是zip格式,jar和APK根据具体的使用场景,增加了一些特殊含义的zip entry, 比如jar和APK都支持签名,增加了META-INF目录下的文件.jar可以创建成executable jar,就需要在MF文件中配置Main-Class,并且需要有签名格式正确的main方法(public static void main(String[] args)).Executable jar运行时,将启动虚拟机,装载jar中的类文件,执行entry point类的main方法,执行完毕后,退出虚拟机进程.如果是在java环境中, 启动java虚拟机, 如果是在android环境中,则启动android虚拟机.java环境中的executable jar,比如proguard.jar(代码混淆),apktool.jar(反编译APK)等.android环境中的executable jar,比如系统自带的pm.jar等.在adb shell环境中,运行pm命令时,其执行流程为:首先会执行pm脚本文件(/system/bin),该脚本文件配置好环境变量以后,会启动app_process可执行文件(/system/bin),该程序启动android虚拟机,运行Pm类中的main方法,解析命令行参数,执行相应逻辑,执行完毕退出虚拟机进程.

Android系统中存在不同的逻辑流程要解析zip文件格式,包括签名验证和安装过程,签名验证使用java实现,安装过程使用c实现,由于二者实现的差异以及不严谨,曾导致过若干个安全漏洞,可以绕过签名验证执行任意代码.Masterkey漏洞是在原有的classes.dex前面放置同名的含有恶意代码的文件,这样会通过签名验证,但是却把含有恶意代码的dex安装进来了.int溢出漏洞同样可以通过签名验证,但是可以执行任意文件名称的恶意代码.

Android系统的APK签名(v1版本实现)基本继承了java的jar签名机制,即在zip格式中引入了META-INF目录,存在的区别有:APK是自签名,而且android系统不会校验证书的有效日期.APK文件既可以使用jarsigner进行签名,也可以使用android系统实现的apksigner进行签名.APK签名后,会在META-INF目录下生成MF文件,signature文件(SF)signature block文件(RSA/DSA/EC). Signature block文件包含证书信息以及对signature文件的签名,signature文件是MF文件section的摘要,MF文件包含zip entry 文件的摘要.

Android系统安装APK文件时,进行签名验证,需要进行zip解析,包括的主要步骤有:

  1. 遍历meta-inf目录下的zip entry, 以文件名和文件内容建立一个HashMap(StrictJarFile.java).
private HashMap<String, byte[]> getMetaEntries() throws IOException {
    HashMap<String, byte[]> metaEntries = new HashMap<String, byte[]>();

    Iterator<ZipEntry> entryIterator = new EntryIterator(nativeHandle, "META-INF/");
    while (entryIterator.hasNext()) {
        final ZipEntry entry = entryIterator.next();
        metaEntries.put(entry.getName(), Streams.readFully(getInputStream(entry)));
    }

    return metaEntries;
}
  1. 确认MF中计算摘要的文件在ZIP中对应的entry是存在的(StrictJarFile.java).其中StrictJarManifest会解析MF中的项目,获得文件名,该步骤执行完毕后,继续执行verifier.readCertificates继续签名验证.
/**
 * @param name of the archive (not necessarily a path).
 * @param fd seekable file descriptor for the JAR file.
 * @param verify whether to verify the file's JAR signatures and collect the corresponding
 *        signer certificates.
 * @param signatureSchemeRollbackProtectionsEnforced {@code true} to enforce protections against
 *        stripping newer signature schemes (e.g., APK Signature Scheme v2) from the file, or
 *        {@code false} to ignore any such protections. This parameter is ignored when
 *        {@code verify} is {@code false}.
 */
private StrictJarFile(String name,
        FileDescriptor fd,
        boolean verify,
        boolean signatureSchemeRollbackProtectionsEnforced)
                throws IOException, SecurityException {
    this.nativeHandle = nativeOpenJarFile(name, fd.getInt$());
    this.fd = fd;

    try {
        // Read the MANIFEST and signature files up front and try to
        // parse them. We never want to accept a JAR File with broken signatures
        // or manifests, so it's best to throw as early as possible.
        if (verify) {
            HashMap<String, byte[]> metaEntries = getMetaEntries();
            this.manifest = new StrictJarManifest(metaEntries.get(JarFile.MANIFEST_NAME), true);
            this.verifier =
                    new StrictJarVerifier(
                            name,
                            manifest,
                            metaEntries,
                            signatureSchemeRollbackProtectionsEnforced);
            Set<String> files = manifest.getEntries().keySet();
            for (String file : files) {
                if (findEntry(file) == null) {
                    throw new SecurityException("File " + file + " in manifest does not exist");
                }
            }

            isSigned = verifier.readCertificates() && verifier.isSignedJar();
        } else {
            isSigned = false;
            this.manifest = null;
            this.verifier = null;
        }
    } catch (IOException | SecurityException e) {
        nativeClose(this.nativeHandle);
        IoUtils.closeQuietly(fd);
        closed = true;
        throw e;
    }

    guard.open("close");
}
  1. 以signature block文件名找到对应的signature文件名,并进行签名验证(StrictJarVerifier.java).
/**
 * If the associated JAR file is signed, check on the validity of all of the
 * known signatures.
 *
 * @return {@code true} if the associated JAR is signed and an internal
 *         check verifies the validity of the signature(s). {@code false} if
 *         the associated JAR file has no entries at all in its {@code
 *         META-INF} directory. This situation is indicative of an invalid
 *         JAR file.
 *         <p>
 *         Will also return {@code true} if the JAR file is <i>not</i>
 *         signed.
 * @throws SecurityException
 *             if the JAR file is signed and it is determined that a
 *             signature block file contains an invalid signature for the
 *             corresponding signature file.
 */
synchronized boolean readCertificates() {
    if (metaEntries.isEmpty()) {
        return false;
    }

    Iterator<String> it = metaEntries.keySet().iterator();
    while (it.hasNext()) {
        String key = it.next();
        if (key.endsWith(".DSA") || key.endsWith(".RSA") || key.endsWith(".EC")) {
            verifyCertificate(key);
            it.remove();
        }
    }
    return true;
}

/**
 * @param certFile
 */
private void verifyCertificate(String certFile) {
    // Found Digital Sig, .SF should already have been read
    String signatureFile = certFile.substring(0, certFile.lastIndexOf('.')) + ".SF";
    byte[] sfBytes = metaEntries.get(signatureFile);
    if (sfBytes == null) {
        return;
    }

    byte[] manifestBytes = metaEntries.get(JarFile.MANIFEST_NAME);
    // Manifest entry is required for any verifications.
    if (manifestBytes == null) {
        return;
    }

    byte[] sBlockBytes = metaEntries.get(certFile);
    try {
        Certificate[] signerCertChain = verifyBytes(sBlockBytes, sfBytes);
        if (signerCertChain != null) {
            certificates.put(signatureFile, signerCertChain);
        }
    } catch (GeneralSecurityException e) {
      throw failedVerification(jarName, signatureFile, e);
    }

    // Verify manifest hash in .sf file
    Attributes attributes = new Attributes();
    HashMap<String, Attributes> entries = new HashMap<String, Attributes>();
    try {
        StrictJarManifestReader im = new StrictJarManifestReader(sfBytes, attributes);
        im.readEntries(entries, null);
    } catch (IOException e) {
        return;
    }

    // If requested, check whether APK Signature Scheme v2 signature was stripped.
    if (signatureSchemeRollbackProtectionsEnforced) {
        String apkSignatureSchemeIdList =
                attributes.getValue(
                        ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_NAME);
        if (apkSignatureSchemeIdList != null) {
            // This field contains a comma-separated list of APK signature scheme IDs which
            // were used to sign this APK. If an ID is known to us, it means signatures of that
            // scheme were stripped from the APK because otherwise we wouldn't have fallen back
            // to verifying the APK using the JAR signature scheme.
            boolean v2SignatureGenerated = false;
            StringTokenizer tokenizer = new StringTokenizer(apkSignatureSchemeIdList, ",");
            while (tokenizer.hasMoreTokens()) {
                String idText = tokenizer.nextToken().trim();
                if (idText.isEmpty()) {
                    continue;
                }
                int id;
                try {
                    id = Integer.parseInt(idText);
                } catch (Exception ignored) {
                    continue;
                }
                if (id == ApkSignatureSchemeV2Verifier.SF_ATTRIBUTE_ANDROID_APK_SIGNED_ID) {
                    // This APK was supposed to be signed with APK Signature Scheme v2 but no
                    // such signature was found.
                    v2SignatureGenerated = true;
                    break;
                }
            }

            if (v2SignatureGenerated) {
                throw new SecurityException(signatureFile + " indicates " + jarName
                        + " is signed using APK Signature Scheme v2, but no such signature was"
                        + " found. Signature stripped?");
            }
        }
    }

    // Do we actually have any signatures to look at?
    if (attributes.get(Attributes.Name.SIGNATURE_VERSION) == null) {
        return;
    }

    boolean createdBySigntool = false;
    String createdBy = attributes.getValue("Created-By");
    if (createdBy != null) {
        createdBySigntool = createdBy.indexOf("signtool") != -1;
    }

    // Use .SF to verify the mainAttributes of the manifest
    // If there is no -Digest-Manifest-Main-Attributes entry in .SF
    // file, such as those created before java 1.5, then we ignore
    // such verification.
    if (mainAttributesEnd > 0 && !createdBySigntool) {
        String digestAttribute = "-Digest-Manifest-Main-Attributes";
        if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
            throw failedVerification(jarName, signatureFile);
        }
    }

    // Use .SF to verify the whole manifest.
    String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
    if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
        Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry<String, Attributes> entry = it.next();
            StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
            if (chunk == null) {
                return;
            }
            if (!verify(entry.getValue(), "-Digest", manifestBytes,
                    chunk.start, chunk.end, createdBySigntool, false)) {
                throw invalidDigest(signatureFile, entry.getKey(), jarName);
            }
        }
    }
    metaEntries.put(signatureFile, null);
    signatures.put(signatureFile, entries);
}
  1. 确认MF文件中section的完整性.摘要算法已经从SHA1升级到SHA2系列(StrictJarVerifier.java).
// Use .SF to verify the mainAttributes of the manifest
// If there is no -Digest-Manifest-Main-Attributes entry in .SF
// file, such as those created before java 1.5, then we ignore
// such verification.
if (mainAttributesEnd > 0 && !createdBySigntool) {
    String digestAttribute = "-Digest-Manifest-Main-Attributes";
    if (!verify(attributes, digestAttribute, manifestBytes, 0, mainAttributesEnd, false, true)) {
        throw failedVerification(jarName, signatureFile);
    }
}

// Use .SF to verify the whole manifest.
String digestAttribute = createdBySigntool ? "-Digest" : "-Digest-Manifest";
if (!verify(attributes, digestAttribute, manifestBytes, 0, manifestBytes.length, false, false)) {
    Iterator<Map.Entry<String, Attributes>> it = entries.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<String, Attributes> entry = it.next();
        StrictJarManifest.Chunk chunk = manifest.getChunk(entry.getKey());
        if (chunk == null) {
            return;
        }
        if (!verify(entry.getValue(), "-Digest", manifestBytes,
                chunk.start, chunk.end, createdBySigntool, false)) {
            throw invalidDigest(signatureFile, entry.getKey(), jarName);
        }
    }
}
  1. 执行函数collectCertificates,确认zip entry文件内容的完整性,从代码逻辑可以判断出,如果是系统签名的包,只检查AndroidManifest.xml文件的完整性,否则需要检查zip中所有entry文件的完整性(PackageParser.java).
// APK's integrity needs to be verified using JAR signature scheme.
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV1");
final List<ZipEntry> toVerify = new ArrayList<>();
toVerify.add(manifestEntry);

// If we're parsing an untrusted package, verify all contents
if ((parseFlags & PARSE_IS_SYSTEM_DIR) == 0) {
    final Iterator<ZipEntry> i = jarFile.iterator();
    while (i.hasNext()) {
        final ZipEntry entry = i.next();

        if (entry.isDirectory()) continue;

        final String entryName = entry.getName();
        if (entryName.startsWith("META-INF/")) continue;
        if (entryName.equals(ANDROID_MANIFEST_FILENAME)) continue;

        toVerify.add(entry);
    }
}

// Verify that entries are signed consistently with the first entry
// we encountered. Note that for splits, certificates may have
// already been populated during an earlier parse of a base APK.
for (ZipEntry entry : toVerify) {
    final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
    if (ArrayUtils.isEmpty(entryCerts)) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
                "Package " + apkPath + " has no certificates at entry "
                + entry.getName());
    }
    final Signature[] entrySignatures = convertToSignatures(entryCerts);

    if (pkg.mCertificates == null) {
        pkg.mCertificates = entryCerts;
        pkg.mSignatures = entrySignatures;
        pkg.mSigningKeys = new ArraySet<PublicKey>();
        for (int i=0; i < entryCerts.length; i++) {
            pkg.mSigningKeys.add(entryCerts[i][0].getPublicKey());
        }
    } else {
        if (!Signature.areExactMatch(pkg.mSignatures, entrySignatures)) {
            throw new PackageParserException(
                    INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES, "Package " + apkPath
                            + " has mismatched certificates at entry "
                            + entry.getName());
        }
    }
}

在android系统中, 有时候需要获得jar包或者APK的公钥.对于已经安装的API,可以通过android系统的PackageInfo对象获得.而对于jar包或者没有安装的APK文件,就需要进行zip解析来获得公钥.下面是解析zip格式获得公钥的典型实现:
这里写图片描述
通过解析zip文件格式获取公钥时,需要注意的是,可以使用zip中的任何entry,但是不能是目录类型,也不能是META-INF目录下的文件entry.因为在jar的initEntry方法中不会为这两类entry附加证书.另外,必须是验证过zip entry的完整性以后,才能读取该entry的公钥,这是jar的实现规范.因此,必须先把entry的内容读取完毕后,才能获取公钥.

android系统的签名算法v1版本的实现是基于zip格式设计的,存在着固有的缺陷:

  1. 安全性.
    尽管v1版本的算法对zip entry类型为文件的删除,增加,修改会有感知,但是不能检测到zip entry类型为目录的更改,也不能检测zip元数据的更改,比如有些APP在comments字段添加数据,来控制一些代码逻辑.
  2. 性能.
    APK在安装的时候,v1签名算法需要读出所有的zip entry文件进行完整性验证,而读取的过程中需要解压缩,严重影响安装过程.

android系统在7.0引入了v2版本的签名算法,该算法对zip格式进行了扩展,即在zip中增加了一个APK Signing Block段.v2签名算法通过扩展zip文件格式,实现zip文件全文摘要的效果,相比较v1签名算法,v2算法既快又安全.v2算法扩展后的zip分段:
这里写图片描述
同时,v2算法在进行zip文件的全文摘要的时候,对zip文件进行分块,实现并行计算,有可以增加更多控制.zip文件的并行分块:
这里写图片描述
需要注意的是,android系统采用v2签名算法以后,基于java平台的jarsigner将无法支持v2算法,只能使用android特有的签名程序apksigner. 关于android系统的v2签名机制,请参考android官网https://source.android.com/security/apksigning/v2 .

Android应用源码45套安卓源码合集: android文离线发音引擎FOCTTS使用源码.rar Android应用源码(精)LBS签到应用源码.rar Android应用源码(精)xUtils2.2.5.rar Android应用源码(精)仿博客园客户端源码.rar Android应用源码(精)手机控制电脑鼠标.rar Android应用源码(精)记事本小程序,加注释,适合阅读.rar Android应用源码Android平台下通过HTTP协议实现断点续传下载.rar Android应用源码Hibernate4Android.rar Android应用源码http、udp、tcp网络交互组件.rar Android应用源码ListView实现的目录树结构.rar Android应用源码SdCard读写文件实例.rar Android应用源码SlidingMenu使用例子.rar Android应用源码串口通信(JNI)例子.rar Android应用源码任务提醒源码.rar Android应用源码仿360手机助手首页浮动菜单.rar Android应用源码仿Iphone抖动效果Shake Icon.rar Android应用源码仿QQ分组列表修改版.rar Android应用源码使用listView实现的树状结构.rar Android应用源码俄罗斯方块注释超详细版.rar Android应用源码利用poi将内容填到word模板.rar Android应用源码动态列表布局.rar Android应用源码单Java文件实现的计算器.rar Android应用源码基于百度云推送的聊天工具源码.rar Android应用源码安卓多边形布局例子.rar Android应用源码安卓拍照上传实现代码附带php端.rar Android应用源码实现动态交叉布局.rar Android应用源码小说翻页效果源码.rar Android应用源码广告轮播效果源码.rar Android应用源码强大的统计图表库.rar Android应用源码微享,微信分享实例.rar Android应用源码有米广告SDK例子.rar Android应用源码模仿zaker风景页面滑动效果修改版.rar Android应用源码水波纹动画效果.rar Android应用源码泡泡效果bubble.rar Android应用源码猜猜红桃A.rar Android应用源码百度统计例子.rar Android应用源码简单的Android图片轮播.rar Android应用源码简单的仿微信实现了表情效果.rar Android应用源码结合数据库进行摇一摇的实例.rar Android应用源码花姑娘之部分UI源码.rar Android应用源码获取手机信息.rar Android应用源码讯飞语音测试源码.rar Android应用源码飞碟说欢迎界面.rar
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值