版权归作者所有,如有转发,请注明文章出处: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)
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
得到字节码和对应的 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 ¶mType) {
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 jfloa