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

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. 【转帖】linux 软连接的使用

    https://www.cnblogs.com/sueyyyy/p/10985443.html 软连接是linux中一个常用命令,它的功能是为某一个文件在另外一个位置建立一个同不的链接. 具体用法是: ...

  2. [转帖]ELF文件详解

    一.ELF概述 1.ELF的定义 ELF(Executable and Linkable Format)文件是一种目标文件格式,常见的ELF格式文件包括:可执行文件.可重定位文件(.o).共享目标文件 ...

  3. [转帖]【JVM】JVM概述

    1.JVM定义 JVM 是Java Virtual Machine(JVM )的缩写,Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令进行执行,这样实现了Java"一次编译, ...

  4. js加减乘除运算出现精度丢失

    做乘法运算出现精度丢失 let aa= 2106.49 console.log( aa*10000 ) //21064899.999999996 console.log( Math.round(aa* ...

  5. 【K哥爬虫普法】淘宝一亿快递信息泄漏,有人正在盯着你的网购!

    我国目前并未出台专门针对网络爬虫技术的法律规范,但在司法实践中,相关判决已屡见不鲜,K 哥特设了"K哥爬虫普法"专栏,本栏目通过对真实案例的分析,旨在提高广大爬虫工程师的法律意识, ...

  6. 情侣纪念日网站html5源码教程

    个人名片: 对人间的热爱与歌颂,可抵岁月冗长 Github‍:念舒_C.ying CSDN主页️:念舒_C.ying 个人博客 :念舒_C.ying 预览图 直接进入我的网站吧 >> Z_ ...

  7. String 中的Trim

    Trim 切除首尾指定字符 var newStr=""; char[] trimChars={'@','#','$',' '}; string strC="@Hello# ...

  8. 面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?

    JIT.逃逸分析.锁消除.栈上分配和标量替换等都属于 JVM 的优化手段,JVM 优化手段是指在运行 Java 程序时,通过对字节码的编译和执行过程进行优化,以提升程序的性能和效率. JVM 优化手段 ...

  9. Linux虚拟机追加扩展磁盘

    一.使用VMware给虚拟机追加磁盘 使用VMware打开虚拟机设置对话框,选择硬盘,点击右侧的扩展按钮,输入扩展后的磁盘容量. 点击扩展按钮.提示磁盘已成功扩展. 二.对闲置的空间进行分区 上面扩展 ...

  10. 利用 ASP.NET Core 开发单机应用

    前言 现在是分布式微服务开发的时代,除了小工具和游戏之类刚需本地运行的程序已经很少见到纯单机应用.现在流行的Web应用由于物理隔离天然形成了分布式架构,核心业务由服务器运行,边缘业务由客户端运行.对于 ...