Analyzing and Altering the Source of Apktool

As I said in last posting "Crack Android apk with Apktool & Apksigner", I've faced some troubles in the rebuilding phase.

A) No resource identifier found for attribute "XXX" in package "com.XXX.XXX"

B) No resource found that matches the given name (at "XXX" with value "XXX")

As you can see, these problems are caused by custom attribute in a private namespace of a resource file.

For case A, Let's have a closer look at res\menu\a.xml

The attribute name in question is ktv:showAsAction.

Let's check out its internal id by aapt.

AAPT is an Android Asset Packaging Tool, which is able to dump or package the compiled Zip-compatible archive.

Let's try to dump the designated res\menu\a.xml as which is refered by r\p\a.xml before being unarchived.

aapt d xmltree anonymity.apk r/p/a.xml

As the result showed above, the internal id of attribute "showAsAction" is 0x7f01020a. Pay attention to the path which is composed of slash "/", not backslash "\".

Search the very id 0x7f01020a in the global resource id table -- res\values\public.xml -- and we'll find that it represents an attribute named "jt" which is obviously obfuscated.

<public type="attr" name="jt" id="0x7f01020a" />

Replace the so-called "showAsAction" which is definte incorrect into "jt", then recompile. As a result, the errors relating to "ktv:showAsAction" disappear.

It works. But we can't certainly do such a tedious work in which we change the erroneous attribute name one by one.

We're gonna plunge into the source code of Apktool and find out an elegant and efficient way to substantially change all the false attribute names or values.

Apktool recommend that we use IntelliJ IDEA to manage the project for convenient building and dependencies deployment with gradle.

But it's a new tool for me. Who feels complicated would be better looking to the official tutorials for how to leverage it to build and export a usable jar archive rather than searching out the Internet inefficiently.

Let's get going and unveil the code.

Apktool start decoding from brut.androlib.ApkDecoder.

    public void decode() throws AndrolibException, IOException, DirectoryException {
        try {
            File outDir = getOutDir();
            AndrolibResources.sKeepBroken = mKeepBrokenResources;

            if (!mForceDelete && outDir.exists()) {
                throw new OutDirExistsException();
            }

            if (!mApkFile.isFile() || !mApkFile.canRead()) {
                throw new InFileNotFoundException();
            }

            try {
                OS.rmdir(outDir);
            } catch (BrutException ex) {
                throw new AndrolibException(ex);
            }
            outDir.mkdirs();

            LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());

            // 1. Decode resources.
            if (hasResources()) {
                switch (mDecodeResources) {
                case DECODE_RESOURCES_NONE:
                    mAndrolib.decodeResourcesRaw(mApkFile, outDir);
                    if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
                        setTargetSdkVersion();
                        setAnalysisMode(mAnalysisMode, true);

                        // done after raw decoding of resources because copyToDir overwrites dest files
                        if (hasManifest()) {
                            mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
                        }
                    }
                    break;
                case DECODE_RESOURCES_FULL:
                    setTargetSdkVersion();
                    setAnalysisMode(mAnalysisMode, true);

                    if (hasManifest()) {
                        mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
                    }
                    mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());
                    break;
                }
            } else {
                // if there's no resources.arsc, decode the manifest without looking
                // up attribute references
                if (hasManifest()) {
                    if (mDecodeResources == DECODE_RESOURCES_FULL
                            || mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) {
                        mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable());
                    } else {
                        mAndrolib.decodeManifestRaw(mApkFile, outDir);
                    }
                }
            }

            // 2.1. Decode dex files.
            if (hasSources()) {
                switch (mDecodeSources) {
                case DECODE_SOURCES_NONE:
                    mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
                    break;
                case DECODE_SOURCES_SMALI:
                    mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi);
                    break;
                }
            }

            // 2.2. Merge multiple dexes.
            if (hasMultipleSources()) {
                // foreach unknown dex file in root, lets disassemble it
                Set<String> files = mApkFile.getDirectory().getFiles(true);
                for (String file : files) {
                    if (file.endsWith(".dex")) {
                        if (!file.equalsIgnoreCase("classes.dex")) {
                            switch (mDecodeSources) {
                            case DECODE_SOURCES_NONE:
                                mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
                                break;
                            case DECODE_SOURCES_SMALI:
                                mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApi);
                                break;
                            }
                        }
                    }
                }
            }

            // 3. Handle files of miscellaneous types
            mAndrolib.decodeRawFiles(mApkFile, outDir, mDecodeAssets);
            mAndrolib.decodeUnknownFiles(mApkFile, outDir, mResTable);
            mUncompressedFiles = new ArrayList<String>();
            mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
            mAndrolib.writeOriginalFiles(mApkFile, outDir);
            writeMetaFile();
        } catch (Exception ex) {
            throw ex;
        } finally {
            try {
                mApkFile.close();
            } catch (IOException ignored) {
            }
        }
    }

We can see that there are three major steps:

  1. Decode resources.
  2. Decode dex files.
  3. Handle files of miscellaneous types

I'll merely expand the first step in order to find out the solotion to our faults in the process of res-decoding.

Let's scoot ahead.

1. Check if there is an achive named "resources.arsc"

    public boolean hasResources() throws AndrolibException {
        try {
            return mApkFile.getDirectory().containsFile("resources.arsc");
        } catch (DirectoryException ex) {
            throw new AndrolibException(ex);
        }
    }

The whole of resource files are originally packaged in the .arsc archive. We have to make sure it is being ready to be extracted.

2. Build the resource table and extract the sdk version

    public void setTargetSdkVersion() throws AndrolibException, IOException {
        // Build a resource table
        if (mResTable == null) {
            mResTable = mAndrolib.getResTable(mApkFile);
        }

        // Retrieve sdk version
        Map<String, String> sdkInfo = mResTable.getSdkInfo();
        if (sdkInfo.get("targetSdkVersion") != null) {
            mApi = Integer.parseInt(sdkInfo.get("targetSdkVersion"));
        }
    }

The procedure for building resource table is one of the most important steps. A resource table is a collection of the header information of each resource file. We're gonna decrypt and reorganize target files by this table.

Let's check out how to build it.

    public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
            throws AndrolibException {
        ResTable resTable = new ResTable(this);
        if (loadMainPkg) {
            //.arsc file may be made of a certain number 
            // (which is basically one) of packages.
            loadMainPkg(resTable, apkFile);
        }
        return resTable;
    }

In this method, apktool create a new ResTable and bind it with the AndrolibResources singleton which manages all the resource affairs.

The main logic is execute loadMainPkg method in which we will inflate the newly generated ResTable.

Let's move on to explore what loadMainPkg would have done.

    public ResPackage loadMainPkg(ResTable resTable, ExtFile apkFile)
            throws AndrolibException {
        LOGGER.info("Loading resource table...");
        
        // 1. Retrieve details of ResTable
        // including ones of Packages, Types, Resources in it.
        ResPackage[] pkgs = getResPackagesFromApk(apkFile, resTable, sKeepBroken);
        ResPackage pkg = null;

        switch (pkgs.length) {
            case 1:
                pkg = pkgs[0];
                break;
            case 2:
                if (pkgs[0].getName().equals("android")) {
                    LOGGER.warning("Skipping \"android\" package group");
                    pkg = pkgs[1];
                    break;
                } else if (pkgs[0].getName().equals("com.htc")) {
                    LOGGER.warning("Skipping \"htc\" package group");
                    pkg = pkgs[1];
                    break;
                }

            default:
                pkg = selectPkgWithMostResSpecs(pkgs);
                break;
        }

        if (pkg == null) {
            throw new AndrolibException("arsc files with zero packages or no arsc file found.");
        }

        // 2. Put the main package in the ResTable.
        resTable.addPackage(pkg, true);
        return pkg;
    }

The main idea of loadMainPkg is retrieving details of ResTable including ones of Packages, Types, Resources in the package.

A Table has got a tree-like internal structure.

  • Package 1 (com.XXX.XXX)
    • Type 1 (layout, drawable, menu, anim, ...)
      • Res 1 (a.xml, b.png, ResAttr, ResStringValue, ...)
      • Res 2
        ...
    • Type 2
      ...
  • Package 2
    ...

A package is a unit represented by a package id stored in AndroidManifest.xml looks like "package='com.XXX.XXX'"

A Type is actually a folder name under "res" directory like "layout, drawable, menu, anim, ...".

A Res is a resource under respect Type folder like "a.xml, b.png, ResAttr, ResStringValue, ..." which is in different formats.

No matter a Type or a Res has a header information called Spec which may be the abbreviation of "specification", involves its id, file name, type (aka folder name), etc.

Here is the involking chain of loadMainPkg method.

Apktool read table, read packages in table recursively, read types which is the "folder" in a package recursively, read entry which is the "Res" in a type recursively. Apktool puts raw bytes of every resource into a particular class like "ResAttr, ResStringValue, ..." to be waited to process.

At last, getResPackagesFromApk method will return all the packages read in the ResTable after traversing and recording all of their resources. loadMainPkg method puts the main package of these packages into current ResTable instance.

Actually, there is often only one package which is also the main package.

As of now, the ResTable has been built over.

3. Decode AndroidManifest.xml

mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());

I won't expand this step since it's simply decoding the manifest file and do some adjusted work which we will cover in the next step.

4. Decode the whole of resources and reorganize them.

mAndrolib.decodeResourcesFull(mApkFile, outDir, getResTable());

This is the most important step which involves the key to our recompiling errors.

    public void decode(ResTable resTable, ExtFile apkFile, File outDir)
            throws AndrolibException {
        // 1. Prepare the key XML Decoder & Parser to process xml files.
        Duo<ResFileDecoder, AXmlResourceParser> duo = getResFileDecoder();
        ResFileDecoder fileDecoder = duo.m1;
        ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder();

        // 2. Bind the main package with AXmlResourceParser. 
        attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next());
        Directory inApk, in = null, out;

        try {
            
            // 3. Custom particular input and output directory
            out = new FileDirectory(outDir);

            inApk = apkFile.getDirectory();
            out = out.createDir("res");
            if (inApk.containsDir("res")) {
                in = inApk.getDir("res");
            }
            if (in == null && inApk.containsDir("r")) {
                in = inApk.getDir("r");
            }
            if (in == null && inApk.containsDir("R")) {
                in = inApk.getDir("R");
            }
        } catch (DirectoryException ex) {
            throw new AndrolibException(ex);
        }

        // 4. Prepare an xml serializer for storing xml elements.
        ExtMXSerializer xmlSerializer = getResXmlSerializer();
        for (ResPackage pkg : resTable.listMainPackages()) {
            attrDecoder.setCurrentPackage(pkg);

            LOGGER.info("Decoding file-resources...");
            for (ResResource res : pkg.listFiles()) {
                // 5. Handle every resource file.
                fileDecoder.decode(res, in, out);
            }

            LOGGER.info("Decoding values */* XMLs...");
            for (ResValuesFile valuesFile : pkg.listValuesFiles()) {
                // 6. Put every value-type resource into a specified file, ex. value.xml. 
                generateValuesFile(valuesFile, out, xmlSerializer);
            }
            
            // 7. List the header information of every resource in the public.xml
            // including its id, type, name, etc.
            generatePublicXml(pkg, out, xmlSerializer);
        }

        AndrolibException decodeError = duo.m2.getFirstError();
        if (decodeError != null) {
            throw decodeError;
        }
    }

I have concluded 7 substeps in the brut.androlib.res.AndrolibResources.decode method. The most crucial step is step 5. It traverse all the resource objects we've got in the loadMainPkg method and generate generate resource file one by one.

Let's go and dig into it.

    public void decode(ResResource res, Directory inDir, Directory outDir)
            throws AndrolibException {

        // 1. Prepare the input and output path.
        ResFileValue fileValue = (ResFileValue) res.getValue();
        String inFileName = fileValue.getStrippedPath();
        String outResName = res.getFilePath();
        String typeName = res.getResSpec().getType().getName();
        
        String ext = null;
        String outFileName;
        int extPos = inFileName.lastIndexOf(".");
        if (extPos == -1) {
            outFileName = outResName;
        } else {
            ext = inFileName.substring(extPos).toLowerCase();
            outFileName = outResName + ext;
        }
        
        // 2. Decode resource files according to their types.
        try {
            if (typeName.equals("raw")) {
                decode(inDir, inFileName, outDir, outFileName, "raw");
                return;
            }
            if (typeName.equals("font") && !".xml".equals(ext)) {
                decode(inDir, inFileName, outDir, outFileName, "raw");
                return;
            }
            if (typeName.equals("drawable") || typeName.equals("mipmap")) {
                if (inFileName.toLowerCase().endsWith(".9" + ext)) {
                    outFileName = outResName + ".9" + ext;

                    // check for htc .r.9.png
                    if (inFileName.toLowerCase().endsWith(".r.9" + ext)) {
                        outFileName = outResName + ".r.9" + ext;
                    }

                    // check for raw 9patch images
                    for (String extension : RAW_9PATCH_IMAGE_EXTENSIONS) {
                        if (inFileName.toLowerCase().endsWith("." + extension)) {
                            copyRaw(inDir, outDir, outFileName);
                            return;
                        }
                    }

                    // check for xml 9 patches which are just xml files
                    if (inFileName.toLowerCase().endsWith(".xml")) {
                        decode(inDir, inFileName, outDir, outFileName, "xml");
                        return;
                    }

                    try {
                        decode(inDir, inFileName, outDir, outFileName, "9patch");
                        return;
                    } catch (CantFind9PatchChunk ex) {
                        LOGGER.log(
                                Level.WARNING,
                                String.format(
                                        "Cant find 9patch chunk in file: \"%s\". Renaming it to *.png.",
                                        inFileName), ex);
                        outDir.removeFile(outFileName);
                        outFileName = outResName + ext;
                    }
                }

                // check for raw image
                for (String extension : RAW_IMAGE_EXTENSIONS) {
                    if (inFileName.toLowerCase().endsWith("." + extension)) {
                        copyRaw(inDir, outDir, outFileName);
                        return;
                    }
                }

                if (!".xml".equals(ext)) {
                    decode(inDir, inFileName, outDir, outFileName, "raw");
                    return;
                }
            }

            // *Key entrance*
            decode(inDir, inFileName, outDir, outFileName, "xml");
        } catch (AndrolibException ex) {
            LOGGER.log(Level.SEVERE, String.format(
                    "Could not decode file, replacing by FALSE value: %s",
                    inFileName), ex);
            res.replace(new ResBoolValue(false, 0, null));
        }
    }

What decode method do is to decode resource file as to its specific type.

I have marked a "key entrance" to decode an xml file in which our errors occured.

By the way, the first step of this code passage will generate the input path (like r/p/a.xml) and the output path (like res/menu/a.xml), by which we can sumarize a mapping between them.

That's why I knew res\menu\a.xml is refered by r\p\a.xml above when I leveraged aapt to inquire the attribute name id.

    @Override
    public void decode(InputStream in, OutputStream out)
            throws AndrolibException {
        try {
            XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance();
            XmlPullParserWrapper par = factory.newPullParserWrapper(mParser);
            final ResTable resTable = ((AXmlResourceParser) mParser).getAttrDecoder().getCurrentPackage().getResTable();

            XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory) {
                boolean hideSdkInfo = false;
                boolean hidePackageInfo = false;

                @Override
                public void event(XmlPullParser pp)
                        throws XmlPullParserException, IOException {
                    int type = pp.getEventType();

                    if (type == XmlPullParser.START_TAG) {
                        if ("manifest".equalsIgnoreCase(pp.getName())) {
                            try {
                                hidePackageInfo = parseManifest(pp);
                            } catch (AndrolibException ignored) {}
                        } else if ("uses-sdk".equalsIgnoreCase(pp.getName())) {
                            try {
                                hideSdkInfo = parseAttr(pp);
                                if (hideSdkInfo) {
                                    return;
                                }
                            } catch (AndrolibException ignored) {}
                        }
                    } else if (hideSdkInfo && type == XmlPullParser.END_TAG
                            && "uses-sdk".equalsIgnoreCase(pp.getName())) {
                        return;
                    } else if (hidePackageInfo && type == XmlPullParser.END_TAG
                            && "manifest".equalsIgnoreCase(pp.getName())) {
                        super.event(pp);
                        return;
                    }
                    super.event(pp);
                }

                ......

            };

            par.setInput(in, null);
            ser.setOutput(out, null);

            // Each xml is wrapped by a AXmlResourceParser
            while (par.nextToken() != XmlPullParser.END_DOCUMENT) {
                ser.event(par);
            }
            ser.flush();
        } catch (XmlPullParserException ex) {
            throw new AndrolibException("Could not decode XML", ex);
        } catch (IOException ex) {
            throw new AndrolibException("Could not decode XML", ex);
        }
    }

Apktool creates a inner class StaticXmlSerializerWrapper which override the same class of org.xmlpull.v1.wrapper.classic.StaticXmlSerializerWrapper which is in the xpp3 jar. Xpp3 is a fast, lightweight xml parser that comforms to the "Pull Stream API"

The main course is so clear that tokens are read from input raw xml data and are wrapped with an AXmlResourceParser object. This parser will be delivered to the serializer which is the inner class which is just created.

ser.event(par);

Then the inner class will call outer StaticXmlSerializerWrapper by

super.event(pp);

    public void event(XmlPullParser pp) throws XmlPullParserException, IOException {
        int eventType = pp.getEventType();
        switch (eventType) {
            case XmlPullParser.START_DOCUMENT:
                //use Boolean.TRUE to make it standalone
                Boolean standalone = (Boolean) pp.getProperty(PROPERTY_XMLDECL_STANDALONE);
                startDocument(pp.getInputEncoding(), standalone);
                break;

            case XmlPullParser.END_DOCUMENT:
                endDocument();
                break;

                // XML token entrance
            case XmlPullParser.START_TAG:
                writeStartTag(pp);
                break;

            case XmlPullParser.END_TAG:
                endTag(pp.getNamespace (), pp.getName ());
                break;

            case XmlPullParser.IGNORABLE_WHITESPACE:
                //comment it to remove ignorable whtespaces from XML infoset
                String s = pp.getText ();
                ignorableWhitespace(s);
                break;

            case XmlPullParser.TEXT:
                if(pp.getDepth() > 0)  {
                    text(pp.getText ());
                } else {
                    ignorableWhitespace(pp.getText ());
                }
                break;

            case XmlPullParser.ENTITY_REF:
                entityRef (pp.getName ());
                break;

            case XmlPullParser.CDSECT:
                cdsect( pp.getText () );
                break;

            case XmlPullParser.PROCESSING_INSTRUCTION:
                processingInstruction( pp.getText ());
                break;

            case XmlPullParser.COMMENT:
                comment (pp.getText ());
                break;

            case XmlPullParser.DOCDECL:
                docdecl (pp.getText ());
                break;
        }
    }

writeStartTag(pp); will be invoked.

    private void writeStartTag(XmlPullParser pp) throws XmlPullParserException, IOException {
        // Set prefix resembling <xmlns:ns_name="ns_value">
        if (!pp.getFeature (XmlPullParser.FEATURE_REPORT_NAMESPACE_ATTRIBUTES)) {
            int nsStart = pp.getNamespaceCount(pp.getDepth()-1);
            int nsEnd = pp.getNamespaceCount(pp.getDepth());
            for (int i = nsStart; i < nsEnd; i++) {
                String prefix = pp.getNamespacePrefix(i);
                String ns = pp.getNamespaceUri(i);
                setPrefix(prefix, ns);
            }
        }
        
        // Set start tag of an item resembling
        // <ns_name:tag_name ...>
        startTag(pp.getNamespace(), pp.getName());

        // Set following attribute resembling 
        // <... ns_name:attr_name="attr_value" ...>
        for (int i = 0; i < pp.getAttributeCount(); i++) {
            attribute
                (pp.getAttributeNamespace (i),
                 pp.getAttributeName (i),
                 pp.getAttributeValue (i));
        }
    }

Here we can locate where our errors happened:

  1. Case A occured at pp.getAttributeName (i), so that the attribute name under a private namespace is invalid.
  2. In the same way, Case B occured at pp.getAttributeValue (i) and the corresponding values are faulty.

We'll have to examine the exact name and value in accordance with the id of a ResSpec

StaticXmlSerializerWrapper class is a facade of the real AXmlResourceParser of which is actually taken advantage.

I have modified 3 methods relating to the logic of xml parsing.

    /////////////////////////////////////////////////
    //
    // brut.androlib.res.decoder.AXmlResourceParser
    //
    /////////////////////////////////////////////////
    
    public String getAttributeName(int index) {
        int offset = getAttributeOffset(index);
        int nameIndex = m_attributes[offset + ATTRIBUTE_IX_NAME];
        if (nameIndex == -1) return "";
        String name = m_strings.getString(nameIndex);
        String namespace = getAttributeNamespace(index);
        
        // If attribute name is lacking or a private namespace emerges, 
        // retrieve the exact attribute name by its id.
        if (name == null || name.length() == 0) {
            try {
                name = mAttrDecoder
                        .decodeManifestAttr(getAttributeNameResource(index));
                if (name == null) name = "";
            } catch (AndrolibException e) {name = "";}
        } else if (! namespace.equals(android_ns)) {
            try {
                String obfuscatedName = mAttrDecoder
                        .decodeManifestAttr(getAttributeNameResource(index));
                if (! (obfuscatedName == null || obfuscatedName.equals(name))) {
                    name = obfuscatedName;
                }
            } catch (AndrolibException e) {}
        }
        return name;
    }

    public String getAttributeValue(int index) {
        int offset = getAttributeOffset(index);
        int valueType = m_attributes[offset + ATTRIBUTE_IX_VALUE_TYPE];
        int valueId = m_attributes[offset + ATTRIBUTE_IX_VALUE_DATA];
        int valueRaw = m_attributes[offset + ATTRIBUTE_IX_VALUE_STRING];        
        
        if (mAttrDecoder != null) {
            try {
                String value = valueRaw == -1 ? null : ResXmlEncoders
                        .escapeXmlChars(m_strings.getString(valueRaw));
                String obfuscatedValue = mAttrDecoder
                        .decodeManifestAttr(valueId);
                if (! (value == null || obfuscatedValue == null)) {
                    int slashPos = value.lastIndexOf("/");
                    
                    if (slashPos != -1) {
                        // Handle a value with a format of "@yyy/xxx"
                        String dir = value.substring(0, slashPos);
                        value = dir + "/"+ obfuscatedValue;
                    } else if (! value.equals(obfuscatedValue)) {
                        value = obfuscatedValue;
                    }
                }

                return mAttrDecoder.decode(
                        valueType,
                        valueId,
                        value,
                        getAttributeNameResource(index));
            } catch (AndrolibException ex) {
                setFirstError(ex);
                LOGGER.log(Level.WARNING, String.format("Could not decode attr value, using undecoded value "
                                + "instead: ns=%s, name=%s, value=0x%08x",
                        getAttributePrefix(index),
                        getAttributeName(index),
                        valueId), ex);
            }
        }
        return TypedValue.coerceToString(valueType, valueId);
    }
    /////////////////////////////////////////////////
    //
    // brut.androlib.res.decoder.ResAttrDecoder
    //
    /////////////////////////////////////////////////

    public String decodeManifestAttr(int attrResId)
            throws AndrolibException {
        if (attrResId != 0) {
            int attrId = attrResId;

            // See also: brut.androlib.res.data.ResTable.getResSpec
            if (attrId >> 24 == 0) {
                ResPackage pkg =  getCurrentPackage();
                int packageId = pkg.getId();
                int pkgId = (packageId == 0 ? 2 : packageId);
                attrId = (0xFF000000 & (pkgId << 24)) | attrId;
            }

            // Retrieve the ResSpec in a package by its id
            ResID resId = new ResID(attrId);
            ResPackage pkg = getCurrentPackage();
            if (pkg.hasResSpec(resId)) {
                ResResSpec resResSpec = pkg.getResSpec(resId);
                if (resResSpec != null) {return resResSpec.getName();}
            }
        }

        return null;
    }

The basic ideas:

  1. Get the attribute id in m_attributes by offset
  2. Invoke brut.androlib.res.decoder.ResAttrDecoder.decodeManifestAttr method in order to inquire the exact attribute value by id
  3. Replace the invalid one in a sensible condition.

In the end, we decode and rebuild the apk.
Thanks goodness, it works anyway.

Decode
Rebuilt
Re-sign
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,692评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,482评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,995评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,223评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,245评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,208评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,091评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,929评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,346评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,570评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,739评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,437评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,037评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,677评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,833评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,760评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,647评论 2 354