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

版权归作者所有,如有转发,请注明文章出处: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.mzph.cn/web/65970.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于华为atlas的重车(满载)空车(空载)识别

该教程主要是想摸索出华为atlas的基于ACL的推理模式。最终实现通过煤矿磅道上方的摄像头&#xff0c;识别出车辆的重车&#xff08;满载&#xff09;、空车&#xff08;空载&#xff09;情况。本质上是一个简单的检测问题。 但是整体探索过程比较坎坷&#xff0c;Tianxiaomo的…

《零基础Go语言算法实战》【题目 2-25】goroutine 的执行权问题

《零基础Go语言算法实战》 【题目 2-25】goroutine 的执行权问题 请说明以下这段代码为什么会卡死。 package main import ( "fmt" "runtime" ) func main() { var i byte go func() { for i 0; i < 255; i { } }() fmt.Println("start&quo…

IntelliJ IDEA中Maven项目的配置、创建与导入全攻略

大家好&#xff0c;我是袁庭新。 IntelliJ IDEA是当前最流行的Java IDE&#xff08;集成开发环境&#xff09;之一&#xff0c;也是业界公认最好用的Java开发工具之一。IntelliJ IDEA支持Maven的全部功能&#xff0c;通过它我们可以很轻松地实现创建Maven项目、导入Maven项目、…

TypeScript语言的学习路线

TypeScript语言的学习路线 TypeScript&#xff08;TS&#xff09;是由Microsoft开发的一种开源编程语言&#xff0c;是JavaScript的超集&#xff0c;提供了严格的类型检查和基于类的面向对象编程特性。随着前端开发的不断进步&#xff0c;TypeScript逐渐成为了现代前端开发的主…

计算机网络之---静态路由与动态路由

静态路由 静态路由是由网络管理员手动配置并固定的路由方式。路由器通过静态配置的路由条目来转发数据包&#xff0c;而不会自动调整。它不依赖于任何路由协议。 特点&#xff1a; 手动配置&#xff1a;网络管理员需要手动在路由器中配置每条静态路由。不自动更新&#xff1a;…

【Rust】函数

目录 思维导图 1. 函数的基本概念 1.1 函数的定义 2. 参数的使用 2.1 单个参数的示例 2.2 多个参数的示例 3. 语句与表达式 3.1 语句与表达式的区别 3.2 示例 4. 带返回值的函数 4.1 返回值的示例 4.2 返回值与表达式 5. 错误处理 5.1 错误示例 思维导图 1. 函数…

Cython全教程2 多种定义方式

—— 本篇文章&#xff0c;主要讲述Cython中的四种定义关键字 全教程2 多种定义方式&#xff1a; 在Cython中&#xff0c;关于定义的关键字有四个&#xff0c;分别是&#xff1a; cdef、def、cpdef、DEF 一、cdef定义关键字 顾名思义&#xff0c;cdef关键字定义的是一个C函数…

Web开发(一)HTML5

Web开发&#xff08;一&#xff09;HTML5 写在前面 参考黑马程序员前端Web教程做的笔记&#xff0c;主要是想后面自己搭建网页玩。 这部分是前端HTML5CSS3移动web视频教程的HTML5部分。主要涉及到HTML的基础语法。 HTML基础 标签定义 HTML定义 HTML(HyperText Markup Lan…

MATLAB学习笔记目录

MATLAB学习笔记-生成纯音并保存-CSDN博客 MATLAB学习笔记-各种格式之间的转换 - 知乎 MATLAB学习笔记-胞组&#xff08;cell array&#xff09;转换为矩阵&#xff0c;cell2mat_matlab如何把元胞数组改为矩阵-CSDN博客MATLAB学习笔记-判断数组、结构体、数值、字符串是否相同…

Java-数据结构-栈与队列(常考面试题与单调栈)

在上一篇的学习中&#xff0c;我们学习了栈和队列的基本知识&#xff0c;以及它们对应都有哪些方法&#xff0c;在什么应用场景下如何使用&#xff0c;并且还对它们进行了模拟实现&#xff0c;而其实对于栈和队列的相关知识还远不止于此&#xff0c;而今天我们就对栈与队列进行…

JSON.stringify(res,null,2)的含义

JSON.stringify(res, null, 2) 是 JavaScript 中将对象转换为 JSON 字符串的方法&#xff0c;具体说明如下&#xff1a; 参数解释 res&#xff1a;要转换的对象。它可以是 JavaScript 中的任意类型&#xff0c;如对象、数组、字符串、数字等。例如&#xff0c;{name: "K…

Spring 项目 基于 Tomcat容器进行部署

文章目录 一、前置知识二、本地Idea运行Spring项目1. 将写好的 Spring 项目先打包成 war 包2. 查看项目工件&#xff08;Artifact&#xff09;是否存在3. 配置 Tomcat3.1 添加一个本地 Tomcat 容器3.2 将项目部署到 Tomcat 4. 运行项目 三、基于 Tomcat 部署及多实例部署1. Spr…

usbredir学习

文章目录 背景典型场景编译usbredirparserusbredirfilterusbredirparser/usbredirproto usbredirhostusbredirect/usbredirtestclient参考 背景 usbredir 是一种用于通过网络转发 USB 设备流量的网络协议。它也是一个软件包的名称&#xff0c;该软件包提供了一个解析库、一个 …

ESXI 安装教程(3) ---​vCenter Server 安装

不涉及复杂的操作此项可不安装 1.镜像加载到虚拟光盘 对应的网盘文件 2.打开文件路径 双击运行文件installer.exe 3.调整安装语言 4.点击安装 5. 6. 证书,有效问题导致此提示,非专业网络管理人员,不知道如何处理,此处点是即可 证书有效开始时间是安装时间8小时 证书有效结束…

【初识扫盲】逆概率加权

我们正在处理一个存在缺失数据的回归模型&#xff0c;并且希望采用一种非参数的逆概率加权方法来调整估计&#xff0c;以应对这种缺失数据的情况。 首先&#xff0c;我们需要明确问题的背景。我们有样本 { ( Y i , X i , r i ) : i 1 , … , n } \left\{\left(Y_i, \boldsym…

极客说|Azure AI Agent Service 结合 AutoGen/Semantic Kernel 构建多智能体解决⽅案

作者&#xff1a;卢建晖 - 微软高级云技术布道师 「极客说」 是一档专注 AI 时代开发者分享的专栏&#xff0c;我们邀请来自微软以及技术社区专家&#xff0c;带来最前沿的技术干货与实践经验。在这里&#xff0c;您将看到深度教程、最佳实践和创新解决方案。关注「极客说」&am…

【集成学习】Boosting算法详解

文章目录 1. 集成学习概述2. Boosting算法详解3. Gradient Boosting算法详解3.1 基本思想3.2 公式推导 4. Python实现 1. 集成学习概述 集成学习&#xff08;Ensemble Learning&#xff09;是一种通过结合多个模型的预测结果来提高整体预测性能的技术。相比于单个模型&#xf…

小米vela系统(基于开源nuttx内核)——如何使用信号量进行PV操作

如何使用信号量进行PV操作 前言信号量1. 信号量简介2. NuttX中信号量的创建与使用2.1 Nuttx信号量的初始化和销毁2.2 信号量的等待和发布 3. 信号量的实际应用&#xff1a;下载任务示例3.1 实际代码3.2 代码说明3.3 执行说明 4. 信号量的优势与应用场景5. 常见应用场景&#xf…

CMake学习笔记(2)

1. 嵌套的CMake 如果项目很大&#xff0c;或者项目中有很多的源码目录&#xff0c;在通过CMake管理项目的时候如果只使用一个CMakeLists.txt&#xff0c;那么这个文件相对会比较复杂&#xff0c;有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt文件&#xff…

旅游网站设计与实现

文末附有完整项目代码 在当今数字化时代&#xff0c;旅游网站成为人们获取旅游信息的重要途径。本文将详细介绍旅游网站的设计与实现&#xff0c;让你轻松了解其中的技术奥秘&#xff01; 一、项目背景 随着社会经济的发展&#xff0c;人们对精神消费愈发重视&#xff0c;旅游…