当前位置:首页 » 《随便一记》 » 正文

Apktool 源码分析那些一定要懂的细节(下篇)_北京朝阳区精神病院院长

14 人参与  2022年05月09日 17:06  分类 : 《随便一记》  评论

点击全文阅读


前言

距写完上一篇 Apktool 源码分析那些一定要懂的细节(上篇)、大约快接近9个月,9个月时间转瞬即逝。记得写上篇文章时候还在上一家公司干活。此时却在新公司任职。

原计划下篇文章可能会被无限搁置的,因为从源码理解、分析和梳理,再通过文章的形式写出来,是非常耗时耗力的。最近这半年,从职业生涯、技术和思想认知上有了一定提升。有想要改变一下自己想法。所以决定把下篇文章写完。在我的认知里:程-序-人-生,程序、人生。写程序和做人一样的,有始有终 。不是吗?

表情包.png

梳理构建逻辑

一个正常apk反编译(解码)后的文件结构和层级,如下图所示的。apktool可以解码,同样也有构建(build)的能力。构建不难理解,把需要修改的文件,修改好后重新构建生成 apk。那如何构建apk的呢? 这个操作你好奇吗?好奇就跟着我的逻辑一起梳理一下。

构建前.png

常规情况下在终端或者dos窗口 输入 apktool b /Users/user/Downloads/Apktool/demo 过一会就可在dist文件夹下找到构建好的apk。但是要注意dist/xx.apk是重新构建后的,需要重新签名才可以安装在手机上的。

构建后.png

在终端或者dos窗口 总会看到如下日志输出,这个日志自上到下已经很好的解释了构建的逻辑,跟着日志一起来看一下源码。 注:本文基于2.6.1版本的源码做分析,较2.5. x逻辑上区别不大

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()看起,当你输入apktool b 的时候已经执行到cmdBuild() ,如果不指定其他命令参数,可以走到最核心方法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:反编译后根据app名称生成同名文件夹。就是 apktool b 后面的路径xxx/xxx/demo可以结合上图来看。

outFile:是输出具体位置和apk名字。你不输入-o 参数,默认是null,后面有代码对null进行处理逻辑。

如下图所示,回编最核心的逻辑,为了防止大家迷糊。我画了一份脑图供大家参考,我会根据脑图所画内容进行梳理。(逻辑顺序自上而下一个个来说)

apktool构建逻辑.png

 //构建回编核心逻辑代码展示
 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(); //创建 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...");
    }

前置准备

readMetaFile: 从命名上可知,意为读取apktool.yml文件的意思。这部分内容较为简单,是通过反编译时把对应的文件数据写到 apktool.yml(第三方开源库 snakeyaml),构建时再把写入的数据读出来 ,在对应的设置到MetaInfo 对象属性里。mMinSdkVersion 是从apktool.yml读取出来的值,最后生成dex文件时候会用到。

outFile:在上面提及过,如果你没指定了 -O 命令参数, outFile对null进行逻辑处理。既然你没有指定apk输出路径,那apktool 默认给你指定一个路径。如果在apktool.yml读取到的apkFileName属性等于null,构建后的apk会以dist/out.apk 作为文件名构建输出,反之dist/demo.apk(我的apkFileName属性等于demo.apk)为 文件名构建输出。

构建前demo文件资源参考.png

buildSources

核心逻辑部分,这部分逻辑重点是处理的是smali和dex文件,这么说你可能不是很理解。接着往下看,我会详细说明。

buildSourcesRaw
首先会判断demo/classes.dex是否存在。常规操作(apktool b demo.apk)会直接返回false,不在执行后续逻辑。但是你要是增加了-s 或者–no-src 参数命令(意思是不处理dex文件,此命令适用于只修改资源和清单文件的需求,会增快构建速度)。后续逻辑还是会执行的。

demo/classes.dex存在,说明用到了–no-src命名参数。执行的逻辑比较简单粗暴直接把classes.dex文件,通过流的形式写入到demo/build/apk/文件夹下。

buildSourcesSmali
首先会判断demo/smali文件夹是否存在,不存在返回false。正常情况(apktool b demo.apk)下smali文件夹是一定存在,存在smali文件夹就会继续往下执行逻辑。接着有两个条件作为判断(必定有一个条件满足),第一个条件文件是否覆盖,指的是 -f 参数命令 。第二个条件文件是否做修改。

两个条件中有任何一个成立,执行SmaliBuilder.build(),该方法执行逻辑就是将smali文件夹里面的.smali文件 转换合并成classes.dex文件。这个不往下深究,作者进行了适当的封装,最终通过传过来的参数,调用第三方库org.jf.dexlib2。

总结:buildSources()简单说 ,就是对安卓代码进行处理,要重新生成apk,安卓代码文件肯定是要以.dex文件形式存在。代码无外乎.dex文件或者是smali文件夹形式存在。buildSources()分别对这两种情况进行逻辑处理。

buildNonDefaultSources

大家看了上面buildSources(),这部分也就好说了不少。首先会遍历demo/所有文件夹,如果找到smali_开头,执行buildSourcesRaw()和buildSourcesSmali(),代码在上面说的很细我就不再啰嗦。

如果找不到smali_开头,首先会跳过classes.dex文件,为啥?因为在buildSources()已经把逻辑处理过了,没必要重复处理。只要demo文件夹下有.dex后缀结尾文件,执行buildSourcesRaw()也就是把对应多.dex文件复制到demo/build/apk/下。

总结:这部分内容就是buildSources()升级,对多个dex文件和多个smali文件夹进行处理。你无法保证apk不会方法超限,方法超限必定会用Multidex。

buildManifestFile

这部分的逻辑很有意思。首先会判断demo文件夹下如果存在resource.arsc文件,直接返回不继续往下执行逻辑。这块是考虑到 -r或者–no-res参数命令(不反编译资源文件,适用于只修改代码的操作。这个命令的好处就是能增加编译和编译的速度)情况。

如果不存在resource.arsc文件继续往下看喽,先对demo文件夹下的清单文件做判断,如果清单文件存在并且是一个文件,条件成立,如果AndroidManifest.xml.orig临时文件存在,则删除。

不存在将原来的清单文件复制一份,名为AndroidManifest.xml.orig文件,为了让大家更好理解,可以看下图所示。AndroidManifest.xml.orig留到最后我再说一下这个文件作用。

AndroidManifest.xml.orig文件.png

还有一个要说的重点: fixingPublicAttrsInProviderAttributes(manifest);先说这个方法是干什么的。原文注释中解释的:清单文件中字符串类型是可以@string方式引用的,但是在构建中容易中断,从而阻止应用程序安装,这是来自aosp中的错误,公共资源不能成为provider标签内属性一部分。

fixingPublicAttrsInProviderAttributes()经过多次的实验和断点,执行的逻辑:通过Document形式读取清单文件,找到清单文件中manifest/application/provide/authorities 属性和 /manifest/application/activity/intent-filter/data/scheme属性.

如果是@string形式引用的,那么会在res/value/string.xml匹配并替换掉具体的文字值。这个操作目的就是为了避免安卓系统自身的bug,引起的错误。回答完毕。

忘了补充一个知识点,也是fixingPublicAttrsInProviderAttributes()有关,在2.2.2 版本 apktool有两个致命的漏洞 。

  • XXE漏洞

  • 路径穿越漏洞

有兴趣的同学可以关注一下这个知识点,什么是XXE漏洞和路径穿越的漏洞,没有兴趣的同学继续往下看。

//此部分为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) {
            }
        }
    }

    /**
     * 检查是否对节点进行了正确的替换。
     * @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版本修复了。

修复的方式就是设置了setAttribute属性,增加禁止外部实体访问的设置。重点可以观察2.6.1版本ACCESS_EXTERNAL_DTD 、ACCESS_EXTERNAL_SCHEMA 、FEATURE_LOAD_DTD 、FEATURE_DISABLE_DOCTYPE_DECL 这4个属性。

总结:其实计算机并没有漏洞,漏洞在于人的身上(别人说过的,我认同这个观点)

buildResources

这部分逻辑是处理资源文件的。资源逻辑包含resource.arsc、res、manifest三类。作者考虑到了三种情况作为逻辑判断。下列三种,会分别介绍说明。

buildResources.png

buildResourcesRaw

判断resource.arsc文件是否存在,不存在返回false。常规情况下,也就是没有指定-r或者–no-res参数命令下,只会返回false不会往下执行。说一下resource.arsc文件存在的逻辑。这部分逻辑和buildSourcesRaw内容基本无差别。

有强制覆盖 -f(buildOptions.forceBuildAll)参数命令或者文件未被修改,满足其中任意一个条件,将执行resource.arsc、res、manifest三个文件、文件夹复制到build/apk/,可参考上面buildResources.png截图。

buildResourcesFull

先判断res是否存在,不存在返回false。反之继续执行逻辑,说一下继续执行的逻辑(常规情况下res肯定是存在的)。buildOptions.debugMode 需要设定 -d 或者–debug 参数命令才会执行的逻辑,说一下设定-d 命令参数逻辑。

先会判断你是否设置了aapt2命令参数,没有设置就是用默认的aapt,执行的逻辑:删除清单文件调试标签(android:debuggable)。设置了aapt2参数命令,执行的逻辑:添加android:debuggable=“true”标签属性,即app是否可以断点调试。

mAndRes.aaptPackage() 我简单概述一下。首先会区分不同系统(Mac 、Window、Linux)的aapt和aapt2,这个是apktool源码中内置的文件路径。然后判断是否设置了aapt2命令参数(use-aapt2),设置了执行aapt2的命令会将res和清单文件 合并成resource.arsc(这里用到 aapt p 命令,后面参数拼接字符串 执行生成resource.arsc),反之用aapt执行(执行的逻辑和aapt2一样)。

接着说tmpDir.copyToDir(),从作者的角度考虑到应用程序可能是没有resource.arsc资源的情况,如果执行复制操作可能会出问题。所以加上一段逻辑判断,如果demo/res文件夹存在,则执行resource.arsc、res、manifest三个文件、文件夹复制到build/apk/。res文件夹不存在则复制resource.arsc、manifest两个文件、文件夹复制到build/apk/。 可参考上面buildResources.png截图。

buildManifest

这部分内容可以参考buildResourcesFull逻辑,操作没啥区别,没有细说的必要。唯一的不同就是单独只考虑有清单文件情况下 ,复制操作也只是针对清单文件。(偷个懒,手动滑稽)

buildLibs

这段知识点较少,我简单说一下。lib、libs、kotlin、META-INF/servcies、四类文件、文件夹。该类文件或者文件夹不存在直接返回,不继续执行逻辑。如果存在直接复制到build/apk/文件下 ,例如demo/build/apk/libs。

科普一下META-INF/servcies:第三方jar包用到java的SPI机制,正常签名时,会在META-INF文件夹下增加services文件夹及其内容(这些都是APP运行时要用到的)

buildCopyOriginalFiles

这部分内容很简单,是否设置了-c 或者–copy-original参数命令。没有设置接下来的逻辑不会执行。设置了此命令参数,执行的逻辑将原始的清单文件和META-INF文件复制到build/apk/文件夹下。没什么可说了!!!

buildApk

准备构建apk.png

执行完buildLibs()逻辑后,build/apk下所有文件,满足生成apk条件。apk本质上就是一个zip压缩包,干过安卓的都应该知道。

先说一下代码执行逻辑,先判断一下dist/xx.apk是否存在,存在删除,不存在创建一个dist/xxx.apk文件夹,然后对assets文件夹进行判断,不存在等于null,存在直接将路径传到zipPackage()里面作为参数。zipPackage() ----> ZipUtils.zipFolders() ----> processFolder()这个是调用顺序。

processFolder()这里面有一个比较关键的属性,ZipEntry.STORED:打包归档存储,意思是仅打包归档存储,文件大小基本不变。ZipEntry.DEFLATED:压缩存储。

processFolder()遍历的是build/apk下的所有文件。先判断是否是一个正常的文件,正常的文件通过zip流的形式写入到dist/xx.apk里。如果是文件夹再次执行processFolder()直到把所有流数据写完(无限套娃,手动滑稽)。IOUtils.copy()看命名意思是复制,其实是对流文件读、写数据。

之前提到的assets文件夹如果存在,也是执行processFolder(),无限套娃直到把数据写到dist/xx.apk写完为止。下图就是执行的效果图。方便大家理解

buildApk.png

总结: 此部分代码就是将build/apk所有文件 ,通过zip文件流构建到一个新的apk里面。

buildUnknownFiles

执行完buildApk()逻辑你以为apk就构建完了吗? 不不 ,还有未知文件的存在。未知文件是什么意思我在上篇已经说过,不再复述。好了一家人就要板板正正的在一起,怎么能少了未知文件呢!!!

copyExistingFiles:首先将dist/demo.apk重新命名成demo.apk_apktool_temp,然后新创建一个demo.apk的文件。把demo.apk_apktool_temp数据通过zip流的形式写入到新创建的demo.apk文件中

copyExistingFiles.png

copyUnknownFiles:将demo/unknown里面的文件遍历读取一遍,这里还会获取apktool.yml里面的unknowFiles属性。区分一下ZipEntry.STORED等于0(不压缩)和ZipEntry.DEFLATED等于 8(压缩)。

下面截图是apktool.yml文件里的属性,key就是未知文件文件名 (代码里面处理了路径穿越的漏洞,感兴趣了可以了解一下),value就是 0或者8 ,分别对应着压缩和不压缩的标识。

最终将demo/unknown里面的所有数据通过zip流形式写到新生成的demo.apk文件中。最终将dist/demo.apk.apktool_temp文件删除。

copyUnknownFiles.jpg

总结:demo.apk.apktool_temp就是做数据交换用的, 把原始数据和未知文件数据,写入到新的demo.apk中,这样新生成的apk文件不会缺少文件。

buildUnknownFiles执行完成候,别忘了还有AndroidManifest.xml.orig文件要处理掉的,先会判断 清单文件是否是一个文件并且清单文件要存在,还要AndroidManifest.xml.orig存在。三个条件都满足删除原始的AndroidManifest.xml.,然后把AndroidManifest.xml.orig重命名为AndroidManifest.xml。到此apktool构建流程终了。

谈谈我的收获

当前北京时间 2021年11月21日凌晨2点45,不知不觉写的很晚。总算是把文章写完了,也算是对自己和关注的同学有个交代吧。说说我通读源码后的感受和收获

  1. 先说说作者方面,类名、方法名 ,变量名还有属性命名非常专业规范,完全不需要任何多余的注释就能知道作者要做的逻辑是什么。再谈谈我,虽然命名上也在不断的规范,个人感觉总是少点什么。通读apktool源码后已经有个一个明确的方向。我还有一个不好习惯,不管方法名是否通俗易懂都喜欢加上注释方便别人能理解。现在我对这个思想有个改观,好的规范简单明了一看就知道什么意思,完全不需要多余的注释,除非是真的不好理解,才加注释。

  2. 安全方面:理解源码的目的就是为站在巨人的肩膀上看世界,从作者的思想角度看问题。学习理解源码也是不断的提升自己的见识和逻辑,从而应用到工作中。我是搞游戏SDK方面的,我们的依赖库中也包含了XML文件的读写操作,那是不是也会存在apktool源码中路径穿越和XXE漏洞呢? 既然文中已经有办法解决这个漏洞是否把这样的操作移植到工作中(陈述句)。安全方面也是容易忽略的点,个人觉得在写完代码和逻辑后,也要考虑一下安全方面的隐患。

  3. 逻辑细节:通读源码给我最直观的感觉,作者和他的团队的逻辑真的可怕。把任何可能发生各个细节都考虑的非常到位。联想到自己,平时写代码时候是不是应该把学到这份细节和逻辑带到工作中,把一些可能发生的问题都处理到位。

  4. 阅读源码:刚刚接触apktool源码的时候,完全无从下手,不知道从哪里看起,跟无头苍蝇一样。后来我找到一个窍门,虽然源码看起来很庞大,核心的功能只有两个 解码 和构建。那么为何不从一个切点看起,先看如果解码apk。通篇只看解码的流程,第一遍肯定是理解很少,多看几遍。看了几遍基本上能模糊的了解大概,然后找一个apk 下断点一步一步的执行和分析。多分析多执行,你会发现思路和逻辑越来越清晰。而且还要问自己,为什么要这样写?这样写有什么好处。从作者的角度和思想做考虑。

全篇结尾

本文我写的很细,众口难调也不知道能否让各位看官满意。希望大家喜欢,有不了解的地方。欢迎私信交流。你的点赞 支持 是我写文的最大动力 。


点击全文阅读


本文链接:http://zhangshiyu.com/post/39796.html

文件  逻辑  执行  
<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1