版權歸作者所有,如有轉發,請注明文章出處: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)
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 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 區域。
這里簡單通過字符串索引和字符串做關聯,代碼實現如下:
// 模擬字符串常量池
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 接口發起調用。
具體代碼實現
// 解析并執行 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 執行結果如下:
和原來算法對比結果是一樣的。
安全性增強
-
指令流加密:比如使用 AES 加密指令流,在運行時解密執行。
-
動態加載:使用 dex 動態加載虛擬機和指令流。
-
多態指令集:每次保護代碼時動態生成不同的指令集,防止通過固定指令集逆向。
-
反調試檢測:檢測調試器附加、內存修改或運行環境,防止虛擬機被分析。
優點與局限
優點
-
提高逆向難度:通過指令集和虛擬機隱藏關鍵邏輯。
-
動態保護:運行時加載和執行,防止靜態分析。
局限
-
性能開銷:解釋執行比原生代碼慢。
-
開發成本:需要設計和實現虛擬機框架。
通過上述方法,可以實現一個基本的自定義 Android 虛擬機保護,并根據需要逐步增強安全性。
源碼
完整源碼:https://github.com/CYRUS-STUDIO/AndroidExample