开源项目地址

https://github.com/chago/ADVMP

vmp 加固可以说时各大加固厂商的拳头产品了,这个开源项目虽然不是十分完善,让我们可以一览vmp加固的原理,是十分好的学习资源

vmp 全称: virtual machine protect , 本质是将原来smali对应的代码转化为自定义的代码,然后通过自定义的解释器进行解释和执行
ADVMP 实现了 基本计算相关指令的解释和执行,而一些调用 ,引用 framework 相关api的部分没有实现,但也可以一窥究竟了
源码目录说明
AdvmpTest:测试用的项目。
base:Java项目。里面是一些工具类代码。
control-centre:Java项目。控制加固流程。
separator:Java项目。抽离方法指令,然后将抽离的指令按照自定义格式输出,并同时输出C文件。
template/jni:C代码。里面包含了解释器的代码。
ycformat:自定义的文件格式,用于保存抽取出来指令等数据。

加壳流程分析

control-centre 的 EntryPoint 是加固流程的入口

public static void main(String[] args) {
log.info("------ 进入控制中心 ------");
try {
...... ControlCentre controlCentre = new ControlCentre(opt);
log.info("开始加固。");
if (controlCentre.shell()) {
//log.info
} } catch (ParseException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
log.info("------ 离开控制中心 ------");
}

主要是执行controlCentre.shell() 这里是加壳主流程

    public boolean shell() {
boolean bRet = false;
try {
// 1. 找到所谓的第一个类,比如Application 或者MainActivity
TypeDescription classDesc = AndroidManifestHelper.findFirstClass(new File(mApkUnpackDir, "AndroidManifest.xml"));
//2. 找到第一个类的clinit方法,在当中插入System.loadLibrary指令
InstructionInsert01 instructionInsert01 = new InstructionInsert01(new File(mApkUnpackDir, "classes.dex"), classDesc);
instructionInsert01.insert(); // 3. 运行抽离器。将codeItem 抽出,转换,打包 生成yc文件
runSeparator(); // 4. 从template目录中拷贝jni文件。
copyJniFiles(); // 5. 更新jni文件的内容。
updateJniFiles(); // 6. 编译native代码。
buildNative(); // 7. 将libs目录重命名为lib。
mOpt.libDir = new File(mOpt.jniDir.getParentFile(), "lib");
new File(mOpt.jniDir.getParentFile(), "libs").renameTo(mOpt.libDir); // 8. 移动yc文件。
File assetsDir = new File(mApkUnpackDir, "assets");
if (!assetsDir.exists()) {
assetsDir.mkdir();
}
File newYcFile = new File(assetsDir, "classes.yc");
Files.move(mOpt.outYcFile.toPath(), newYcFile.toPath()); // 9. 移动classes.dex文件。
Utils.copyFile(new File(mOpt.outYcFile.getParent(), "classes.dex").getAbsolutePath(), new File(mApkUnpackDir, "classes.dex").getAbsolutePath());
// 10. 拷贝lib目录。
Utils.copyFolder(mOpt.libDir.getAbsolutePath(), mApkUnpackDir.getAbsolutePath() + File.separator + "lib"); // 11. 打包
String name = mOpt.apkFile.getName();
name = name.substring(0, name.lastIndexOf('.'));
File outApkFile = new File(mOpt.outDir, name + ".shelled.apk");
ZipHelper.doZip(mApkUnpackDir.getAbsolutePath(), outApkFile.getAbsolutePath()); bRet = true;
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return bRet;
}

看一下比较核心的 3.运行抽离器

    private boolean runSeparator() throws IOException {
SeparatorOption opt = new SeparatorOption();
opt.dexFile = new File(mApkUnpackDir, "classes.dex");
File outDir = new File(mOpt.workspace, "separator");
opt.outDexFile = new File(outDir, "classes.dex");
opt.outYcFile = mOpt.outYcFile = new File(outDir, "classes.yc");
opt.outCPFile = mOpt.outYcCPFile = new File(outDir, "advmp_separator.cpp"); Separator separator = new Separator(opt);
return separator.run();
}

核心流程:是为了生成classes.ycadvmp_separator.cpp

classes.yc 是一个按格式规则写入的文件,类似之前的二代壳,但这里多了一部指令替换,逆向人员拿到这个文件写入也反编译不出来,(还需要拿到对照表将指令还原才可以)

advmp_separator.cpp 是一个生成的cpp模板代码文件(可以看出壳的本质还是借助生成CPP,然后加入到native源码中打包生成so完成的)

看一下 separator.run()
    public boolean run() {
boolean bRet = false;
// 1. 重新生成dex(重要)。
DexFile newDexFile = mDexRewriter.rewriteDexFile(mDexFile);
try {
// 2. 将新dex输出到文件。
DexFileFactory.writeDexFile(mOpt.outDexFile.getAbsolutePath(), newDexFile); // 3.写Yc文件。
writeYcFile(); // 4.写C文件。
writeCFile(); bRet = true;
} catch (IOException e) {
e.printStackTrace();
}
return bRet;
}

这个mDexRewriter 就很精髓,利用的是dexlib2的能力,对dex中的方法进行了重写

 @Nonnull
@Override
public Rewriter<Method> getMethodRewriter(Rewriters rewriters) {
return new MethodRewriter(rewriters) {
@Nonnull
@Override
public Method rewrite(@Nonnull Method value) {
if (mConfigHelper.isValid(value)) {
mSeparatedMethod.add(value);
// 抽取代码。
YcFormat.SeparatorData separatorData = new YcFormat.SeparatorData();
separatorData.methodIndex = mSeparatorData.size();
separatorData.accessFlag = value.getAccessFlags();
separatorData.paramSize = value.getParameters().size();
separatorData.registerSize = value.getImplementation().getRegisterCount(); separatorData.paramShortDesc = new StringItem();
separatorData.paramShortDesc.str = MethodHelper.genParamsShortDesc(value).getBytes();
separatorData.paramShortDesc.size = separatorData.paramShortDesc.str.length; separatorData.insts = MethodHelper.getInstructions((DexBackedMethod) value);
separatorData.instSize = separatorData.insts.length;
separatorData.size = 4 + 4 + 4 + 4 + 4 + separatorData.paramShortDesc.size + 4 + (separatorData.instSize * 2) + 4;
mSeparatorData.add(separatorData); // 下面这么做的目的是要把方法的name删除,否则生成的dex安装的时候会有这个错误:INSTALL_FAILED_DEXOPT。
List<? extends MethodParameter> oldParams = value.getParameters();
List<ImmutableMethodParameter> newParams = new ArrayList<>();
for (MethodParameter mp : oldParams) {
newParams.add(new ImmutableMethodParameter(mp.getType(), mp.getAnnotations(), null));
} // 生成一个新的方法。
return new ImmutableMethod(value.getDefiningClass(), value.getName(), newParams, value.getReturnType(), value.getAccessFlags() | AccessFlags.NATIVE.getValue(), value.getAnnotations(), null);
} return super.rewrite(value);
}
};
}

重写过程中生成了YcFormat(用于生成Yc文件)和mSeparatedMethod(一个Method对象列表)

最后返回了一个空方法

new ImmutableMethod(value.getDefiningClass(), value.getName(), newParams, value.getReturnType(), value.getAccessFlags() | AccessFlags.NATIVE.getValue(), value.getAnnotations(), null);

实现dex中方法代码的抽离

然后调用writeYcFile() 对YcFormat对象进行解析和写入文件(和二代壳类似)
然后调用 writeCFile() (关键)
    private void writeCFile() throws IOException {
SeparatorCWriter separatorCWriter = new SeparatorCWriter(mOpt.outCPFile, mSeparatedMethod);
separatorCWriter.write();
}

separatorCWriter.write

    public void write() throws IOException {
try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(mOutFile))) {
int index = 0;
for (Method method : mSeparatedMethod) {
String definingClass = method.getDefiningClass();
if (classes.containsKey(definingClass)) {
classes.get(definingClass).add(method);
} else {
List<Method> ms = new ArrayList<>();
ms.add(method);
classes.put(definingClass, ms);
} writeMethod(index, method, fileWriter);
index++;
} write_registerNatives(fileWriter); fileWriter.write("void registerFunctions(JNIEnv* env) {");
fileWriter.newLine();
for (String registerNativesName : registerNativesNames) {
fileWriter.write(String.format("if (!%s(env)) { MY_LOG_ERROR(\"register method fail.\"); return; }", registerNativesName));
fileWriter.newLine();
}
fileWriter.newLine();
fileWriter.write("}");
fileWriter.newLine();
}
}

这里就是想解析mSeparatedMethod(方法列表),生成一个动态注册的模板代码

private void writeMethod(int index, Method method, BufferedWriter fileWriter) throws IOException {
StringBuffer sb = new StringBuffer();
sb.append(MethodHelper.genTypeInNative(method));
sb.append(" ");
sb.append(method.getName());
sb.append(" (");
sb.append(MethodHelper.genParamTypeListInNative(method));
sb.append(") {");
fileWriter.write(sb.toString());
fileWriter.newLine(); sb.delete(0, sb.length());
sb.append("jvalue result = BWdvmInterpretPortable(gAdvmp.ycFile->GetSeparatorData(");
sb.append(index);
sb.append("), env, thiz"); List<? extends CharSequence> params = method.getParameterTypes();
for (int i = 0; i < params.size(); i++) {
sb.append(", ");
sb.append(MethodHelper.paramNames[i]);
}
sb.append(");");
fileWriter.write(sb.toString());
fileWriter.newLine(); sb.delete(0, sb.length());
sb.append("return ");
char cType = method.getReturnType().charAt(0);
switch (cType) {
case 'Z':
sb.append("result.z");
break;
case 'B':
sb.append("result.b");
break;
case 'S':
sb.append("result.s");
break;
case 'C':
sb.append("result.c");
break;
case 'I':
sb.append("result.i");
break;
case 'J':
sb.append("result.j");
break;
case 'F':
sb.append("result.f");
break;
case 'D':
sb.append("result.d");
break;
case 'L':
sb.append("result.l");
break;
case '[':
sb.append("result.l");
break;
}
sb.append(";}");
fileWriter.write(sb.toString());
fileWriter.newLine();
}

每个java侧对应的native方法,都由BWdvmInterpretPortable进行转发执行,这个方法十分关键,会转发给自定义解释器进行执行

到这里 抽取步骤就完成了,

然后是构建生成so的步骤,即ControlCenter shell的后续
            // 从template目录中拷贝jni文件。
copyJniFiles(); // 更新jni文件的内容。
updateJniFiles(); // 编译native代码。
buildNative();

copyJniFiles和buildNative都是常规操作, 关键是updateJniFiles,这里有对模板代码进一步的更新

    private void updateJniFiles() throws IOException {
File file;
File tmpFile;
StringBuffer sb = new StringBuffer(); // 更新avmp.cpp文件中的内容。
try (BufferedReader reader = new BufferedReader(new FileReader(mOpt.outYcCPFile))) {
String line = null;
while (null != (line = reader.readLine())) {
sb.append(line);
sb.append(System.getProperty("line.separator"));
}
} file = new File(mOpt.jniDir.getAbsolutePath() + File.separator + "advmpc" + File.separator + "avmp.cpp");
tmpFile = new File(mOpt.jniDir.getAbsolutePath() + File.separator + "advmpc" + File.separator + "avmp.cpp" + ".tmp");
try (BufferedReader reader = new BufferedReader(new FileReader(file));
BufferedWriter writer = new BufferedWriter(new FileWriter(tmpFile))) {
String line = null;
while (null != (line = reader.readLine())) {
if ("#ifdef _AVMP_DEBUG_".equals(line)) {
writer.write("#if 0");
writer.newLine();
} else if ("//+${replaceAll}".equals(line)) {
writer.write(sb.toString());
} else {
writer.write(line);
writer.newLine();
}
}
}
file.delete();
tmpFile.renameTo(file);
sb.delete(0, sb.length());
}

这里是将advmp_separator.cpp 的代码和advmp.cpp 的代码合并,生成新的advmp.cpp,看一下编译选项

template/jni/Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := advmp

LOCAL_SRC_FILES := ioapi.c \
unzip.c \
Globals.cpp \
avmp.cpp \ # 我们生成的代码文件
BitConvert.cpp \
InterpC.cpp \
io.cpp \
Utils.cpp \
YcFile.cpp LOCAL_SRC_FILES += DexOpcodes.cpp \
Exception.cpp LOCAL_LDLIBS := -llog -lz include $(BUILD_SHARED_LIBRARY)

最后构建生成 advmp.so

最后调用ZipHelper.doZip 重新打包成apk,完成(没有重新签名),由用户自行签名

ADVMP 三代壳(vmp加固)原理分析(加壳流程)的更多相关文章

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

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

  2. android apk 防止反编译技术第一篇-加壳技术

    做android framework方面的工作将近三年的时间了,现在公司让做一下android apk安全方面的研究,于是最近就在网上找大量的资料来学习.现在将最近学习成果做一下整理总结.学习的这些成 ...

  3. 加壳&脱壳 - 前言(4.17更新)

    0x00 闲谈 最近打算学习学习加壳脱壳相关的知识,大致会有以下几个部分 1.upx壳的加壳原理及脱壳方法 --UPX压缩壳的工作原理 --脱upx壳--初试--单步追踪 -- 0x01 参考链接 1 ...

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

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

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

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

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

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

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

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

  8. android黑科技系列——Apk的加固(加壳)原理解析和实现

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

  9. Android中对Apk加固(加壳)续篇之---对Native层(so文件)进行加固

    有人说Android程序用Java代码写的,再怎么弄都是不安全的,很容易破解的,现在晚上关于应用加固的技术也很多了,当然这些也可以用于商业发展的,梆梆加密和爱加密就是很好的例子,当然这两家加固的Apk ...

  10. PHP-自动加载原理分析

    说起PHP的自动加载,很多同学可能都会想到各种框架的自动加载功能,PHP规范中的PSR0和PSR4原则,Composer的自动加载功能等等,这些都为我们的开发提供了很大的方便. 那么PHP自动加载的前 ...

随机推荐

  1. [转帖]TIDB_HOT_REGIONS

    https://docs.pingcap.com/zh/tidb/stable/information-schema-tidb-hot-regions TIDB_HOT_REGIONS 表提供了关于当 ...

  2. [转帖]Fiddler抓取Chrome浏览器访问baiud.com报NET::ERR_CERT_COMMON_NAME_INVALID

    错误现象 解决方法: 1.Chrome浏览器地址栏中输:chrome://net-internals/#hsts 2.在Query HSTS/PKP domain处搜索www.baidu.com网站, ...

  3. [转帖]金仓数据库KWR使用说明

    金仓数据库KWR使用说明 1.KWR性能报告介绍 2.部署KWR性能报告功能 2.1.修改配置文件 2.2.使配置文件中参数生效 2.3.创建KWR扩展 3.使用KWR快照功能 3.1.创建KWR快照 ...

  4. 【转帖】Dockerfile文件指令介绍

    https://blog.whsir.com/post-5327.html Dockerfile其实就是一个文本文件,这个文本文件名称叫Dockerfile,里面包含了一些指令(可以理解成多个指令集合 ...

  5. [转帖]Elasticsearch 技术分析(五):如何通过SQL查询Elasticsearch

    https://www.cnblogs.com/jajian/p/10053504.html 前言# 这篇博文本来是想放在全系列的大概第五.六篇的时候再讲的,毕竟查询是在索引创建.索引文档数据生成和一 ...

  6. JVM启动参数脚本的再学习与研究

    JVM启动参数脚本的再学习与研究 摘要 学无止境 前段时间一直再研究JVM参数调优. 但是最近也在想不应该仅研究如何调优. 因为不管怎么设置, 总有猪队友会把环境搞崩. 所以应该想办法在无人值守的情况 ...

  7. Redis IO多线程的简要测试结果

    Redis IO多线程的简要测试结果 摘要 最近想简单确认一下IO多线程的对吞吐量的提升情况. 正好手头有鲲鹏的机器, 所以想直接进行一下验证 顺便用一下4216 进行一下对比. 发现 在CPU核心比 ...

  8. Linux 一行命令 仅显示某一个网卡的ip地址

    最简答的方法 1. 先使用 ifconfig 查看网卡的设备名 2. 然后输入命令 ifconfig ens192 |grep 'inet ' |cut -d " " -f 10命 ...

  9. element-ui中Select 选择器value-key的使用

    场景描述 很多时候我们都需要使用下拉框 Select 选择器. 在获取值的时候,通常只需要传递对应的id给后端就行了. 但是特殊情况,后端不想去查库,不仅需要我们id,还有name,code之类的. ...

  10. Ant Design Vue表单验证失败

    表单验证遇见的坑 01 如果你受控数据是这样写的话 const formState= reactive({ youForm:{ youNaNe:'', useSlectValue: '001', da ...