GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- apk加固 之 vmp

vmp一句话来讲就是自定义字节码的虚拟机类似JVM,使得解析dex的工具失效从而大幅增加逆向分析的门槛。

详细来讲就是 ,将java字节码转义并将转义的解析器写入C++,解析器会解析java指令和数据,解析器会将java指令的寄存器、堆栈用c++技术模拟实现,并将最终的结果返回给java

有dex的vmp和so的dmp

 

技术实现原理核心阐述

1. 核心思想:代码虚拟化

Android VMP壳的核心思想是代码虚拟化。它不直接执行Android系统(Dalvik/ART虚拟机)能够识别的标准Dex字节码,而是创造了一个自定义的、私有的执行环境。

  • 原始逻辑:Java/Kotlin源码 -> 编译为 Dex字节码 -> ART/Dalvik虚拟机 解释/编译执行。
  • VMP保护后逻辑:Java/Kotlin源码 -> 编译为 Dex字节码 -> VMP转换器 -> 自定义字节码 -> 自定义虚拟机(解释器) 解释执行。

2. 两大关键组成部分

a. VMP转换层 (离线处理,通常在服务端完成)

  • 作用:充当“编译器”的角色,但目标是生成自定义指令。
  • 工作流程
    1. 分析:解析原始APK/Dex文件,定位到需要保护的关键方法(如文章中的sign方法)。
    2. 转换:将这些方法的原始Dex指令一条一条地映射为自己定义的一套自定义指令集(Custom IR)。这个映射关系是私有的,是VMP安全性的基础。
    3. 替换与存储
    • 将原始方法的方法体清空或替换为一个极其简单的外壳方法(Stub),这个外壳方法只有一个作用:调用本地库(Native Lib)中的虚拟机解释器。
    • 将上一步生成的自定义指令流和所需的字符串常量池等资源,加密后作为数据文件打包进APK,或隐藏在Dex的某个角落。

b. VMP虚拟机解释器 (运行时在Native层工作)

  • 作用:充当“CPU”的角色,负责执行自定义指令。
  • 工作流程
    1. 初始化:当外壳方法被调用时,它启动解释器。解释器会初始化执行环境,主要是模拟的寄存器数组(文章中用std::variant registers[10]实现)和字符串常量池
    2. 取指与解码:解释器从指定的位置(如传入的bytecode数组)读取自定义操作码(Opcode)
    3. 分支执行:根据读到的操作码,通过一个巨大的switch-case或函数表跳转到对应的指令处理函数(如handleConstString, handleInvokeStatic)。
    4. 执行与模拟:指令处理函数会从字节流中解码出操作数(如目标寄存器索引、常量池索引等),然后对模拟的寄存器进行读写操作,或通过JNI接口调用真实的Android系统方法,以此来模拟原始Dex指令的功能
    5. 循环:重复取指->解码->执行的过程,直到遇到return指令,将最终结果返回给调用者。

3. 安全性来源

  • 私有指令集:逆向分析者无法直接理解自定义字节码的含义,必须逆向分析整个解释器才能还原逻辑,难度极大。
  • 代码形态变化:原始逻辑被转化为一段无法直接阅读的数据(指令流)和一个通用的解释器,破坏了静态分析工具(如反编译器)的工作基础。
  • 抗逆向:即使分析者找到了解释器,也需要理解每一条自定义指令的语义,并写一个反编译器将自定义字节码“翻译”回可读的代码,工作量巨大。

举例说明

我们以SignUtil.sign()方法为例,看看VMP是如何一步步保护它的。

原始Smali代码片段:
smali
const-string v0, "SHA-256" # 将字符串"SHA-256"加载到寄存器v0
invoke-static {v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest; # 调用静态方法
move-result-object v0 # 将结果(MessageDigest实例)移动到v0
...

1. VMP转换层的工作:

转换器会分析这些指令,并为其生成对应的自定义字节码。假设我们定义:

  • 0x1A 对应 const-string
  • 0x71 对应 invoke-static
  • 0x0C 对应 move-result-object

转换器会生成如下字节码数据(用十六进制表示):

1A 00 2C 00 // const-string v0, string_pool[0x002C00] -> "SHA-256"
71 10 1C 00 00 00 // invoke-static {v0}, method_id[0x101C00] -> MessageDigest.getInstance
0C 00 // move-result-object v0

同时,它会建立一个字符串常量池,确保索引0x002C00对应字符串"SHA-256";建立一个方法索引表,确保0x101C00对应方法java/security/MessageDigest->getInstance。

最后,原始的sign方法被替换成一个空壳:
kotlin
// 这是保护后留在Dex里的方法体,非常简单
fun sign(input: String): String {
return SimpleVMP.execute(protectedSignBytecode, input) // 调用Native解释器
}

而上面生成的自定义字节码protectedSignBytecode会被加密存储。

2. VMP虚拟机解释器的工作:

当App运行到SimpleVMP.execute(...)时:

  1. Native函数execute被调用,初始化寄存器,并将输入参数input放入模拟寄存器的指定位置(如registers[5])。
  2. 开始循环读取protectedSignBytecode:
    • 读到第一个字节0x1A,switch-case跳转到handleConstString函数。
    • handleConstString读取后续字节,得知目标寄存器是v0(索引0),字符串索引是0x002C00。
    • 它查询模拟的字符串常量池,拿到字符串"SHA-256",然后通过JNI的NewStringUTF创建一个jstring,并将其存入registers[0]。
    • 程序计数器(PC) 向前移动4字节,指向下一条指令。
  3. 继续取下一条指令,第一个字节是0x71,跳转到handleInvokeStatic。
    • 该函数解析出方法索引是0x101C00,查询方法映射表得知这是MessageDigest.getInstance,参数是registers[0](即"SHA-256"字符串)。
    • 它通过JNI的FindClass、GetStaticMethodID找到真正的Java方法ID,然后用CallStaticObjectMethod实际调用Android系统的这个静态方法。
    • 调用结果是一个JNI层的jobject(MessageDigest实例)。
  4. 解释器执行下一条指令0x0C(move-result-object)。
    • handleMoveResultObject函数将上一步JNI调用的返回结果(那个jobject)存储到指令指定的目标寄存器v0(registers[0])中。这样就完美模拟了原始Smali中move-result-object v0的行为。

通过这种方式,所有原始逻辑都在自定义虚拟机中被模拟执行了一遍。对于外界(逆向者)来说,他们只能看到Dex里有一个调用了SimpleVMP.execute的空壳方法,真正的算法逻辑隐藏在自定义字节码和Native层的解释器里,从而达到了保护的目的。

 

 

 

手写 Android Dex VMP 壳:自定义虚拟机 + 指令解释执行全流程

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

前言

在 Android 安全领域,VMP(Virtual Machine Protection,虚拟机保护)壳 一直被视为最难攻克的加固技术之一。它通过将 Dex 指令转换为自定义字节码,再由虚拟机解释执行,从而大幅增加逆向分析的门槛。

带你一步步手写一个 Dex VMP 壳 :

  • 如何把 Java/Dex 转换为字节码指令流

  • 如何构建一个最小可用的解释器

  • 如何模拟寄存器、字符串池

  • 如何解析并执行典型指令(const-string、invoke、return 等)

Android Dex VMP 壳执行流程

Android Dex VMP 壳执行流程图述:

┌─────────────────────────┐
│  Java/Kotlin 源代码      │
└─────────────┬───────────┘
              │
              ▼
┌─────────────────────────┐
│   编译 → Dex 字节码      │
│  (标准 Android APK)     │
└─────────────┬───────────┘
              │
              ▼
┌─────────────────────────┐
│  VMP 转换层              │
│  - 扫描目标类/方法        │
│  - 将指令映射为自定义字节码│
│  - 生成指令流 (Custom IR) │
└─────────────┬───────────┘
              │
              ▼
┌─────────────────────────┐
│  VMP 虚拟机解释器        │
│  - 初始化寄存器池         │
│  - 加载字符串常量池       │
│  - 逐条取指、解析、执行   │
└─────────────┬───────────┘
              │
              ▼
┌─────────────────────────┐
│   指令解析与执行逻辑     │
│   1. const-string        │
│   2. invoke-static       │
│   3. move-result-object  │
│   4. sget-object         │
│   5. invoke-virtual      │
│   6. return-object       │
└─────────────┬───────────┘
              │
              ▼
┌─────────────────────────┐
│   最终运行效果           │
│   - 表面上像普通代码执行  │
│   - 实际走自定义 VM 流程  │
│   - 增加逆向/还原难度    │
└─────────────────────────┘

流程说明 :

  1. 源代码 → Dex 字节码:普通编译产物。

  2. VMP 转换层:把 Dex 中的方法替换为“虚拟机入口函数”,真实逻辑转为自定义字节码流存储。

  3. 虚拟机解释器:运行时加载自定义字节码,模拟执行环境(寄存器 + 常量池)。

  4. 指令执行:一步步解析、执行,还原原始逻辑。

  5. 对抗逆向:逆向者即使拿到 Dex,也只能看到“VM.run()”,逻辑被隐藏在虚拟机里。

Android 示例代码

比如,通过实现一个 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)
    }
}

把 Java/Dex 转换为字节码指令流

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

word/media/image1.png

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

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

VMP 虚拟机解释器

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

创建 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;
    }
}

注册 VMP 虚拟机解释器

在 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

 

 

 

手写 Android Dex VMP 壳:指令流 AES 加密 + 动态加载全流程

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

前言

在上一篇《手写 Android Dex VMP 壳:自定义虚拟机 + 指令解释执行全流程》中,我们从零实现了一个简易的 Dex VMP 壳,通过自定义虚拟机和指令解释执行,让应用代码在运行时以“虚拟指令流”的方式运行,大大增加了逆向分析的难度。

如果攻击者能够直接读取 Dex 中的虚拟指令流,那么依然可能进行还原。为了解决这一问题,本篇将进一步升级:在 Dex VMP 基础上引入指令流加密与动态加载 。

通过 AES 算法 对指令流进行加密,运行时再解密并交给虚拟机执行,从而让静态分析几乎无从下手。

Dex VMP 指令流加密 + 动态加载完整流程

Dex VMP 指令流加密 + 动态加载执行完整流程大概如下:

 ┌──────────────────────┐
 │     原始 Dex 指令流   │
 └──────────┬───────────┘
            │
            ▼
 ┌─────────────────────────┐
 │ 保存指令流到文件 / 内存   │
 └──────────┬──────────────┘
            │
            ▼
 ┌─────────────────────────┐
 │       使用 AES 加密      │
 │ (key/iv 固定或动态生成)   │
 └──────────┬──────────────┘
            │
            ▼
 ┌─────────────────────────┐
 │    得到加密后的指令流文件 │
 │ (静态分析无法直接还原)  │
 └──────────┬──────────────┘
            │
   ┌────────▼──────────────┐
   │   Android 运行时启动   │
   └────────┬──────────────┘
            │
            ▼
 ┌────────────────────────┐
 │  动态读取加密指令流文件  │
 └──────────┬─────────────┘
            │
            ▼
 ┌────────────────────────┐
 │       AES 解密恢复      │
 │  (内存中得到明文指令流)│
 └──────────┬─────────────┘
            │
            ▼
 ┌──────────────────────────┐
 │   将解密后的指令流交给 VMP │
 │   → 自定义虚拟机解释执行   │
 └──────────┬───────────────┘
            │
            ▼
 ┌─────────────────────────┐
 │    App 正常运行业务逻辑   │
 │ (逆向者难以静态还原逻辑) │
 └─────────────────────────┘

核心思路 :

  1. 编译/打包阶段:把 Dex 指令流抽取出来,AES 加密,存储到文件中。

  2. 运行时阶段:App 启动时,从文件中加载 → AES 解密 → 交给虚拟机解释执行。

  3. 保护效果:静态分析拿到的 Dex 是“假代码”,真正的逻辑被加密隐藏,只有运行时内存里才会出现明文。

保存指令流到文件

在 010Editor 中搜索找到 sign 方法的字节码并复制

word/media/image1.png

新建 Hex 文件

word/media/image2.png

把 sign 方法字节码粘贴到新建的文件保存文件为 sign

word/media/image3.png

AES加解密

编写一个 kotlin 语言 AES 加解密算法工具类

package com.cyrus.vmp

import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec

object AESUtils {

    private const val ALGORITHM = "AES"
    private const val TRANSFORMATION = "AES/ECB/PKCS5Padding" // AES 加密模式

    // 生成一个 128 位的 AES 密钥
    fun generateSecretKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
        keyGenerator.init(128) // AES 128 位
        return keyGenerator.generateKey()
    }

    // 使用给定的密钥加密数据
    fun encrypt(data: ByteArray, key: SecretKey): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, key)
        return cipher.doFinal(data)
    }

    // 使用给定的密钥解密数据
    fun decrypt(data: ByteArray, key: SecretKey): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.DECRYPT_MODE, key)
        return cipher.doFinal(data)
    }

    // 将文件内容加密并导出到新文件
    fun encryptFile(inputFile: File, outputFile: File, keyFile: File) {
        // 读取文件内容
        val fileData = readFile(inputFile)

        // 生成密钥
        val secretKey = generateSecretKey()

        // 加密文件内容
        val encryptedData = encrypt(fileData, secretKey)

        // 保存加密后的数据到新文件(.vmp 文件)
        writeFile(outputFile, encryptedData)

        // 保存密钥到文件
        saveKeyToFile(secretKey, keyFile)
    }

    // 解密文件内容并导出到新文件
    fun decryptFile(inputFile: File, outputFile: File, keyFile: File) {
        // 从文件加载密钥
        val secretKey = loadKeyFromFile(keyFile)

        // 读取加密后的文件内容
        val encryptedData = readFile(inputFile)

        // 解密文件内容
        val decryptedData = decrypt(encryptedData, secretKey)

        // 保存解密后的数据到文件
        writeFile(outputFile, decryptedData)
    }

    // 读取文件内容并返回字节数组
    fun readFile(file: File): ByteArray {
        val fis = FileInputStream(file)
        val baos = ByteArrayOutputStream()
        val buffer = ByteArray(1024)
        var bytesRead: Int
        while (fis.read(buffer).also { bytesRead = it } != -1) {
            baos.write(buffer, 0, bytesRead)
        }
        fis.close()
        return baos.toByteArray()
    }

    // 将字节数组写入到文件
    fun writeFile(file: File, data: ByteArray) {
        val fos = FileOutputStream(file)
        fos.write(data)
        fos.close()
    }

    // 保存密钥到文件
    private fun saveKeyToFile(key: SecretKey, keyFile: File) {
        val fos = FileOutputStream(keyFile)
        fos.write(key.encoded)
        fos.close()
    }

    // 从文件加载密钥
    fun loadKeyFromFile(keyFile: File): SecretKey {
        val keyBytes = ByteArray(keyFile.length().toInt())
        val fis = FileInputStream(keyFile)
        fis.read(keyBytes)
        fis.close()
        return SecretKeySpec(keyBytes, ALGORITHM)
    }

}

指令流加密

把 sign 文件放到工程中如下路径

word/media/image4.png

调用 AESUtils 类中方法对 sign 进行加密并输出加密文件和密钥

package com.cyrus.vmp

import java.io.File

fun main() {
    // 获取工程根目录路径
    val projectRoot = System.getProperty("user.dir")

    // 设置相对路径
    val encryptedFile = File(projectRoot, "vmp/sign/sign.vmp") // 相对路径
    val keyFile = File(projectRoot, "vmp/sign/sign.key") // 相对路径

    // 输入文件路径
    val inputFile = File(projectRoot, "vmp/sign/sign") // 需要加密的文件


    try {
        // 使用 AES 加密文件
        AESUtils.encryptFile(inputFile, encryptedFile, keyFile)
        println("File encryption completed, saved as: ${encryptedFile.absolutePath}")
        println("Key saved as: ${keyFile.absolutePath}")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

指令流解密

package com.cyrus.vmp

import com.cyrus.vmp.AESUtils.loadKeyFromFile
import com.cyrus.vmp.AESUtils.readFile
import com.cyrus.vmp.AESUtils.writeFile
import java.io.File


fun main() {

    // 获取工程根目录路径
    val projectRoot = System.getProperty("user.dir")

    // 输入加密文件路径
    val encryptedFile = File(projectRoot, "vmp/sign/sign.vmp")

    // 密钥文件路径
    val keyFile = File(projectRoot, "vmp/sign/sign.key")

    // 输出解密文件路径
    val decryptedFile = File(projectRoot, "vmp/sign/sign_")

    try {
        // 从文件加载密钥
        val secretKey = loadKeyFromFile(keyFile)

        // 解密文件
        val encryptedData = readFile(encryptedFile)
        val decryptedData: ByteArray = AESUtils.decrypt(encryptedData, secretKey)

        // 保存解密后的文件
        writeFile(decryptedFile, decryptedData)
        println("File decryption completed, saved as: ${decryptedFile.absolutePath}")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

Android 中运行时解密并执行指令流

将 .vmp 和 .key 文件放在 Android 应用的 assets 目录下

word/media/image5.png

编写工具类,用于读取 assets 文件并解密

package com.cyrus.example.vmp

import android.content.Context
import java.io.InputStream
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec

object AESUtils {
    private const val ALGORITHM = "AES"
    private const val TRANSFORMATION = "AES/ECB/PKCS5Padding"

    // 从 assets 中读取文件并解密
    fun decryptFileFromAssets(context: Context, vmpFileName: String, keyFileName: String): ByteArray? {
        // 读取密钥文件
        val key = loadKeyFromAssets(context, keyFileName)

        // 读取加密的 vmp 文件
        val encryptedData = readFileFromAssets(context, vmpFileName)

        // 解密
        return decrypt(encryptedData, key)
    }

    // 读取文件内容为字节数组
    private fun readFileFromAssets(context: Context, fileName: String): ByteArray {
        val inputStream: InputStream = context.assets.open(fileName)
        return inputStream.readBytes()
    }

    // 从 assets 中加载密钥文件
    private fun loadKeyFromAssets(context: Context, keyFileName: String): SecretKey {
        val keyBytes = readFileFromAssets(context, keyFileName)
        return SecretKeySpec(keyBytes, ALGORITHM)
    }

    // 解密
    private fun decrypt(data: ByteArray, key: SecretKey): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.DECRYPT_MODE, key)
        return cipher.doFinal(data)
    }
}

调用解密方法并读取指令流

private fun readInstructionFromAssets(): ByteArray? {
    // 文件名:在 assets 中放置的加密文件和密钥文件
    val vmpFileName = "sign.vmp"
    val keyFileName = "sign.key"

    // 解密文件
    val decryptedData = AESUtils.decryptFileFromAssets(this, vmpFileName, keyFileName)
    return decryptedData
}

得到解密后的指令流后调用 VMP 执行指令流对 input 参数加密

val input = "example"

// 解密并执行指令流
val bytecode = readInstructionFromAssets()

// 通过 VMP 解析器执行指令流
if (bytecode != null) {

    val result = SimpleVMP.execute(bytecode, input)

    // 显示 Toast
    Toast.makeText(this, result, Toast.LENGTH_SHORT).show()
}

测试

执行结果如下

word/media/image6.png

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

word/media/image7.png

完整源码

开源地址:https://github.com/CYRUS-STUDIO/AndroidExample

 

posted on 2025-09-12 00:05  GKLBB  阅读(119)  评论(0)    收藏  举报