preface

Last time I gave you an overview of the ApkTool project, how ApkTool is compiled, how to run, and the introduction of various parameters. How does ApkTool analyze resources. Arsc files

The overall process

We first execute the command apktool d xxx.apk and see the output as follows

I: Using Apktool 2.3.1 on douyin. Apk I: Loading Resource table... I: Decoding AndroidManifest.xml with resources... I: Loading resource table from file: C:\Users\hch\AppData\Local\apktool\framework\1.apk I: Regular manifest package... I: Decoding file-resources... I: Decoding values */* XMLs... I: Baksmaling classes.dex... I: Baksmaling classes2.dex... I: Baksmaling classes3.dex... I: Copying assets and libs... I: Copying unknown files... I: Copying original files...Copy the code

In this case, apktool does the following steps

  • Load the resource table
  • Decoding AndroidManifest. XML
  • Decode some resource files
  • Decode dex files
  • Copy Remaining files

Today I want to discuss only the first step, about how ApkTool resolves resources.arsc.

How to initialize ApkDecoder member variable mResTable, the rest will be discussed next time.

Ps: If you want to see the general result, skip to the last image.

Resources. The format of arsc

Resources. Arsc is a binary file, and to parse it he must first understand what the file format looks like. Let’s start with a picture from the Internet. (Image source and network, abuse, deletion)

Resource table, String Pool,Package Header, etc.

These formats are in the Android source code, and the specific file is resourcetypes.h, for example:

struct ResChunk_header
{
    uint16_t type;
    uint16_t headerSize;
    uint32_t size;
};

enum {
    RES_NULL_TYPE               = 0x0000,
    RES_STRING_POOL_TYPE        = 0x0001,
    RES_TABLE_TYPE              = 0x0002,
    RES_XML_TYPE                = 0x0003.// Chunk types in RES_XML_TYPE
    RES_XML_FIRST_CHUNK_TYPE    = 0x0100,
    RES_XML_START_NAMESPACE_TYPE= 0x0100,
    RES_XML_END_NAMESPACE_TYPE  = 0x0101,
    RES_XML_START_ELEMENT_TYPE  = 0x0102,
    RES_XML_END_ELEMENT_TYPE    = 0x0103,
    RES_XML_CDATA_TYPE          = 0x0104,
    RES_XML_LAST_CHUNK_TYPE     = 0x017f.// This contains a uint32_t array mapping strings in the string
    // pool back to resource identifiers. It is optional.
    RES_XML_RESOURCE_MAP_TYPE   = 0x0180.// Chunk types in RES_TABLE_TYPE
    RES_TABLE_PACKAGE_TYPE      = 0x0200,
    RES_TABLE_TYPE_TYPE         = 0x0201,
    RES_TABLE_TYPE_SPEC_TYPE    = 0x0202
};

struct ResStringPool_header
{
    struct ResChunk_header header;
    uint32_t stringCount;
    uint32_t styleCount;
    enum {
        SORTED_FLAG = 1<<0,
        UTF8_FLAG = 1<<8
    };
    uint32_t flags;
    uint32_t stringsStart;
    uint32_t stylesStart;
};

Copy the code

Because of the length of the reason, so the annotation part was deleted, we can refer to the specific source code, there is also a good source code reading site to share with you, if you want to see it can not download, see it directly online.

Source site address,

Analytical process

Let’s start with main.java

public static void main(String[] args) throws IOException, InterruptedException, BrutException {

        / /... A little...
        boolean cmdFound = false;
        for (String opt : commandLine.getArgs()) {
            if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("decode")) {
                // Mainly here, the cmdDecode method is implemented to decode
                cmdDecode(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("b") || opt.equalsIgnoreCase("build")) {
                cmdBuild(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("if") || opt.equalsIgnoreCase("install-framework")) {
                cmdInstallFramework(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("empty-framework-dir")) {
                cmdEmptyFrameworkDirectory(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("publicize-resources")) {
                cmdPublicizeResources(commandLine);
                cmdFound = true; }}/ /... A little...
}
Copy the code

CmdDecode is mainly called to decode method, let’s follow in to see

private static void cmdDecode(CommandLine cli) throws AndrolibException {
        // First new APkDecoder class, mainly use this class to decode
        ApkDecoder decoder = new ApkDecoder();

        int paraCount = cli.getArgList().size();
        String apkName = cli.getArgList().get(paraCount - 1);
        File outDir;

        // Here is mainly according to some parameters we set, and then set the corresponding decoder class member variables,
        // The main thing is to set up the output directory, some mode, and version, etc
        if(/ /... A little... {
            / /... A little...
        } else {
            // make out folder manually using name of apk
            String outName = apkName;
            outName = outName.endsWith(".apk")? outName.substring(0,
                    outName.length() - 4).trim() : outName + ".out";

            // Set the output directory
            outName = new File(outName).getName();
            outDir = new File(outName);
            decoder.setOutDir(outDir);
        }
        / /... A little...
        decoder.setApkFile(new File(apkName));

        try {
            // Start decoding
            decoder.decode();
        } catch (OutDirExistsException ex) {
           / /... A little...
        } finally {
            / /... A little...}}Copy the code

We follow the decoder.decode() method to have a look

public void decode(a) throws AndrolibException, IOException, DirectoryException {
        try {
            // Get the output directory
            File outDir = getOutDir();
            // This is related to the keep-broken-res parameter we entered
            AndrolibResources.sKeepBroken = mKeepBrokenResources;
            // Determine whether overwriting is required
            if(! mForceDelete && outDir.exists()) {throw new OutDirExistsException();
            }
            // Check whether the APK file is valid
            if(! mApkFile.isFile() || ! mApkFile.canRead()) {throw new InFileNotFoundException();
            }

            // Clean up the directory to be output, ready to write
            try {
                OS.rmdir(outDir);
            } catch (BrutException ex) {
                throw new AndrolibException(ex);
            }
            outDir.mkdirs();
            Apktool d xxx.apk
            LOGGER.info("Using Apktool " + Androlib.getVersion() + " on " + mApkFile.getName());
            // Check if there are resources. Arsc files in apK.
            if (hasResources()) {
                // Determine decode Resources
                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); }}}/ /... A little...
      
    }
Copy the code

Typically, we execute to the DECODE_RESOURCES_FULL branch, where the first step is setTargetSdkVersion.

Let’s focus on the internal implementation of the setTargetSdkVersion method

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

    Map<String, String> sdkInfo = mResTable.getSdkInfo();
    if (sdkInfo.get("targetSdkVersion") != null) {
        mApi = Integer.parseInt(sdkInfo.get("targetSdkVersion")); }}Copy the code

In fact, ApkDecoder internal is to maintain a mResTable, any of our information is based on mResTable to take, that may ask, the ApkDecoder internal ResTable is what it is, in fact, he is our part of the above said that the classic figure.

When ApkDecoder finds that the mResTable variable is empty, it initializes it. Next we’ll look at Androlib’s getResTable method, which reads the mResTable from the apkFile and analyzes its format.

// androidlib.java file contents
public ResTable getResTable(ExtFile apkFile)
        throws AndrolibException {
    return mAndRes.getResTable(apkFile, true);
}

// the getResTable method of androlibResources.java
public ResTable getResTable(ExtFile apkFile, boolean loadMainPkg)
            throws AndrolibException {
    ResTable resTable = new ResTable(this);
    if (loadMainPkg) {
        loadMainPkg(resTable, apkFile);
    }
    return resTable;
}

Copy the code

The above code takes mAndRes’ getResTable method and then internally calls the loadMainPkg method. We follow up with the internal implementation


public ResPackage loadMainPkg(ResTable resTable, ExtFile apkFile)
            throws AndrolibException {
    // Print the log information
    LOGGER.info("Loading resource table...");
    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.");
    }

    resTable.addPackage(pkg, true);
    return pkg;
}
Copy the code

The getResPackagesFromApk method is executed to retrieve the ResPackage information.

private ResPackage[] getResPackagesFromApk(ExtFile apkFile,ResTable resTable, boolean keepBroken)
            throws AndrolibException {
    try {
        Directory dir = apkFile.getDirectory();
        BufferedInputStream bfi = new BufferedInputStream(dir.getFileInput("resources.arsc"));
        try {
            // This is the main method to parse the resources.arsc file
            return ARSCDecoder.decode(bfi, false, keepBroken, resTable).getPackages();
        } finally {
            try {
                bfi.close();
            } catch (IOException ignored) {}
        }
    } catch (DirectoryException ex) {
        throw new AndrolibException("Could not load resources.arsc from file: "+ apkFile, ex); }}Copy the code

We follow the DECODE method of ARSCDecoder


public static ARSCData decode(InputStream arscStream, boolean findFlagsOffsets, boolean keepBroken,
                                  ResTable resTable)
            throws AndrolibException {
    try {
        // Start with an ARSCDecoder based on the input stream, resTable and other parameters
        ARSCDecoder decoder = new ARSCDecoder(arscStream, resTable, findFlagsOffsets, keepBroken);
        //
        ResPackage[] pkgs = decoder.readTableHeader();
        return new ARSCData(pkgs, decoder.mFlagsOffsets == null
                ? null
                : decoder.mFlagsOffsets.toArray(new FlagsOffset[0]), resTable);
    } catch (IOException ex) {
        throw new AndrolibException("Could not decode arsc file", ex); }}Copy the code
private ResPackage[] readTableHeader() throws IOException, AndrolibException {
    
    nextChunkCheckType(Header.TYPE_TABLE);
    
    int packageCount = mIn.readInt();

    mTableStrings = StringBlock.read(mIn);
    ResPackage[] packages = new ResPackage[packageCount];

    nextChunk();
    for (int i = 0; i < packageCount; i++) {
        mTypeIdOffset = 0;
        packages[i] = readTablePackage();
    }
    return packages;
}
Copy the code

The key point is that the ChunkCheckType is read, The value of header. TYPE_TABLE is 0x0002, where type corresponds to RES_TABLE_TYPE = 0x0002 (ResourceTable)

We follow up the nextChunkCheckType method,

/ / ARSCDecoder within the class
private void nextChunkCheckType(int expectedType) throws IOException, AndrolibException {
    nextChunk();
    // Where the parameter expectedType is 2, RES_TABLE_TYPE,
    checkChunkType(expectedType);
}

/ / ARSCDecoder within the class
private Header nextChunk(a) throws IOException {
    return mHeader = Header.read(mIn, mCountIn);
}

/ / the Header in the class
public static Header read(ExtDataInput in, CountingInputStream countIn) throws IOException {
        short type;
        int start = countIn.getCount();
        try {
            // First read type,
            type = in.readShort();
        } catch (EOFException ex) {
            return new Header(TYPE_NONE, 0.0, countIn.getCount());
        }
        // Define the four parameters.
        // The type corresponding to the first parameter type is 2 bytes
        // The second argument header is 2 bytes in size
        // The third parameter file size is 4 bytes
        // For the fourth argument we temporarily set the start position to 0
        // Then return the Header from new
        return new Header(type, in.readShort(), in.readInt(), start);
    }

    private void checkChunkType(int expectedType) throws AndrolibException {
        // Check if the type of the header is the same as that passed in
        if(mHeader.type ! = expectedType) {throw new AndrolibException(String.format("Invalid chunk type: expected=0x%08x, got=0x%08x", expectedType, mHeader.type)); }}Copy the code

Reading a Chunk, as shown above, calls the relationship, with the key points annotated.

NextChunkCheckType (header.type_table) mainly reads the part circled in red below.

Let’s move on to the readTableHeader method.

private ResPackage[] readTableHeader() throws IOException, AndrolibException {
    // Read the value of the red circle
    nextChunkCheckType(Header.TYPE_TABLE);
    // Read the packageCount variable after the red circle in the figure above, 4 bytes
    int packageCount = mIn.readInt();
    //接下来就是主要分析这里了,读取Global String Pool 
    mTableStrings = StringBlock.read(mIn);
    ResPackage[] packages = new ResPackage[packageCount];

    nextChunk();
    for (int i = 0; i < packageCount; i++) {
        mTypeIdOffset = 0;
        packages[i] = readTablePackage();
    }
    return packages;
}

Copy the code

Next, we’ll focus on the read method of a StringBlock


public static StringBlock read(ExtDataInput reader) throws IOException {
    // RES_STRING_POOL_TYPE and the header size are skipped.
    // The verification method is to compare with CHUNK_STRINGPOOL_TYPE, whose value is 0x001C0001
    // This is because RES_STRING_POOL_TYPE has a value of 0x0001 and a header size of 0x001C, so CHUNK_STRINGPOOL_TYPE is 0x001C0001
    reader.skipCheckInt(CHUNK_STRINGPOOL_TYPE);
    // Read the block size in the Global String Pool
    int chunkSize = reader.readInt();

    // ResStringPool_header
    // Number of strings
    int stringCount = reader.readInt();
    / / style number
    int styleCount = reader.readInt();
    //flags flags, 1 is SORTED_FLAG, 256 is UTF8_FLAG
    int flags = reader.readInt();
    // Start position of string
    int stringsOffset = reader.readInt();
    //style start position
    int stylesOffset = reader.readInt();
    / / new a StringBlock
    StringBlock block = new StringBlock();
    // Set blocks based on flags readblock.m_isUTF8 = (flags & UTF8_FLAG) ! =0;
    // Initialize the block variable
    block.m_stringOffsets = reader.readIntArray(stringCount);
    block.m_stringOwns = new int[stringCount];
    Arrays.fill(block.m_stringOwns, -1);
    // Initialize the block internal style
    if(styleCount ! =0) {
        block.m_styleOffsets = reader.readIntArray(styleCount);
    }

    int size = ((stylesOffset == 0)? chunkSize : stylesOffset) - stringsOffset; block.m_strings =new byte[size];
    reader.readFully(block.m_strings);

    if(stylesOffset ! =0) {
        size = (chunkSize - stylesOffset);
        block.m_styles = reader.readIntArray(size / 4);

        // read remaining bytes
        int remaining = size % 4;
        if (remaining >= 1) {
            while (remaining-- > 0) { reader.readByte(); }}}// Return the final result
    return block;
}

Copy the code

Reader.skipcheckint (CHUNK_STRINGPOOL_TYPE) skips the following:

private ResPackage[] readTableHeader() throws IOException, AndrolibException {
    // Read the value of the red circle
    nextChunkCheckType(Header.TYPE_TABLE);
    // Read the packageCount variable after the red circle in the figure above, 4 bytes
    int packageCount = mIn.readInt();
    //接下来就是主要分析这里了,读取Global String Pool 
    mTableStrings = StringBlock.read(mIn);
    // At this point in time, it is time to analyze ResPackage
    ResPackage[] packages = new ResPackage[packageCount];

    nextChunk();
    for (int i = 0; i < packageCount; i++) {
        mTypeIdOffset = 0;
        // Use the readTablePackage method to analyze
        packages[i] = readTablePackage();
    }
    return packages;
}

Copy the code

Repetitive task

Emmmmm… Blogger analysis here, if you can read this I feel very happy ah, hope to help you. ReadTablePackage readTablePackage readTablePackage readTablePackage readTablePackage readTablePackage

So, I will not take you to understand, the main thing is to understand that picture, and then understand how ApkTool is to analyze it.

The advantage of this is that if an APK is doing something with the resource. Arsc file, we can debug it to see what is going on and have some ideas for dealing with it ourselves.

After readTablePackage

After reading, the program will step back, at which point our mResTable variable is initialized and we can continue to execute the setTargetSdkVersion method.

The main focus of this blog post is the initialization analysis of the ApkDeocder member variable mResTable. I have drawn a diagram to help you think through the above series of calls


participant Main
participant ApkDecoder
participant Androidlib
participant AndrolibResources
participant ARSCDecoder


Main->Main: cmdDecode
Main->ApkDecoder: decode
ApkDecoder->ApkDecoder: setTargetSdkVersion
ApkDecoder->Androidlib: getResTable
Androidlib->AndrolibResources: getResTable
AndrolibResources->AndrolibResources:loadMainPkg
AndrolibResources->AndrolibResources:getResPackagesFromApk
AndrolibResources->ARSCDecoder: decode
ARSCDecoder->ARSCDecoder: readTableHeader
ARSCDecoder->ARSCDecoder: nextChunkCheckType
ARSCDecoder->ARSCDecoder: nextChunk
ARSCDecoder->ARSCDecoder: readTablePackage
ARSCDecoder-->AndrolibResources:
AndrolibResources-->Androidlib:
Androidlib-->ApkDecoder:
ApkDecoder-->Main:
Copy the code

In case Markdown’s UML diagrams are not supported on some platforms, a special image is included below

Write in the last

It is not difficult to analyze the source code, and I hope everyone can afford to look at it bit by bit and debug it bit by bit. The article is very deep, so it may give readers confused, confused, please contact me, I also like to discuss with readers, there are wrong places to write more advice.

Because of the depth of the call, I drew a UML diagram in the end, hoping to make it easier for you to see

About me

Personal blog: MartinHan’s station

Blog site: Hanhan12312 column

Zhihu: MartinHan01