Apktool源码解析

2021-03-19  本文已影响0人  Ray206

apktool是一个第三方的反编译工具,它可以将资源解码为几乎原始的形式,并在进行修改后进行回编——官方介绍
因为工作原因,我需要经常使用apktool对apk包进行反编译和回编,也会在使用中碰到一些坑。这里对apktool源码进行一次梳理
apktool官网地址
apktool Github
截止到目前官网最新版本为:2.5.0, github上最新版本为:2.5.1-SNAPSHOT

目录介绍

目录.png

从github下载完源码,导入idea

  1. apktool主目录
  2. main函数入口
  3. 反编译和回编业务代码
  4. 常量声明,现在主要是异常
  5. 压缩文件处理
  6. 工具类、cmd
  7. apktool在各个系统中的执行脚本

源码解析

首先从入口函数main开始
Main.java

    public static void main(String[] args) throws IOException, InterruptedException, BrutException {
        ...
            commandLine = parser.parse(allOptions, args, false);
        // 设置日志输出等级
        if (commandLine.hasOption("-v") || commandLine.hasOption("--verbose")) {
            //详细信息
            verbosity = Verbosity.VERBOSE;
        } else if (commandLine.hasOption("-q") || commandLine.hasOption("--quiet")) {
            //不输出日志
            verbosity = Verbosity.QUIET;
        }
        setupLogging(verbosity);

        boolean cmdFound = false;
        for (String opt : commandLine.getArgs()) {
            if (opt.equalsIgnoreCase("d") || opt.equalsIgnoreCase("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")) {
                //安装framework.apk
                cmdInstallFramework(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("empty-framework-dir")) {
                //删除framework目录
                cmdEmptyFrameworkDirectory(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("list-frameworks")) {
               //输出所有framework文件名称
                cmdListFrameworks(commandLine);
                cmdFound = true;
            } else if (opt.equalsIgnoreCase("publicize-resources")) {
               //公共资源
                cmdPublicizeResources(commandLine);
                cmdFound = true;
            }
        }

    }

对于我们重点关注反编译和回编,安装fragmework.apk,主要在appt/appt2回编资源、生成R文件和resources.arsc用。就是android资源编译版本,这个可以在sdk\platforms\android-(android版本如:28)\android.jar这里找到,默认的在项目apktool-lib/src/main/resources/androlib/android-framework.jar.

反编译

命令: java -jar apktool.jar d [option] <apkFile>
在cmdDecode方法中检查参数后使用ApkDecoder进行解码
ApkDecoder.java

    public void decode() throws AndrolibException, IOException, DirectoryException {
        try {
           ...
            //资源文件解码
            //是否有resources.arsc文件
            if (hasResources()) {
                //资源解码模式
                switch (mDecodeResources) {
                    //不解码资源使用“-r”设置
                    //这个模式不解码资源,不解码资源,会节约反编译和回编时间
                    case DECODE_RESOURCES_NONE:
                        //解码资源文件,实际上就是拷贝resources.arsc、AndroidManifest.xml、res和zip方式打开一样
                        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()) {
                            //根据resources.arsc解码androidmanifest
                            mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable());
                        }
                        //根据resources.arsc和apk,生成values目录下的资源文件(attrs.xml、colors.xml、dimens.xml、ids.xml、、、)
                        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);
                    }
                }
            }
            //代码部分解码
            //判断主dex是否存在
            if (hasSources()) {
               //dex解码模式
                switch (mDecodeSources) {
                    //直接拷贝dex不解码,使用“-s”设置
                    case DECODE_SOURCES_NONE:
                        mAndrolib.decodeSourcesRaw(mApkFile, outDir, "classes.dex");
                        break;
                    //解码主dex,
                    case DECODE_SOURCES_SMALI:
                    case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
                        //将dex解码成smali
                        mAndrolib.decodeSourcesSmali(mApkFile, outDir, "classes.dex", mBakDeb, mApi);
                        break;
                }
            }
            //是否有分包dex文件, 如:classes1.dex、classes2.dex
            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") && file.startsWith("classes")) {
                        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;
                                case DECODE_SOURCES_SMALI_ONLY_MAIN_CLASSES:
                                    if (file.startsWith("classes") && file.endsWith(".dex")) {
                                        mAndrolib.decodeSourcesSmali(mApkFile, outDir, file, mBakDeb, mApi);
                                    } else {
                                        mAndrolib.decodeSourcesRaw(mApkFile, outDir, file);
                                    }
                                    break;
                            }
                        }
                    }
                }
            }
            //将apk包中的lib、assets、libs、kotlin文件解压
            mAndrolib.decodeRawFiles(mApkFile, outDir, mDecodeAssets);
            //将apk中未知的文件拷贝出来如okhttp
            mAndrolib.decodeUnknownFiles(mApkFile, outDir, mResTable);
            mUncompressedFiles = new ArrayList<String>();
            //记录上面拷贝的未知文件
            mAndrolib.recordUncompressedFiles(mApkFile, mUncompressedFiles);
            mAndrolib.writeOriginalFiles(mApkFile, outDir);
            //将记录的信息写入到apktool.yml
            writeMetaFile();
        } catch (Exception ex) {
            throw ex;
        } finally {
            try {
                mApkFile.close();
            } catch (IOException ignored) {}
        }
    }

整个反编译流程已经结束完了,可以对反编译后的资源或字节码进行操作

反编译目录

反编译目录.png
方便生成的目录大概就是这样,
1|2|5|6|8|9:apk的assets|动态链接库|res资源文件(布局资源配置)|smali字节码(代码部分)|androidmanifest|apktool反编译配置信息,回编的时候需要读取
3|4||7译未识别文件,但是包体里存在的文件,直接拷贝到目录
配置apktool.yml
!!brut.androlib.meta.MetaInfo
apkFileName: apk名称,回编时候生成的文件名称
compressionType: 默认false
doNotCompress:不进行压缩的文件列表
...
isFrameworkApk: false
packageInfo:
  forcedPackageId: '127'
  renameManifestPackage: null
sdkInfo:SDK信息
  minSdkVersion: '15' apk适配最低版本
  targetSdkVersion: '23' apk适配版本,这里修改包体适配版本,回编后生效
sharedLibrary: false
sparseResources: false
unknownFiles:
  android-support-multidex.version.txt: '8'
  sources.list: '8'
usesFramework:
  ids:
  - 1
  tag: null
version: apktool版本
versionInfo: apk版本信息,这里修改apk版本信息,对应的包体也会修改,回编后生效
  versionCode: '202' 
  versionName: 2.1.0

回编

命令:java -jar apktool.jar -b [option] <apkSourcesPath>
Androidlib.ava

 public void build(ExtFile appDir, File outFile)
            throws BrutException {
      //读取反编译的本地配置信息
      MetaInfo meta = readMetaFile(appDir);
       ...
        //将smali文件回编成dex
        //主dex回编
        buildSources(appDir);
        //除主dex外的其他dex回编
        buildNonDefaultSources(appDir);
        //检查androidManifest
        buildManifestFile(appDir, manifest, manifestOriginal);
        //将res目录下的文件回编成二进制文件
        buildResources(appDir, meta.usesFramework);
        //添加动态链接库
        buildLibs(appDir);
        //处理original目录
        buildCopyOriginalFiles(appDir);
        //生成apk
        buildApk(appDir, outFile);
        //未识别的添加到apk中
        buildUnknownFiles(appDir, outFile, meta);
    }

    public void buildSources(File appDir)
            throws AndrolibException {
        //两个判断条件,第一个是未将dex反编译成smali的,直接拷贝到回编目录,第二个是将smali回编成dex,使用的是smali(一个第三方库,将smali回编成dex)工具
        if (!buildSourcesRaw(appDir, "classes.dex") && !buildSourcesSmali(appDir, "smali", "classes.dex")) {
            LOGGER.warning("Could not find sources");
        }
    }
    
  //调用第三方工具smali把smali文件回编成dex文件
    public boolean buildSourcesSmali(File appDir, String folder, String filename)
            throws AndrolibException {
        ExtFile smaliDir = new ExtFile(appDir, folder);
        if (!smaliDir.exists()) {
            return false;
        }
        File dex = new File(appDir, APK_DIRNAME + "/" + filename);
        if (! apkOptions.forceBuildAll) {
            LOGGER.info("Checking whether sources has changed...");
        }
        if (apkOptions.forceBuildAll || isModified(smaliDir, dex)) {
            LOGGER.info("Smaling " + folder + " folder into " + filename + "...");
            dex.delete();
            SmaliBuilder.build(smaliDir, dex, apkOptions.forceApi > 0 ? apkOptions.forceApi : mMinSdkVersion);
        }
        return true;
    }

这里咋们来看看资源如何回编

    public void buildResources(ExtFile appDir, UsesFramework usesFramework)
            throws BrutException {
        //第一个判断,如果资源未反编译,直接拷贝编译目录
        //第二个判断资源反编译了,进行编译
        if (!buildResourcesRaw(appDir) && !buildResourcesFull(appDir, usesFramework)
                && !buildManifest(appDir, usesFramework)) {
            LOGGER.warning("Could not find resources");
        }
    }

    public boolean buildResourcesFull(File appDir, UsesFramework usesFramework)
            throws AndrolibException {
          ...
          使用appt对资源进行编译
          mAndRes.aaptPackage(apkFile, new File(appDir,
                                "AndroidManifest.xml"), new File(appDir, "res"),
                        ninePatch, null, parseUsesFramework(usesFramework));
    }

AndroidLibResources.java

    public void aaptPackage(File apkFile, File manifest, File resDir, File rawDir, File assetDir, File[] include)
            throws AndrolibException {

        String aaptPath = apkOptions.aaptPath;
        boolean customAapt = !aaptPath.isEmpty();
        List<String> cmd = new ArrayList<String>();

        try {
            //获取aapt路径,项目中apktool-lib/src/resources/prebuilt下
            String aaptCommand = AaptManager.getAaptExecutionCommand(aaptPath, getAaptBinaryFile());
            cmd.add(aaptCommand);
        } catch (BrutException ex) {
            LOGGER.warning("aapt: " + ex.getMessage() + " (defaulting to $PATH binary)");
            cmd.add(AaptManager.getAaptBinaryName(getAaptVersion()));
        }

        if (apkOptions.isAapt2()) {
           //aapt2编译
            aapt2Package(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt);
            return;
        }
        //aapt1编译
        aapt1Package(apkFile, manifest, resDir, rawDir, assetDir, include, cmd, customAapt);
    }

apktool的反编译和回编就已经完成了。这里有aapt2和aapt1编译资源,如果有使用aapt生成R文件的需求,最好使用aapt2,因为aapt2支持的版本比aapt1高

上一篇 下一篇

猜你喜欢

热点阅读