Apktool: Fixing Bug 713

This is my first technical blog exploring the path of fixing one of Apktool's more complicated bugs

Around December of 2014, Lollipop builds of LG started being leaked to the public. I got a bug report, shortly after, saying that decoding had stopped working in Apktool. Already having previously patched Lollipop support I was quite confused in what had broken.

So lets step in and see what the problem is.

$apktool if lge-res.apk -t bug713
I: Framework installed to: /home/ibotpeaches/apktool/framework/0-bug713.apk  
$apktool if framework-res.apk -t bug713
I: Framework installed to: /home/ibotpeaches/apktool/framework/1-bug713.apk  

Then lets do the decode.

$apktool d UnifiedEULA.apk -t bug713
I: Using Apktool 2.0.0-RC3 on UnifiedEULA.apk  
I: Loading resource table  
I: Decoding AndroidManifest.xml with resources...  
I: Loading resource table from file: /home/ibotpeaches/apktool/framework/1-bug713.apk  
W: Could not decode attr value, using undecoded value instead: ns=android, name=label, value=0x7f080000  
W: Could not decode attr value, using undecoded value instead: ns=android, name=icon, value=0x7f02000c  
W: Could not decode attr value, using undecoded value instead: ns=android, name=label, value=0x7f08001e  
I: Regular manifest package...  
I: Decoding file-resources...  
I: Decoding values */* XMLs...  
Exception in thread "main" brut.androlib.err.UndefinedResObject: resource spec: 0x7f080000  

Wait what? Why did lge-res.apk install as 0-bug713.apk? The leading integer represents the packageId, and I haven't ever seen a valid packageId of 0 before. Generally Android uses 1 for framework-res.apk and 2-9 for other framework files. So we have spotted our first error in that lge-res.apk is installing incorrectly.

Secondly, UnifiedEULA.apk is failing to decode with a few resources. If we examine the first resource 0x7f080000. We can pull that resourceId apart to understand it.

  • 0x7f = packageId
  • 0x08 = typeId
  • 0x0000 = entryId

So a packageId of 0x7f represents a local package. This means the resource table we are looking for is present in this APK (UnifiedEULA). So why didn't it find it? Lets double check using aapt (Android Asset Packaging Tool) to make sure this resource is in fact here.

$aapt d resources UnifiedEULA.apk | grep '0x7f080000'
  spec resource 0x7f080000 com.lge.eula:string/app_name: flags=0x00000004
    resource 0x7f080000 com.lge.eula:string/app_name: t=0x03 d=0x00000060 (s=0x0008 r=0x00)

So yes, the resource we are looking for is infact in this APK. This points to a fault in the decoding process of Apktool.

So why is lge-res installing as pkgId 0?

Lets go back to our trustworthy aapt and see what it has to say.

$aapt d resources lge-res.apk | more
  Package Groups (1)
   Package Group 0 id=0x02 packageCount=1 name=com.lge
    Package 0 id=0x00 name=com.lge

Uh what. How come aapt is reporting the packageId as 0x02 but Apktool is identifying it as 0x00?

Lets take a peek into the source of Apktool and see if a mistake is occurring.

Well thats not good. Reading the int directly from the stream reports as 0 as well. So lets take a look at a hex dump of the resources.arsc file.

As you can see the int (4 bytes prior to highlighted packageName) is 0. So this means we have a change in AOSP that has changed the basic storage of packageIds.

I started to look at the release documents of Lollipop but found nothing that had anything to do with packageIds. So off to the repository I go. I generally look in platform_frameworks_base and the subfolder tools/aapt. This gave me about ~100 commits to look through between Kitkat and now.

I started skimming the commit messages and spotted "Shared library resource support". This sounded like something related and sure enough the commit message confirmed.

Shared libraries can now export resources for applications to use.

Exporting resources works the same way the framework exports  
resources, by defining the public symbols in res/values/public.xml.

Building a shared library requires aapt to be invoked with the  
--shared-lib option. Shared libraries will be assigned a package
ID of 0x00 at build-time. At runtime, all loaded shared libraries  
will be assigned a new package ID.

Currently, shared libraries should not import other shared libraries,  
as those dependencies will not be loaded at runtime.

At runtime, reflection is used to update the package ID of resource  
symbols in the shared library's R class file. The package name of  
the R class file is assumed to be the same as the shared library's  
package name declared in its manifest. This will be customizable in  
a future commit.

See /tests/SharedLibrary/ for examples of a shared library and its  
client.

Bug:12724178  
Change-Id: I60c0cb8ab87849f8f8a1a13431562fe8603020a7  

So incase you missed it. Check out the 3rd paragraph.

Building a shared library requires aapt to be invoked with the --shared-lib option. Shared libraries will be assigned a package ID of 0x00 at build-time. At runtime, all loaded shared libraries will be assigned a new package ID.

So we have found our culprit. This APK is using this new method of shared library resources. Thus, its packageId is set at runtime instead of buildtime.

The problem still remains though that our APKs can't decode unless they know the packageId of the resources. If you remember from above though, aapt had correctly determined the packageId of 0x02. How did it do that?

We take a look at the libs/androidfw/ResourceTypes.cpp file.

ResTable::ResTable()  
-    : mError(NO_INIT)
+    : mError(NO_INIT), mNextPackageId(2)

As you can see from the diff report. The packageId is literally just hardcoded as 2.

So I patched Apktool quite abruptly with.

if (id == 0) {  
    // This means we are dealing with a Library Package, we should just temporarily
    // set the packageId to the next available id . This will be set at runtime regardless, but
    // for Apktool's use we need a non-zero packageId.
    // AOSP indicates 0x02 is next, as 0x01 is system and 0x7F is private.
    id = 2;
}

Would it be that simple? Lets try again.

$apktool if lge-res.apk -t bug713
I: Framework installed to: /home/ibotpeaches/apktool/framework/2-bug713.apk  

Good good. We have a correct packageId now. Lets try the decode again. I'll save you the stacktrace and just tell you. It failed, with the same error as before.

So why aren't resources being decoded? In Apktool we have two sets that hold all the resources decoded.

private final Set<ResPackage> mMainPackages = new LinkedHashSet<ResPackage>();  
private final Set<ResPackage> mFramePackages = new LinkedHashSet<ResPackage>();  

with each ResPackage containing a Map of resources indexed by the resourceId.

private final Map<ResID, ResResSpec> mResSpecs = new LinkedHashMap<ResID, ResResSpec>();  

Each ResResSpec contains

public ResResSpec(ResID id, String name, ResPackage pkg, ResType type) {  
    this.mId = id;
    this.mName = name;
    this.mPackage = pkg;
    this.mType = type;
}

So I set a breakpoint during this failure and was astounded. The mMainPackages count for mResSpecs was at 0. We had 0 ResResSpecs. So decoding was failing badly. Badly enough that not even a single resource was decoded.

So I went as far back into the Apktool procedure where mResSpecs was populated. Back in the process of Apktool, during decoding of the ResPackage, we read the ResTypes which in turn reads the configs (readConfigs), which in turn reads the resources (readEntry) of that configuration.

while (mHeader.type == Header.TYPE_TYPE) {  
    readType();
}

So why was this check (shown above) failing? Header.TYPE_TYPE was 0x0202, and mHeader.type was 0x0203 and Apktool has no support for whatever that was corresponding to.

It appears we have something new, so back to the source (include/androidfw/ResourceTypes.h) we go.

-    RES_TABLE_TYPE_SPEC_TYPE    = 0x0202
+    RES_TABLE_TYPE_SPEC_TYPE    = 0x0202,
+    RES_TABLE_LIBRARY_TYPE      = 0x0203

We have found the new type and its called LIBRARY_TYPE. This type is before TYPE_SPEC_TYPE in the current APK. So Apktool is failing because it has no support for these. So referencing the linked commit above I found these additional structs.

/**
 * A package-id to package name mapping for any shared libraries used
 * in this resource table. The package-id's encoded in this resource
 * table may be different than the id's assigned at runtime. We must
 * be able to translate the package-id's based on the package name.
 */
struct ResTable_lib_header  
{
    struct ResChunk_header header;

    // The number of shared libraries linked in this resource table.
    uint32_t count;
};

/**
 * A shared library package-id to package name entry.
 */
struct ResTable_lib_entry  
{
    // The package-id this shared library was assigned at build time.
    // We use a uint32 to keep the structure aligned on a uint32 boundary.
    uint32_t packageId;

    // The package name of the shared library. \0 terminated.
    char16_t packageName[128];
};

So in short. If we encounter a chunk with a value of 0x0203 or TYPE_LIBRARY in the header, we then need to iterate over count and read the packageId & packageName for each count. Adding a quick patch into the brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/ARSCDecoder.java file.

We add the following function.

private void readLibraryType() throws AndrolibException, IOException {  
    checkChunkType(Header.TYPE_LIBRARY);
    int libraryCount = mIn.readInt();

    int packageId;
    String packageName;

    for (int i = 0; i < libraryCount; i++) {
        packageId = mIn.readInt();
        packageName = mIn.readNullEndedString(128, true);
        LOGGER.info(String.format("Decoding Shared Library (%s), pkgId: %d", packageName, packageId));
    }
}

With the check added in readPackage() we are left with this new functionality.

     mPkg = new ResPackage(mResTable, id, name);
     nextChunk();

+    while (mHeader.type == Header.TYPE_LIBRARY) {
+        readLibraryType();
+    }

     while (mHeader.type == Header.TYPE_TYPE) {
         readType();
     }

This full commit may be found here. So we now are going to check for TYPE_LIBRARY and TYPE_TYPE respectively. Time to test again with our previously broken UnifiedEULA.apk.

$apktool d UnifiedEULA.apk -t bug713
I: Using Apktool 2.0.0-RC3 on UnifiedEULA.apk  
I: Loading resource table...  
I: Decoding Shared Library (com.lge), pkgId: 2  
I: Decoding AndroidManifest.xml with resources...  
I: Loading resource table from file: /home/ibotpeaches/apktool/framework/1-bug713.apk  
I: Regular manifest package...  
I: Decoding file-resources...  
I: Decoding values */* XMLs...  
I: Baksmaling classes.dex...  
I: Copying assets and libs...  
I: Copying unknown files...  
I: Copying original files...  
$

Now lets check if the shared resource is correctly referenced during rebuild.

$ apktool b UnifiedEULA
I: Using Apktool 2.0.0-RC3  
I: Checking whether sources has changed...  
I: Smaling smali folder into classes.dex...  
I: Checking whether resources has changed...  
I: Building resources...  
I: Building apk file...  
$

It built! So we correctly found the undocumented feature of "shared library resources" and added preliminary support for it.

That was the journey of fixing bug 713 of Apktool. While writing about it was quite easy, there was lots of delays in the original patch while I looked for commits, source, etc.

Thanks for the read and hope you liked my first technical writeup. Comments/critiques good or bad are appreciated!.

Top