首页 > 系统相关 >dpt-shell 抽取壳实现原理分析(加壳逻辑)

dpt-shell 抽取壳实现原理分析(加壳逻辑)

时间:2023-04-12 17:22:56浏览次数:50  
标签:dex shell String apk apkMainProcessPath 加壳 dpt File new

开源项目位置(为大佬开源精神点赞)

https://github.com/luoyesiqiu/dpt-shell

抽取壳分为两个步骤

加壳逻辑:
 一 对apk进行解析,将codeItem抽出到一个文件中,并进行nop填充
 二 对抽取后的apk进行加密
 三 注入壳程序相关文件即配置信息
 
执行逻辑:
  一 壳程序执行
  二 壳解密抽取后的dex,并完成classloader的替换
  三 hook住执行方法,在执行对应函数时进行指令填充,使程序正确执行

加壳逻辑

代码在dpt module中
执行方法为

usage: java -jar dpt.jar [option] -f <apk>
-d,--dump-code        Dump the code item of DEX and save it to .json
                      files.
-f,--apk-file <arg>   Need to protect apk file.
-l,--noisy-log        Open noisy log.
-s,--no-sign          Do not sign apk.

例子

java -jar dpt.jar -f /path/to/apk

主要执行逻辑在 Dpt.java 的processApk 方法中

private static void processApk(String apkPath){
       if(!new File("shell-files").exists()) {
           LogUtils.error("Cannot find shell files!");
           return;
       }
   	// 1. 拿到待加壳的apk 的File对象
       File apkFile = new File(apkPath);

       if(!apkFile.exists()){
           LogUtils.error("Apk not exists!");
           return;
       }
       String apkFileName = apkFile.getName();

       String currentDir = new File(".").getAbsolutePath();  // 当前命令行所在的目录
       if (currentDir.endsWith("/.")){
           currentDir = currentDir.substring(0, currentDir.lastIndexOf("/."));
       }
   	//2. 生成加壳后的文件名
       String output = FileUtils.getNewFileName(apkFileName,"signed");
       LogUtils.info("output: " + output);

   	//3. 生成加壳后文件的File对象xxx_signed.apk
       File outputFile = new File(currentDir, output);
       String outputApkFileParentPath = outputFile.getParent();

       //4. apk文件解压的目录,主要的工作目录,很多临时文件会在这里
       String apkMainProcessPath = ApkUtils.getWorkspaceDir().getAbsolutePath();

       LogUtils.info("Apk main process path: " + apkMainProcessPath);
   	//5. 待加壳apk 解压到在这里
       ApkUtils.extract(apkPath,apkMainProcessPath);
   	//6.读取待加壳的apk真实的包名
       Global.packageName = ManifestUtils.getPackageName(apkMainProcessPath + File.separator + "AndroidManifest.xml");
   	// 7. 抽取apk的dex中的codeItem(核心)
       ApkUtils.extractDexCode(apkMainProcessPath);
   	//8. 重新压缩所有dex文件 到一个名为i11111i111 的文件中,放在asset目录下
       ApkUtils.compressDexFiles(apkMainProcessPath);
   	//9. 将所有dex文件删除(已经保存到i11111i111,这些dex就不需要了)
       ApkUtils.deleteAllDexFiles(apkMainProcessPath);
   	//10. 将原apk的applicationName(manifest.xml 中读取) 写入到app_name文件中,放在asset目录下
       ApkUtils.saveApplicationName(apkMainProcessPath);
   	//11. 将壳的application_name 写入到manifest.xml中
       ApkUtils.writeProxyAppName(apkMainProcessPath);
   	//12. 将原apk的AppComponentFactory写入到app_acf文件中,放在asset目录下
       ApkUtils.saveAppComponentFactory(apkMainProcessPath);
   	//13. 将壳的AppComponentFactory写入到manifest.xml中
       ApkUtils.writeProxyComponentFactoryName(apkMainProcessPath);

   	//14. 把壳程序的dex,搬到主目录来,作为加壳后运行的dex
       ApkUtils.addProxyDex(apkMainProcessPath);
   	//15. 删除META-INF目录下的文件
       ApkUtils.deleteMetaData(apkMainProcessPath);
   	//16. 把壳程序的so搬到主目录来
       ApkUtils.copyShellLibs(apkMainProcessPath, new File(outputApkFileParentPath,"shell-files/libs"));
   	//17. 重新打包和签名
       new BuildAndSignApkTask(false, apkMainProcessPath, output).run();
   	// 善后工作 略
       File apkMainProcessFile = new File(apkMainProcessPath);
       if (apkMainProcessFile.exists()) {
           FileUtils.deleteRecurse(apkMainProcessFile);
       }
       LogUtils.info("All done.");
   }
dpt 通过18个步骤把一个源apk给加上了壳,其中最关键的一步就是7. 抽取apk的dex中的codeItem,看一下是在干了啥
    /**
     * 提取apk里的dex的代码
     * @param apkOutDir
     */
    public static void  extractDexCode(String apkOutDir){
        List<File> dexFiles = getDexFiles(apkOutDir);
        Map<Integer,List<Instruction>> instructionMap = new HashMap<>();
        String appNameNew = "OoooooOooo";
        String dataOutputPath = getOutAssetsDir(apkOutDir).getAbsolutePath() + File.separator + appNameNew;

        CountDownLatch countDownLatch = new CountDownLatch(dexFiles.size());
        for(File dexFile : dexFiles) {
            ThreadPool.getInstance().execute(() -> {
                String newName = dexFile.getName().endsWith(".dex") ? dexFile.getName().replaceAll("\\.dex$", "_tmp.dex") : "_tmp.dex";
                //1. 生成新名字的Dex的File对象
                File dexFileNew = new File(dexFile.getParent(), newName);
                //2. 抽取dex的代码,并将抽取后的dex写入到dexFileNew对象中,返回抽取的指令对象列表
                List<Instruction> ret = DexUtils.extractAllMethods(dexFile, dexFileNew);
                int dexNo = getDexNumber(dexFile.getName());
                //3. 所有指令和其dex的Id进行绑定
                instructionMap.put(dexNo,ret);
                //4. 由于dex的内容变量,需要更新dex的hash
                File dexFileRightHashes = new File(dexFile.getParent(),FileUtils.getNewFileName(dexFile.getName(),"new"));
                DexUtils.writeHashes(dexFileNew,dexFileRightHashes);
                dexFile.delete();
                dexFileNew.delete();
                //5. 新dex重命名为原dex名字进行覆盖
                dexFileRightHashes.renameTo(dexFile);
                countDownLatch.countDown();
            });

        }

        ThreadPool.getInstance().shutdown();

        try {
            countDownLatch.await();
        }
        catch (Exception ignored){
        }
        //6.指令Map生成MultiDexCode(自定义)对象
        MultiDexCode multiDexCode = MultiDexCodeUtils.makeMultiDexCode(instructionMap);
        //7. 按规则格式生成文件,文件名OoooooOooo
        MultiDexCodeUtils.writeMultiDexCode(dataOutputPath,multiDexCode);

    }

那么其中核心的一部就是List<Instruction> ret = DexUtils.extractAllMethods(dexFile, dexFileNew);
最终会执行到extractMethod方法

    private static Instruction extractMethod(Dex dex ,RandomAccessFile outRandomAccessFile,ClassDef classDef,ClassData.Method method)
            throws Exception{
        String returnTypeName = dex.typeNames().get(dex.protoIds().get(dex.methodIds().get(method.getMethodIndex()).getProtoIndex()).getReturnTypeIndex());
        String methodName = dex.strings().get(dex.methodIds().get(method.getMethodIndex()).getNameIndex());
        String className = dex.typeNames().get(classDef.getTypeIndex());
        //native函数,abstract函数
        if(method.getCodeOffset() == 0){
            LogUtils.warn("method code offset is zero,name =  %s.%s , returnType = %s",
                    TypeUtils.getHumanizeTypeName(className),
                    methodName,
                    TypeUtils.getHumanizeTypeName(returnTypeName));
            return null;
        }
        Instruction instruction = new Instruction();
        //16 = registers_size + ins_size + outs_size + tries_size + debug_info_off + insns_size
        int insnsOffset = method.getCodeOffset() + 16;
        Code code = dex.readCode(method);
        //容错处理
        if(code.getInstructions().length == 0){
            LogUtils.warn("method has no code,name =  %s.%s , returnType = %s",
                    TypeUtils.getHumanizeTypeName(className),
                    methodName,
                    TypeUtils.getHumanizeTypeName(returnTypeName));
            return null;
        }
        int insnsCapacity = code.getInstructions().length;
        //insns容量不足以存放return语句,跳过
        byte[] returnByteCodes = getReturnByteCodes(returnTypeName);
        if(insnsCapacity * 2 < returnByteCodes.length){
            LogUtils.warn("The capacity of insns is not enough to store the return statement. %s.%s() ClassIndex = %d -> %s insnsCapacity = %d byte(s) but returnByteCodes = %d byte(s)",
                    TypeUtils.getHumanizeTypeName(className),
                    methodName,
                    classDef.getTypeIndex(),
                    TypeUtils.getHumanizeTypeName(returnTypeName),
                    insnsCapacity * 2,
                    returnByteCodes.length);

            return null;
        }
        instruction.setOffsetOfDex(insnsOffset);
        //这里的MethodIndex对应method_ids区的索引
        instruction.setMethodIndex(method.getMethodIndex());
        //注意:这里是数组的大小
        instruction.setInstructionDataSize(insnsCapacity * 2);
        byte[] byteCode = new byte[insnsCapacity * 2];
        //写入nop指令
        for (int i = 0; i < insnsCapacity; i++) {
            outRandomAccessFile.seek(insnsOffset + (i * 2));
            byteCode[i * 2] = outRandomAccessFile.readByte();
            byteCode[i * 2 + 1] = outRandomAccessFile.readByte();
            outRandomAccessFile.seek(insnsOffset + (i * 2));
            outRandomAccessFile.writeShort(0);
        }
        instruction.setInstructionsData(byteCode);
        outRandomAccessFile.seek(insnsOffset);
        //写出return语句
        outRandomAccessFile.write(returnByteCodes);

        return instruction;
    }

可以看到最后一个循环中,将原位置的代码提出,写入到instruction对象中,而原来的位置使用nop进行了填充(写零),并在循环完成后写了个return进去

这样就得到了抽取指令后的dex和单独保存指令信息的List

加壳过程分析完毕

标签:dex,shell,String,apk,apkMainProcessPath,加壳,dpt,File,new
From: https://www.cnblogs.com/gradyblog/p/17310429.html

相关文章

  • JMeter-BeanShell预处理程序和BeanShell后置处理程序的应用
    一、什么是BeanShell?BeanShell是用Java写成的,一个小型的、免费的、可以下载的、嵌入式的Java源代码解释器,JMeter性能测试工具也充分接纳了BeanShell解释器,封装成了可配置的BeanShell前置和后置处理器,分别是BeanShellPreprocessor(BeanShell预处理程序)和BeanShellPostprocessor......
  • 【shell】curl 命令出现000返回码
    背景业务过程中,使用put接口调用修改时,curl返回的码是000原因put修改的是相同的数据,业务端返回接口较长页面调用swagger,很长时间后才返回200通过加--connect-timeout100-m300解决(主要是-m参数)----connect-timeout<seconds>设置最大请求时间-m/--max-time......
  • [shell] git并发提交
     for((i=1;i<5;i++))dosed-i"s/:$app_name:.*/:$app_name:$app_tag/"$app_filegitadd$app_filegitconfiguser.emailyourname@email.comgitconfiguser.nameyournamegitcommit-m"$app_branch$app_name:$app_tag&q......
  • shell整数运算和小数运算
    整数运算      let      小数运算bc  awk python #bc  #awk     #python  ......
  • Linux VS Powershell by ChatGPT
    CommandLinuxExamplePowerShellExampledstatdstat-taGet-Counter'\Processor(_Total)%ProcessorTime'sarsar-u110Get-Counter'\Processor(_Total)%ProcessorTime'slurmsbatchscript.shStart-Processpowershell.exe-A......
  • zookeeper shell
    zookeepershellzookeepershellzookeeper存储结构类似于Linux文件系统使用根结构node不是文件也不是目录客户端命令行#连接本地服务zkCli.sh#连接其他节点zkCli.sh-serverspark02:2181#这里并不是连接了三个节点,而是按照顺序连接一个,当第一个连接无法获取时,就......
  • shell脚本书写规范规则总结!!
    七年老运维实战中的Shell开发经验总结名名名名名名名名 运维网工 2023-04-1011:50 发表于香港收录于合集#网络运维71个#运维管理58个#运维工程师109个转载:https://blog.csdn.net/cpongo2ppp1/article/details/90172429无论是系统运维,还是应用运维,均可分为......
  • 使用Shell脚本备份网站目录
    目的:通过Shell脚本运行一键备份压缩到指定文件夹cd/tmp/backup/touchtest0622.sh如下:#!/bin/bashdir="/www/wwwroot/mefj.com.cn"backup="/tmp/backup"filename="wordpress.tar.gz"date=`date+%Y%m%d`[!-e"$dir"]&&echo"......
  • 使用Xshell远程连接Linux服务器
     https://blog.csdn.net/weixin_48016395/article/details/123190779?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-2-123190779-blog-129054565.235^v28^pc_relevant_default&spm=1001.2101.3001.4242.2&utm_rele......
  • shell读取配置文件-sed命令
    在编写启动脚本时,涉及到读取配置文件,特地记录下shell脚本读取启动文件的方式。主要提供两种格式的读取方式,方式一配置文件采用“[]”进行分区,方式二配置文件中需要有唯一的配置项名称。配置文件格式如下:#cat-nconfig.ini1#MYSQL配置项2[MYSQL]3DB_HOST......