Android5.1.1 - APK簽名校驗分析和修改源碼繞過簽名校驗

Android5.1.1 - APK簽名校驗分析和修改源碼繞過簽名校驗

@(Android研究)[APK簽名校驗|繞過簽名校驗]java

不歪博客:http://my.oschina.net/ibuwai/blogandroid

本文公開首發於阿里聚安全博客:https://jaq.alibaba.com/community/index.htm?spm=0.0.0.0.ycEUXK數組

APK簽名校驗分析

找到PackageParser類,該類在文件"frameworks/base/core/java/android/content/pm/PackageParser.java"中。PackageParser類的collectCertificates方法會對APK進行簽名校驗,在該方法會遍歷APK中的全部文件,並對每一個文件進行校驗。下面是該方法的部分源碼:安全

private static void collectCertificates(Package pkg, File apkFile, int flags)
        throws PackageParserException {
    final String apkPath = apkFile.getAbsolutePath();

    StrictJarFile jarFile = null;
    try {
        jarFile = new StrictJarFile(apkPath);

        ......

        // 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());
            }
            
            ......
        }
    }
    ......
}

APK是一個ZIP格式的文件因此使用ZIP相關的類進行讀寫。上面代碼中調用了loadCertificates方法,這個方法返回一個二維數組,若是APK中的文件簽名校驗失敗那麼loadCertificates方法會返回一個空數組(多是null,多是數組長度爲0),按照上面代碼的邏輯若是數組爲空則會拋出異常。dom

loadCertificates方法的代碼見下:ui

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
        throws PackageParserException {
    InputStream is = null;
    try {
        // We must read the stream for the JarEntry to retrieve
        // its certificates.
        is = jarFile.getInputStream(entry);
        readFullyIgnoringContents(is);
        return jarFile.getCertificateChains(entry);
    } catch (IOException | RuntimeException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
            "Failed reading " + entry.getName() + " in " + jarFile, e);
    } finally {
        IoUtils.closeQuietly(is);
    }
}

上面代碼中is是JarFile.JarFileInputStream類的對象。loadCertificates方法中調用了readFullyIgnoringContents方法,在readFullyIgnoringContents方法中會調用JarFile.JarFileInputStream.read方法讀取APK中一項的數據,在read方法中會校驗讀取到的數據項的簽名,若是簽名校驗失敗,則會拋出SecurityException類型的異常,即簽名校驗失敗。this

JarFile類在"libcore/luni/src/main/java/java/util/jar/JarFile.java"文件中。.net

上面代碼中調用了StrictJarFile.getCertificateChains方法,下面是它的代碼:code

public Certificate[][] getCertificateChains(ZipEntry ze) {
    if (isSigned) {
        return verifier.getCertificateChains(ze.getName());
    }

    return null;
}

StrictJarFile類在文件"libcore/luni/src/main/java/java/util/jar/StrictJarFile.java"中。htm

上面代碼中isSigned的值是這麼來的:

public StrictJarFile(String fileName) throws IOException {
    this.nativeHandle = nativeOpenJarFile(fileName);
    this.raf = new RandomAccessFile(fileName, "r");

    try {
        ......
        this.verifier = new JarVerifier(fileName, manifest, metaEntries);

        isSigned = verifier.readCertificates() && verifier.isSignedJar();
    } catch (IOException ioe) {
        nativeClose(this.nativeHandle);
        throw ioe;
    }

    guard.open("close");
}

當證書讀取成功,而且當前APK通過了簽名,則isSigned爲true。

回到StrictJarFile.getCertificateChains方法中,當isSigned爲true時會調用JarVerifier.getCertificateChains方法,下面是它的代碼:

Certificate[][] getCertificateChains(String name) {
    return verifiedEntries.get(name);
}

下面是類成員變量verifiedEntries的聲明:

private final Hashtable<String, Certificate[][]> verifiedEntries =
        new Hashtable<String, Certificate[][]>();

verifiedEntries是一個鍵值對,鍵是APK中通過了簽名的文件名,如:classes.dex文件,值是證書數組。若是向已經簽過名的APK中新添加一個文件而後安裝這個APK,當程序邏輯執行到JarVerifier.getCertificateChains方法中時,在verifiedEntries變量中沒法找到新添加的文件名(由於這個新文件是在APK簽名以後添加),那麼JarVerifier.getCertificateChains方法將返回null。

繞過簽名校驗

源碼修改點一

找到PackageParser.loadCertificates方法,下面是部分源碼:

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
        throws PackageParserException {
    ......
    try {
        ......
    } catch (IOException | RuntimeException e) {
        throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
            "Failed reading " + entry.getName() + " in " + jarFile, e);
        }
    } finally {
        IoUtils.closeQuietly(is);
    }
}

將上面代碼catch塊中的throw語句替換爲:return null;

下面是修改後的代碼:

private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry entry)
        throws PackageParserException {
    InputStream is = null;
    try {
        ......
    } catch (IOException | RuntimeException e) {
        return null;
    } finally {
        IoUtils.closeQuietly(is);
    }
}

代碼修改完後,當APK中文件簽名校驗失敗時不會拋出異常,APK還會繼續安裝。

源碼修改點二

找到PackageParser.collectCertificates方法,找到代碼中調用loadCertificates方法的地方:

private static void collectCertificates(Package pkg, File apkFile, int flags)
        throws PackageParserException {
    ......
    try {
        ......

        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());
            }
            ......
        }
    }......
}

將上面的throw語句替換爲:continue;

修改後的代碼:

private static void collectCertificates(Package pkg, File apkFile, int flags)
        throws PackageParserException {
    ......
    try {
        ......

        for (ZipEntry entry : toVerify) {
            final Certificate[][] entryCerts = loadCertificates(jarFile, entry);
            if (ArrayUtils.isEmpty(entryCerts)) {
                continue;
            }
            ......
        }
    }......
}

代碼修改完後,當遇到APK中沒有通過簽名的文件時不會拋出異常,APK還會繼續安裝。

相關文章
相關標籤/搜索