Two Files Named classes.dex — Java Bug Hunt

Modelled on the Android "Master Key" vulnerability disclosed by Bluebox Security in July 2013 (Android bug 8219321).

  • Language: Java
  • Layer: Backend
  • Difficulty: Medium
  • Concepts: Security, Validation, Parsing
  • Modelled on: Android Master Key · 2013
  • Visible tests: an untouched package verifies; a second classes.dex after the signed one is rejected
  • Reward: 50 XP for a complete fix

Briefing

Modelled on the Android "Master Key" vulnerability disclosed by Bluebox Security in July 2013 (Android bug 8219321). An APK is a ZIP archive, and a ZIP can hold two entries with the same name. The signature check and the installer resolved the duplicate name to different entries, so a signed app could carry modified code without its signature failing. Google shipped a fix to device makers that rejects such archives.

In this reconstruction the verifier keeps the first entry of each name while the installer (locked) keeps the last.

Fix ApkVerifier.verify so the two can never disagree.

Bug report

BUG-8219321 · Priority: Critical · Reported by: security research

ApkVerifier.verify(entries, signed) returns true only if ALL hold:

  • no two entries share a name (any duplicate name, even with identical content, makes the package invalid)
  • the set of entry names equals the set of names in the signed manifest
  • every entry's Digest.of(data) equals its signed digest

Observed: a package with the original classes.dex followed by a second, modified classes.dex verifies — and the installer unpacks the modified one.

Logs

[pm] verify com.example.app: signatures OK (2 entries checked)
[pm] install com.example.app: classes.dex sha:1c4f9e02 (not the signed digest)

The code as shipped

src/pm/ApkVerifier.java (editable)

class ApkVerifier {
    static boolean verify(List<ApkEntry> entries, Map<String, String> signed) {
        Map<String, ApkEntry> byName = new LinkedHashMap<>();
        for (ApkEntry e : entries) {
            if (!byName.containsKey(e.name)) byName.put(e.name, e);
        }
        if (!byName.keySet().equals(signed.keySet())) return false;
        for (ApkEntry e : byName.values()) {
            if (!Digest.of(e.data).equals(signed.get(e.name))) return false;
        }
        return true;
    }
}

Read-only context: src/pm/ApkEntry.java.

Open the hunt to edit the files, run the visible tests and submit against the hidden ones. More Java bug hunts.