Just An Application

July 28, 2013

The Great Android Security Hole Of ’08 ? – Part Nine: The Root Cause ? — Signed JARs Considered Harmful

I would argue that the root cause of the Great Android Security Hole Of ’08 was the decision, either explicit or implicit, to use signed JARs as the mechanism for supporting the authentication and verification of Android Applications.

1.0 What Are Signed JARs For ?

If you want to ensure the integrity of a JAR as a self-contained entity such as an Application then the ability to sign individual files is not a requirement.

In fact it is difficult to see in what circumstances the ability to sign individual files and only individual files could be a requirement.

Because it is only possible to sign individual files, a signed JAR is really nothing more than a collection of files which may or may not be signed and the verification of a signed JAR is a very convoluted way of determining into which category each file belongs.

All of which leads to question of what signed JARs are actually for ?

The ability to package files in this way was presumably considered useful when the specification was produced but it is clear that it is a decidedly sub-optimal way of attempting to ensure the integrity of an Application made up of a number of files which have been packaged as a ZIP file.

2.0 Complexity

While signed JARs undoubtedly constitute a flexible mechanism for doing something, its just not clear what, they do so at a cost.

As we have seen the cost is the complexity of the verification process and the inconclusiveness of the result.

The process of verification is ridiculously complicated and consequently dangerously error-prone which is not what you want from something which is a key part of ensuring the security of your platform.

3.0 Conclusion

If you need some way to both authenticate and ensure the integrity of self-contained Applications delivered in ZIP files don’t use signed JARs.

If for some reason you really feel you must

  1. do an exhaustive analysis of your requirements

  2. identify every single assumption you are making at every stage of the verification and authentication process, and then

  3. generate the enormous amount of test collateral you will require to ensure that your assumptions remain true in the face of every single thing that can be done legitimately and illegitimately to signed JARs and ZIP files in isolation and in combination

And then when you have done all that, then think about using something else instead.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 27, 2013

The Great Android Security Hole Of ’08 ? – Part Eight: Signed JAR Verification Revisited

1.0 Basic Verification Using The java.util.jar.JarFile API

1.1 JARVerifierV1

A class that does basic verification of a signed JAR using the java.util.jar.JarFile API.

    final class JARVerifierV1 
                implements
                    JARVerifier
    {
        public boolean verify(File theFile)
        {
            Log.d(TAG, "verify: verifying " + theFile);
            try
            {
                JarFile jf = new JarFile(theFile, true);
			
                try
                {
                    verify(jf);
                    Log.d(TAG, "verify: " + theFile + " verified");
                    return true;
                }
                finally
                {
                    jf.close();
                }
            }
            catch (Exception e)
            {
                Log.d(TAG, "verify: verification failed");
                e.printStackTrace();
                return false;
            }
        }
	
        //
	
        JARVerifierV1()
        {
            buffer = new byte[8192];
        }
	
        //

        private void verify(JarFile theJARFile) 
                     throws
                         Exception
        {
            Enumeration<JarEntry> entries = theJARFile.entries();
		
            while (entries.hasMoreElements())
            {
                verify(theJARFile, entries.nextElement());
            }
		
        }

        private void verify(JarFile theJARFile, JarEntry theEntry) 
                     throws
                         Exception
        {
            Log.d(TAG, "verify: " + theEntry.getName());
		
            InputStream is = theJARFile.getInputStream(theEntry);
		
            while (is.read(buffer) != -1)
            {
			
            }
        }
	
        //
	
        private byte[] buffer;
	
        //
	
        private static final String TAG = "JARVerifierV1";
    }

1.1 Verification Of A Vanilla APK

If we create a vanilla APK which looks like this

    Archive:  GoodApp.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
         1588  07-10-13 21:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366112  07-10-13 21:11   classes.dex
         1034  07-10-13 21:11   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
     --------                   -------
       389395                   15 files

and we attempt to verify it using JARVerifierV1 we succeed.

    ...

    D/JARVerifierV1(  281): verify: verifying /data/data/xper.jv/files/GoodApp.apk
    D/JARVerifierV1(  281): verify: res/layout/activity_main.xml
    D/JARVerifierV1(  281): verify: res/menu/activity_main.xml
    D/JARVerifierV1(  281): verify: AndroidManifest.xml
    D/JARVerifierV1(  281): verify: resources.arsc
    D/JARVerifierV1(  281): verify: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV1(  281): verify: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV1(  281): verify: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV1(  281): verify: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV1(  281): verify: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV1(  281): verify: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV1(  281): verify: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV1(  281): verify: classes.dex
    D/JARVerifierV1(  281): verify: META-INF/MANIFEST.MF
    D/JARVerifierV1(  281): verify: META-INF/CERT.SF
    D/JARVerifierV1(  281): verify: META-INF/CERT.RSA
    D/JARVerifierV1(  281): verify: /data/data/xper.jv/files/GoodApp.apk verified

    ...

This is what we would expect but it is not much of a test.

We need to know what happens when we modify a signed JAR in some way.

1.2 Verification After File Modification

If we modify our vanilla APK by replacing the original version of AndroidManifest.xml with an empty one

    Archive:  GoodApp_modified.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
            0  07-21-13 11:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366112  07-10-13 21:11   classes.dex
         1034  07-10-13 21:11   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
     --------                   -------
       387807                   15 files

and then attempt to verify it we get a SecurityException which is what we would expect.

    ...

    D/JARVerifierV1(  280): verify: verifying /data/data/xper.jv/files/GoodApp_modified.apk
    D/JARVerifierV1(  280): verify: res/layout/activity_main.xml
    D/JARVerifierV1(  280): verify: res/menu/activity_main.xml
    D/JARVerifierV1(  280): verify: AndroidManifest.xml
    D/JARVerifierV1(  280): verify: verification failed
    W/System.err(  280): java.lang.SecurityException: META-INF/MANIFEST.MF has \
         invalid digest for AndroidManifest.xml in /data/data/xper.jv/files/GoodApp_modified.apk
    W/System.err(  280): 	at java.util.jar.JarVerifier$VerifierEntry.verify(JarVerifier.java:129)
    W/System.err(  280): 	at java.util.jar.JarFile$JarFileInputStream.read(JarFile.java:127)
    W/System.err(  280): 	at java.io.FilterInputStream.read(FilterInputStream.java:130)
    W/System.err(  280): 	at xper.jv.JARVerifierV1.verify(JARVerifierV1.java:71)
    W/System.err(  280): 	at xper.jv.JARVerifierV1.verify(JARVerifierV1.java:58)
    W/System.err(  280): 	at xper.jv.JARVerifierV1.verify(JARVerifierV1.java:24)
    W/System.err(  280): 	at xper.jv.XperJVActivity.verify(XperJVActivity.java:127)
    W/System.err(  280): 	at xper.jv.XperJVActivity.access$0(XperJVActivity.java:117)
    W/System.err(  280): 	at xper.jv.XperJVActivity$1.run(XperJVActivity.java:111)
    W/System.err(  280): 	at java.lang.Thread.run(Thread.java:1096)

    ...

1.3 Verification After File Addition

If we add the file assets/new_asset to our vanilla APK

    Archive:  GoodApp_added.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
         1588  07-10-13 21:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366112  07-10-13 21:11   classes.dex
         1034  07-10-13 21:11   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
           48  07-24-13 21:56   assets/new_asset
     --------                   -------
       389443                   16 files

and then we attempt to verify it we succeed which is a bit disconcerting.

    ...

    D/JARVerifierV1(  285): verify: verifying /data/data/xper.jv/files/GoodApp_added.apk
    D/JARVerifierV1(  285): verify: res/layout/activity_main.xml
    D/JARVerifierV1(  285): verify: res/menu/activity_main.xml
    D/JARVerifierV1(  285): verify: AndroidManifest.xml
    D/JARVerifierV1(  285): verify: resources.arsc
    D/JARVerifierV1(  285): verify: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV1(  285): verify: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV1(  285): verify: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV1(  285): verify: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV1(  285): verify: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV1(  285): verify: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV1(  285): verify: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV1(  285): verify: classes.dex
    D/JARVerifierV1(  285): verify: META-INF/MANIFEST.MF
    D/JARVerifierV1(  285): verify: META-INF/CERT.SF
    D/JARVerifierV1(  285): verify: META-INF/CERT.RSA
    D/JARVerifierV1(  285): verify: assets/new_asset
    D/JARVerifierV1(  285): verify: /data/data/xper.jv/files/GoodApp_added.apk verified

    ...

In fact the JarFile implementation does not distinguish between a file whose computed digest matches the digest specified in the manifest, and a file which does not have a digest specified in the manifest.

This is a feature just not necessarily a good one.

1.4 Verification After File Removal

If we were to remove a file from our vanilla APK then we know a priori that verification would still succeed because all we are doing is iterating over the files that are in the JAR and verifying them.

To determine whether files had been removed from the JAR would require additional code to look for digests and for the digests of digests.

2.0 Fun With Digests

We can modify the code so that it checks for digest entries in the manifest.

2.1 JarVerifierV2

    final class JARVerifierV2 
                implements
                    JARVerifier
    {
        public boolean verify(File theFile)
        {
            Log.d(TAG, "verify: verifying " + theFile);
            try
            {
                JarFile jf = new JarFile(theFile, true);
			
                try
                {
                    verify(jf);
                    Log.d(TAG, "verify: " + theFile + " verified");
                    return true;
                }
                finally
                {
                    jf.close();
                }
            }
            catch (Exception e)
            {
                Log.d(TAG, "verify: verification failed");
                e.printStackTrace();
                return false;
            }
        }
	
        //
	
        JARVerifierV2()
        {
            buffer = new byte[8192];
        }
	
        //

        private void verify(JarFile theJARFile) 
                     throws
                         Exception
        {
            Enumeration<JarEntry> entries = theJARFile.entries();
		
            while (entries.hasMoreElements())
            {
                verify(theJARFile, entries.nextElement());
            }
		
        }

        private void verify(JarFile theJARFile, JarEntry theEntry) 
                     throws
                         Exception
        {
            Log.d(TAG, "verify: " + theEntry.getName());
		
            ensureDigest(theEntry);
		
            InputStream is = theJARFile.getInputStream(theEntry);
		
            while (is.read(buffer) != -1)
            {
			
            }
        }
	
        private void ensureDigest(JarEntry theEntry)
                     throws
                         Exception
        {
            if (!theEntry.getName().startsWith(META_INF))
            {
                Log.d(TAG, "ensureDigest: " + theEntry.getName());
		
                Attributes attributes = theEntry.getAttributes();
		
                if (attributes != null)
                {
                    for (Object o : attributes.keySet())
                    {
                        if (o.toString().endsWith(DIGEST_SUFFIX))
                        {
                            return;
                        }
                    }
                }
                throw new SecurityException("No digest attribute: " + theEntry.getName());
            }
        }
	
        //
	
        private byte[] buffer;
	
        //
	

        private static final String DIGEST_SUFFIX = "-Digest";
	
        private static final String META_INF      = "META-INF/";
	
        //
	
        private static final String TAG = "JARVerifierV2";
    }


2.2 Verification After File Addition

This time when we attempt we get a SecurityException, which is nice

    ...

    D/JARVerifierV2(  316): verify: verifying /data/data/xper.jv/files/GoodApp_added.apk
    D/JARVerifierV2(  316): verify: res/layout/activity_main.xml
    D/JARVerifierV2(  316): ensureDigest: res/layout/activity_main.xml
    D/JARVerifierV2(  316): verify: res/menu/activity_main.xml
    D/JARVerifierV2(  316): ensureDigest: res/menu/activity_main.xml
    D/JARVerifierV2(  316): verify: AndroidManifest.xml
    D/JARVerifierV2(  316): ensureDigest: AndroidManifest.xml
    D/JARVerifierV2(  316): verify: resources.arsc
    D/JARVerifierV2(  316): ensureDigest: resources.arsc
    D/JARVerifierV2(  316): verify: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV2(  316): verify: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV2(  316): verify: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV2(  316): verify: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV2(  316): verify: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV2(  316): verify: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV2(  316): verify: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV2(  316): ensureDigest: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV2(  316): verify: classes.dex
    D/JARVerifierV2(  316): ensureDigest: classes.dex
    D/JARVerifierV2(  316): verify: META-INF/MANIFEST.MF
    D/JARVerifierV2(  316): verify: META-INF/CERT.SF
    D/JARVerifierV2(  316): verify: META-INF/CERT.RSA
    D/JARVerifierV2(  316): verify: assets/new_asset
    D/JARVerifierV2(  316): ensureDigest: assets/new_asset
    D/JARVerifierV2(  316): verify: verification failed
    W/System.err(  316): java.lang.SecurityException: No digest attribute: assets/new_asset
    W/System.err(  316): 	at xper.jv.JARVerifierV2.ensureDigest(JARVerifierV2.java:100)
    W/System.err(  316): 	at xper.jv.JARVerifierV2.verify(JARVerifierV2.java:70)
    W/System.err(  316): 	at xper.jv.JARVerifierV2.verify(JARVerifierV2.java:59)
    W/System.err(  316): 	at xper.jv.JARVerifierV2.verify(JARVerifierV2.java:25)
    W/System.err(  316): 	at xper.jv.XperJVActivity.verify(XperJVActivity.java:127)
    W/System.err(  316): 	at xper.jv.XperJVActivity.access$0(XperJVActivity.java:117)
    W/System.err(  316): 	at xper.jv.XperJVActivity$1.run(XperJVActivity.java:111)
    W/System.err(  316): 	at java.lang.Thread.run(Thread.java:1096)

    ...

2.3 Manipulating The Manifest

There is a slight problem with this approach, namely that it is possible to add entries to the manifest of a signed JAR.

We can add an entry for our new file to the manifest like so

    Manifest-Version: 1.0
    Created-By: 1.0 (Android)

    Name: res/drawable-xhdpi/ic_launcher.png
    SHA1-Digest: TJE1tg63y88xLdIALKGpuLmgh0s=

    Name: res/drawable-mdpi/ic_action_search.png
    SHA1-Digest: XjltJdEB3tvakTn9CN7KwdaNT68=

    Name: AndroidManifest.xml
    SHA1-Digest: iXwTXVvLX3gY+ByGhybEJvIxP08=

    Name: res/drawable-mdpi/ic_launcher.png
    SHA1-Digest: aM73uFWfPOOvq5Kk9Ffd3cWm0OQ=

    Name: res/drawable-hdpi/ic_launcher.png
    SHA1-Digest: ioH2V9g4FYKkqpDHk7jPryMKtcE=

    Name: res/layout/activity_main.xml
    SHA1-Digest: zd1eNopMfZ7CmMk8GjyPGPpYloc=

    Name: resources.arsc
    SHA1-Digest: 7nYq90PW0OZVg8ytyjBEcwQhYiA=

    Name: classes.dex
    SHA1-Digest: b80KFg5gdqlORweY5LrUS87tbVU=

    Name: res/drawable-hdpi/ic_action_search.png
    SHA1-Digest: rOWnNxWCWKwkicf+FpYGMg1fX2Y=

    Name: res/drawable-xhdpi/ic_action_search.png
    SHA1-Digest: WzSVDDIHZpn0cFeuxgKRJete4TU=

    Name: res/menu/activity_main.xml
    SHA1-Digest: TfxVPQP5IRVlJun7A7PdOqdkp+I=

    Name: res/drawable-ldpi/ic_launcher.png
    SHA1-Digest: r6Mdl54h2qEvVnqgfsgxU4CysiI=

    Name: assets/new_asset
    SHA1-Digest: PW9oQUbA9MQOVYfbtHutF0urHCg=

and then create a new APK containing the added file and the modified manifest

   Archive:  GoodApp_added_modified_manifest.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
         1588  07-10-13 21:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366112  07-10-13 21:11   classes.dex
         1103  07-26-13 14:51   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
           48  07-24-13 21:56   assets/new_asset
     --------                   -------
       389512                   16 files

If we attempt to verify it we succeed once more

    ...

    D/JARVerifierV2(  280): verify: verifying /data/data/xper.jv/files/GoodApp_added_modified_manifest.apk
    D/JARVerifierV2(  280): verify: res/layout/activity_main.xml
    D/JARVerifierV2(  280): ensureDigest: res/layout/activity_main.xml
    D/JARVerifierV2(  280): verify: res/menu/activity_main.xml
    D/JARVerifierV2(  280): ensureDigest: res/menu/activity_main.xml
    D/JARVerifierV2(  280): verify: AndroidManifest.xml
    D/JARVerifierV2(  280): ensureDigest: AndroidManifest.xml
    D/JARVerifierV2(  280): verify: resources.arsc
    D/JARVerifierV2(  280): ensureDigest: resources.arsc
    D/JARVerifierV2(  280): verify: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV2(  280): verify: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV2(  280): verify: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV2(  280): verify: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV2(  280): verify: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV2(  280): verify: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV2(  280): verify: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV2(  280): ensureDigest: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV2(  280): verify: classes.dex
    D/JARVerifierV2(  280): ensureDigest: classes.dex
    D/JARVerifierV2(  280): verify: META-INF/MANIFEST.MF
    D/JARVerifierV2(  280): verify: META-INF/CERT.SF
    D/JARVerifierV2(  280): verify: META-INF/CERT.RSA
    D/JARVerifierV2(  280): verify: assets/new_asset
    D/JARVerifierV2(  280): ensureDigest: assets/new_asset
    D/JARVerifierV2(  280): verify: /data/data/xper.jv/files/GoodApp_added_modified_manifest.apk verified

    ...

Back to square one.

2.4 But What About The Digest Manifest ?

Given that the signature file contains a digest for the manifest and that has not been changed why is it possible to modify the manifest ?

The answer is because the digest manifest is considered to be an ‘optimization’ not a way of preventing the manifest from being changed.

If the computed digest of the manifest matches the one in the signature file then it is known to be unchanged and there is no need to check the individual digest entries in the manifest.

This is the ‘optimization’

If the computed digest of the manifest does not match then the individual digests in the signature file are used to check the corresponding digest entries in the manifest.

3.0 It Will Have To Be The Certificates Then

3.1 The JarEntry getCertificates Method

The Javadoc for the JerEntry getCertificates method for the implementation we are using says

Returns an array of Certificate Objects associated with this entry or null if none exists. Make sure that the everything is read from the input stream before calling this method, or else the method returns null.

The Javadoc for a.n.other implementation says

Returns the Certificate objects for this entry, or null if none. This method can only be called once the JarEntry has been completely verified by reading from the entry input stream until the end of the stream has been reached. Otherwise, this method will return null.

The returned certificate array comprises all the signer certificates that were used to verify this entry. Each signer certificate is followed by its supporting certificate chain (which may be empty). Each signer certificate and its supporting certificate chain are ordered bottom-to-top (i.e., with the signer certificate first and the (root) certificate authority last).

which is a bit more useful.

If the getCertificates method returns an array of Certificates we know that there is a digest entry in the manifest and a digest of the digest entry in a signature file which has been ‘verified’

3.2 JARVerifierV3

    final class JARVerifierV3 
                implements
                    JARVerifier
    {
        public boolean verify(File theFile)
        {
            Log.d(TAG, "verify: verifying " + theFile);
		    try
            {
                JarFile jf = new JarFile(theFile, true);
			
                try
                {
                    verify(jf);
                    Log.d(TAG, "verify: " + theFile + " verified");
                    return true;
                }
                finally
                {
                    jf.close();
                }
            }
            catch (Exception e)
            {
                Log.d(TAG, "verify: verification failed");
                e.printStackTrace();
                return false;
            }
        }
	
        //
	
        JARVerifierV3()
        {
            buffer = new byte[8192];
        }
	
        //

        private void verify(JarFile theJARFile) 
                     throws
                         Exception
        {
            Enumeration<JarEntry> entries = theJARFile.entries();
		
            while (entries.hasMoreElements())
            {
                verify(theJARFile, entries.nextElement());
            }
		
        }

        private void verify(JarFile theJARFile, JarEntry theEntry) 
                     throws
                         Exception
        {
            Log.d(TAG, "verify: " + theEntry.getName());
		
		
		
            InputStream is = theJARFile.getInputStream(theEntry);
		
            while (is.read(buffer) != -1)
            {
			
            }
		
            ensureCertificates(theEntry);
        }
	
        private void ensureCertificates(JarEntry theEntry)
                     throws
                         Exception
        {
            if (!theEntry.getName().startsWith(META_INF))
            {
                Log.d(TAG, "ensureCertificates: " + theEntry.getName());
		
                Certificate[] certificates = theEntry.getCertificates();
			
                if (certificates == null)
                {
                    throw new SecurityException("No certificates: " + theEntry.getName());
                }
            }
        }
	
        //
	
        private byte[] buffer;
	
        //
		
        private static final String META_INF      = "META-INF/";
	
        //
	
        private static final String TAG = "JARVerifierV3";
    }

3.3 Verification After File Addition

If we attempt to verify our APK with added file and modified manifest using JARVerifierV3 we return to the status quo ante.

    ...

    D/JARVerifierV3(  280): verify: verifying /data/data/xper.jv/files/GoodApp_added_modified_manifest.apk
    D/JARVerifierV3(  280): verify: res/layout/activity_main.xml
    D/JARVerifierV3(  280): ensureCertificates: res/layout/activity_main.xml
    D/JARVerifierV3(  280): verify: res/menu/activity_main.xml
    D/JARVerifierV3(  280): ensureCertificates: res/menu/activity_main.xml
    D/JARVerifierV3(  280): verify: AndroidManifest.xml
    D/JARVerifierV3(  280): ensureCertificates: AndroidManifest.xml
    D/JARVerifierV3(  280): verify: resources.arsc
    D/JARVerifierV3(  280): ensureCertificates: resources.arsc
    D/JARVerifierV3(  280): verify: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV3(  280): verify: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV3(  280): verify: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV3(  280): verify: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV3(  280): verify: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV3(  280): verify: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV3(  280): verify: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV3(  280): ensureCertificates: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV3(  280): verify: classes.dex
    D/JARVerifierV3(  280): ensureCertificates: classes.dex
    D/JARVerifierV3(  280): verify: META-INF/MANIFEST.MF
    D/JARVerifierV3(  280): verify: META-INF/CERT.SF
    D/JARVerifierV3(  280): verify: META-INF/CERT.RSA
    D/JARVerifierV3(  280): verify: assets/new_asset
    D/JARVerifierV3(  280): ensureCertificates: assets/new_asset
    D/JARVerifierV3(  280): verify: verification failed
    W/System.err(  280): java.lang.SecurityException: No certificates: assets/new_asset
    W/System.err(  280): 	at xper.jv.JARVerifierV3.ensureCertificates(JARVerifierV3.java:95)
    W/System.err(  280): 	at xper.jv.JARVerifierV3.verify(JARVerifierV3.java:80)
    W/System.err(  280): 	at xper.jv.JARVerifierV3.verify(JARVerifierV3.java:60)
    W/System.err(  280): 	at xper.jv.JARVerifierV3.verify(JARVerifierV3.java:26)
    W/System.err(  280): 	at xper.jv.XperJVActivity.verify(XperJVActivity.java:128)
    W/System.err(  280): 	at xper.jv.XperJVActivity.access$0(XperJVActivity.java:118)
    W/System.err(  280): 	at xper.jv.XperJVActivity$1.run(XperJVActivity.java:112)
    W/System.err(  280): 	at java.lang.Thread.run(Thread.java:1096)

    ...

3.4 Signing Additional Manifest Entries

Needless to say there is a catch. Not only is it possible to add digest entries to the manifest of a signed JAR it is possible to sign them as well.

We can create a second signature file BERT.SF which contains the digest of the added manifest entry

    Signature-Version: 1.0

    Name: assets/new_asset
    SHA1-Digest: 6G+NGDseZOsVO904ZG1VLkFAr30=

and a signed signature file BERT.DSA and then create a new APK

    Archive:  GoodApp_added_modified_manifest_ds.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
         1588  07-10-13 21:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366112  07-10-13 21:11   classes.dex
         1103  07-26-13 14:51   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
           48  07-24-13 21:56   assets/new_asset
         2365  07-26-13 22:10   META-INF/BERT.DSA
           95  07-26-13 22:10   META-INF/BERT.SF
     --------                   -------
       391972                   18 files

Now our new file assets/new_asset has certificates associated with it and verification succeeds one more

    ...

    D/JARVerifierV3(  281): verify: verifying /data/data/xper.jv/files/GoodApp_added_modified_manifest_ds.apk
    D/JARVerifierV3(  281): verify: res/layout/activity_main.xml
    D/dalvikvm(  281): GC_FOR_MALLOC freed 4701 objects / 294968 bytes in 59ms
    D/JARVerifierV3(  281): ensureCertificates: res/layout/activity_main.xml
    D/JARVerifierV3(  281): verify: res/menu/activity_main.xml
    D/JARVerifierV3(  281): ensureCertificates: res/menu/activity_main.xml
    D/JARVerifierV3(  281): verify: AndroidManifest.xml
    D/JARVerifierV3(  281): ensureCertificates: AndroidManifest.xml
    D/JARVerifierV3(  281): verify: resources.arsc
    D/JARVerifierV3(  281): ensureCertificates: resources.arsc
    D/JARVerifierV3(  281): verify: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-hdpi/ic_action_search.png
    D/JARVerifierV3(  281): verify: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-hdpi/ic_launcher.png
    D/JARVerifierV3(  281): verify: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-ldpi/ic_launcher.png
    D/JARVerifierV3(  281): verify: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-mdpi/ic_action_search.png
    D/JARVerifierV3(  281): verify: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-mdpi/ic_launcher.png
    D/JARVerifierV3(  281): verify: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-xhdpi/ic_action_search.png
    D/JARVerifierV3(  281): verify: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV3(  281): ensureCertificates: res/drawable-xhdpi/ic_launcher.png
    D/JARVerifierV3(  281): verify: classes.dex
    D/JARVerifierV3(  281): ensureCertificates: classes.dex
    D/JARVerifierV3(  281): verify: META-INF/MANIFEST.MF
    D/JARVerifierV3(  281): verify: META-INF/CERT.SF
    D/JARVerifierV3(  281): verify: META-INF/CERT.RSA
    D/JARVerifierV3(  281): verify: assets/new_asset
    D/JARVerifierV3(  281): ensureCertificates: assets/new_asset
    D/JARVerifierV3(  281): verify: META-INF/BERT.DSA
    D/JARVerifierV3(  281): verify: META-INF/BERT.SF
    D/JARVerifierV3(  281): verify: /data/data/xper.jv/files/GoodApp_added_modified_manifest_ds.apk verified

    ...

Back to square one again

4.0 PackageParser collectCertificates Again

This is why the PackageParser collectCertificates method is doing this

                ...

                Enumeration<JarEntry> entries = jarFile.entries();
                final Manifest manifest = jarFile.getManifest();
                while (entries.hasMoreElements()) {
                    final JarEntry je = entries.nextElement();
                    if (je.isDirectory()) continue;

                    final String name = je.getName();

                    if (name.startsWith("META-INF/"))
                        continue;

                    if (ANDROID_MANIFEST_FILENAME.equals(name)) {
                        final Attributes attributes = manifest.getAttributes(name);
                        pkg.manifestDigest = ManifestDigest.fromAttributes(attributes);
                    }

                    final Certificate[] localCerts = loadCertificates(jarFile, je, readBuffer);
                    if (DEBUG_JAR) {
                        Slog.i(TAG, "File " + mArchiveSourcePath + " entry " + je.getName()
                                + ": certs=" + certs + " ("
                                + (certs != null ? certs.length : 0) + ")");
                    }

                    if (localCerts == null) {
                        Slog.e(TAG, "Package " + pkg.packageName
                                + " has no certificates at entry "
                                + je.getName() + "; ignoring!");
                        jarFile.close();
                        mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
                        return false;
                    } else if (certs == null) {
                        certs = localCerts;
                    } else {
                        // Ensure all certificates match.
                        for (int i=0; i<certs.length; i++) {
                            boolean found = false;
                            for (int j=0; j<localCerts.length; j++) {
                                if (certs[i] != null &&
                                    certs[i].equals(localCerts[j])) {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found || certs.length != localCerts.length) {
                                Slog.e(TAG, "Package " + pkg.packageName
                                        + " has mismatched certificates at entry "
                                        + je.getName() + "; ignoring!");
                                jarFile.close();
                                mParseError = PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
                                return false;
                            }
                        }
                    }
                }
                
                ...

not only checking for the existence of certificates associated with each file

                    ...

                    if (localCerts == null) {
                        Slog.e(TAG, "Package " + pkg.packageName
                                + " has no certificates at entry "
                                + je.getName() + "; ignoring!");
                        jarFile.close();
                        mParseError = PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES;
                        return false;
                    } 
                    
                    ...

but also checking, albeit in a somewhat convoluted manner, that all the files have been signed with the same set of certificates .

                        ...

                        // Ensure all certificates match.
                        for (int i=0; i<certs.length; i++) {
                            boolean found = false;
                            for (int j=0; j<localCerts.length; j++) {
                                if (certs[i] != null &&
                                    certs[i].equals(localCerts[j])) {
                                    found = true;
                                    break;
                                }
                            }
                            if (!found || certs.length != localCerts.length) {
                                Slog.e(TAG, "Package " + pkg.packageName
                                        + " has mismatched certificates at entry "
                                        + je.getName() + "; ignoring!");
                                jarFile.close();
                                mParseError = PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
                                return false;
                            }
                        }

                        ...

This is what verification of a signed JAR amounts to.

Establishing in the most complicated way possible whether all of the files present in the JAR have been signed with the ‘same’set of certificates.

Note that this does not address the question of whether files have been removed.

Note also that this relies on establishing the ‘equality’ of certficates which is an ‘interesting’ problem in its own right.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 17, 2013

The Great Android Security Hole Of ’08 ? – Part Seven: The Gory Details — The Loading Classes From The ‘Wrong’ classes.dex Edition

As we have seen, during the construction of the PathClassLoader to be used as an application’s ClassLoader the function dvmJarFileOpen is used to open an application’s APK.

dvmJarFileOpen in turn calls dexZipOpenArchive which calls parseZipArchive.

The function parseZipArchive is the equivalent of the java.util.ZipFile readCentralDirectory method.

It is responsible for reading the Central Directory and populating a hashtable which maps file names to File Headers.

1.0 parseZipArchive Again

Source: $(ANDROID_SRC)/dalvik/libdex/ZipArchive.cpp

    static int parseZipArchive(ZipArchive* pArchive)
    {
        int result = -1;
        const u1* cdPtr = (const u1*)pArchive->mDirectoryMap.addr;
        size_t cdLength = pArchive->mDirectoryMap.length;
        int numEntries = pArchive->mNumEntries;

        /*
         * Create hash table.  We have a minimum 75% load factor, possibly as
         * low as 50% after we round off to a power of 2.  There must be at
         * least one unused entry to avoid an infinite loop during creation.
         */
        pArchive->mHashTableSize = dexRoundUpPower2(1 + (numEntries * 4) / 3);
        pArchive->mHashTable = (ZipHashEntry*)
                calloc(pArchive->mHashTableSize, sizeof(ZipHashEntry));

        /*
         * Walk through the central directory, adding entries to the hash
         * table and verifying values.
         */
        const u1* ptr = cdPtr;
        int i;
        for (i = 0; i < numEntries; i++) {
            if (get4LE(ptr) != kCDESignature) {
                ALOGW("Zip: missed a central dir sig (at %d)", i);
                goto bail;
            }
            if (ptr + kCDELen > cdPtr + cdLength) {
                ALOGW("Zip: ran off the end (at %d)", i);
                goto bail;
            }

            long localHdrOffset = (long) get4LE(ptr + kCDELocalOffset);
            if (localHdrOffset >= pArchive->mDirectoryOffset) {
                ALOGW("Zip: bad LFH offset %ld at entry %d", localHdrOffset, i);
                goto bail;
            }

            unsigned int fileNameLen, extraLen, commentLen, hash;
            fileNameLen = get2LE(ptr + kCDENameLen);
            extraLen = get2LE(ptr + kCDEExtraLen);
            commentLen = get2LE(ptr + kCDECommentLen);

            /* add the CDE filename to the hash table */
            hash = computeHash((const char*)ptr + kCDELen, fileNameLen);
            addToHash(pArchive, (const char*)ptr + kCDELen, fileNameLen, hash);

            ptr += kCDELen + fileNameLen + extraLen + commentLen;
            if ((size_t)(ptr - cdPtr) > cdLength) {
                ALOGW("Zip: bad CD advance (%d vs %zd) at entry %d",
                    (int) (ptr - cdPtr), cdLength, i);
                goto bail;
            }
        }
        ALOGV("+++ zip good scan %d entries", numEntries);

        result = 0;

    bail:
        return result;
    }

The function is passed a pointer to a ZipArchive struct which represents the ZIP file.

    struct ZipArchive {
        /* open Zip archive */
        int         mFd;

        /* mapped central directory area */
        off_t       mDirectoryOffset;
        MemMapping  mDirectoryMap;

        /* number of entries in the Zip archive */
        int         mNumEntries;

        /*
         * We know how many entries are in the Zip archive, so we can have a
         * fixed-size hash table.  We probe on collisions.
         */
        int         mHashTableSize;
        ZipHashEntry* mHashTable;
    };

At this point the Central Directory of the ZIP file has been mapped into memory.

The function starts by allocating the memory for the hashtable which maps file names to File Headers.

The hashtable is implemented as a fixed size linear hashtable which maps file names to ZipHashEntry structs.

The struct ZipHashEntry is defined like this

    struct ZipHashEntry {
    const char*     name;
    unsigned short  nameLen;
};

and the allocated hashtable is simply an array of them.

The size of the hashtable is a power of two so the computation x mod hashtable size can be ‘optimized’.

Having allocated the hashtable it iterates over the File Headers in the Central Directory. For each one it does some basic sanity checking and then adds a pointer to the file name to the hashtable using the addToHash function.

Note that like the readCentralDirectory method it does not check to see whether there is more than one file with the same name.

1.1 computeHash

Source: $(ANDROID_SRC)/dalvik/libdex/ZipArchive.cpp

    static unsigned int computeHash(const char* str, int len)
    {
        unsigned int hash = 0;

        while (len--)
            hash = hash * 31 + *str++;

        return hash;
    }

The computeHash employs the canonical java.lang.String hashCode algorithm.

1.2 addToHash Again

Source: $(ANDROID_SRC)/dalvik/libdex/ZipArchive.cpp

    static void addToHash(ZipArchive* pArchive, const char* str, int strLen, unsigned int hash)
    {
        const int hashTableSize = pArchive->mHashTableSize;
        int ent = hash & (hashTableSize - 1);

        /*
         * We over-allocated the table, so we're guaranteed to find an empty slot.
         */
        while (pArchive->mHashTable[ent].name != NULL)
            ent = (ent + 1) & (hashTableSize-1);

        pArchive->mHashTable[ent].name = str;
        pArchive->mHashTable[ent].nameLen = strLen;
    }

The function addToHash starts by computing ent, the index at which to add the entry for the name.

The index is simply the hash value mod the hashtable size.

It then examines the entry at the index ent. If the entry is empty it adds the name there, otherwise it proceeds to look for the next empty entry.

Since the size of the hashtable is larger than the number of entries it is guaranteed to find one.

1.3 Example

Given the Central Directory of the APK shown in this example then the addition of the names to the hashtable by the parseZipArchive function goes like this.

Note that there are 16 files in the example APK so the size of the hashtable is 32.

The first seven names are all added without collisions.

Name Hash Index Of Entry
res/layout/activity_main.xml 3289686140 28
res/menu/activity_main.xml 3702837041 17
AndroidManifest.xml 4186266567 7
resources.arsc 810769514 10
res/drawable-hdpi/ic_action_search.png 2475784161 1
res/drawable-hdpi/ic_launcher.png 1773930598 6
res/drawable-ldpi/ic_launcher.png 138633698 2

The name res/drawable-mdpi/ic_action_search.png hashes to 679672038, which mod 32 is 6, so it collides with res/drawable-hdpi/ic_launcher.png.

The next index is 7 which gives another collision, this time with AndroidManifest.xml.

The entry at is 8 is empty, so it is added here.

The name res/drawable-mdpi/ic_launcher.png hashes to 4024776769 which mod 32 is 1 so it collides with res/drawable-hdpi/ic_action_search.png.

It collides again at 2 with res/drawable-ldpi/ic_launcher.png, and is added at 3.

The next three names are added without collisions

Name Hash Index Of Entry
res/drawable-xhdpi/ic_action_search.png 2409726889 9
res/drawable-xhdpi/ic_launcher.png 3887447966 30
classes.dex 4055048783 15

Then classes.dex is added for the second time.

This necessarily collides with the existing entry for the first classes.dex at 15.

The entry at 16 is empty so it is added there.

The name META-INF/MANIFEST.MF hashes to 1539143842 which mod 32 is 2 so it collides with
res/drawable-ldpi/ic_launcher.png.

It collides again with res/drawable-mdpi/ic_launcher.png at 3 before being added at 4.

The name META-INF/CERT.SF hashes to 3935803975 which mod 32 is 7 so it collides with AndroidManifest.xml

It then collides three more times at 8, 9, and 10 with

  • res/drawable-mdpi/ic_action_search.png
  • res/drawable-xhdpi/ic_action_search.png
  • resources.arsc

respectively before being added at 11.

The name META-INF/CERT.RSA hashes to 1750838444 which mod 32 is 12. The entry is empty so it is added at 12.

After all that the hashtable looks like this

Index Name
0:

<empty>
1:

res/drawable-hdpi/ic_action_search.png
2:

res/drawable-ldpi/ic_launcher.png
3:

res/drawable-mdpi/ic_launcher.png
4:

META-INF/MANIFEST.MF
5:

<empty>
6:

res/drawable-hdpi/ic_launcher.png
7:

AndroidManifest.xml
8:

res/drawable-mdpi/ic_action_search.png
9:

res/drawable-xhdpi/ic_action_search.png
10:

resources.arsc
11:

META-INF/CERT.SF
12:

META-INF/CERT.RSA
13:

<empty>
14:

<empty>
15:

classes.dex
16:

classes.dex
17:

res/menu/activity_main.xml
18:

<empty>
19:

<empty>
20:

<empty>
21:

<empty>
22:

<empty>
23:

<empty>
24:

<empty>
25:

<empty>
26:

<empty>
27:

<empty>
28:

res/layout/activity_main.xml
29:

<empty>
30:

res/drawable-xhdpi/ic_launcher.png
31:

<empty>

1.4 dexZipFindEntry Again

    ZipEntry dexZipFindEntry(const ZipArchive* pArchive, const char* entryName)
    {
        int nameLen = strlen(entryName);
        unsigned int hash = computeHash(entryName, nameLen);
        const int hashTableSize = pArchive->mHashTableSize;
        int ent = hash & (hashTableSize-1);

        while (pArchive->mHashTable[ent].name != NULL) {
            if (pArchive->mHashTable[ent].nameLen == nameLen &&
                memcmp(pArchive->mHashTable[ent].name, entryName, nameLen) == 0)
            {
                /* match */
                return (ZipEntry)(long)(ent + kZipEntryAdj);
            }

            ent = (ent + 1) & (hashTableSize-1);
        }

        return NULL;
    }

As you would expect the dexZipFindEntry function uses exactly the same algorithm to find an entry matching the given name as addToHash does to find where to add a given name.

The return value is a ZipEntry which is defined like this

    typedef void* ZipEntry;

Because NULL is returned when an entry is not found, the constant kZipEntryAdj has to be added to the value to be returned when an entry is found in case it is at index 0.

1.5 Example Continued

Having used the function dexZipOpenArchive to open the application’s APK, dvmJarFileOpen then calls the function dexZipFindEntry to find the application’s classes.dex file.

Continuing the example above, when dexZipFindEntry is passed the string

    "classes.dex"

it will compute the hash which gives 4055048783, which mod 32 is 15 as we know.

The entry at 15 will match and dexZipFindEntry will return.

We know that this is the entry for the first classes.dex in the APK.

We also know that this is the classes.dex file which did not get verified during installation.


Copyleft (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 15, 2013

The Great Android Security Hole Of ’08 ? – Part Six: The Gory Details — The APK Verification Considered Ineffective Edition

As we have seen, verification of the files within an APK occurs during the execution of the PackageParser method collectCertificates.

This method constructs a JarFile instance using the JarFile(File) constructor.

It then calls the JarFile method entries to obtain an Enumeration which it uses to iterate over all the files in the APK.

It is the entries method that is the issue, or rather, what it returns.

1.0 The JarFile entries Method

Class: java.util.jar.JarFile

Source: $(ANDROID_SRC)/libcore/luni/src/main/java/java/util/jar/JarFile.java

    @Override
    public Enumeration entries() {
        class JarFileEnumerator implements Enumeration<JarEntry> {
            Enumeration<? extends ZipEntry> ze;

            JarFile jf;

            JarFileEnumerator(Enumeration<? extends ZipEntry> zenum, JarFile jf) {
                ze = zenum;
                this.jf = jf;
            }

            public boolean hasMoreElements() {
                return ze.hasMoreElements();
            }

            public JarEntry nextElement() {
                JarEntry je = new JarEntry(ze.nextElement());
                je.parentJar = jf;
                return je;
            }
        }
        return new JarFileEnumerator(super.entries(), this);
    }

It turns out that this method is effectively just wrapping the entries method of the super class java.util.zip.ZipFile.

2.0 The ZipFile entries Method

Class: java.util.zip.ZipFile

Source: $(ANDROID_SRC)/libcore/luni/src/main/java/java/util/zip/ZipFile.java

    public Enumeration<? extends ZipEntry> entries() {
        checkNotClosed();
        final Iterator<ZipEntry> iterator = entries.values().iterator();

        return new Enumeration<ZipEntry>() {
            public boolean hasMoreElements() {
                checkNotClosed();
                return iterator.hasNext();
            }

            public ZipEntry nextElement() {
                checkNotClosed();
                return iterator.next();
            }
        };

This method is in turn wrapping the values held in the entries instance variable.

3.0 The ZipFile entries Instance Variable

The entries instance variable is declared like this

    private final LinkedHashMap<String, ZipEntry> entries = new LinkedHashMap<String, ZipEntry>();

It is populated by the readCentralDirectory method which is invoked by the ZipFile(File,int) constructor.

4.0 ZipFile readCentralDirectory

The readCentralDirectory method starts, as you might expect, by scanning backwards through the file looking for the End of Central Directory record.

        ...

        // Scan back, looking for the End Of Central Directory field. If the zip file doesn't
        // have an overall comment (unrelated to any per-entry comments), we'll hit the EOCD
        // on the first try.
        // No need to synchronize raf here -- we only do this when we first open the zip file.
        long scanOffset = raf.length() - ENDHDR;
        if (scanOffset < 0) {
            throw new ZipException("File too short to be a zip file: " + raf.length());
        }

        long stopOffset = scanOffset - 65536;
        if (stopOffset < 0) {
            stopOffset = 0;
        }

        final int ENDHEADERMAGIC = 0x06054b50;
        while (true) {
            raf.seek(scanOffset);
            if (Integer.reverseBytes(raf.readInt()) == ENDHEADERMAGIC) {
                break;
            }

            scanOffset--;
            if (scanOffset < stopOffset) {
                throw new ZipException("EOCD not found; not a zip file?");
            }
        }
        
        ...

Once it has found it, it reads the fields it is interested in and does a basic sanity check.

        ...

        // Read the End Of Central Directory. We could use ENDHDR instead of the magic number 18,
        // but we don't actually need all the header.
        byte[] eocd = new byte[18];
        raf.readFully(eocd);

        // Pull out the information we need.
        BufferIterator it = HeapBufferIterator.iterator(eocd, 0, eocd.length, ByteOrder.LITTLE_ENDIAN);
        int diskNumber = it.readShort() & 0xffff;
        int diskWithCentralDir = it.readShort() & 0xffff;
        int numEntries = it.readShort() & 0xffff;
        int totalNumEntries = it.readShort() & 0xffff;
        it.skip(4); // Ignore centralDirSize.
        long centralDirOffset = ((long) it.readInt()) & 0xffffffffL;

        if (numEntries != totalNumEntries || diskNumber != 0 || diskWithCentralDir != 0) {
            throw new ZipException("spanned archives not supported");
        }
        
        ...

It then seeks to the start of the Central Directory and reads the File Headers and adds them to the entries LinkedHashMap.

        ...

        RAFStream rafStream = new RAFStream(raf, centralDirOffset);
        BufferedInputStream bufferedStream = new BufferedInputStream(rafStream, 4096);
        byte[] hdrBuf = new byte[CENHDR]; // Reuse the same buffer for each entry.
        for (int i = 0; i < numEntries; ++i) {
            ZipEntry newEntry = new ZipEntry(hdrBuf, bufferedStream);
            entries.put(newEntry.getName(), newEntry);
        }
        
        ...

It is the line in red that is the problem.

No check is made to see whether the ZIP file contains multiple files with the same name.

As a result, in the unlikely event of a ZIP file containing N files of the same name, then the JarFile entries method will only return the N'th. The preceding N-1 files will silently disappear.

5.0 Implications For The Verification Of An APK

If an APK just happens to have two files of the same name in it, then only the second one which gets verified, which is a bit unfortunate really if those files are called classes.dex, as we shall see.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog's author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

The Great Android Security Hole Of ’08 ? – Part Five: And The Answer Is …

    Archive:  GoodAppBadApp.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
         1588  07-10-13 21:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366152  07-10-13 21:11   classes.dex
       366112  07-10-13 21:11   classes.dex
         1034  07-10-13 21:11   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
     --------                   -------
       755547                   16 files

Obviously !


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 14, 2013

The Great Android Security Hole Of ’08 ? – Part Four: What Exactly Is/Was The Problem ?

Given any APK it is possible to create a new one which is identical to the original except that it contains a second version of one of the members of the original.

This new APK can be installed successfully on a device running the Andoid Runtime without the bug fix and the runtime will use the second version of the member in the APK rather than the first.

For the moment it is left as an exercise for the reader to deduce, using their skill and judgement, which member of the APK it might be.


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

July 13, 2013

The Great Android Security Hole Of ’08 ? – Part Three: dalvik.system.PathClassLoader

When an Android application is launched an instance of dalvik.system.PathClassLoader is created for use as the application’s ClassLoader.

The dalvik.system.PathClassLoader constructor is passed a ‘path’ which includes the application’s APK.

1.0 PathClassLoader

Class: dalvik.system.PathClassLoader

Source: $(ANDROID_SRC)/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java

1.1 The Constructor

    public PathClassLoader(String dexPath, ClassLoader parent) 
    {
        super(dexPath, null, null, parent);
    }

The PathClassLoader constructor simply invokes the constructor of the super class which is BaseDexClassLoader.

2.0 BaseDexClassLoader

Class: dalvik.system.BaseDexClassLoader

Source: $(ANDROID_SRC)/libcore/libdvm/src/main/java/dalvik/system/BaseDexClassLoader.java

2.1 The Constructor

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) 
    {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

The BaseDexClassLoader constructor creates an instance of DexPathList which it will use when attempting to load classes.

It passes the DexPathList constructor itself plus its dexPath, libraryPath and optimizedDirectory arguments.

3.0 DexPathList

Class: dalvik.system.DexPathList

Source: $(ANDROID_SRC)/libcore/libdvm/src/main/java/dalvik/system/DexPathList.java

3.1 The Constructor

    public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory)

The constructor calls the method makeDexElements to create an array of Elements.

An Element represents either a directory, a Dex file, or a ZIP file which was specified in the dexPath argument.

3.2 makeDexElements

    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory)

The method iterates over the elements in the files argument.

If the name of the File ends with one of

  • .apk

  • .jar

  • .zip

it invokes the loadDexFile method passing it the File and the optimizedDirectory argument.

3.3 loadDexFile

    private static DexFile loadDexFile(File file, File optimizedDirectory)

If the optimizedDirectory argument is null then the method creates a DexFile instance passing the File argument to the constructor.

4.0 DexFile

Class: dalvik.system.DexFile

Source: $(ANDROID_SRC)/libcore/libdvm/src/main/java/dalvik/system/DexFile.java

Source: $(ANDROID_SRC)/dalvik/vm/native/dalvik_system_DexFile.cpp

4.1 DexFile(File)

    public DexFile(File file) throws IOException {
        this(file.getPath());
    }

4.2 DexFile(String)

    public DexFile(String fileName) throws IOException

This constructor calls the method openDexFile passing it the fileName argument, null, and 0.

4.3 openDexFile

    native private static int openDexFile(
                                  String sourceName, 
                                  String outputName,
                                  int    flags) 
                              throws 
                                 IOException;

If the sourceName argument does not end with the suffix “.dex”, openDexFile calls the function dvmJarFileOpen passing it the C++ equivalents of its sourceName and outputName arguments plus a pointer to a JarFile* on the stack and false.

5.0 JarFile

Source: $(ANDROID_SRC)/dalvik/vm/JarFile.cpp

5.1 dvmJarFileOpen

     int dvmJarFileOpen(const char* fileName, const char* odexOutputName, JarFile** ppJarFile, bool isBootstrap)

The function starts by calling the function dexZipOpenArchive to open the named file.

If this is successful it then calls the function dexZipFindEntry to find the classes.dex file within the ZIP file.

6.0 ZipArchive

Source: $(ANDROID_SRC)/dalvik/libdex/ZipArchive.cpp

6.1 dexZipOpenArchive

     int dexZipOpenArchive(const char* fileName, ZipArchive* pArchive)

The function attempts to open the named file using the open system call.

If this is successful it then calls dexZipPrepArchive passing it the file descriptor returned by the call to open
and the fileName and pArchive arguments.

6.2 dexZipPrepArchive

    int dexZipPrepArchive(int fd, const char* debugFileName, ZipArchive* pArchive)

The function dexZipPrepArchive starts by mapping the Central Directory of the ZIP file into memory using the function mapCentralDirectory.

It this is successful it then calls parseZipArchive

6.2 parseZipArchive

    static int parseZipArchive(ZipArchive* pArchive)

The parseZipArchive function begins by creating a linear hash table.

It then iterates over the File Headers in the Central Directory.

For each File Header it calls the function addtoHash to create an entry in the linear hash table using the file name from the file header as the key.

The entry allocated within the linear hash table contains the address of the file name field within the File Header and the length of the file name.

Given the address of the file name field it is possible to determine the address of the File Header itself within the Central Directory.

6.4 addToHash

    static void addToHash(ZipArchive* pArchive, const char* str, int strLen, unsigned int hash)
    {
        const int hashTableSize = pArchive->mHashTableSize;
        int ent = hash & (hashTableSize - 1);

        /*
         * We over-allocated the table, so we're guaranteed to find an empty slot.
         */
        while (pArchive->mHashTable[ent].name != NULL)
            ent = (ent + 1) & (hashTableSize-1);

        pArchive->mHashTable[ent].name = str;
        pArchive->mHashTable[ent].nameLen = strLen;
   }

6.5 dexZipFindEntry

    ZipEntry dexZipFindEntry(const ZipArchive* pArchive, const char* entryName)
    {
        int nameLen = strlen(entryName);
        unsigned int hash = computeHash(entryName, nameLen);
        const int hashTableSize = pArchive->mHashTableSize;
        int ent = hash & (hashTableSize-1);

        while (pArchive->mHashTable[ent].name != NULL) {
            if (pArchive->mHashTable[ent].nameLen == nameLen &&
                memcmp(pArchive->mHashTable[ent].name, entryName, nameLen) == 0)
            {
                /* match */
                return (ZipEntry)(long)(ent + kZipEntryAdj);
            }

            ent = (ent + 1) & (hashTableSize-1);
        }

        return NULL;
    }

Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

The Great Android Security Hole Of ’08 ? – Part Two: Signed JARs

1.0 Structure

An Android APK is a signed JAR and a signed JAR is a good old-fashioned ZIP file.

If you list the contents of an APK using unzip you will see something like this

    Archive:  GoodAppBadApp.apk
      Length     Date   Time    Name
     --------    ----   ----    ----
          624  07-10-13 21:11   res/layout/activity_main.xml
          464  07-10-13 21:11   res/menu/activity_main.xml
         1588  07-10-13 21:11   AndroidManifest.xml
         2504  07-10-13 21:11   resources.arsc
          409  07-10-13 21:11   res/drawable-hdpi/ic_action_search.png
         4129  07-10-13 21:11   res/drawable-hdpi/ic_launcher.png
         1756  07-10-13 21:11   res/drawable-ldpi/ic_launcher.png
          311  07-10-13 21:11   res/drawable-mdpi/ic_action_search.png
         2654  07-10-13 21:11   res/drawable-mdpi/ic_launcher.png
          491  07-10-13 21:11   res/drawable-xhdpi/ic_action_search.png
         5456  07-10-13 21:11   res/drawable-xhdpi/ic_launcher.png
       366112  07-10-13 21:11   classes.dex
         1034  07-10-13 21:11   META-INF/MANIFEST.MF
         1087  07-10-13 21:11   META-INF/CERT.SF
          776  07-10-13 21:11   META-INF/CERT.RSA
     --------                   -------
       389395                   15 files

It is the presence of the manifest file

    META-INF/MANIFEST.MF

which make this ZIP file a JAR, and it is the presence of the ‘signature’ files

    META-INF/CERT.SF
    META-INF/CERT.RSA

that makes it a signed JAR.

2.0 The Meta Files

2.1 MANIFEST.MF

This is what’s in the MANIFEST.MF file contained in the APK shown in the example above

    Manifest-Version: 1.0
    Created-By: 1.0 (Android)

    Name: res/drawable-xhdpi/ic_launcher.png
    SHA1-Digest: TJE1tg63y88xLdIALKGpuLmgh0s=

    Name: res/drawable-mdpi/ic_action_search.png
    SHA1-Digest: XjltJdEB3tvakTn9CN7KwdaNT68=

    Name: AndroidManifest.xml
    SHA1-Digest: iXwTXVvLX3gY+ByGhybEJvIxP08=

    Name: res/drawable-mdpi/ic_launcher.png
    SHA1-Digest: aM73uFWfPOOvq5Kk9Ffd3cWm0OQ=

    Name: res/drawable-hdpi/ic_launcher.png
    SHA1-Digest: ioH2V9g4FYKkqpDHk7jPryMKtcE=

    Name: res/layout/activity_main.xml
    SHA1-Digest: zd1eNopMfZ7CmMk8GjyPGPpYloc=

    Name: resources.arsc
    SHA1-Digest: 7nYq90PW0OZVg8ytyjBEcwQhYiA=

    Name: classes.dex
    SHA1-Digest: b80KFg5gdqlORweY5LrUS87tbVU=

    Name: res/drawable-hdpi/ic_action_search.png
    SHA1-Digest: rOWnNxWCWKwkicf+FpYGMg1fX2Y=

    Name: res/drawable-xhdpi/ic_action_search.png
    SHA1-Digest: WzSVDDIHZpn0cFeuxgKRJete4TU=

    Name: res/menu/activity_main.xml
    SHA1-Digest: TfxVPQP5IRVlJun7A7PdOqdkp+I=

    Name: res/drawable-ldpi/ic_launcher.png
    SHA1-Digest: r6Mdl54h2qEvVnqgfsgxU4CysiI=

As you can see there is a section for each file in the APK with the exception of those contained in the META-INF directory.

The SHA1-Digest attribute specifies the SHA-1[1] digest of the named file. The value is Base64 encoded.

2.2 CERT.SF

The CERT.SF file is a ‘signature file’. This is odd because technically it does not contain any signatures in the cryptographic sense, but there it is.

Signature files are identified by the suffix .SF

This is what’s in the CERT.SF file contained in the APK shown in the example above

    Signature-Version: 1.0
    Created-By: 1.0 (Android)
    SHA1-Digest-Manifest: uy1/gV4Q91cFNXDPKFWnIkdp7Hs=

    Name: res/drawable-xhdpi/ic_launcher.png
    SHA1-Digest: uQhdAiwpzle4OMU4KiZ8rHZZW0s=

    Name: res/drawable-mdpi/ic_action_search.png
    SHA1-Digest: jAGFiT7RUDOYcmBQqk2FP01Xtb0=

    Name: AndroidManifest.xml
    SHA1-Digest: app7CkR5QClO2DngK2SwqyoYT/4=

    Name: res/drawable-mdpi/ic_launcher.png
    SHA1-Digest: 7AYLMxFKX2g7vudLLZTXlC9DWHI=

    Name: res/drawable-hdpi/ic_launcher.png
    SHA1-Digest: diYsvf3zRlU+JNDApc7+KLVhhk0=

    Name: res/layout/activity_main.xml
    SHA1-Digest: tK4fMdQKbbzD/KoE5awGKM7o++Y=

    Name: resources.arsc
    SHA1-Digest: mMTrtN8w3SByhMHUQ19nJ3OT3wo=

    Name: res/drawable-hdpi/ic_action_search.png
    SHA1-Digest: WvsoZfcfGbz7vtTCwzXLh3VcxFw=

    Name: classes.dex
    SHA1-Digest: /okgNUxVNi1erXMkC7ftetu9S9U=

    Name: res/drawable-xhdpi/ic_action_search.png
    SHA1-Digest: 772ITFXOlprwo44bOLvLMa3Vdeg=

    Name: res/menu/activity_main.xml
    SHA1-Digest: K1PTOAgvzmJK47MHIsJIWgVr/04=

    Name: res/drawable-ldpi/ic_launcher.png
    SHA1-Digest: zC+Z/B5ib1ntZmz9D80Rj3muYqE=

The SHA1-Digest-Manifest in the first section specifies the SHA-1 digest of the manifest file.

In the following sections the SHA1-Digest attribute specifies the SHA-1 digest of the corresponding section in the manifest file.

2.3 CERT.RSA

The CERT.RSA file is the ‘signed signature file’.

The CERT.RSA file contained in the APK shown in the example above is a binary file, so this is a representation of its contents produced using the ubiquitous dumpasn1.


   0  772: SEQUENCE {
   4    9:   OBJECT IDENTIFIER signedData (1 2 840 113549 1 7 2)
  15  757:   [0] {
  19  753:     SEQUENCE {
  23    1:       INTEGER 1
  26   11:       SET {
  28    9:         SEQUENCE {
  30    5:           OBJECT IDENTIFIER sha1 (1 3 14 3 2 26)
  37    0:           NULL
         :           }
         :         }
  39   11:       SEQUENCE {
  41    9:         OBJECT IDENTIFIER data (1 2 840 113549 1 7 1)
         :         }
  52  489:       [0] {
  56  485:         SEQUENCE {
  60  334:           SEQUENCE {
  64    3:             [0] {
  66    1:               INTEGER 2
         :               }
  69    4:             INTEGER 1322302491
  75   13:             SEQUENCE {
  77    9:               OBJECT IDENTIFIER
         :                 sha1withRSAEncryption (1 2 840 113549 1 1 5)
  88    0:               NULL
         :               }
  90   55:             SEQUENCE {
  92   11:               SET {
  94    9:                 SEQUENCE {
  96    3:                   OBJECT IDENTIFIER countryName (2 5 4 6)
 101    2:                   PrintableString 'US'
         :                   }
         :                 }
 105   16:               SET {
 107   14:                 SEQUENCE {
 109    3:                   OBJECT IDENTIFIER organizationName (2 5 4 10)
 114    7:                   PrintableString 'Android'
         :                   }
         :                 }
 123   22:               SET {
 125   20:                 SEQUENCE {
 127    3:                   OBJECT IDENTIFIER commonName (2 5 4 3)
 132   13:                   PrintableString 'Android Debug'
         :                   }
         :                 }
         :               }
 147   30:             SEQUENCE {
 149   13:               UTCTime 26/11/2011 10:14:51 GMT
 164   13:               UTCTime 18/11/2041 10:14:51 GMT
         :               }
 179   55:             SEQUENCE {
 181   11:               SET {
 183    9:                 SEQUENCE {
 185    3:                   OBJECT IDENTIFIER countryName (2 5 4 6)
 190    2:                   PrintableString 'US'
         :                   }
         :                 }
 194   16:               SET {
 196   14:                 SEQUENCE {
 198    3:                   OBJECT IDENTIFIER organizationName (2 5 4 10)
 203    7:                   PrintableString 'Android'
         :                   }
         :                 }
 212   22:               SET {
 214   20:                 SEQUENCE {
 216    3:                   OBJECT IDENTIFIER commonName (2 5 4 3)
 221   13:                   PrintableString 'Android Debug'
         :                   }
         :                 }
         :               }
 236  159:             SEQUENCE {
 239   13:               SEQUENCE {
 241    9:                 OBJECT IDENTIFIER
         :                   rsaEncryption (1 2 840 113549 1 1 1)
 252    0:                 NULL
         :                 }
 254  141:               BIT STRING, encapsulates {
 258  137:                 SEQUENCE {
 261  129:                   INTEGER
         :                   00 8A 0A 1D 53 48 FC 26 6D BF 65 57 04 8B 0B E9
         :                   58 EE E7 11 CE A8 A2 01 72 7B 34 C1 31 60 B4 64
         :                   FC D3 0D 1E 94 BE B3 5B 55 7F 23 97 B9 3D 6F E8
         :                   17 17 FE CA 50 EB F3 EA 4B 7C F6 F9 B6 6E E7 5D
         :                   0D 64 79 00 8F 12 1A 76 09 C1 32 4F 28 E8 27 94
         :                   34 F5 C8 50 C3 0D E9 2E FF A2 97 71 6C 18 7F 7C
         :                   8A 78 EA 37 50 20 8D DA 6C F2 3F 65 85 67 6A 7B
         :                   35 BF 5A 8D 53 DE AD 9D 50 70 30 B4 0D A4 60 D4
         :                           [ Another 1 bytes skipped ]
 393    3:                   INTEGER 65537
         :                   }
         :                 }
         :               }
         :             }
 398   13:           SEQUENCE {
 400    9:             OBJECT IDENTIFIER
         :               sha1withRSAEncryption (1 2 840 113549 1 1 5)
 411    0:             NULL
         :             }
 413  129:           BIT STRING
         :             2E 77 85 3F F4 AF EE 1E 81 9B 8B BB E2 9C AA 95
         :             BE 38 1E 20 EA E3 63 BE 2B A7 F2 AB 8A F4 F5 27
         :             35 F6 7C 67 CF E5 07 82 D0 B5 D8 00 77 CC 9A 8B
         :             86 DD 4C DD 41 88 2E C5 B4 B9 7E E9 C4 B2 B9 FA
         :             31 A0 60 6F DA 8D D1 F9 FA F6 94 08 92 C2 58 D6
         :             BF 93 1E 4D B8 DF DD D3 C1 11 89 14 22 C8 8B 85
         :             98 04 7A F5 00 D5 1D F2 E0 54 42 82 46 2D FE 2D
         :             C3 AB 2B C6 BC E7 07 A2 B8 CD 04 26 01 F1 EB E5
         :           }
         :         }
 545  228:       SET {
 548  225:         SEQUENCE {
 551    1:           INTEGER 1
 554   63:           SEQUENCE {
 556   55:             SEQUENCE {
 558   11:               SET {
 560    9:                 SEQUENCE {
 562    3:                   OBJECT IDENTIFIER countryName (2 5 4 6)
 567    2:                   PrintableString 'US'
         :                   }
         :                 }
 571   16:               SET {
 573   14:                 SEQUENCE {
 575    3:                   OBJECT IDENTIFIER organizationName (2 5 4 10)
 580    7:                   PrintableString 'Android'
         :                   }
         :                 }
 589   22:               SET {
 591   20:                 SEQUENCE {
 593    3:                   OBJECT IDENTIFIER commonName (2 5 4 3)
 598   13:                   PrintableString 'Android Debug'
         :                   }
         :                 }
         :               }
 613    4:             INTEGER 1322302491
         :             }
 619    9:           SEQUENCE {
 621    5:             OBJECT IDENTIFIER sha1 (1 3 14 3 2 26)
 628    0:             NULL
         :             }
 630   13:           SEQUENCE {
 632    9:             OBJECT IDENTIFIER rsaEncryption (1 2 840 113549 1 1 1)
 643    0:             NULL
         :             }
 645  128:           OCTET STRING
         :             22 52 11 23 F5 AA 38 2E 28 FD 96 1E 2C AB 96 FC
         :             E7 85 95 54 A0 F7 14 49 93 F4 19 A7 F0 E5 CC B9
         :             FF 82 B6 8D 84 A2 3A DD F6 CA 6A 4F 3A 38 B4 EA
         :             69 23 BF B1 29 5F C2 49 48 34 63 35 84 59 C6 69
         :             0C E3 F1 D3 90 B5 02 F5 0D 79 CC B4 B0 25 0C BD
         :             38 4F B2 FC D1 D7 D4 11 84 EA A1 66 31 F0 08 DA
         :             0F F6 94 23 96 5C 83 96 C9 DA CA C0 FC 60 84 C2
         :             54 21 DA CA E4 C4 D0 7D FD 98 C4 91 8A 2C 5D E4
         :           }
         :         }
         :       }
         :     }
         :   }

If you are well-versed in the arcana of ASN.1 and the myriad security standards you will recognize this as ‘signed data’ as specified by PKCS#7. If you aren’t, you won’t, but that’s what it is nonetheless.

It is the SHA-1 digest of the CERT.SF file which has been encrypted using the private key corresponding to the public key contained in the accompanying certificate.

There is a bit more to it than that, but that is basically it.

3.0 Signed JAR Verification In Theory

To make life more exciting there can in fact be multiple signature files and signed signature files in a signed JAR but in what follows I am going to assume we are attempting to verify the APK shown in the example above in which there is just the one signature/signed signature file pair.

3.1 Decide Whether You ‘Trust’ The Signer Of The JAR

The signer of the JAR is the entity identified by the certificate which accompanies the signed data in the signed signature file (CERT.RSA).

The decision to ‘trust’ that entity is often made by going up the chain of certificates looking for one which is defined to be a ‘trust root’.

If the certificate that accompanies the signed data in the signed signature file is self-signed or was not signed directly or indirectly by a certificate defined to be a ‘trust root’ then the JAR should be rejected.

3.2 Verify The Signature File

Compute the digest of the signature file (CERT.SF) and use the signature in the signed signature file (CERT.SF) to verify it. If verification fails then reject the JAR.

3.3 Verify The Manifest File

If the expected digest of the manifest file is specified in the signature file compute the digest of the manifest file. If the expected and computed digests do not match then reject the JAR.

Alternatively, for each file in the JAR other than those in the META-INF directory find the corresponding section in the manifest.

If there is no corresponding section reject the JAR.

Otherwise find the expected digest of the section in the signature file.

If there is no expected digest fo the section in the signature file reject the JAR.

Otherwise compute the hash of the section in the manifest. If the computed and expected digests do not match reject the JAR.

3.4 Verify The Remaining Files In The JAR

For each file in the JAR that has not already been verified check that its digest has been specified in the manifest file.

If it has not then reject the JAR.

If the expected digest is specified in the manifest file then compute the actual digest. If the expected and computed digests do not match then reject the JAR.

If all the files in the JAR are successfully verified then accept the JAR.

4.0 Signed JAR Verification In Practice

The Java class java.util.JarFile supports the piecemeal implicit verification of files in signed JARs

4.1 java.util.JarFile

Source: $(ANDROID_SRC)/libcore/luni/src/main/java/java/util/jar/JarFile.java

4.1.1 Constructing A JarFile

4.1.1.1 JarFile(File)

    public JarFile(File file) throws IOException {
        this(file, true);
    }

4.1.1.2 JarFile(File, boolean)

    public JarFile(File file, boolean verify) throws IOException

Passing this constructor a File argument which represents a signed JAR and a verify argument of true will result in a JarFile instance which supports the verification of individual files within the underlying signed JAR.

4.2 The Verification Of Individual Files Within A Signed JAR Using A JarFile Instance

Given a JarFile instance initialized to verify an underlying signed JAR as descibed above there is surprisingly no way to explicitly verify the entire contents of the signed JAR.

Nor is there any way to explicitly verify an individual file within the signed JAR.

The only way to verify an individual file is to read the entire contents of the file. Doing this will implicitly verify the file but only at the point when all the contents have been read.

Since verification of a file involves computing the digest of the contents it is obviously necessary to read the entire contents before being able to verify it.

What is extremely problematic is that there is nothing whatsoever in the API documentation that explains that this is what happens and more particularly that it is not safe to act on anything in the contents of a file in a signed JAR, despite the fact that the contents are being made available via the API, until the entire file has been read.

4.2.1 The ‘Verification’ API

4.2.1.1 getInputStream

     public InputStream getInputStream(ZipEntry ze) throws IOException

The getInputStream method returns an InputStream which can be used to read the contents of the file represented by the JarEntry argument.

If the JarFile instance was constructed with a signed JAR and it is being verified then the method returns a an instance of the inner class JarFileInputStream with an associated JarVerifier.VerifyEntry instance.

4.2.1.2 The JarFileInputStream read Methods

The read methods read from the underlying stream. They then write the bytes read to the associated verifier.

If the call results in all of the file data having been read then verify method of the associated verifier is called. This will throw a SecurityException if the verification fails.

5.0 Signed JAR Verification During The Installation Of An APK By The Android PackageManagerService

When the PackageManagerService finds an APK while scanning a directory it calls the method scanPackageLI.

This triggers the verification of the APK as a signed JAR as follows.

5.1 PackageManagerService

Source: $(ANDROID_SRC)/frameworks/base/services/java/com/android/server/pm/PackageManagerService.java

5.1.1 scanPackageLI

    private PackageParser.Package scanPackageLI(
                                      File       scanFile, 
                                      int        parseFlags, 
                                      int        scanMode, 
                                      long       currentTime, 
                                      UserHandle user)

After determining whether it already knows about the APK, in which case it may return, scanPackageLI calls the method collectCertificatesLI.

5.1.2 collectCertificatesLI

private boolean collectCertificatesLI(
                    PackageParser         pp, 
                    PackageSetting        ps,
                    PackageParser.Package pkg, 
                    File                  srcFile, 
                    int                   parseFlags)

The method calls collectCertificates on the PackageParser argument to do the actual work.

5.2 PackageParser

Class: android.content.pm.PackageParser

Source: $(ANDROID_SRC)/frameworks/base/core/java/android/content/pm/PackageParser.java

5.2.1 collectCertificates

    public boolean collectCertificates(Package pkg, int flags)

The method begins by creating a JarFile using the JarFile(File) constructor.

It then calls the method entries on the JarFile instance.

It then iterates over these entries. For each entry it calls the method loadCertificates.

5.2.2 loadCertificates

    private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer)

The method begins by calling getInputStream on the JarFile argument passing it the JarEntry argument.

It then calls read repeatedly on the InputStream, ignoring what it is actually read, until the entire contents of the file have been read.

As we have seen it is the call to the read method which results in the last of the data being returned which causes the verification of that file.

Notes

  1. The official SHA standard is here (This is a PDF document).


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

The Great Android Security Hole Of ’08 ? – Part One: ZIP Files

1.0 The Structure

A ZIP file comprises one or more Files, followed by a Central Directory, followed by an End of Central Directory Record.

zip_structure

1.1 Files

A File comprises a Local File Header, followed by the File data, optionally followed by a Data Descriptor.

1.1.1 Local File Header

Signature: 0x04034b50

Field Size In Bytes
signature 4
version needed to extract 2
general purpose bit flags 2
compression method 2
last mod file time 2
last mod file data 2
crc-32 4
compressed size 4
uncompressed size 4
file name length 2
extra field length 2
file name file name length
extra field extra field length

The Local File Header contains information about the file whose data immediately follows.

Unfortunately it does not always contain the information you actually want which is the crc32 of the File data and the compressed and uncompressed sizes.

This is because it is permissible to generate a Local File Header with all these values as zero.

This makes it possible to write a ZIP file without ever seeking backwards which is great for the writer of the file but rubbish for anyone trying to read it.

If the crc32 and compressed and uncompressed sizes are set to zero for this reason, then bit three of the general purpose bit flags field should be set and a Data Descriptor containing the correct values should follow the File data.

1.1.2 File Data

The File Data is the contents of the file. Its format is specified by the value of the ‘compression method’ of the preceding Local File Header.

1.1.3 Data Descriptor

Signature: 0x08074b50

Field Size In Bytes
signature [optional] 4
crc-32 4
compressed size 4
uncompressed size 4

A Data Descriptor should be present immediately following the File data if bit 3 of the general purpose bit flags field of the preceding Local File Header is set.

Just to make it more interesting for anyone writing code to read ZIP files, the signature field does not have to be present.

This is because originally it did not have a signature.

1.2 The Central Directory

The Central Directory comprises one or more File Headers

1.2.1 File Header

Signature: 0x02014b50

Field Size In Bytes
signature 4
version made by 2
version needed to extract 2
general purpose bit flags 2
compression method 2
last mod file time 2
last mod file data 2
crc-32 4
compressed size 4
uncompressed size 4
file name length 2
extra field length 2
file comment length 2
disk number start 2
internal file attributes 2
external file attributes 4
relative offset of local header 4
file name file name length
extra field extra field length
file comment file comment length

A File Header contains information about a ‘File’ somewhere ahead of the Central Directory in the ZIP file.

Unlike the corresponding Local File Header the crc32, compressed size and uncompressed size fields must be present and correct.

The ‘relative offset of local header field’ specifies the offset from the start of the ZIP file of the Local File Header of the File
the information in the File Header applies to.

This means that given a File Header it is always possible to read the File data of the associated file.

1.3 The End Of Central Directory Record

Signature: 0x06054b50

Field Size In Bytes
signature 4
6
total number of entries in the central directory 2
size of the central directory 4
offset of start of central directory 4
.ZIP file comment length 2
.ZIP file comment .ZIP file comment length

The End of Central Directory Record marks the end of the Central Directory. It is the last thing in the ZIP file.

It contains the location of the first File Header in the Central Directory, the ‘offset of start of central directory’ field, as well as the number of File Headers in the Central Directory.

2.0 The Idiosyncrasies

2.1 You Cannot Read A ZIP File From The Front

This is not strictly true.

You can attempt to read a ZIP file from the front, that is, starting at byte 0, but if, as is often the case, the Local File Headers do not include the size of the file data which follows then you have a problem.

At this point you have to start scanning forward looking for the next recognizable signature, which can be the signature of a Data Descriptor, a Local File Header, or a File Header.

This is slow and tedious and assumes that file data will never include a valid record signature which is a tad optimistic.

2.2 Finding The Central Directory

So if you cannot reliably read a ZIP file from the front, what do you do ?

Answer, you read it from the back.

The only guaranteed way to find out what is supposed to be in a ZIP file is by examining the Central Directory, so the first thing you do with a ZIP file is read that.

But to read the Central Directory you need to know where it is.

Helpfully the End of Central Directory Record contains the offset of the start of the Central Directory, so actually the first thing to do is to read that, and we know where that is.

Well we do and we don’t because although it is at the end of the ZIP file it is not fixed length.

So the first thing we need to do is scan backwards from the end of the ZIP file looking for the signature of the End of Central Directory Record.

3.0 The Semantics

A ZIP file is simply a container and as such it has no semantics of its own, which is perfectly reasonable.

As long as the format is correct a ZIP file can contain pretty much anything.

This is something of a trap for the unwary programmer writing code to handle ZIP files as part of an ‘application’ that accepts them as some sort of ‘input’.

A great deal of effort will be expended dealing with the idiosyncrasies of the ZIP file format because otherwise it won’t be possible to do whatever it is with the contents of the ZIP file which prompted the writing of the code in the first place.

Rather less effort may go in to thinking about what ought to be in the ZIP file being handled and perhaps more importantly what ought not to be.

The problem is exacerbated by the fact that the test data for the code, hopefully there will be some, will probably be created by a tool that effectively enforces some set of implicit semantics of its own and which is guaranteed to generate well-formed vanilla ZIP files.

One result of all this may be that the code is ill-prepared to handle an inadvertently or intentionally generated ZIP file which contains things it ought not to,


Copyright (c) 2013 By Simon Lewis. All Rights Reserved.

Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.

Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.

Create a free website or blog at WordPress.com.