Apktool 源碼分析那些一定要懂的細(xì)節(jié)(下篇)

前言

距寫完上一篇 Apktool 源碼分析那些一定要懂的細(xì)節(jié)(上篇)、大約快接近9個月,9個月時間轉(zhuǎn)瞬即逝。記得寫上篇文章時候還在上一家公司干活。此時卻在新公司任職。

原計劃下篇文章可能會被無限擱置的,因為從源碼理解、分析和梳理,再通過文章的形式寫出來,是非常耗時耗力的。最近這半年,從職業(yè)生涯、技術(shù)和思想認(rèn)知上有了一定提升。有想要改變一下自己想法。所以決定把下篇文章寫完。在我的認(rèn)知里:程-序-人-生,程序、人生。寫程序和做人一樣的,有始有終 。不是嗎?

表情包.png

梳理構(gòu)建邏輯

一個正常apk反編譯(解碼)后的文件結(jié)構(gòu)和層級,如下圖所示的。apktool可以解碼,同樣也有構(gòu)建(build)的能力。構(gòu)建不難理解,把需要修改的文件,修改好后重新構(gòu)建生成 apk。那如何構(gòu)建apk的呢? 這個操作你好奇嗎?好奇就跟著我的邏輯一起梳理一下。

構(gòu)建前.png

常規(guī)情況下在終端或者dos窗口 輸入 apktool b /Users/user/Downloads/Apktool/demo 過一會就可在dist文件夾下找到構(gòu)建好的apk。但是要注意dist/xx.apk是重新構(gòu)建后的,需要重新簽名才可以安裝在手機上的。

構(gòu)建后.png

在終端或者dos窗口 總會看到如下日志輸出,這個日志自上到下已經(jīng)很好的解釋了構(gòu)建的邏輯,跟著日志一起來看一下源碼。 注:本文基于2.6.1版本的源碼做分析,較2.5. x邏輯上區(qū)別不大

I: Using Apktool 2.6.1-dirty
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
I: Checking whether sources has changed...
I: Smaling smali_classes2 folder into classes2.dex...
I: Checking whether resources has changed...
I: Building resources...
I: Copying libs... (/lib)
I: Copying libs... (/kotlin)
I: Building apk file...
I: Copying unknown files/dir...
I: Built apk...

先從cmdBuild()看起,當(dāng)你輸入apktool b 的時候已經(jīng)執(zhí)行到cmdBuild() ,如果不指定其他命令參數(shù),可以走到最核心方法new Androlib(buildOptions).build();


 private static void cmdBuild(CommandLine cli) {
        String[] args = cli.getArgs();
        String appDirName = args.length < 2 ? "." : args[1];
        File outFile;
        BuildOptions buildOptions = new BuildOptions();

        // check for build options

        if (cli.hasOption("f") || cli.hasOption("force-all")) {
            buildOptions.forceBuildAll = true;
        }
        if (cli.hasOption("d") || cli.hasOption("debug")) {
            buildOptions.debugMode = true;
        }
        if (cli.hasOption("v") || cli.hasOption("verbose")) {
            buildOptions.verbose = true;
        }
        if (cli.hasOption("a") || cli.hasOption("aapt")) {
            buildOptions.aaptPath = cli.getOptionValue("a");
        }
        if (cli.hasOption("c") || cli.hasOption("copy-original")) {
            System.err.println("-c/--copy-original has been deprecated. Removal planned for v3.0.0 (#2129)");
            buildOptions.copyOriginalFiles = true;
        }
        if (cli.hasOption("p") || cli.hasOption("frame-path")) {
            buildOptions.frameworkFolderLocation = cli.getOptionValue("p");
        }
        if (cli.hasOption("nc") || cli.hasOption("no-crunch")) {
            buildOptions.noCrunch = true;
        }

        // Temporary flag to enable the use of aapt2. This will transform in time to a use-aapt1 flag, which will be
        // legacy and eventually removed.
        if (cli.hasOption("use-aapt2")) {
            buildOptions.useAapt2 = true;
        }
        if (cli.hasOption("api") || cli.hasOption("api-level")) {
            buildOptions.forceApi = Integer.parseInt(cli.getOptionValue("api"));
        }
        if (cli.hasOption("o") || cli.hasOption("output")) {
            outFile = new File(cli.getOptionValue("o"));
        } else {
            outFile = null;
        }

        // try and build apk
        try {
            if (cli.hasOption("a") || cli.hasOption("aapt")) {
                buildOptions.aaptVersion = AaptManager.getAaptVersion(cli.getOptionValue("a"));
            }
            new Androlib(buildOptions).build(new File(appDirName), outFile);
        } catch (BrutException ex) {
            System.err.println(ex.getMessage());
            System.exit(1);
        }
    }

appDirName:反編譯后根據(jù)app名稱生成同名文件夾。就是 apktool b 后面的路徑xxx/xxx/demo可以結(jié)合上圖來看。

outFile:是輸出具體位置和apk名字。你不輸入-o 參數(shù),默認(rèn)是null,后面有代碼對null進行處理邏輯。

如下圖所示,回編最核心的邏輯,為了防止大家迷糊。我畫了一份腦圖供大家參考,我會根據(jù)腦圖所畫內(nèi)容進行梳理。(邏輯順序自上而下一個個來說)

apktool構(gòu)建邏輯.png
 //構(gòu)建回編核心邏輯代碼展示
 public void build(ExtFile appDir, File outFile) throws BrutException {
        LOGGER.info("Using Apktool " + Androlib.getVersion());

        MetaInfo meta = readMetaFile(appDir); //讀取apktool.yml文件
        buildOptions.isFramework = meta.isFrameworkApk;
        buildOptions.resourcesAreCompressed = meta.compressionType;
        buildOptions.doNotCompress = meta.doNotCompress;

        mAndRes.setSdkInfo(meta.sdkInfo);
        mAndRes.setPackageId(meta.packageInfo);
        mAndRes.setPackageRenamed(meta.packageInfo);
        mAndRes.setVersionInfo(meta.versionInfo);
        mAndRes.setSharedLibrary(meta.sharedLibrary);
        mAndRes.setSparseResources(meta.sparseResources);

        if (meta.sdkInfo != null && meta.sdkInfo.get("minSdkVersion") != null) {
            String minSdkVersion = meta.sdkInfo.get("minSdkVersion");
            mMinSdkVersion = mAndRes.getMinSdkVersionFromAndroidCodename(meta, minSdkVersion);
        }

        if (outFile == null) {
            String outFileName = meta.apkFileName;  
            outFile = new File(appDir, "dist" + File.separator + (outFileName == null ? "out.apk" : outFileName)); //outFileName為空 輸出文件為dist/out.apk ,反之dist/demo.apk
        }

        new File(appDir, APK_DIRNAME).mkdirs(); //創(chuàng)建 build/apk 文件夾
        File manifest = new File(appDir, "AndroidManifest.xml");
        File manifestOriginal = new File(appDir, "AndroidManifest.xml.orig");

        buildSources(appDir);
        buildNonDefaultSources(appDir);
        buildManifestFile(appDir, manifest, manifestOriginal);
        buildResources(appDir, meta.usesFramework);
        buildLibs(appDir);  
        buildCopyOriginalFiles(appDir);
        buildApk(appDir, outFile);
        buildUnknownFiles(appDir, outFile, meta); 
  
        if (manifest.isFile() && manifest.exists() && manifestOriginal.isFile()) {
            try {
                if (new File(appDir, "AndroidManifest.xml").delete()) {
                    FileUtils.moveFile(manifestOriginal, manifest);
                }
            } catch (IOException ex) {
                throw new AndrolibException(ex.getMessage());
            }
        }
        LOGGER.info("Built apk...");
    }

前置準(zhǔn)備

readMetaFile: 從命名上可知,意為讀取apktool.yml文件的意思。這部分內(nèi)容較為簡單,是通過反編譯時把對應(yīng)的文件數(shù)據(jù)寫到 apktool.yml(第三方開源庫 snakeyaml),構(gòu)建時再把寫入的數(shù)據(jù)讀出來 ,在對應(yīng)的設(shè)置到MetaInfo 對象屬性里。mMinSdkVersion 是從apktool.yml讀取出來的值,最后生成dex文件時候會用到。

outFile:在上面提及過,如果你沒指定了 -O 命令參數(shù), outFile對null進行邏輯處理。既然你沒有指定apk輸出路徑,那apktool 默認(rèn)給你指定一個路徑。如果在apktool.yml讀取到的apkFileName屬性等于null,構(gòu)建后的apk會以dist/out.apk 作為文件名構(gòu)建輸出,反之dist/demo.apk(我的apkFileName屬性等于demo.apk)為 文件名構(gòu)建輸出。

構(gòu)建前demo文件資源參考.png

buildSources

核心邏輯部分,這部分邏輯重點是處理的是smali和dex文件,這么說你可能不是很理解。接著往下看,我會詳細(xì)說明。

buildSourcesRaw
首先會判斷demo/classes.dex是否存在。常規(guī)操作(apktool b demo.apk)會直接返回false,不在執(zhí)行后續(xù)邏輯。但是你要是增加了-s 或者--no-src 參數(shù)命令(意思是不處理dex文件,此命令適用于只修改資源和清單文件的需求,會增快構(gòu)建速度)。后續(xù)邏輯還是會執(zhí)行的。

demo/classes.dex存在,說明用到了--no-src命名參數(shù)。執(zhí)行的邏輯比較簡單粗暴直接把classes.dex文件,通過流的形式寫入到demo/build/apk/文件夾下。

buildSourcesSmali
首先會判斷demo/smali文件夾是否存在,不存在返回false。正常情況(apktool b demo.apk)下smali文件夾是一定存在,存在smali文件夾就會繼續(xù)往下執(zhí)行邏輯。接著有兩個條件作為判斷(必定有一個條件滿足),第一個條件文件是否覆蓋,指的是 -f 參數(shù)命令 。第二個條件文件是否做修改。

兩個條件中有任何一個成立,執(zhí)行SmaliBuilder.build(),該方法執(zhí)行邏輯就是將smali文件夾里面的.smali文件 轉(zhuǎn)換合并成classes.dex文件。這個不往下深究,作者進行了適當(dāng)?shù)姆庋b,最終通過傳過來的參數(shù),調(diào)用第三方庫org.jf.dexlib2。

總結(jié):buildSources()簡單說 ,就是對安卓代碼進行處理,要重新生成apk,安卓代碼文件肯定是要以.dex文件形式存在。代碼無外乎.dex文件或者是smali文件夾形式存在。buildSources()分別對這兩種情況進行邏輯處理。

buildNonDefaultSources

大家看了上面buildSources(),這部分也就好說了不少。首先會遍歷demo/所有文件夾,如果找到smali_開頭,執(zhí)行buildSourcesRaw()和buildSourcesSmali(),代碼在上面說的很細(xì)我就不再啰嗦。

如果找不到smali_開頭,首先會跳過classes.dex文件,為啥?因為在buildSources()已經(jīng)把邏輯處理過了,沒必要重復(fù)處理。只要demo文件夾下有.dex后綴結(jié)尾文件,執(zhí)行buildSourcesRaw()也就是把對應(yīng)多.dex文件復(fù)制到demo/build/apk/下。

總結(jié):這部分內(nèi)容就是buildSources()升級,對多個dex文件和多個smali文件夾進行處理。你無法保證apk不會方法超限,方法超限必定會用Multidex。

buildManifestFile

這部分的邏輯很有意思。首先會判斷demo文件夾下如果存在resource.arsc文件,直接返回不繼續(xù)往下執(zhí)行邏輯。這塊是考慮到 -r或者--no-res參數(shù)命令(不反編譯資源文件,適用于只修改代碼的操作。這個命令的好處就是能增加編譯和編譯的速度)情況。

如果不存在resource.arsc文件繼續(xù)往下看嘍,先對demo文件夾下的清單文件做判斷,如果清單文件存在并且是一個文件,條件成立,如果AndroidManifest.xml.orig臨時文件存在,則刪除。

不存在將原來的清單文件復(fù)制一份,名為AndroidManifest.xml.orig文件,為了讓大家更好理解,可以看下圖所示。AndroidManifest.xml.orig留到最后我再說一下這個文件作用。

AndroidManifest.xml.orig文件.png

還有一個要說的重點: fixingPublicAttrsInProviderAttributes(manifest);先說這個方法是干什么的。原文注釋中解釋的:清單文件中字符串類型是可以@string方式引用的,但是在構(gòu)建中容易中斷,從而阻止應(yīng)用程序安裝,這是來自aosp中的錯誤,公共資源不能成為provider標(biāo)簽內(nèi)屬性一部分。

fixingPublicAttrsInProviderAttributes()經(jīng)過多次的實驗和斷點,執(zhí)行的邏輯:通過Document形式讀取清單文件,找到清單文件中manifest/application/provide/authorities 屬性和 /manifest/application/activity/intent-filter/data/scheme屬性.

如果是@string形式引用的,那么會在res/value/string.xml匹配并替換掉具體的文字值。這個操作目的就是為了避免安卓系統(tǒng)自身的bug,引起的錯誤?;卮鹜戤叀?/p>

忘了補充一個知識點,也是fixingPublicAttrsInProviderAttributes()有關(guān),在2.2.2 版本 apktool有兩個致命的漏洞 。

  • XXE漏洞

  • 路徑穿越漏洞

有興趣的同學(xué)可以關(guān)注一下這個知識點,什么是XXE漏洞和路徑穿越的漏洞,沒有興趣的同學(xué)繼續(xù)往下看。

//此部分為apktool_2.2.2版本的代碼

package brut.androlib.res.xml;

import brut.androlib.AndrolibException;
import java.io.File;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

public final class ResXmlPatcher {
    public static void removeApplicationDebugTag(File file) throws AndrolibException {
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                NamedNodeMap attr = doc.getElementsByTagName("application").item(0).getAttributes();
                if (attr.getNamedItem("android:debuggable") != null) {
                    attr.removeNamedItem("android:debuggable");
                }
                saveDocument(file, doc);
            } catch (IOException | ParserConfigurationException | TransformerException | SAXException e) {
            }
        }
    }

    public static void fixingPublicAttrsInProviderAttributes(File file) throws AndrolibException {
        Node provider;
        String replacement;
        Node provider2;
        String replacement2;
        boolean saved = false;
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                NodeList nodes = (NodeList) XPathFactory.newInstance().newXPath().compile("/manifest/application/provider").evaluate(doc, XPathConstants.NODESET);
                for (int i = 0; i < nodes.getLength(); i++) {
                    NamedNodeMap attrs = nodes.item(i).getAttributes();
                    if (!(attrs == null || (provider2 = attrs.getNamedItem("android:authorities")) == null || (replacement2 = pullValueFromStrings(file.getParentFile(), provider2.getNodeValue())) == null)) {
                        provider2.setNodeValue(replacement2);
                        saved = true;
                    }
                }
                NodeList nodes2 = (NodeList) XPathFactory.newInstance().newXPath().compile("/manifest/application/activity/intent-filter/data").evaluate(doc, XPathConstants.NODESET);
                for (int i2 = 0; i2 < nodes2.getLength(); i2++) {
                    NamedNodeMap attrs2 = nodes2.item(i2).getAttributes();
                    if (!(attrs2 == null || (provider = attrs2.getNamedItem("android:scheme")) == null || (replacement = pullValueFromStrings(file.getParentFile(), provider.getNodeValue())) == null)) {
                        provider.setNodeValue(replacement);
                        saved = true;
                    }
                }
                if (saved) {
                    saveDocument(file, doc);
                }
            } catch (IOException | ParserConfigurationException | TransformerException | XPathExpressionException | SAXException e) {
            }
        }
    }

    public static String pullValueFromStrings(File directory, String key) throws AndrolibException {
        if (key == null || !key.contains("@")) {
            return null;
        }
        File file = new File(directory, "/res/values/strings.xml");
        String key2 = key.replace("@string/", "");
        if (file.exists()) {
            try {
                Object result = XPathFactory.newInstance().newXPath().compile("/resources/string[@name=\"" + key2 + "\"]/text()").evaluate(loadDocument(file), XPathConstants.STRING);
                if (result != null) {
                    return (String) result;
                }
            } catch (IOException | ParserConfigurationException | XPathExpressionException | SAXException e) {
            }
        }
        return null;
    }

    public static void removeManifestVersions(File file) throws AndrolibException {
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                NamedNodeMap attr = doc.getFirstChild().getAttributes();
                Node vCode = attr.getNamedItem("android:versionCode");
                Node vName = attr.getNamedItem("android:versionName");
                if (vCode != null) {
                    attr.removeNamedItem("android:versionCode");
                }
                if (vName != null) {
                    attr.removeNamedItem("android:versionName");
                }
                saveDocument(file, doc);
            } catch (IOException | ParserConfigurationException | TransformerException | SAXException e) {
            }
        }
    }

    public static void renameManifestPackage(File file, String packageOriginal) throws AndrolibException {
        try {
            Document doc = loadDocument(file);
            doc.getFirstChild().getAttributes().getNamedItem("package").setNodeValue(packageOriginal);
            saveDocument(file, doc);
        } catch (IOException | ParserConfigurationException | TransformerException | SAXException e) {
        }
    }

    private static Document loadDocument(File file) throws IOException, SAXException, ParserConfigurationException {
        return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file);
    }

    private static void saveDocument(File file, Document doc) throws IOException, SAXException, ParserConfigurationException, TransformerException {
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.setOutputProperty("indent", "yes");
        transformer.setOutputProperty("standalone", "yes");
        transformer.transform(new DOMSource(doc), new StreamResult(file));
    }
}

//此部分的代碼是2.6.1版本的

package brut.androlib.res.xml;

import brut.androlib.AndrolibException;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.logging.Logger;

public final class ResXmlPatcher {

    /**
     * Removes "debug" tag from file
     *
     * @param file AndroidManifest file
     * @throws AndrolibException Error reading Manifest file
     */
    public static void removeApplicationDebugTag(File file) throws AndrolibException {
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                Node application = doc.getElementsByTagName("application").item(0);

                // load attr
                NamedNodeMap attr = application.getAttributes();
                Node debugAttr = attr.getNamedItem("android:debuggable");

                // remove application:debuggable
                if (debugAttr != null) {
                    attr.removeNamedItem("android:debuggable");
                }

                saveDocument(file, doc);

            } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
            }
        }
    }

    /**
     * Sets "debug" tag in the file to true
     *
     * @param file AndroidManifest file
     */
    public static void setApplicationDebugTagTrue(File file) {
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                Node application = doc.getElementsByTagName("application").item(0);

                // load attr
                NamedNodeMap attr = application.getAttributes();
                Node debugAttr = attr.getNamedItem("android:debuggable");

                if (debugAttr == null) {
                    debugAttr = doc.createAttribute("android:debuggable");
                    attr.setNamedItem(debugAttr);
                }

                // set application:debuggable to 'true
                debugAttr.setNodeValue("true");

                saveDocument(file, doc);

            } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
            }
        }
    }

    /**
     * Any @string reference in a provider value in AndroidManifest.xml will break on
     * build, thus preventing the application from installing. This is from a bug/error
     * in AOSP where public resources cannot be part of an authorities attribute within
     * a provider tag.
     *
     * This finds any reference and replaces it with the literal value found in the
     * res/values/strings.xml file.
     *
     * @param file File for AndroidManifest.xml
     */
    public static void fixingPublicAttrsInProviderAttributes(File file) {
        boolean saved = false;
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                XPath xPath = XPathFactory.newInstance().newXPath();
                XPathExpression expression = xPath.compile("/manifest/application/provider");

                Object result = expression.evaluate(doc, XPathConstants.NODESET);
                NodeList nodes = (NodeList) result;

                for (int i = 0; i < nodes.getLength(); i++) {
                    Node node = nodes.item(i);
                    NamedNodeMap attrs = node.getAttributes();

                    if (attrs != null) {
                        Node provider = attrs.getNamedItem("android:authorities");

                        if (provider != null) {
                            saved = isSaved(file, saved, provider);
                        }
                    }
                }

                // android:scheme
                xPath = XPathFactory.newInstance().newXPath();
                expression = xPath.compile("/manifest/application/activity/intent-filter/data");

                result = expression.evaluate(doc, XPathConstants.NODESET);
                nodes = (NodeList) result;

                for (int i = 0; i < nodes.getLength(); i++) {
                    Node node = nodes.item(i);
                    NamedNodeMap attrs = node.getAttributes();

                    if (attrs != null) {
                        Node provider = attrs.getNamedItem("android:scheme");

                        if (provider != null) {
                            saved = isSaved(file, saved, provider);
                        }
                    }
                }

                if (saved) {
                    saveDocument(file, doc);
                }

            }  catch (SAXException | ParserConfigurationException | IOException |
                    XPathExpressionException | TransformerException ignored) {
            }
        }
    }

    /**
     * 檢查是否對節(jié)點進行了正確的替換。
     * @param file File we are searching for value
     * @param saved boolean on whether we need to save
     * @param provider Node we are attempting to replace
     * @return boolean
     */
    private static boolean isSaved(File file, boolean saved, Node provider) {
        String reference = provider.getNodeValue();
        String replacement = pullValueFromStrings(file.getParentFile(), reference);

        if (replacement != null) {
            provider.setNodeValue(replacement);
            saved = true;
        }
        return saved;
    }

    /**
     * Finds key in strings.xml file and returns text value
     *
     * @param directory Root directory of apk
     * @param key String reference (ie @string/foo)
     * @return String|null
     */
    public static String pullValueFromStrings(File directory, String key) {
        if (key == null || ! key.contains("@")) {
            return null;
        }

        File file = new File(directory, "/res/values/strings.xml");
        key = key.replace("@string/", "");

        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                XPath xPath = XPathFactory.newInstance().newXPath();
                XPathExpression expression = xPath.compile("/resources/string[@name=" + '"' + key + "\"]/text()");

                Object result = expression.evaluate(doc, XPathConstants.STRING);

                if (result != null) {
                    return (String) result;
                }

            }  catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) {
            }
        }

        return null;
    }

    /**
     * Finds key in integers.xml file and returns text value
     *
     * @param directory Root directory of apk
     * @param key Integer reference (ie @integer/foo)
     * @return String|null
     */
    public static String pullValueFromIntegers(File directory, String key) {
        if (key == null || ! key.contains("@")) {
            return null;
        }

        File file = new File(directory, "/res/values/integers.xml");
        key = key.replace("@integer/", "");

        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                XPath xPath = XPathFactory.newInstance().newXPath();
                XPathExpression expression = xPath.compile("/resources/integer[@name=" + '"' + key + "\"]/text()");

                Object result = expression.evaluate(doc, XPathConstants.STRING);

                if (result != null) {
                    return (String) result;
                }

            }  catch (SAXException | ParserConfigurationException | IOException | XPathExpressionException ignored) {
            }
        }

        return null;
    }

    /**
     * Removes attributes like "versionCode" and "versionName" from file.
     *
     * @param file File representing AndroidManifest.xml
     */
    public static void removeManifestVersions(File file) {
        if (file.exists()) {
            try {
                Document doc = loadDocument(file);
                Node manifest = doc.getFirstChild();
                NamedNodeMap attr = manifest.getAttributes();
                Node vCode = attr.getNamedItem("android:versionCode");
                Node vName = attr.getNamedItem("android:versionName");

                if (vCode != null) {
                    attr.removeNamedItem("android:versionCode");
                }
                if (vName != null) {
                    attr.removeNamedItem("android:versionName");
                }
                saveDocument(file, doc);

            } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
            }
        }
    }

    /**
     * Replaces package value with passed packageOriginal string
     *
     * @param file File for AndroidManifest.xml
     * @param packageOriginal Package name to replace
     */
    public static void renameManifestPackage(File file, String packageOriginal) {
        try {
            Document doc = loadDocument(file);

            // Get the manifest line
            Node manifest = doc.getFirstChild();

            // update package attribute
            NamedNodeMap attr = manifest.getAttributes();
            Node nodeAttr = attr.getNamedItem("package");
            nodeAttr.setNodeValue(packageOriginal);
            saveDocument(file, doc);

        } catch (SAXException | ParserConfigurationException | IOException | TransformerException ignored) {
        }
    }

    /**
     *
     * @param file File to load into Document
     * @return Document
     * @throws IOException
     * @throws SAXException
     * @throws ParserConfigurationException
     */
    private static Document loadDocument(File file)
            throws IOException, SAXException, ParserConfigurationException {

        DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
        docFactory.setFeature(FEATURE_DISABLE_DOCTYPE_DECL, true);
        docFactory.setFeature(FEATURE_LOAD_DTD, false);

        try {
            docFactory.setAttribute(ACCESS_EXTERNAL_DTD, " ");
            docFactory.setAttribute(ACCESS_EXTERNAL_SCHEMA, " ");
        } catch (IllegalArgumentException ex) {
            LOGGER.warning("JAXP 1.5 Support is required to validate XML");
        }

        DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
        // Not using the parse(File) method on purpose, so that we can control when
        // to close it. Somehow parse(File) does not seem to close the file in all cases.
        try (FileInputStream inputStream = new FileInputStream(file)) {
            return docBuilder.parse(inputStream);
        }
    }

    /**
     *
     * @param file File to save Document to (ie AndroidManifest.xml)
     * @param doc Document being saved
     * @throws IOException
     * @throws SAXException
     * @throws ParserConfigurationException
     * @throws TransformerException
     */
    private static void saveDocument(File file, Document doc)
            throws IOException, SAXException, ParserConfigurationException, TransformerException {

        TransformerFactory transformerFactory = TransformerFactory.newInstance();
        Transformer transformer = transformerFactory.newTransformer();
        DOMSource source = new DOMSource(doc);
        StreamResult result = new StreamResult(file);
        transformer.transform(source, result);
    }

    private static final String ACCESS_EXTERNAL_DTD = "http://javax.xml.XMLConstants/property/accessExternalDTD";
    private static final String ACCESS_EXTERNAL_SCHEMA = "http://javax.xml.XMLConstants/property/accessExternalSchema";
    private static final String FEATURE_LOAD_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
    private static final String FEATURE_DISABLE_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";

    private static final Logger LOGGER = Logger.getLogger(ResXmlPatcher.class.getName());
}

我說一下XXE這部分漏洞,上面分別展示了2.2.2版本和最新版本代碼,為方便大家觀察,我用了貼源碼形式而非截圖,大家可以作為參考。XXE漏洞我記得是2.2.3版本修復(fù)了。

修復(fù)的方式就是設(shè)置了setAttribute屬性,增加禁止外部實體訪問的設(shè)置。重點可以觀察2.6.1版本ACCESS_EXTERNAL_DTD 、ACCESS_EXTERNAL_SCHEMA 、FEATURE_LOAD_DTD 、FEATURE_DISABLE_DOCTYPE_DECL 這4個屬性。

總結(jié):其實計算機并沒有漏洞,漏洞在于人的身上(別人說過的,我認(rèn)同這個觀點)

buildResources

這部分邏輯是處理資源文件的。資源邏輯包含resource.arsc、res、manifest三類。作者考慮到了三種情況作為邏輯判斷。下列三種,會分別介紹說明。

buildResources.png

buildResourcesRaw

判斷resource.arsc文件是否存在,不存在返回false。常規(guī)情況下,也就是沒有指定-r或者--no-res參數(shù)命令下,只會返回false不會往下執(zhí)行。說一下resource.arsc文件存在的邏輯。這部分邏輯和buildSourcesRaw內(nèi)容基本無差別。

有強制覆蓋 -f(buildOptions.forceBuildAll)參數(shù)命令或者文件未被修改,滿足其中任意一個條件,將執(zhí)行resource.arsc、res、manifest三個文件、文件夾復(fù)制到build/apk/,可參考上面buildResources.png截圖。

buildResourcesFull

先判斷res是否存在,不存在返回false。反之繼續(xù)執(zhí)行邏輯,說一下繼續(xù)執(zhí)行的邏輯(常規(guī)情況下res肯定是存在的)。buildOptions.debugMode 需要設(shè)定 -d 或者--debug 參數(shù)命令才會執(zhí)行的邏輯,說一下設(shè)定-d 命令參數(shù)邏輯。

先會判斷你是否設(shè)置了aapt2命令參數(shù),沒有設(shè)置就是用默認(rèn)的aapt,執(zhí)行的邏輯:刪除清單文件調(diào)試標(biāo)簽(android:debuggable)。設(shè)置了aapt2參數(shù)命令,執(zhí)行的邏輯:添加android:debuggable=“true”標(biāo)簽屬性,即app是否可以斷點調(diào)試。

mAndRes.aaptPackage() 我簡單概述一下。首先會區(qū)分不同系統(tǒng)(Mac 、Window、Linux)的aapt和aapt2,這個是apktool源碼中內(nèi)置的文件路徑。然后判斷是否設(shè)置了aapt2命令參數(shù)(use-aapt2),設(shè)置了執(zhí)行aapt2的命令會將res和清單文件 合并成resource.arsc(這里用到 aapt p 命令,后面參數(shù)拼接字符串 執(zhí)行生成resource.arsc),反之用aapt執(zhí)行(執(zhí)行的邏輯和aapt2一樣)。

接著說tmpDir.copyToDir(),從作者的角度考慮到應(yīng)用程序可能是沒有resource.arsc資源的情況,如果執(zhí)行復(fù)制操作可能會出問題。所以加上一段邏輯判斷,如果demo/res文件夾存在,則執(zhí)行resource.arsc、res、manifest三個文件、文件夾復(fù)制到build/apk/。res文件夾不存在則復(fù)制resource.arsc、manifest兩個文件、文件夾復(fù)制到build/apk/。 可參考上面buildResources.png截圖。

buildManifest

這部分內(nèi)容可以參考buildResourcesFull邏輯,操作沒啥區(qū)別,沒有細(xì)說的必要。唯一的不同就是單獨只考慮有清單文件情況下 ,復(fù)制操作也只是針對清單文件。(偷個懶,手動滑稽)

buildLibs

這段知識點較少,我簡單說一下。lib、libs、kotlin、META-INF/servcies、四類文件、文件夾。該類文件或者文件夾不存在直接返回,不繼續(xù)執(zhí)行邏輯。如果存在直接復(fù)制到build/apk/文件下 ,例如demo/build/apk/libs。

科普一下META-INF/servcies:第三方j(luò)ar包用到j(luò)ava的SPI機制,正常簽名時,會在META-INF文件夾下增加services文件夾及其內(nèi)容(這些都是APP運行時要用到的)

buildCopyOriginalFiles

這部分內(nèi)容很簡單,是否設(shè)置了-c 或者--copy-original參數(shù)命令。沒有設(shè)置接下來的邏輯不會執(zhí)行。設(shè)置了此命令參數(shù),執(zhí)行的邏輯將原始的清單文件和META-INF文件復(fù)制到build/apk/文件夾下。沒什么可說了?。?!

buildApk

準(zhǔn)備構(gòu)建apk.png

執(zhí)行完buildLibs()邏輯后,build/apk下所有文件,滿足生成apk條件。apk本質(zhì)上就是一個zip壓縮包,干過安卓的都應(yīng)該知道。

先說一下代碼執(zhí)行邏輯,先判斷一下dist/xx.apk是否存在,存在刪除,不存在創(chuàng)建一個dist/xxx.apk文件夾,然后對assets文件夾進行判斷,不存在等于null,存在直接將路徑傳到zipPackage()里面作為參數(shù)。zipPackage() ----> ZipUtils.zipFolders() ----> processFolder()這個是調(diào)用順序。

processFolder()這里面有一個比較關(guān)鍵的屬性,ZipEntry.STORED:打包歸檔存儲,意思是僅打包歸檔存儲,文件大小基本不變。ZipEntry.DEFLATED:壓縮存儲。

processFolder()遍歷的是build/apk下的所有文件。先判斷是否是一個正常的文件,正常的文件通過zip流的形式寫入到dist/xx.apk里。如果是文件夾再次執(zhí)行processFolder()直到把所有流數(shù)據(jù)寫完(無限套娃,手動滑稽)。IOUtils.copy()看命名意思是復(fù)制,其實是對流文件讀、寫數(shù)據(jù)。

之前提到的assets文件夾如果存在,也是執(zhí)行processFolder(),無限套娃直到把數(shù)據(jù)寫到dist/xx.apk寫完為止。下圖就是執(zhí)行的效果圖。方便大家理解

buildApk.png

總結(jié): 此部分代碼就是將build/apk所有文件 ,通過zip文件流構(gòu)建到一個新的apk里面。

buildUnknownFiles

執(zhí)行完buildApk()邏輯你以為apk就構(gòu)建完了嗎? 不不 ,還有未知文件的存在。未知文件是什么意思我在上篇已經(jīng)說過,不再復(fù)述。好了一家人就要板板正正的在一起,怎么能少了未知文件呢?。?!

copyExistingFiles:首先將dist/demo.apk重新命名成demo.apk_apktool_temp,然后新創(chuàng)建一個demo.apk的文件。把demo.apk_apktool_temp數(shù)據(jù)通過zip流的形式寫入到新創(chuàng)建的demo.apk文件中

copyExistingFiles.png

copyUnknownFiles:將demo/unknown里面的文件遍歷讀取一遍,這里還會獲取apktool.yml里面的unknowFiles屬性。區(qū)分一下ZipEntry.STORED等于0(不壓縮)和ZipEntry.DEFLATED等于 8(壓縮)。

下面截圖是apktool.yml文件里的屬性,key就是未知文件文件名 (代碼里面處理了路徑穿越的漏洞,感興趣了可以了解一下),value就是 0或者8 ,分別對應(yīng)著壓縮和不壓縮的標(biāo)識。

最終將demo/unknown里面的所有數(shù)據(jù)通過zip流形式寫到新生成的demo.apk文件中。最終將dist/demo.apk.apktool_temp文件刪除。

copyUnknownFiles.jpg

總結(jié):demo.apk.apktool_temp就是做數(shù)據(jù)交換用的, 把原始數(shù)據(jù)和未知文件數(shù)據(jù),寫入到新的demo.apk中,這樣新生成的apk文件不會缺少文件。

buildUnknownFiles執(zhí)行完成候,別忘了還有AndroidManifest.xml.orig文件要處理掉的,先會判斷 清單文件是否是一個文件并且清單文件要存在,還要AndroidManifest.xml.orig存在。三個條件都滿足刪除原始的AndroidManifest.xml.,然后把AndroidManifest.xml.orig重命名為AndroidManifest.xml。到此apktool構(gòu)建流程終了。

談?wù)勎业氖斋@

當(dāng)前北京時間 2021年11月21日凌晨2點45,不知不覺寫的很晚??偹闶前盐恼聦懲炅?,也算是對自己和關(guān)注的同學(xué)有個交代吧。說說我通讀源碼后的感受和收獲

  1. 先說說作者方面,類名、方法名 ,變量名還有屬性命名非常專業(yè)規(guī)范,完全不需要任何多余的注釋就能知道作者要做的邏輯是什么。再談?wù)勎遥m然命名上也在不斷的規(guī)范,個人感覺總是少點什么。通讀apktool源碼后已經(jīng)有個一個明確的方向。我還有一個不好習(xí)慣,不管方法名是否通俗易懂都喜歡加上注釋方便別人能理解?,F(xiàn)在我對這個思想有個改觀,好的規(guī)范簡單明了一看就知道什么意思,完全不需要多余的注釋,除非是真的不好理解,才加注釋。

  2. 安全方面:理解源碼的目的就是為站在巨人的肩膀上看世界,從作者的思想角度看問題。學(xué)習(xí)理解源碼也是不斷的提升自己的見識和邏輯,從而應(yīng)用到工作中。我是搞游戲SDK方面的,我們的依賴庫中也包含了XML文件的讀寫操作,那是不是也會存在apktool源碼中路徑穿越和XXE漏洞呢? 既然文中已經(jīng)有辦法解決這個漏洞是否把這樣的操作移植到工作中(陳述句)。安全方面也是容易忽略的點,個人覺得在寫完代碼和邏輯后,也要考慮一下安全方面的隱患。

  3. 邏輯細(xì)節(jié):通讀源碼給我最直觀的感覺,作者和他的團隊的邏輯真的可怕。把任何可能發(fā)生各個細(xì)節(jié)都考慮的非常到位。聯(lián)想到自己,平時寫代碼時候是不是應(yīng)該把學(xué)到這份細(xì)節(jié)和邏輯帶到工作中,把一些可能發(fā)生的問題都處理到位。

  4. 閱讀源碼:剛剛接觸apktool源碼的時候,完全無從下手,不知道從哪里看起,跟無頭蒼蠅一樣。后來我找到一個竅門,雖然源碼看起來很龐大,核心的功能只有兩個 解碼 和構(gòu)建。那么為何不從一個切點看起,先看如果解碼apk。通篇只看解碼的流程,第一遍肯定是理解很少,多看幾遍??戳藥妆榛旧夏苣:牧私獯蟾牛缓笳乙粋€apk 下斷點一步一步的執(zhí)行和分析。多分析多執(zhí)行,你會發(fā)現(xiàn)思路和邏輯越來越清晰。而且還要問自己,為什么要這樣寫?這樣寫有什么好處。從作者的角度和思想做考慮。

全篇結(jié)尾

本文我寫的很細(xì),眾口難調(diào)也不知道能否讓各位看官滿意。希望大家喜歡,有不了解的地方。歡迎私信交流。你的點贊 支持 是我寫文的最大動力 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容