详解如何自定义 Android Dex VMP 保护壳

news/2025/1/16 10:58:15/

版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

前言

Android Dex VMP(Virtual Machine Protection,虚拟机保护)壳是一种常见的应用保护技术,主要用于保护 Android 应用的代码免受反编译和逆向工程的攻击。

VMP 保护壳通过将应用的原始 Dex(Dalvik Executable)文件进行加密、混淆、虚拟化等处理,使得恶意用户无法轻易获取到应用的原始代码和逻辑。

比如,实现一个 Android 下的 Dex VMP 保护壳,用来保护 Kotlin 层 sign 算法,防止被逆向

假设 sign 算法源码如下:

package com.cyrus.example.vmpimport java.security.MessageDigest
import java.util.Base64object SignUtil {/*** 对输入字符串进行签名并返回 Base64 编码后的字符串* @param input 要签名的字符串* @return Base64 编码后的字符串*/fun sign(input: String): String {// 使用 SHA-256 计算摘要val digest = MessageDigest.getInstance("SHA-256")val hash = digest.digest(input.toByteArray())// 使用 Base64 编码return Base64.getEncoder().encodeToString(hash)}
}

转换为指令流

把 apk 拖入 GDA,找到 sign 方法,右键选择 SmaliJava(F5)

word/media/image1.png

GDA 是一个开源的 Android 逆向分析工具,可反编译 APK、DEX、ODEX、OAT、JAR、AAR 和 CLASS 文件,支持恶意行为检测、隐私泄露检测、漏洞检测、路径解密、打包器识别、变量跟踪、反混淆、python 和 Java 脚本等等…

  • GDA 下载地址:http://www.gda.wiki:9090/

  • GDA 项目地址:https://github.com/charles2gan/GDA-android-reversing-Tool

Show ByteCode

word/media/image2.png

得到字节码和对应的 smali 指令如下:

1a004e00            | const-string v0, "input"
712020000500        | invoke-static{v5, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V
1a002c00            | const-string v0, "SHA-256"
71101c000000        | invoke-static{v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
0c00                | move-result-object v0
62010900            | sget-object v1, Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;
6e2016001500        | invoke-virtual{v5, v1}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B
0c01                | move-result-object v1
1a024a00            | const-string v2, "getBytes\(...\)"
71201f002100        | invoke-static{v1, v2}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
6e201b001000        | invoke-virtual{v0, v1}, Ljava/security/MessageDigest;->digest([B)[B
0c01                | move-result-object v1
71001e000000        | invoke-static{}, Ljava/util/Base64;->getEncoder()Ljava/util/Base64$Encoder;
0c02                | move-result-object v2
6e201d001200        | invoke-virtual{v2, v1}, Ljava/util/Base64$Encoder;->encodeToString([B)Ljava/lang/String;
0c02                | move-result-object v2
1a034400            | const-string v3, "encodeToString\(...\)"
71201f003200        | invoke-static{v2, v3}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
1102                | return-object v2

构建虚拟机解释器

解释器的任务是执行这些虚拟机指令。我们需要写一个虚拟机,它能够按照虚拟指令集中的指令依次执行操作。

创建 cpp 文件,定义一个 JNI 方法 execute,接收字节码数组和字符串参数,每个字节码指令会被映射为我们定义的虚拟指令。

#define CONST_STRING_OPCODE 0x1A  // const-string 操作码
#define INVOKE_STATIC_OPCODE 0x71  // invoke-static 操作码
#define MOVE_RESULT_OBJECT_OPCODE 0x0c  // move-result-object 操作码
#define SGET_OBJECT_OPCODE 0x62  // sget-object 操作码
#define INVOKE_VIRTUAL_OPCODE 0x6e  // invoke-virtual 操作码
#define RETURN_OBJECT_OPCODE 0x11  // return-object 操作码jstring execute(JNIEnv *env, jobject thiz, jbyteArray bytecodeArray, jstring input) {// 传参存到 v5 寄存器registers[5] = input;// 获取字节码数组的长度jsize length = env->GetArrayLength(bytecodeArray);std::vector <uint8_t> bytecode(length);env->GetByteArrayRegion(bytecodeArray, 0, length, reinterpret_cast<jbyte *>(bytecode.data()));size_t pc = 0;  // 程序计数器try {// 执行字节码中的指令while (pc < bytecode.size()) {uint8_t opcode = bytecode[pc];switch (opcode) {case CONST_STRING_OPCODE:handleConstString(env, bytecode.data(), pc);break;case INVOKE_STATIC_OPCODE:handleInvokeStatic(env, bytecode.data(), pc);break;case SGET_OBJECT_OPCODE:handleSgetObject(env, bytecode.data(), pc);break;case INVOKE_VIRTUAL_OPCODE:handleInvokeVirtual(env, bytecode.data(), pc);break;case RETURN_OBJECT_OPCODE:handleReturnResultObject(env, bytecode.data(), pc);break;default:throw std::runtime_error("Unknown opcode encountered");}}if (std::holds_alternative<jstring>(registers[0])) {jstring result = std::get<jstring>(registers[0]);   // 返回寄存器 v0 的值// 清空寄存器std::fill(std::begin(registers), std::end(registers), nullptr);return result;}} catch (const std::exception &e) {env->ThrowNew(env->FindClass("java/lang/RuntimeException"), e.what());}// 清空寄存器std::fill(std::begin(registers), std::end(registers), nullptr);return nullptr;
}

模拟寄存器

使用 std::variant 来定义一个可以存储多种类型的寄存器值。

// 定义支持的寄存器类型(比如 jstring、jboolean、jobject 等等)
using RegisterValue = std::variant<jstring,jboolean,jbyte,jshort,jint,jlong,jfloat,jdouble,jobject,jbyteArray,jintArray,jlongArray,jfloatArray,jdoubleArray,jbooleanArray,jshortArray,jobjectArray,std::nullptr_t
>;

std::variant 是 C++17 引入的一个模板类,用于表示一个可以存储多种类型中的一种的类型。它类似于联合体(union),但是比联合体更安全,因为它可以明确地跟踪当前存储的是哪一种类型。

定义寄存器个数和寄存器数组

// 定义寄存器数量
constexpr size_t NUM_REGISTERS = 10;// 定义寄存器数组
RegisterValue registers[NUM_REGISTERS];

写寄存器

// 存储不同类型的值到寄存器
template <typename T>
void setRegisterValue(uint8_t reg, T value) {// 通过模板将类型 T 存储到寄存器registers[reg] = value;
}

读寄存器

// 根据类型从寄存器读取对应的值
jvalue getRegisterAsJValue(int regIdx, const std::string &paramType) {const RegisterValue &val = registers[regIdx];jvalue result;if (paramType == "I") {  // int 类型if (std::holds_alternative<jint>(val)) {result.i = std::get<jint>(val);} else {throw std::runtime_error("Type mismatch: Expected jint.");}} else if (paramType == "J") {  // long 类型if (std::holds_alternative<jlong>(val)) {result.j = std::get<jlong>(val);} else {throw std::runtime_error("Type mismatch: Expected jlong.");}} else if (paramType == "F") {  // float 类型if (std::holds_alternative<jfloat>(val)) {result.f = std::get<jfloat>(val);} else {throw std::runtime_error("Type mismatch: Expected jfloat.");}} else if (paramType == "D") {  // double 类型if (std::holds_alternative<jdouble>(val)) {result.d = std::get<jdouble>(val);} else {throw std::runtime_error("Type mismatch: Expected jdouble.");}} else if (paramType == "Z") {  // boolean 类型if (std::holds_alternative<jboolean>(val)) {result.z = std::get<jboolean>(val);} else {throw std::runtime_error("Type mismatch: Expected jboolean.");}} else if (paramType == "B") {  // byte 类型if (std::holds_alternative<jbyte>(val)) {result.b = std::get<jbyte>(val);} else {throw std::runtime_error("Type mismatch: Expected jbyte.");}} else if (paramType == "S") {  // short 类型if (std::holds_alternative<jshort>(val)) {result.s = std::get<jshort>(val);} else {throw std::runtime_error("Type mismatch: Expected jshort.");}} else if (paramType == "Ljava/lang/String;") {  // String 类型if (std::holds_alternative<jstring>(val)) {result.l = std::get<jstring>(val);} else {throw std::runtime_error("Type mismatch: Expected jstring.");}} else if (paramType[0] == 'L') {  // jobject 类型(以 L 开头)if (std::holds_alternative<jstring>(val)) {result.l = std::get<jstring>(val);} else if (std::holds_alternative<jobject>(val)) {result.l = std::get<jobject>(val);} else {throw std::runtime_error("Type mismatch: Expected jobject.");}} else if (paramType[0] == '[') {  // 数组类型// 处理数组类型,判断是基础类型数组还是对象数组if (paramType == "[I") {  // jintArray 类型if (std::holds_alternative<jintArray>(val)) {result.l = std::get<jintArray>(val);  // jvalue 直接存储数组} else {throw std::runtime_error("Type mismatch: Expected jintArray.");}} else if (paramType == "[J") {  // jlongArray 类型if (std::holds_alternative<jlongArray>(val)) {result.l = std::get<jlongArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jlongArray.");}} else if (paramType == "[F") {  // jfloatArray 类型if (std::holds_alternative<jfloatArray>(val)) {result.l = std::get<jfloatArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jfloatArray.");}} else if (paramType == "[D") {  // jdoubleArray 类型if (std::holds_alternative<jdoubleArray>(val)) {result.l = std::get<jdoubleArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jdoubleArray.");}} else if (paramType == "[Z") {  // jbooleanArray 类型if (std::holds_alternative<jbooleanArray>(val)) {result.l = std::get<jbooleanArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jbooleanArray.");}} else if (paramType == "[B") {  // jbyteArray 类型if (std::holds_alternative<jbyteArray>(val)) {result.l = std::get<jbyteArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jbyteArray.");}} else if (paramType == "[S") {  // jshortArray 类型if (std::holds_alternative<jshortArray>(val)) {result.l = std::get<jshortArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jshortArray.");}} else if (paramType == "[Ljava/lang/String;") {  // String[] 类型if (std::holds_alternative<jobjectArray>(val)) {result.l = std::get<jobjectArray>(val);} else {throw std::runtime_error("Type mismatch: Expected String array.");}} else if (paramType[0] == '[' && paramType[1] == 'L') {  // jobject[] 类型(数组的元素为对象)if (std::holds_alternative<jobjectArray>(val)) {result.l = std::get<jobjectArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jobject array.");}} else {throw std::runtime_error("Unsupported array type.");}} else {throw std::runtime_error("Unsupported parameter type.");}return result;
}

模拟字符串常量池

由于指令中用到字符串,所有需要模拟一个字符串常量池去实现指令中字符串的引用。

在 dex 文件中,字符串常量池(string_ids)是一个数组,其中每个条目存储一个字符串的偏移量,这个偏移量指向 dex 文件中 string_data 区域。

word/media/image3.png

这里简单通过字符串索引和字符串做关联,代码实现如下:

// 模拟字符串常量池
std::unordered_map <uint32_t, std::string> stringPool = {{0x004e00, "input"},{0x002c00, "SHA-256"},{0x024a00, "getBytes\\(...\\)"},{0x034400, "encodeToString\\(...\\)"},
};

指令解析执行

虚拟机接收到字节指令流,经过解析操作码并分发到各指令执行函数。接下来实现指令执行函数。

1. const-string

该指令将一个预定义的字符串常量加载到指定的寄存器中。例如:

const-string v0, "Hello, World!"

这条指令的作用是将字符串 “Hello, World!” 加载到寄存器 v0 中。

指令结构

const-string v0, “input” 的字节码为:

1A 00 4E 00

结构解释:

  • 1A (操作码): 表示 const-string 指令。

  • 00 (目标寄存器 v0): 表示字符串将存储到寄存器 v0 中。

  • 4E 00 (字符串索引 0x004E): 表示字符串在字符串常量池中的位置。

具体代码实现

// 处理 const-string 指令
void handleConstString(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode != CONST_STRING_OPCODE) {  // 检查是否为 const-string 指令throw std::runtime_error("Unexpected opcode");}// 获取目标寄存器索引 reg 和字符串索引uint8_t reg = bytecode[pc + 1];  // 目标寄存器// 读取字符串索引(第 2、3、4 字节)uint32_t stringIndex = (bytecode[pc + 1] << 16) | (bytecode[pc + 2] << 8) | bytecode[pc + 3];// 从字符串常量池获取字符串const std::string &value = stringPool[stringIndex];// 创建 jstring 并将其存储到目标寄存器jstring str = env->NewStringUTF(value.c_str());registers[reg] = str;// 更新程序计数器pc += 4;  // const-string 指令占用 4 字节
}

2. invoke-static

invoke-static 指令用于执行类的静态方法。例如:

invoke-static {v5, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V

各部分的解释:

  • invoke-static:这是调用静态方法的指令

  • {v5, v0}:这是方法调用时传递的参数寄存器

  • Lkotlin/jvm/internal/Intrinsics;:目标类的名称。

  • ->checkNotNullParameter:这是要调用的静态方法的名称

  • (Ljava/lang/Object;Ljava/lang/String;):这是方法的参数签名

  • V:表示方法的返回类型是 void。

指令结构

一个标准的 invoke-static 字节码指令通常如下所示(6个字节):

71 <reg_count> <method_index> <reg> 00操作码 (1 字节) | 寄存器数量 (1 字节) | 方法索引 (2 字节) | 目标寄存器 (1 字节) | 填充字节,指令对齐 (1 字节)
  • 71:操作码,表示 invoke-static。

  • <reg_count>:寄存器数量,参数个数。

  • <method_index>:目标方法在方法表中的索引。

  • :目标寄存器,表示要将传参存储到的寄存器。

  • 00:填充字节,指令对齐

实现 invoke 指令,需要根据指令中的 method index 从 dex 中找到 method,然后通过 jni 接口发起调用。

word/media/image4.png

具体代码实现

// 解析并执行 invoke-static 指令
void handleInvokeStatic(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode != INVOKE_STATIC_OPCODE) {  // 检查是否为 invoke-staticthrow std::runtime_error("Unexpected opcode for invoke-static");}// 第 5 个字节表示了要使用的寄存器uint8_t reg1 = bytecode[pc + 4] & 0xF;         // 低4位表示第一个寄存器uint8_t reg2 = (bytecode[pc + 4] >> 4) & 0xF;  // 高4位表示第二个寄存器// 读取方法索引(第 2、3、4 字节)uint32_t methodIndex = (bytecode[pc + 1] << 16) | (bytecode[pc + 2] << 8) | bytecode[pc + 3];// 类名和方法信息std::string className;std::string methodName;std::string methodSignature;// 根据 methodIndex 来解析并设置类名、方法名、签名switch (methodIndex) {case 0x202000:  // checkNotNullParameterclassName = "kotlin/jvm/internal/Intrinsics";methodName = "checkNotNullParameter";methodSignature = "(Ljava/lang/Object;Ljava/lang/String;)V";break;case 0x101c00:  // getInstance (MessageDigest)className = "java/security/MessageDigest";methodName = "getInstance";methodSignature = "(Ljava/lang/String;)Ljava/security/MessageDigest;";break;case 0x201f00:  // checkNotNullExpressionValueclassName = "kotlin/jvm/internal/Intrinsics";methodName = "checkNotNullExpressionValue";methodSignature = "(Ljava/lang/Object;Ljava/lang/String;)V";break;case 0x001e00:  // getEncoder (Base64)className = "java/util/Base64";methodName = "getEncoder";methodSignature = "()Ljava/util/Base64$Encoder;";break;default:throw std::runtime_error("Unknown method index");}// 获取目标类jclass targetClass = env->FindClass(className.c_str());if (targetClass == nullptr) {throw std::runtime_error("Class not found: " + className);}// 获取方法 IDjmethodID methodID = env->GetStaticMethodID(targetClass, methodName.c_str(), methodSignature.c_str());if (methodID == nullptr) {throw std::runtime_error("Method not found: " + methodName);}// 解析方法签名,得到参数个数和返回值类型std::vector<std::string> paramTypes;std::string returnType;parseMethodSignature(methodSignature, paramTypes, returnType);int paramCount = paramTypes.size();// 动态获取参数uint8_t reg_list[] = {reg1, reg2};std::vector <jstring> params(paramCount);for (size_t i = 0; i < paramCount; ++i) {// 获取寄存器中的值并转化为 JNI 参数jvalue value = getRegisterAsJValue(reg_list[i], paramTypes[i]);params[i] = static_cast<jstring>(value.l);}// 更新程序计数器pc += 6;  // invoke-static 指令占用 6 字节// 调用静态方法// 根据返回值类型决定调用方式if (returnType == "V") {  // void 返回值if (paramCount == 0) {env->CallStaticVoidMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {env->CallStaticVoidMethod(targetClass, methodID, params[0]);} else {env->CallStaticVoidMethod(targetClass, methodID, params[0], params[1]);}} else if (returnType == "Z") {  // boolean 返回值jboolean boolResult;if (paramCount == 0) {boolResult = env->CallStaticBooleanMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {boolResult = env->CallStaticBooleanMethod(targetClass, methodID, params[0]);} else {boolResult = env->CallStaticBooleanMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, boolResult);} else if (returnType == "B") {  // byte 返回值jbyte byteResult;if (paramCount == 0) {byteResult = env->CallStaticByteMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {byteResult = env->CallStaticByteMethod(targetClass, methodID, params[0]);} else {byteResult = env->CallStaticByteMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, byteResult);} else if (returnType == "S") {  // short 返回值jshort shortResult;if (paramCount == 0) {shortResult = env->CallStaticShortMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {shortResult = env->CallStaticShortMethod(targetClass, methodID, params[0]);} else {shortResult = env->CallStaticShortMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, shortResult);} else if (returnType == "I") {  // int 返回值jint intResult;if (paramCount == 0) {intResult = env->CallStaticIntMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {intResult = env->CallStaticIntMethod(targetClass, methodID, params[0]);} else {intResult = env->CallStaticIntMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, intResult);} else if (returnType == "J") {  // long 返回值jlong longResult;if (paramCount == 0) {longResult = env->CallStaticLongMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {longResult = env->CallStaticLongMethod(targetClass, methodID, params[0]);} else {longResult = env->CallStaticLongMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, longResult);} else if (returnType == "F") {  // float 返回值jfloat floatResult;if (paramCount == 0) {floatResult = env->CallStaticFloatMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {floatResult = env->CallStaticFloatMethod(targetClass, methodID, params[0]);} else {floatResult = env->CallStaticFloatMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, floatResult);} else if (returnType == "D") {  // double 返回值jdouble doubleResult;if (paramCount == 0) {doubleResult = env->CallStaticDoubleMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {doubleResult = env->CallStaticDoubleMethod(targetClass, methodID, params[0]);} else {doubleResult = env->CallStaticDoubleMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, doubleResult);} else if (returnType[0] == 'L') {  // 对象返回值jobject objResult;if (paramCount == 0) {objResult = env->CallStaticObjectMethod(targetClass, methodID);  // 无参数} else if (paramCount == 1) {objResult = env->CallStaticObjectMethod(targetClass, methodID, params[0]);} else {objResult = env->CallStaticObjectMethod(targetClass, methodID, params[0], params[1]);}// 处理返回的对象if (objResult) {if(returnType == "Ljava/lang/String;"){jstring strResult = static_cast<jstring>(objResult);handleMoveResultObject(env, bytecode, pc, strResult);}else{handleMoveResultObject(env, bytecode, pc, objResult);}}} else {throw std::runtime_error("Unsupported return type: " + returnType);}
}

3. move-result-object

move-result-object 用于从方法调用的结果中将对象类型的返回值移动到指定的寄存器中。例如:

move-result-object v0

解释:

  • move-result-object:这条指令的作用是将最近一次方法调用的返回结果移动到指定的寄存器中。

  • v0:指定目标寄存器,返回的对象会被存储在 v0 寄存器中。

指令结构

一个标准的 move-result-object 字节码指令通常如下所示(2个字节):

0c <reg>操作码 (1 字节)  | 目标寄存器 (1 字节)  

具体代码实现

// move-result-object
template <typename T>
void handleMoveResultObject(JNIEnv *env, const uint8_t *bytecode, size_t &pc, T result) {uint8_t opcode = bytecode[pc];if (opcode == MOVE_RESULT_OBJECT_OPCODE) {uint8_t reg = bytecode[pc + 1];  // 目标寄存器setRegisterValue(reg, result);// 更新程序计数器pc += 2;  // move-result-object 指令占用 2 字节}
}

4. sget-object

sget-object 是一条静态字段读取指令。它用于从一个类的静态字段中获取一个引用类型(对象)的值,并存储到指定的寄存器中。

例如:

sget-object v1, Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;

解释:

  • sget-object:表示从类的静态字段中获取对象类型的值。

  • v1:目标寄存器,指令执行后,字段值(一个对象)会被存储在 v1 寄存器中。

  • Lkotlin/text/Charsets;:目标类的名称。

  • ->UTF_8:表示静态字段 UTF_8。

  • :Ljava/nio/charset/Charset;:字段的类型描述符,表示该字段的类型是 java.nio.charset.Charset。

指令结构

一个标准的 sget-object 字节码指令通常如下所示(4个字节):

62 <reg> <field_index>操作码 (1 字节)  | 目标寄存器 (1 字节)  | 字段索引 (2 字节)  

具体代码实现

// 解析和执行 sget-object 指令
void handleSgetObject(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode != SGET_OBJECT_OPCODE) {  // 检查是否为 sget-objectthrow std::runtime_error("Unexpected opcode for sget-object");}// 解析指令uint8_t reg = bytecode[pc + 1];          // 目标寄存器uint16_t fieldIndex = (bytecode[pc + 2] << 8) | bytecode[pc + 3]; // 字段索引// 类名和方法信息std::string className;std::string fieldName;std::string fieldType;// 解析每条指令,依据方法的不同来设置类名、方法名、签名switch (fieldIndex) {case 0x0900:  // Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;className = "kotlin/text/Charsets";fieldName = "UTF_8";fieldType = "Ljava/nio/charset/Charset;"; // 字段类型为 Charsetbreak;default:throw std::runtime_error("Unknown field index");}// 1. 获取 Java 类jclass clazz = env->FindClass(className.c_str());if (clazz == nullptr) {LOGI("Failed to find class %s", className.c_str());return;}// 2. 获取静态字段的 Field IDjfieldID fieldID = env->GetStaticFieldID(clazz, fieldName.c_str(), fieldType.c_str());if (fieldID == nullptr) {LOGI("Failed to get field ID for %s", fieldName.c_str());return;}// 3. 获取静态字段的值jobject field = env->GetStaticObjectField(clazz, fieldID);if (field == nullptr) {LOGI("%s field is null", fieldName.c_str());return;}// 保存到目标寄存器setRegisterValue(reg, field);// 更新程序计数器pc += 4; // sget-object 指令占用 4 字节
}

5. invoke-virtual

invoke-virtual 指令会调用指定对象的实例方法。例如

invoke-virtual {v5, v1}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B

解释:

  • invoke-virtual:表示调用对象的实例方法。

  • {v5, v1}:传递给目标方法的参数寄存器。这里,v5 和 v1 寄存器的值会作为参数传递给方法。

  • Ljava/lang/String;:目标类的名称。

  • ->getBytes:目标方法的名称。

  • (Ljava/nio/charset/Charset;):方法的参数签名。

  • [B:方法的返回类型签名,表示该方法返回一个字节数组。

指令结构

一个标准的 invoke-virtual 字节码指令通常如下所示(6个字节):

6e <reg_count> <method_index> <reg> 00操作码 (1 字节) | 寄存器数量 (1 字节) | 方法索引 (2 字节) | 目标寄存器 (1 字节) | 填充字节,指令对齐 (1 字节)
  • 6e:操作码,表示 invoke-static。

  • <reg_count>:寄存器数量,参数个数。

  • <method_index>:目标方法在方法表中的索引。

  • :目标寄存器,表示要将传参存储到的寄存器。

  • 00:填充字节,指令对齐

具体代码实现

// invoke-virtual 指令
void handleInvokeVirtual(JNIEnv* env, const uint8_t* bytecode, size_t& pc) {// 解析指令uint8_t opcode = bytecode[pc];  // 获取操作码if (opcode != INVOKE_VIRTUAL_OPCODE) {  // 确保是 invoke-virtual 操作码throw std::runtime_error("Expected invoke-virtual opcode");}// 获取寄存器数量uint8_t regCount = (bytecode[pc + 1] >> 4) & 0xF;// 第 5 个字节表示了要使用的寄存器uint8_t reg1 = bytecode[pc + 4] & 0xF;         // 低4位表示第一个寄存器uint8_t reg2 = (bytecode[pc + 4] >> 4) & 0xF;  // 高4位表示第二个寄存器// 读取方法索引(第 2、3、4 字节)uint32_t methodIndex = (bytecode[pc + 1] << 16) | (bytecode[pc + 2] << 8) | bytecode[pc + 3];// 类名和方法信息std::string className;std::string methodName;std::string methodSignature;// 根据 methodIndex 来解析并设置类名、方法名、签名switch (methodIndex) {case 0x201600:  // Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[BclassName = "java/lang/String";methodName = "getBytes";methodSignature = "(Ljava/nio/charset/Charset;)[B";break;case 0x201b00:  // Ljava/security/MessageDigest;->digest([B)[BclassName = "java/security/MessageDigest";methodName = "digest";methodSignature = "([B)[B";break;case 0x201d00:  // Ljava/util/Base64$Encoder;->encodeToString([B)Ljava/lang/String;className = "java/util/Base64$Encoder";methodName = "encodeToString";methodSignature = "([B)Ljava/lang/String;";break;default:throw std::runtime_error("Unknown method index: " + std::to_string(methodIndex));}// 查找类和方法jclass clazz = env->FindClass(className.c_str());if (!clazz) {throw std::runtime_error("Class not found: " + className);}// 获取方法 IDjmethodID methodID = env->GetMethodID(clazz, methodName.c_str(), methodSignature.c_str());if (!methodID) {throw std::runtime_error("Method not found: " + methodName);}// 解析方法签名,得到参数个数和返回值类型std::vector<std::string> paramTypes;std::string returnType;parseMethodSignature(methodSignature, paramTypes, returnType);int paramCount = paramTypes.size();// 目标对象的类型std::stringstream ss;ss << "L" << className << ";";std::string classType = ss.str();// 获取目标对象(寄存器中的第一个参数,通常是方法的目标对象)jobject targetObject = getRegisterAsJValue(reg1, classType).l;// 参数std::vector <jvalue> params(paramCount);if(paramCount > 0){params[0] = getRegisterAsJValue(reg2, paramTypes[0]);}// 更新程序计数器pc += 6;// 检查返回值的类型,并调用适当的方法if (returnType == "V") {  // 如果没有返回值 (void 方法)// 调用 void 方法env->CallVoidMethodA(targetObject, methodID, params.data());} else if (returnType == "[B") {  // 如果返回值是 byte 数组jbyteArray result = (jbyteArray) env->CallObjectMethodA(targetObject, methodID, params.data());// 处理返回的 byte 数组if (result) {handleMoveResultObject(env, bytecode, pc, result);}} else if (returnType[0] == 'L') {  // 如果返回值是对象jobject objResult = env->CallObjectMethodA(targetObject, methodID, params.data());// 处理返回的对象if (objResult) {if(returnType == "Ljava/lang/String;"){jstring strResult = static_cast<jstring>(objResult);handleMoveResultObject(env, bytecode, pc, strResult);}else{handleMoveResultObject(env, bytecode, pc, objResult);}}} else if (returnType == "I") {  // 如果返回值是 intjint result = env->CallIntMethodA(targetObject, methodID, params.data());// 处理返回的 inthandleMoveResultObject(env, bytecode, pc, result);} else if (returnType == "Z") {  // 如果返回值是 booleanjboolean result = env->CallBooleanMethodA(targetObject, methodID, params.data());// 处理返回的 booleanhandleMoveResultObject(env, bytecode, pc, result);} else if (returnType == "D") {  // 如果返回值是 doublejdouble result = env->CallDoubleMethodA(targetObject, methodID, params.data());// 处理返回的 doublehandleMoveResultObject(env, bytecode, pc, result);} else if (returnType == "F") {  // 如果返回值是 floatjfloat result = env->CallFloatMethodA(targetObject, methodID, params.data());// 处理返回的 floathandleMoveResultObject(env, bytecode, pc, result);} else {throw std::runtime_error("Unsupported return type in method: " + returnType);}
}

6. return-object

这条指令通常用于结束一个方法的执行,并将指定寄存器中的对象作为返回值返回给调用者。

例如:

return-object v2

解释:

  • return-object:表示方法执行结束时,返回一个对象类型的值。

  • v2:表示返回的对象存储在寄存器 v2 中。执行这条指令时,寄存器 v2 中的对象将作为方法的返回值。

指令结构

一个标准的 return-object 字节码指令通常如下所示(2个字节):

11 <reg>操作码 (1 字节)  | 目标寄存器 (1 字节)  

具体代码实现

// return-object
void handleReturnResultObject(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode == RETURN_OBJECT_OPCODE) {uint8_t reg = bytecode[pc + 1];  // 目标寄存器// 把目标寄存器中的值设置到 v0 寄存器setRegisterValue(0, registers[reg]);// 更新程序计数器pc += 2;}
}

注册解析器

在 kotlin 层中定义 VMP 入口方法 execute

package com.cyrus.example.vmpclass SimpleVMP {companion object {// 加载本地库init {System.loadLibrary("vmp-lib")}// 定义静态方法 execute@JvmStaticexternal fun execute(bytecode: ByteArray, input: String): String}
}

在 JNI_Onload 中调用 RegisterNatives 方法动态注册 C++ 中的 execute 方法到 com/cyrus/example/vmp/SimpleVMP

// 定义方法签名
static JNINativeMethod gMethods[] = {{"execute", "([BLjava/lang/String;)Ljava/lang/String;", (void*)execute}
};// JNI_OnLoad 动态注册方法
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env = nullptr;if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {return JNI_ERR;}jclass clazz = env->FindClass("com/cyrus/example/vmp/SimpleVMP");if (clazz == nullptr) {return JNI_ERR; // 类未找到}// 注册所有本地方法jint result = env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0]));if (result != JNI_OK) {return JNI_ERR; // 注册失败}return JNI_VERSION_1_6;
}

测试

把 sign 方法的调用改为通过 VMP 执行 sign 算法计算 input 参数的加密结果。

// 参数
val input = "example"// 模拟 smali 指令的字节流
val bytecode = byteArrayOf(0x1A, 0x00, 0x4E, 0x00, // const-string v0, "input"0x71, 0x20, 0x20, 0x00, 0x05, 0x00, // invoke-static{v5, v0}, checkNotNullParameter0x1A, 0x00, 0x2C, 0x00, // const-string v0, "SHA-256"0x71, 0x10, 0x1C, 0x00, 0x00, 0x00, // invoke-static{v0}, getInstance0x0C, 0x00, // move-result-object v00x62, 0x01, 0x09, 0x00, // sget-object v1, UTF_80x6E, 0x20, 0x16, 0x00, 0x15, 0x00, // invoke-virtual{v5, v1}, getBytes0x0C, 0x01, // move-result-object v10x6E, 0x20, 0x1B, 0x00, 0x10, 0x00, // invoke-virtual{v0, v1}, digest0x0C, 0x01, // move-result-object v10x71, 0x00, 0x1E, 0x00, 0x00, 0x00, // invoke-static{}, getEncoder0x0C, 0x02, // move-result-object v20x6E, 0x20, 0x1D, 0x00, 0x12, 0x00, // invoke-virtual{v2, v1}, encodeToString0x0C, 0x02, // move-result-object v20x11, 0x02  // return-object v2
)// 通过 VMP 解析器执行指令流
val result = SimpleVMP.execute(bytecode, input)// 显示 Toast
Toast.makeText(this, result, Toast.LENGTH_SHORT).show()

通过 VMP 执行结果如下:

word/media/image5.png

和原来算法对比结果是一样的。

word/media/image6.png

安全性增强

  1. 指令流加密:比如使用 AES 加密指令流,在运行时解密执行。

  2. 动态加载:使用 dex 动态加载虚拟机和指令流。

  3. 多态指令集:每次保护代码时动态生成不同的指令集,防止通过固定指令集逆向

  4. 反调试检测:检测调试器附加、内存修改或运行环境,防止虚拟机被分析。

优点与局限

优点

  • 提高逆向难度:通过指令集和虚拟机隐藏关键逻辑。

  • 动态保护:运行时加载和执行,防止静态分析。

局限

  • 性能开销:解释执行比原生代码慢。

  • 开发成本:需要设计和实现虚拟机框架。

通过上述方法,可以实现一个基本的自定义 Android 虚拟机保护,并根据需要逐步增强安全性。

源码

完整源码:https://github.com/CYRUS-STUDIO/AndroidExample


http://www.ppmy.cn/news/1563574.html

相关文章

Franka例程学习——examples_common

这一次我们学习Franka所有例程里面都要调用的examples_common.h和examples_common.cpp&#xff0c;一个是.h头文件放置声明的函数、类、变量以及宏等内容&#xff0c;.c文件里面是具体的函数实现。 一、源代码 examples_common.h // Copyright (c) 2017 Franka Emika GmbH /…

字节二面:了解环形队列吗?有哪些使用场景?

大家好&#xff0c;我是君哥。 在日常开发工作中&#xff0c;环形队列的使用并不多&#xff0c;但其实环形队列是一个很有用的数据结构&#xff0c;而且有不少使用场景。今天来聊一聊环形队列的使用场景。 1.环形队列 队列这个数据结构最大的特点就是先进先出&#xff0c;它…

少一点If/Else - 状态模式(State Pattern)

状态模式&#xff08;State Pattern&#xff09; 状态模式&#xff08;State Pattern&#xff09;状态模式&#xff08;State Pattern&#xff09;概述状态模式&#xff08;State Pattern&#xff09;结构图状态模式&#xff08;State Pattern&#xff09;涉及的角色 talk is c…

xxl-job的使用历程

一.为什么会用xxl-job 项目要求&#xff0c;单开一个服务专门跑定时任务&#xff0c;不使用框架自带的&#xff0c;而选择技术中台的xxl-job进行集成使用。 二.集成过程报错 按照文档进行集成&#xff0c;发现各种报错&#xff0c;联系技术中台&#xff0c;回复的是我们自己…

ESP32学习笔记_FreeRTOS(5)——Mutex

摘要(From AI): 这篇博客内容围绕 FreeRTOS 中的**互斥量&#xff08;Mutex&#xff09;和递归互斥量&#xff08;Recursive Mutex&#xff09;**的使用进行了详细的介绍。整体结构清晰&#xff0c;涵盖了互斥量的基本概念、使用方式以及与其他同步机制&#xff08;如二进制信号…

Django缓存系统详解:使用Redis提升应用性能

1. 引言 在现代Web开发中,性能优化是一个永恒的主题。随着用户数量的增加和功能的复杂化,如何保持应用的高效运行成为了开发者面临的主要挑战之一。Django,作为一个成熟的Web框架,提供了强大的缓存系统来应对这一挑战。本文将深入探讨Django缓存系统,特别关注如何使用Red…

安装本地测试安装apache-doris

一、安装前规划 我的服务器是三台麒麟服务器,2台跑不起来,这是我本地的,内存分配的也不多。 fe192.168.1.13 主数据库端口9030访问 8Gbe192.168.1.13内存4G 硬盘50be192.168.1.14内存4G 硬盘50be192.168.1.12内存4G 硬盘5013同时安装的fe和be 。 原理:192.168.1.13 服…

2025windows环境下安装RabbitMQ

官网下载地址&#xff1a; Installing on Windows | RabbitMQ下载速度挺快的加速推荐 &#xff1a; BitComet(比特彗星) 官网下载 &#xff1a; 先完成 exe 的下载&#xff0c;参考对照关系&#xff0c;下载对应Erlang 参考关系对照表完成下载 &#xff1a; OTP Versions Tre…