详解如何自定义 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.vmp

import java.security.MessageDigest
import java.util.Base64

object 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-static
        throw 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:  // checkNotNullParameter
            className = "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:  // checkNotNullExpressionValue
            className = "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);
    }

    // 获取方法 ID
    jmethodID 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-result
        handleMoveResultObject(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-result
        handleMoveResultObject(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-result
        handleMoveResultObject(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-result
        handleMoveResultObject(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-result
        handleMoveResultObject(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-result
        handleMoveResultObject(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-result
        handleMoveResultObject(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-object
        throw 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;"; // 字段类型为 Charset
            break;
        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 ID
    jfieldID 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;)[B
            className = "java/lang/String";
            methodName = "getBytes";
            methodSignature = "(Ljava/nio/charset/Charset;)[B";
            break;
        case 0x201b00:  // Ljava/security/MessageDigest;->digest([B)[B
            className = "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);
    }

    // 获取方法 ID
    jmethodID 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") {  // 如果返回值是 int
        jint result = env->CallIntMethodA(targetObject, methodID, params.data());
        // 处理返回的 int
        handleMoveResultObject(env, bytecode, pc, result);
    } else if (returnType == "Z") {  // 如果返回值是 boolean
        jboolean result = env->CallBooleanMethodA(targetObject, methodID, params.data());
        // 处理返回的 boolean
        handleMoveResultObject(env, bytecode, pc, result);
    } else if (returnType == "D") {  // 如果返回值是 double
        jdouble result = env->CallDoubleMethodA(targetObject, methodID, params.data());
        // 处理返回的 double
        handleMoveResultObject(env, bytecode, pc, result);
    } else if (returnType == "F") {  // 如果返回值是 float
        jfloat result = env->CallFloatMethodA(targetObject, methodID, params.data());
        // 处理返回的 float
        handleMoveResultObject(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.vmp

class SimpleVMP {

    companion object {
        // 加载本地库
        init {
            System.loadLibrary("vmp-lib")
        }

        // 定义静态方法 execute
        @JvmStatic
        external 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}, checkNotNullParameter
    0x1A, 0x00, 0x2C, 0x00, // const-string v0, "SHA-256"
    0x71, 0x10, 0x1C, 0x00, 0x00, 0x00, // invoke-static{v0}, getInstance
    0x0C, 0x00, // move-result-object v0
    0x62, 0x01, 0x09, 0x00, // sget-object v1, UTF_8
    0x6E, 0x20, 0x16, 0x00, 0x15, 0x00, // invoke-virtual{v5, v1}, getBytes
    0x0C, 0x01, // move-result-object v1
    0x6E, 0x20, 0x1B, 0x00, 0x10, 0x00, // invoke-virtual{v0, v1}, digest
    0x0C, 0x01, // move-result-object v1
    0x71, 0x00, 0x1E, 0x00, 0x00, 0x00, // invoke-static{}, getEncoder
    0x0C, 0x02, // move-result-object v2
    0x6E, 0x20, 0x1D, 0x00, 0x12, 0x00, // invoke-virtual{v2, v1}, encodeToString
    0x0C, 0x02, // move-result-object v2
    0x11, 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/954669.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

rabbitmqp安装延迟队列

在RabbitMQ中&#xff0c;延迟队列是一种特殊的队列类型。当消息被发送到此类队列后&#xff0c;不会立即投递给消费者&#xff0c;而是会等待预设的一段时间&#xff0c;待延迟期满后才进行投递。这种队列在多种场景下都极具价值&#xff0c;比如可用于处理需要在特定时间触发…

向量数据库如何助力Text2SQL处理高基数类别数据

01. 导语 Agent工作流和 LLMs &#xff08;大语言模型&#xff09;的出现&#xff0c;让我们能够以自然语言交互的模式执行复杂的SQL查询&#xff0c;并彻底改变Text2SQL系统的运行方式。其典型代表是如何处理High-Cardinality Categorical Data &#xff08;高基数类别数据&am…

Docker实践:部署Docker管理工具DockerUI

Docker实践&#xff1a;部署Docker管理工具DockerUI 前言一、DockerUI介绍1.1 DockerUI概述1.2 镜像说明 二、检查本地Docker环境三、拉取DockerUI镜像四、创建DockerUI容器五、访问DockerUI六、DockerUI的基本使用6.1 查询宿主机容器情况6.2 查询Docker镜像列表6.3 查看容器配…

【excel】VBA股票数据获取(搜狐股票)

文章目录 一、序二、excel 自动刷新股票数据三、付费获取 一、序 我其实不会 excel 的函数和 visual basic。因为都可以用matlab和python完成。 今天用了下VBA&#xff0c;还挺不错的。分享下。 上传写了个matlab获取股票数据的&#xff0c;是雅虎财经的。这次是搜狐股票的数…

解锁企业数据管理统一身份认证难题,EasyMR助力企业敏捷提效

在数字经济迅猛发展的当下&#xff0c;企业数据量正以令人惊叹的速度持续增长。据IDC研究显示&#xff0c;至2025年&#xff0c;全球数据总量预计将超175 ZB。数据的爆发式增长对企业而言&#xff0c;既是机遇&#xff0c;更是巨大挑战。 如今&#xff0c;大数据已然成为企业决…

IntelliJ IDEA Type Hierarchy Scope Pattern 学习指南

IntelliJ IDEA Type Hierarchy Scope Pattern 学习指南 什么是 Type Hierarchy&#xff1f; Type Hierarchy 是 IntelliJ IDEA 提供的一个工具&#xff0c;允许开发者查看某个类的继承关系及其实现的接口结构。它是理解类关系的重要工具&#xff0c;尤其在处理复杂的继承体系…

ukui-quick 计数器

作品简介 使用ukui-quick框架进行开发&#xff0c;实现了在任务栏中计数器的插件&#xff0c;方便用户的日常使用。 技术架构 用于实现一个具有点击计数功能的QML应用程序。这个架构将包括C后端和QML前端&#xff0c;通过Qt的信号和属性绑定机制进行交互。 实现过程 开发环…

Flutter:封装ActionSheet 操作菜单

演示效果图 action_sheet_util.dart import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import package:demo/common/index.dart;class ActionSheetUtil {/// 底部操作表/// [context] 上下文/// [title] 标题/// [items] 选项列表 …

【混合开发】CefSharp+Vue 解决Cookie问题

问题表现 使用Element-admin架构搭建Vue前端项目&#xff0c;在与CefSharp搭配时&#xff0c;出现无法使用cookie的问题。 无法将token存入cookiecookie无法被读取 如下图&#xff0c;Cookies下显示file://。 正常的Cookies显示&#xff0c;Cookies显示为http://域名&#x…

IIO(Industrial I/O)驱动介绍

文章目录 IIO&#xff08;Industrial I/O&#xff09;驱动是Linux内核中用于工业I/O设备的子系统&#xff0c;主要用于处理传感器数据采集和转换。以下是其关键点&#xff1a; 功能 数据采集&#xff1a;从传感器读取数据。数据处理&#xff1a;对原始数据进行滤波、校准等操作…

Flutter插件制作、本地/远程依赖及缓存机制深入剖析(原创-附源码)

Flutter插件在开发Flutter项目的过程中扮演着重要的角色&#xff0c;我们从 ​​​​​​https://pub.dev 上下载添加到项目中的第三方库都是以包或者插件的形式引入到代码中的&#xff0c;这些第三方工具极大的提高了开发效率。 深入的了解插件的制作、发布、工作原理和缓存机…

C#轻松实现条形码二维码生成及识别

一、前言 大家好&#xff01;我是付工。 今天给大家分享一下&#xff0c;如何基于C#来生成并识别条形码或者二维码。 二、ZXing.Net 实现二维码生成的库有很多&#xff0c;我们这里采用的是http://ZXing.Net。 ZXing是一个开放源码的&#xff0c;用Java实现的多种格式的一…

一些常见的Java面试题及其答案

Java基础 1. Java中的基本数据类型有哪些&#xff1f; 答案&#xff1a;Java中的基本数据类型包括整数类型&#xff08;byte、short、int、long&#xff09;、浮点类型&#xff08;float、double&#xff09;、字符类型&#xff08;char&#xff09;和布尔类型&#xff08;boo…

PyTorch框架——基于深度学习YOLOv5神经网络水果蔬菜检测识别系统

基于深度学习YOLOv5神经网络水果蔬菜检测识别系统&#xff0c;其能识别的水果蔬菜有15种&#xff0c;# 水果的种类 names: [黑葡萄, 绿葡萄, 樱桃, 西瓜, 龙眼, 香蕉, 芒果, 菠萝, 柚子, 草莓, 苹果, 柑橘, 火龙果, 梨子, 花生, 黄瓜, 土豆, 大蒜, 茄子, 白萝卜, 辣椒, 胡萝卜,…

用css和html制作太极图

目录 css相关参数介绍 边距 边框 伪元素选择器 太极图案例实现、 代码 效果 css相关参数介绍 边距 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><style>*{margin: 0;padding: 0;}div{width: …

WPF、控件模板(ControlTemplate)和数据模板(DataTemplate)

前言 在 WPF 中&#xff0c;控件种类丰富且功能非常完善。一个显著的优点是 WPF 提供了强大的自定义能力和灵活的用户界面表现&#xff0c;能够满足各种复杂的应用需求。其中&#xff0c;ControlTemplate 和 DataTemplate 是两个非常重要的概念&#xff0c;分别用于自定义控件…

RAG实战_01代码生成_02智能检索

整理了RAG案例的Git代码 https://github.com/LGRY/RAG_Tutorial/tree/main 【注意事项】 01 代码生成系统源代码中使用的weaviate向量数据库&#xff0c;不支持window系统&#xff0c;建议换系统/换向量数据库02 智能检索系统 同样需要配置向量数据库&#xff0c;可以先安…

【Linux系统编程】—— 自动化构建工具Makefile指南

文章目录 背景基本使用推导过程适度扩展语法 背景 Makefile 是衡量开发者是否具备完成大型工程能力的一个重要标志。在一个工程中&#xff0c;源文件的数量可能极多&#xff0c;这些文件会按照类型、功能或模块分布在多个目录中。Makefile 通过定义一系列规则&#xff0c;指定…

【JavaWeb01】JavaWeb开发基础:HTML的深度解析与应用

文章目录 前言&#x1f30d;一.B/S 软件开发架构简述&#x1f30d;二.HTML 介绍❄️2.1 官方文档❄️2.2 网页的组成❄️2.3 HTML 是什么❄️2.4html基本结构 &#x1f30d;三.HTML标签1.html 的标签/元素-说明2. html 标签注意事项和细节3.font 字体标签4.标题标签5.超链接标签…

Android-目前最稳定和高效的UI适配方案

谈到适配&#xff0c;首先需要介绍几个基本单位&#xff1a; 1、密度无关像素&#xff08;dp&#xff09;&#xff1a; 含义&#xff1a;density-independent pixel&#xff0c;叫dp或dip&#xff0c;与终端上的实际物理像素点无关 单位&#xff1a;dp&#xff0c;可以保证在…