ADVMP 三代壳(vmp加固)原理分析(加壳流程)
开源项目地址
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.yc
和advmp_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加固)原理分析(加壳流程)的更多相关文章
- UPX源码分析——加壳篇
0x00 前言 UPX作为一个跨平台的著名开源压缩壳,随着Android的兴起,许多开发者和公司将其和其变种应用在.so库的加密防护中.虽然针对UPX及其变种的使用和脱壳都有教程可查,但是至少在中文网 ...
- android apk 防止反编译技术第一篇-加壳技术
做android framework方面的工作将近三年的时间了,现在公司让做一下android apk安全方面的研究,于是最近就在网上找大量的资料来学习.现在将最近学习成果做一下整理总结.学习的这些成 ...
- 加壳&脱壳 - 前言(4.17更新)
0x00 闲谈 最近打算学习学习加壳脱壳相关的知识,大致会有以下几个部分 1.upx壳的加壳原理及脱壳方法 --UPX压缩壳的工作原理 --脱upx壳--初试--单步追踪 -- 0x01 参考链接 1 ...
- VMP虚拟机加壳的原理学习
好久没有到博客写文章了,9月份开学有点忙,参加了一个上海的一个CHINA SIG信息比赛,前几天又无锡南京来回跑了几趟,签了阿里巴巴的安全工程师,准备11月以后过去实习,这之前就好好待在学校学习了. ...
- Android中的Apk的加固(加壳)原理解析和实现
一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...
- Android中的Apk的加固(加壳)原理解析和实现(转)
一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...
- 【转】Android中的Apk的加固(加壳)原理解析和实现
一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...
- android黑科技系列——Apk的加固(加壳)原理解析和实现
一.前言 今天又到周末了,憋了好久又要出博客了,今天来介绍一下Android中的如何对Apk进行加固的原理.现阶段.我们知道Android中的反编译工作越来越让人操作熟练,我们辛苦的开发出一个apk, ...
- Android中对Apk加固(加壳)续篇之---对Native层(so文件)进行加固
有人说Android程序用Java代码写的,再怎么弄都是不安全的,很容易破解的,现在晚上关于应用加固的技术也很多了,当然这些也可以用于商业发展的,梆梆加密和爱加密就是很好的例子,当然这两家加固的Apk ...
- PHP-自动加载原理分析
说起PHP的自动加载,很多同学可能都会想到各种框架的自动加载功能,PHP规范中的PSR0和PSR4原则,Composer的自动加载功能等等,这些都为我们的开发提供了很大的方便. 那么PHP自动加载的前 ...
随机推荐
- [转帖]shell编程之循环语句
目录 一.循环语句 for循环 for语句的结构 嵌套循环 while语句的结构 while语句应用示例 until语句的结构 until语句示例 二.跳出循环 continue跳出循环 break跳 ...
- [转帖]Docker容器无法访问网络问题(网段冲突)
近日在使用docker在腾讯云服务器上部署项目 运行容器时死活访问不了网络,不论是外网还是内网. 最后找到原因是docker容器ip网段与服务器内网ip网段冲突导致的 使用此命令查看到 ifconfi ...
- [转帖]拯救关键业务上线:DBA 的惊魂24小时
一个电话,打破深夜的宁静 9月20日晚上10点 刚完成外地一个重点项目为期2周的现场支持,从机场回家的路上,一阵急促的铃声惊醒了出租车上昏昏欲睡的我,多年的工作经验告诉我这么晚来电一定是出事了,接起电 ...
- [转帖]CentOS 7 下用 firewall-cmd / iptables 实现 NAT 转发供内网服务器联网
https://www.cnblogs.com/hope250/p/8033818.html 自从用 HAProxy 对服务器做了负载均衡以后,感觉后端服务器真的没必要再配置并占用公网IP资源.而且由 ...
- [转帖]@Autowired 和 @Resource 的区别
@Autowired 和 @Resource 的区别 默认注入方式不同 @Autowired 默认的注入方式为byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口 ...
- Docker 安装Oracle12c的镜像修改字符集 并且进行启动的简单过程
学习来自 昨天晚上转帖的文章 这里面添加一些自己的内容 首先获取配置文件 git clone https://github.com/oracle/docker-images.git 获取之后比较容易了 ...
- git查看自己是从那个分支建的分支
可能发生的情况 很多时候,开始建分支的时候, 能够确认自己是那个分支建的,但是当写完功能之后, 再去回想,有时是忘记自己基于那个分支建的分支. 这时有一个命令的话就可以很快的定位了. 查看创建的分支来 ...
- 将input 中的小写字母转化为大写字母
小写转换为大写,使用toLocaleUpperCase() options.element.find(".CarNumber").textbox({ label: '车牌号:', ...
- 从零开始配置 vim(8)——文件类型检测
在上一章介绍自动命令的时候,我们提到可以使用 FileType来根据文件类型来触发事件,但是关于文件类型并没有深入的介绍,本篇我们来补充关于文件类型相关的内容,让大家更好的理解,看不懂也没关系,你只需 ...
- vim 从嫌弃到依赖(17)——查找模式
最开始介绍vim的时候,提到vim有普通模式.插入模式.可视模式和命令行模式,并且已经对这几个模式做了详细的介绍了.除了这几个模式以外,vim还有一个非常强大的模式--查找模式,为什么最开始没有将其列 ...