从 Apk 提取代码到单独 dex

从 Apk 提取代码到单独 dex_第1张图片

从 Apk 提取代码到单独 dex

Android 中动态加载是指应用程序在运行时加载和执行 Dex 文件的过程,可以在运行时加载不同的代码或功能,而无需重新编译整个应用程序,动态加载 Dex 文件通常涉及以下步骤:

  • 创建 Dex 文件
    我们接触到的通常是 Android studio 等 IDE 工具将 Java 或 Kotlin 代码编译成 Dex 格式的字节码文件。
  • 将 Dex 文件打包到 APK 中
    Apk 就是一个压缩包,通常也是 IDE 将编译好的 Dex 文件打包到应用程序的 APK 文件中。
  • 运行时加载 Dex 文件
    当应用程序启动时,Android 系统会加载应用程序的代码。如果应用程序中包含 Dex 文件,系统会自动将其加载到内存中,以便应用程序可以执行其中的代码。
  • 执行 Dex 文件中的代码:
    一旦 Dex 文件被加载到内存中,应用程序就可以执行其中的代码。

通过动态加载 Dex 文件,可以实现更加灵活和可扩展的功能,可以编写插件或模块化的代码,并在运行时根据需要加载它们。DexClassLoader 是 Android 中的一个类加载器,用于动态加载包含 Dex 文件的 jar 或 apk 文件,它的工作原理大概是:加载 Dex 文件,DexClassLoader 会从指定的路径中读取 Dex 文件,并将其加载到内存中… …无序过多描述,我们知道它可以加载 dex 文件即可。

本文讲的如下图所示:

如何从 apk 中获取特定包名下的代码并保存到 dex 文件中,然后再把此 dex 放到 assets 目录下,最后重新打包成 APK。

从 Apk 提取代码到单独 dex_第2张图片

本次通过 Java 实现,最终产物是一个可执行的 Jar,使用方式如下所示:

java -jar xpluginJs.jar -mapping mapping.txt -apk 1.apk -dexname hello -pkg com.primer.pay.manager,com.primer.login.manager

意思是:
根据 mapping.txt 映射文件从 1.apk 安装包中提取所有 com.primer.pay.manager,com.primer.login.manager 包下的代码,将所提取代码写入 hello.dex 文件,hello.dex 文件将被放到包体内 assets 目录下。

/**
 * 
 * 1、必须参数
 *      -mapping:安装包的映射文件
 *      -apk:待处理 apk 文件
 *      -dexname:处理后存放在 assets 目录下的 dex 文件名
 *      -pkg:需要提取的包名(混淆前的包名),多个包名用英文逗号分隔
 * 
 * 
 * 2、当前目录运行环境目录结构
 *      tools:内置工具(apktool、smali)
 *      xpluginJs.jar:主程序
 *      mapping.txt:映射文件
 *      1.apk:apk 文件
 *      output-dex2jar:中间产物输出目录
 *      output-dex2jar\apk-decode\dist\***.apk:最终产物
 */

从 Apk 提取代码到单独 dex_第3张图片

0、线性执行

一图概述~
从 Apk 提取代码到单独 dex_第4张图片

    public void run() {
        System.err.println("=================== 1、Initializes the output directory");
        initOutPutDir();
        mDexConfig.setspecifyPkgSmailOutputPath(OUTPUT_PATH_SMAIL);

        System.err.println("=================== 2、Load the local mapping configuration");
        mObsMapping = loadMappingConfig(mDexConfig.getMappingFilePath());

        System.err.println("=================== 3、Gets a list of mapping names for the specified package name");
        List<String> specifyMappingPkgList = ObsMappingUtils.collectionSpecifyPkgMapping(mObsMapping,
                mDexConfig.getSpecifiyPkgList());

        System.err.println("=================== 4、Decompile Apk using apktool");
        String decodeOutPath = decodeApk(mDexConfig.getApkFilePath(), mDexConfig.getApkDecodeOutputDirname());
        mDexConfig.setApkDecodeOutputDirpath(decodeOutPath);

        System.err.println("=================== 5、Collects and moves the smail file under the specified package name");
        collectionAndMoveSpecifyPkgSmail(decodeOutPath, specifyMappingPkgList);

        System.err.println("=================== 6、Package the smali file as dex");
        String smailToDexPath = encodeSmailToDex(mDexConfig.getSpecifyPkgSmailOutputPath());
        if (CommUtil.isEmptyOrNoExists(smailToDexPath)) {
            throw new IllegalArgumentException("smailToDexPath is null");
        }
        System.err.println("\tsmailToDexPath: " + smailToDexPath);

        System.err.println("=================== 7、dex encryption");
        String encodeDexPath = encodeDex(mDexConfig.getEncodeDexFilename(), smailToDexPath);
        System.err.println("\tencodeDexPath: " + encodeDexPath);

        String decodeDexPath = decodeDex(mDexConfig.getEncodeDexFilename() + "-decode", encodeDexPath);
        System.err.println("\tencodeDexPath: " + decodeDexPath);

        System.err.println("=================== 8、copy dex encryption");
        boolean isCopy = copyDexToAssets(encodeDexPath, decodeOutPath);
        if (!isCopy) {
            throw new IllegalArgumentException("copy dex to assets error");
        }

        System.err.println("=================== 9、Apk compile back");
        String unsignApkFilePath = encodeApk(decodeOutPath);
        System.err.println("\t [Successfully~] unsignApkFilePath: " + unsignApkFilePath);

    }

1、参数解析

按格式解析参数,收集必要的信息,这里参数解析或许有漏洞,但你可以根据自己的想法写出更好的参数解析,避免因外部使用的多样化导致内部解析异常。

package com.primer;

import java.util.Arrays;
import com.primer.bean.CmdArgs;

public class Main {

    private static DexToolManager mDexToolManager;
    private static CmdArgs mCmdArgs;

    public static void main(String[] args) {
        mCmdArgs = parserArgs(args);
        initDexToolManager();
    }
	
	//解析 java -jar 传入的参数
    private static CmdArgs parserArgs(String[] args) {
        if (args == null || args.length == 0) {
            throw new IllegalArgumentException(" argument not be empty");
        }

        CmdArgs cmdArgs = new CmdArgs();
        String current;

        for (int i = 0; i < args.length; i++) {
            current = args[i];
            if (current.equals("-mapping")) {
                if (i + 1 >= args.length) {
                    throw new IllegalArgumentException("-mapping args error");
                }
                cmdArgs.mappingPath = args[i + 1];
            } else if (current.equals("-apk")) {
                if (i + 1 >= args.length) {
                    throw new IllegalArgumentException("-apk args error");
                }
                cmdArgs.apkPath = args[i + 1];
            } else if (current.equals("-dexname")) {
                if (i + 1 >= args.length) {
                    throw new IllegalArgumentException("-dexname args error");
                }
                cmdArgs.encodeDexFilename = args[i + 1];
            } else if (current.equals("-pkg")) {
                if (i + 1 >= args.length) {
                    throw new IllegalArgumentException("-pkg args error");
                }

                String str = args[i + 1];
                String[] pkgList = str.split(",");
                if (pkgList == null || pkgList.length == 0) {
                    throw new IllegalArgumentException("args error");
                }

                cmdArgs.specifiyPkgList = Arrays.asList(pkgList);
            }
        }
        cmdArgs.checkArgument();
        return cmdArgs;
    }

    private static void initDexToolManager() {
        DexConfig dexConfig = new DexConfig.Builder()
                .setApkFilePath(mCmdArgs.apkPath)
                .setMappingFilePath(mCmdArgs.mappingPath)
                .setSpecifyPkgList(mCmdArgs.specifiyPkgList)
                .setEncodeDexFilename(mCmdArgs.encodeDexFilename)
                .setApkDecodeOutputDirname("apk-decode")
                .build();
                
		//简单得对参数是否有效做检查
        dexConfig.checkConfigIllegal();

        mDexToolManager = new DexToolManager();
        mDexToolManager.setDexConfig(dexConfig);
        mDexToolManager.run();
    }
}

2、准备工作目录

  • initOutPutDir:创建输出目录、清空输出目录残留的文件

如果让自己写删除目录及目录下的所有文件,很容易让我们想起了递归遍历,可以递归删除文件。


 public void traversalFile(File dirFile, FileTraversal traversal) {
        if (dirFile == null) {
            return;
        }

        for (File file : dirFile.listFiles()) {
            if (file.isDirectory()) {
                traversalFile(file, traversal);
                traversal.processDir(file);
            } else {
                traversal.processFile(file);
            }
        }
    }
 
//使用
traversalFile(outFile, new FileTraversal() {
            @Override
            public void processFile(File file) {
                if (file.exists()) {
                    file.delete();
                }
            }

            @Override
            public void processDir(File file) {
                if (file.exists()) {
                    file.delete();
                }
            }
        });

3、加载映射

  • loadMappingConfig:加载 mapping.txt 映射文件

我们知道 build/output/**/ release/mapping.txt 就是 Android 开启混淆打包生成的映射文件,就是根据该文件的格式进行解析,可以从中解析获取混淆前类的全限定名(包名+类名),当然也可以拿到混淆前后的方法名等信息。

com.opos.mobad.service.tasks.a -> com.opos.mobad.service.tasks.a:
    java.lang.String a -> a
    java.io.FileFilter b -> b
    java.util.HashMap getPayMap(android.content.Context,boolean,int) -> a
    boolean d(android.content.Context) -> d
    java.lang.String j(android.content.Context) -> j

根据映射文件我们可以这样简单写出解析存储数据的 bean 类及关系。

//一个映射文件包含很多类的映射
public class ObsMapping {
    private LinkedList<ClassObsMapping> obsMapping;
    //略
}

//一个类包含类名映射、多个成员变量映射、多个方法的映射(我们没有使用到成员变量,所以可以不要)
public class ClassObsMapping {
	//类名映射
    private MappingItem classMapping;
    //方法映射
    private LinkedList<MappingItem> methodsMapping;
    //略
}

//映射的基本元素是混淆前后的名称
public class MappingItem {
	//混淆前名称
    private String originalName;
    //混淆后名称
    private String mappingName;
    //略
}

4、确认提取目标

  • collectionSpecifyPkgMapping:根据 -pkg 参数列表指定的包名,从映射文件中收集混淆后的包名。

如 com.primer.manager.A -> com.android.manager.AA,那么收集的是 com.android.manager,最终返回的是混淆后的类的包名。

5、apk 反编译

  • decodeApk:利用 apktool 工具反编译

简单地封装 Runtime.getRuntime(),通过调用 runtime.exec(cmdline) 执行控制台命令,这里是通过控制台执行 bat 脚本,再由脚本执行 java 命令执行 apktool。

APKTOOL_BAT_PATH 指向的一个 bat 脚本路径:

:: 参数1-apktool  参数2-apk路径  参数3-输出路径  -f:强制覆盖
java -jar %1 d %2 -o %3 -f
/**
     * java -jar apktool_2.7.0.jar d [apk file] -o [out name]
     *
     * @param inputApkPath
     * @param outDirName
     * @return
     */
    private String decodeApk(String inputApkPath, String outDirName) {
        if (inputApkPath == null || inputApkPath.isEmpty()) {
            throw new IllegalArgumentException("inputApkPath is null");
        }

        String cmdline;
        String outApkPath = OUTPUT_PATH + File.separator + outDirName;
        StringBuilder sb = new StringBuilder();

        sb.append(APKTOOL_BAT_PATH)
                .append(" ")
                .append(APKTOOL_JAR_PATH)
                .append(" ")
                .append(inputApkPath)
                .append(" ")
                .append(outApkPath);
        if (sb.toString().contains("\\") && !sb.toString().contains("\\\\")) {
            cmdline = sb.toString().replace("\\", "\\\\");
        } else {
            cmdline = sb.toString();
        }

        System.out.println("decodeApk cmd: " + cmdline);
        CommUtil.executeCmdline(cmdline, false);
        return outApkPath;
    }

6、smali 收集

  • collectionAndMoveSpecifyPkgSmail:根据上述收集到的映射,在反编译目录下查找文件并存放到额外目录下。

通过递归遍历过了 smali 找到映射文件,再把目标文件已到指定目录待下一步处理。

    private void collectionAndMoveSpecifyPkgSmail(String path, List<String> specifyPkgList) {
        if (CommUtil.isEmptyOrNoExists(specifyPkgList)) {
            return;
        }

        traversalFile(new File(path), new FileTraversal() {
            @Override
            public void processFile(File file) {
                if (file.getName().endsWith(".smali")) {
                    SmailFile smailFile = splitClassPkgname(file.getAbsolutePath());
                    if (!CommUtil.isEmptyOrNoExists(smailFile.pakcgeName)) {
                        for (String pkg : specifyPkgList) {
                            if (pkg.equals(smailFile.pakcgeName)) {
                                moveAndDeleteTargetFile(file, smailFile);
                                break;
                            }
                        }
                    }
                }
            }

            @Override
            public void processDir(File file) {

            }
        });
    }
public class SmailFile {
	//映射后的文件名(类名)
    public String filename;
    //映射后的包名,如 com.primer.manager
    public String pakcgeName;
    //映射后的包路径,如 com/primer/manager
    //因为提取到外部目录下也应该创建相同包名的目录,再不 smali 文件存放到该目录下,确保前后一致
    public String pakagePath;

    @Override
    public String toString() {
        return "SmailFile: " + filename + ", " + pakcgeName + ", " + pakagePath;
    }
}

7、smali 打包

  • encodeSmailToDex:使用 smali.jar 把一组 smali 文件打包成 dex

这里有一点需要注意的是,最新版 smali.jar 打包参数是 assemble ,好像以前的包版本打包参数是 b。

:: java -jar smali.jar b out_directory -o output.dex
:: 参数1:smali.jar
:: 参数2:out_directory smali 文件目录
java -jar %1 assemble %2

8、dex 加密

  • encodeDex:对 dex 文件应用自己的加密算法

dex 文件本质上是一个二进制文件,二进制文件读取出来就是一组字节数组 byte[],简单的对字节数组进行特殊操作(插入偏移量等)就是对文件的加密。

9、dex 加密文件放入包体

  • copyDexToAssets:如果你想把提取部分的代码 dex 后续通过动态加载方式执行,你可以重新把它打入包体的其他地方存储备用,也可以后续通过远程请求获取再加载等。

10、apk 打包

  • encodeApk:同样的,也是使用 apktool 工具

这里还是先调用 bat 脚本,再由 bat 执行 java 命令执行 apktool,你也可以以自己的方式处理。

java -jar apktool_2.7.0.jar b [apk decode file path]

:: 参数1-apktool  参数2-apk路径
java -jar %1 b %2

TODO:
当然,有了想法你可以做很多诸如此类的事情!

  • 你可以在 apk 打包完成功之后,完成重新签名
  • 等等等

你可能感兴趣的:(Android,android,java)