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

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

加壳过程分析完毕

dpt-shell 抽取壳实现原理分析(加壳逻辑)的更多相关文章

  1. 什么是App加壳,以及App加壳的利与弊

    非著名程序员涩郎 非著名程序员,字耿左直右,号涩郎,爱搞机,爱编程,是爬行在移动互联网中的一名码匠!个人微信号:loonggg,微博:涩郎,专注于移动互联网的开发和研究,本号致力于分享IT技术和程序猿 ...

  2. UPX源码分析——加壳篇

    0x00 前言 UPX作为一个跨平台的著名开源压缩壳,随着Android的兴起,许多开发者和公司将其和其变种应用在.so库的加密防护中.虽然针对UPX及其变种的使用和脱壳都有教程可查,但是至少在中文网 ...

  3. Android Linker 与 SO 加壳技术

    1. 前言 Android 系统安全愈发重要,像传统pc安全的可执行文件加固一样,应用加固是Android系统安全中非常重要的一环.目前Android 应用加固可以分为dex加固和Native加固,N ...

  4. 【腾讯Bugly干货分享】Android Linker 与 SO 加壳技术

    本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57e3a3bc42eb88da6d4be143 作者:王赛 1. 前言 Andr ...

  5. AndroidLinker与SO加壳技术之下篇

    点此查看上篇<AndroidLinker与SO加壳技术之上篇> 2.4 链接 链接过程由 soinfo_link_image 函数完成,主要可以分为四个主要步骤: 1. 定位 dynami ...

  6. NET程序的代码混淆、加壳与脱壳

    通常我们通过代码混淆.加密的形式达到软件保护的目的.在Web开发里我们接触过的可能就是JS代码加密了,可以通过对JS代码进行混淆.加密从而实现对核心JS代码的保护.如果没有接触过的可以在这里简单了解一 ...

  7. VMP虚拟机加壳的原理学习

    好久没有到博客写文章了,9月份开学有点忙,参加了一个上海的一个CHINA SIG信息比赛,前几天又无锡南京来回跑了几趟,签了阿里巴巴的安全工程师,准备11月以后过去实习,这之前就好好待在学校学习了. ...

  8. Android中的Apk的加固(加壳)原理解析和实现

    一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...

  9. Android中的Apk的加固(加壳)原理解析和实现(转)

    一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...

  10. 【转】Android中的Apk的加固(加壳)原理解析和实现

    一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...

随机推荐

  1. [转帖]goproxy 使用说明

    Go 版本要求 建议您使用 Go 1.13 及以上版本, 可以在这里下载最新的 Go 稳定版本. 配置 Goproxy 环境变量 Bash (Linux or macOS) export GOPROX ...

  2. minio性能测试

    minio性能测试 minio的使用 前期使用了s3fs 但是想验证一下性能相关, 所以使用今天简单验证了一下, 其实也可以使用一下fio 但是s3fs 是对象存储 没有修改 只有上传, 所以感觉还是 ...

  3. [转帖]Jmeter学习笔记(八)——监听器元件之聚合报告

    https://www.cnblogs.com/pachongshangdexuebi/p/11507298.html 1.聚合报告添加 聚合报告是常用的监听器之一,添加路径: 点击线程组->添 ...

  4. [转帖]linux磁盘IO读写性能优化

    在LINUX系统中,如果有大量读请求,默认的请求队列或许应付不过来,我们可以 动态调整请求队列数来提高效率,默认的请求队列数存放在/sys/block/xvda/queue/nr_requests 文 ...

  5. [转帖]Nginx-https证书认证详解

    https://developer.aliyun.com/article/885650?spm=a2c6h.24874632.expert-profile.306.7c46cfe9h5DxWK 简介: ...

  6. [转帖]shell脚本变量详解(自定义变量、环境变量、变量赋值、变量运算、变量内容替换)

    https://developer.aliyun.com/article/885658 简介: shell变量 shell变量是指用一个特定的字符串去表示不固定的内容 1.变量的类型 1.1自定义变量 ...

  7. 文盘Rust -- 安全连接 TiDB/Mysql

    作者:京东科技 贾世闻 最近在折腾rust与数据库集成,为了偷懒,选了Tidb Cloud Serverless Tier 作为数据源.Tidb 无疑是近五年来最优秀的国产开源分布式数据库,Tidb ...

  8. vite多入口

    创建多页面入口 1.在根目录下创建 demo1.htm1,demo2.htm1这两个文件 2.在vite.config.js文件中配置入口 3.在src下创建文件夹和文件,src/demo1/app. ...

  9. kettle系统列文章03---如何建立一个作业

    上篇文章我们建立好了转换,我们希望这个转换可以做成定时任务,每一分钟执行一次 第一步:创建作业开始节点:文件---->新建---->作业----核心对象---->通用-----> ...

  10. RocketMQ—引言

    RocketMQ-引言 MQ介绍 在学习RocketMQ之前,我们先来看以下MQ的意思. MQ是Message Queue的首字母缩写. Message:意思为消息,在我们生活中可以是一句话/一个短信 ...