詳解如何自定義 Android Dex VMP 保護殼

版權歸作者所有,如有轉發,請注明文章出處:https://cyrus-studio.github.io/blog/

前言

Android Dex VMP(Virtual Machine Protection,虛擬機保護)殼是一種常見的應用保護技術,主要用于保護 Android 應用的代碼免受反編譯和逆向工程的攻擊。

VMP 保護殼通過將應用的原始 Dex(Dalvik Executable)文件進行加密、混淆、虛擬化等處理,使得惡意用戶無法輕易獲取到應用的原始代碼和邏輯。

比如,實現一個 Android 下的 Dex VMP 保護殼,用來保護 Kotlin 層 sign 算法,防止被逆向。

假設 sign 算法源碼如下:

package com.cyrus.example.vmpimport java.security.MessageDigest
import java.util.Base64object SignUtil {/*** 對輸入字符串進行簽名并返回 Base64 編碼后的字符串* @param input 要簽名的字符串* @return Base64 編碼后的字符串*/fun sign(input: String): String {// 使用 SHA-256 計算摘要val digest = MessageDigest.getInstance("SHA-256")val hash = digest.digest(input.toByteArray())// 使用 Base64 編碼return Base64.getEncoder().encodeToString(hash)}
}

轉換為指令流

把 apk 拖入 GDA,找到 sign 方法,右鍵選擇 SmaliJava(F5)

word/media/image1.png

GDA 是一個開源的 Android 逆向分析工具,可反編譯 APK、DEX、ODEX、OAT、JAR、AAR 和 CLASS 文件,支持惡意行為檢測、隱私泄露檢測、漏洞檢測、路徑解密、打包器識別、變量跟蹤、反混淆、python 和 Java 腳本等等…

  • GDA 下載地址:http://www.gda.wiki:9090/

  • GDA 項目地址:https://github.com/charles2gan/GDA-android-reversing-Tool

Show ByteCode

word/media/image2.png

得到字節碼和對應的 smali 指令如下:

1a004e00            | const-string v0, "input"
712020000500        | invoke-static{v5, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V
1a002c00            | const-string v0, "SHA-256"
71101c000000        | invoke-static{v0}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
0c00                | move-result-object v0
62010900            | sget-object v1, Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;
6e2016001500        | invoke-virtual{v5, v1}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B
0c01                | move-result-object v1
1a024a00            | const-string v2, "getBytes\(...\)"
71201f002100        | invoke-static{v1, v2}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
6e201b001000        | invoke-virtual{v0, v1}, Ljava/security/MessageDigest;->digest([B)[B
0c01                | move-result-object v1
71001e000000        | invoke-static{}, Ljava/util/Base64;->getEncoder()Ljava/util/Base64$Encoder;
0c02                | move-result-object v2
6e201d001200        | invoke-virtual{v2, v1}, Ljava/util/Base64$Encoder;->encodeToString([B)Ljava/lang/String;
0c02                | move-result-object v2
1a034400            | const-string v3, "encodeToString\(...\)"
71201f003200        | invoke-static{v2, v3}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
1102                | return-object v2

構建虛擬機解釋器

解釋器的任務是執行這些虛擬機指令。我們需要寫一個虛擬機,它能夠按照虛擬指令集中的指令依次執行操作。

創建 cpp 文件,定義一個 JNI 方法 execute,接收字節碼數組和字符串參數,每個字節碼指令會被映射為我們定義的虛擬指令。

#define CONST_STRING_OPCODE 0x1A  // const-string 操作碼
#define INVOKE_STATIC_OPCODE 0x71  // invoke-static 操作碼
#define MOVE_RESULT_OBJECT_OPCODE 0x0c  // move-result-object 操作碼
#define SGET_OBJECT_OPCODE 0x62  // sget-object 操作碼
#define INVOKE_VIRTUAL_OPCODE 0x6e  // invoke-virtual 操作碼
#define RETURN_OBJECT_OPCODE 0x11  // return-object 操作碼jstring execute(JNIEnv *env, jobject thiz, jbyteArray bytecodeArray, jstring input) {// 傳參存到 v5 寄存器registers[5] = input;// 獲取字節碼數組的長度jsize length = env->GetArrayLength(bytecodeArray);std::vector <uint8_t> bytecode(length);env->GetByteArrayRegion(bytecodeArray, 0, length, reinterpret_cast<jbyte *>(bytecode.data()));size_t pc = 0;  // 程序計數器try {// 執行字節碼中的指令while (pc < bytecode.size()) {uint8_t opcode = bytecode[pc];switch (opcode) {case CONST_STRING_OPCODE:handleConstString(env, bytecode.data(), pc);break;case INVOKE_STATIC_OPCODE:handleInvokeStatic(env, bytecode.data(), pc);break;case SGET_OBJECT_OPCODE:handleSgetObject(env, bytecode.data(), pc);break;case INVOKE_VIRTUAL_OPCODE:handleInvokeVirtual(env, bytecode.data(), pc);break;case RETURN_OBJECT_OPCODE:handleReturnResultObject(env, bytecode.data(), pc);break;default:throw std::runtime_error("Unknown opcode encountered");}}if (std::holds_alternative<jstring>(registers[0])) {jstring result = std::get<jstring>(registers[0]);   // 返回寄存器 v0 的值// 清空寄存器std::fill(std::begin(registers), std::end(registers), nullptr);return result;}} catch (const std::exception &e) {env->ThrowNew(env->FindClass("java/lang/RuntimeException"), e.what());}// 清空寄存器std::fill(std::begin(registers), std::end(registers), nullptr);return nullptr;
}

模擬寄存器

使用 std::variant 來定義一個可以存儲多種類型的寄存器值。

// 定義支持的寄存器類型(比如 jstring、jboolean、jobject 等等)
using RegisterValue = std::variant<jstring,jboolean,jbyte,jshort,jint,jlong,jfloat,jdouble,jobject,jbyteArray,jintArray,jlongArray,jfloatArray,jdoubleArray,jbooleanArray,jshortArray,jobjectArray,std::nullptr_t
>;

std::variant 是 C++17 引入的一個模板類,用于表示一個可以存儲多種類型中的一種的類型。它類似于聯合體(union),但是比聯合體更安全,因為它可以明確地跟蹤當前存儲的是哪一種類型。

定義寄存器個數和寄存器數組

// 定義寄存器數量
constexpr size_t NUM_REGISTERS = 10;// 定義寄存器數組
RegisterValue registers[NUM_REGISTERS];

寫寄存器

// 存儲不同類型的值到寄存器
template <typename T>
void setRegisterValue(uint8_t reg, T value) {// 通過模板將類型 T 存儲到寄存器registers[reg] = value;
}

讀寄存器

// 根據類型從寄存器讀取對應的值
jvalue getRegisterAsJValue(int regIdx, const std::string &paramType) {const RegisterValue &val = registers[regIdx];jvalue result;if (paramType == "I") {  // int 類型if (std::holds_alternative<jint>(val)) {result.i = std::get<jint>(val);} else {throw std::runtime_error("Type mismatch: Expected jint.");}} else if (paramType == "J") {  // long 類型if (std::holds_alternative<jlong>(val)) {result.j = std::get<jlong>(val);} else {throw std::runtime_error("Type mismatch: Expected jlong.");}} else if (paramType == "F") {  // float 類型if (std::holds_alternative<jfloat>(val)) {result.f = std::get<jfloat>(val);} else {throw std::runtime_error("Type mismatch: Expected jfloat.");}} else if (paramType == "D") {  // double 類型if (std::holds_alternative<jdouble>(val)) {result.d = std::get<jdouble>(val);} else {throw std::runtime_error("Type mismatch: Expected jdouble.");}} else if (paramType == "Z") {  // boolean 類型if (std::holds_alternative<jboolean>(val)) {result.z = std::get<jboolean>(val);} else {throw std::runtime_error("Type mismatch: Expected jboolean.");}} else if (paramType == "B") {  // byte 類型if (std::holds_alternative<jbyte>(val)) {result.b = std::get<jbyte>(val);} else {throw std::runtime_error("Type mismatch: Expected jbyte.");}} else if (paramType == "S") {  // short 類型if (std::holds_alternative<jshort>(val)) {result.s = std::get<jshort>(val);} else {throw std::runtime_error("Type mismatch: Expected jshort.");}} else if (paramType == "Ljava/lang/String;") {  // String 類型if (std::holds_alternative<jstring>(val)) {result.l = std::get<jstring>(val);} else {throw std::runtime_error("Type mismatch: Expected jstring.");}} else if (paramType[0] == 'L') {  // jobject 類型(以 L 開頭)if (std::holds_alternative<jstring>(val)) {result.l = std::get<jstring>(val);} else if (std::holds_alternative<jobject>(val)) {result.l = std::get<jobject>(val);} else {throw std::runtime_error("Type mismatch: Expected jobject.");}} else if (paramType[0] == '[') {  // 數組類型// 處理數組類型,判斷是基礎類型數組還是對象數組if (paramType == "[I") {  // jintArray 類型if (std::holds_alternative<jintArray>(val)) {result.l = std::get<jintArray>(val);  // jvalue 直接存儲數組} else {throw std::runtime_error("Type mismatch: Expected jintArray.");}} else if (paramType == "[J") {  // jlongArray 類型if (std::holds_alternative<jlongArray>(val)) {result.l = std::get<jlongArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jlongArray.");}} else if (paramType == "[F") {  // jfloatArray 類型if (std::holds_alternative<jfloatArray>(val)) {result.l = std::get<jfloatArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jfloatArray.");}} else if (paramType == "[D") {  // jdoubleArray 類型if (std::holds_alternative<jdoubleArray>(val)) {result.l = std::get<jdoubleArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jdoubleArray.");}} else if (paramType == "[Z") {  // jbooleanArray 類型if (std::holds_alternative<jbooleanArray>(val)) {result.l = std::get<jbooleanArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jbooleanArray.");}} else if (paramType == "[B") {  // jbyteArray 類型if (std::holds_alternative<jbyteArray>(val)) {result.l = std::get<jbyteArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jbyteArray.");}} else if (paramType == "[S") {  // jshortArray 類型if (std::holds_alternative<jshortArray>(val)) {result.l = std::get<jshortArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jshortArray.");}} else if (paramType == "[Ljava/lang/String;") {  // String[] 類型if (std::holds_alternative<jobjectArray>(val)) {result.l = std::get<jobjectArray>(val);} else {throw std::runtime_error("Type mismatch: Expected String array.");}} else if (paramType[0] == '[' && paramType[1] == 'L') {  // jobject[] 類型(數組的元素為對象)if (std::holds_alternative<jobjectArray>(val)) {result.l = std::get<jobjectArray>(val);} else {throw std::runtime_error("Type mismatch: Expected jobject array.");}} else {throw std::runtime_error("Unsupported array type.");}} else {throw std::runtime_error("Unsupported parameter type.");}return result;
}

模擬字符串常量池

由于指令中用到字符串,所有需要模擬一個字符串常量池去實現指令中字符串的引用。

在 dex 文件中,字符串常量池(string_ids)是一個數組,其中每個條目存儲一個字符串的偏移量,這個偏移量指向 dex 文件中 string_data 區域。

word/media/image3.png

這里簡單通過字符串索引和字符串做關聯,代碼實現如下:

// 模擬字符串常量池
std::unordered_map <uint32_t, std::string> stringPool = {{0x004e00, "input"},{0x002c00, "SHA-256"},{0x024a00, "getBytes\\(...\\)"},{0x034400, "encodeToString\\(...\\)"},
};

指令解析執行

虛擬機接收到字節指令流,經過解析操作碼并分發到各指令執行函數。接下來實現指令執行函數。

1. const-string

該指令將一個預定義的字符串常量加載到指定的寄存器中。例如:

const-string v0, "Hello, World!"

這條指令的作用是將字符串 “Hello, World!” 加載到寄存器 v0 中。

指令結構

const-string v0, “input” 的字節碼為:

1A 00 4E 00

結構解釋:

  • 1A (操作碼): 表示 const-string 指令。

  • 00 (目標寄存器 v0): 表示字符串將存儲到寄存器 v0 中。

  • 4E 00 (字符串索引 0x004E): 表示字符串在字符串常量池中的位置。

具體代碼實現

// 處理 const-string 指令
void handleConstString(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode != CONST_STRING_OPCODE) {  // 檢查是否為 const-string 指令throw std::runtime_error("Unexpected opcode");}// 獲取目標寄存器索引 reg 和字符串索引uint8_t reg = bytecode[pc + 1];  // 目標寄存器// 讀取字符串索引(第 2、3、4 字節)uint32_t stringIndex = (bytecode[pc + 1] << 16) | (bytecode[pc + 2] << 8) | bytecode[pc + 3];// 從字符串常量池獲取字符串const std::string &value = stringPool[stringIndex];// 創建 jstring 并將其存儲到目標寄存器jstring str = env->NewStringUTF(value.c_str());registers[reg] = str;// 更新程序計數器pc += 4;  // const-string 指令占用 4 字節
}

2. invoke-static

invoke-static 指令用于執行類的靜態方法。例如:

invoke-static {v5, v0}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullParameter(Ljava/lang/Object;Ljava/lang/String;)V

各部分的解釋:

  • invoke-static:這是調用靜態方法的指令

  • {v5, v0}:這是方法調用時傳遞的參數寄存器

  • Lkotlin/jvm/internal/Intrinsics;:目標類的名稱。

  • ->checkNotNullParameter:這是要調用的靜態方法的名稱

  • (Ljava/lang/Object;Ljava/lang/String;):這是方法的參數簽名

  • V:表示方法的返回類型是 void。

指令結構

一個標準的 invoke-static 字節碼指令通常如下所示(6個字節):

71 <reg_count> <method_index> <reg> 00操作碼 (1 字節) | 寄存器數量 (1 字節) | 方法索引 (2 字節) | 目標寄存器 (1 字節) | 填充字節,指令對齊 (1 字節)
  • 71:操作碼,表示 invoke-static。

  • <reg_count>:寄存器數量,參數個數。

  • <method_index>:目標方法在方法表中的索引。

  • :目標寄存器,表示要將傳參存儲到的寄存器。

  • 00:填充字節,指令對齊

實現 invoke 指令,需要根據指令中的 method index 從 dex 中找到 method,然后通過 jni 接口發起調用。

word/media/image4.png

具體代碼實現

// 解析并執行 invoke-static 指令
void handleInvokeStatic(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode != INVOKE_STATIC_OPCODE) {  // 檢查是否為 invoke-staticthrow std::runtime_error("Unexpected opcode for invoke-static");}// 第 5 個字節表示了要使用的寄存器uint8_t reg1 = bytecode[pc + 4] & 0xF;         // 低4位表示第一個寄存器uint8_t reg2 = (bytecode[pc + 4] >> 4) & 0xF;  // 高4位表示第二個寄存器// 讀取方法索引(第 2、3、4 字節)uint32_t methodIndex = (bytecode[pc + 1] << 16) | (bytecode[pc + 2] << 8) | bytecode[pc + 3];// 類名和方法信息std::string className;std::string methodName;std::string methodSignature;// 根據 methodIndex 來解析并設置類名、方法名、簽名switch (methodIndex) {case 0x202000:  // checkNotNullParameterclassName = "kotlin/jvm/internal/Intrinsics";methodName = "checkNotNullParameter";methodSignature = "(Ljava/lang/Object;Ljava/lang/String;)V";break;case 0x101c00:  // getInstance (MessageDigest)className = "java/security/MessageDigest";methodName = "getInstance";methodSignature = "(Ljava/lang/String;)Ljava/security/MessageDigest;";break;case 0x201f00:  // checkNotNullExpressionValueclassName = "kotlin/jvm/internal/Intrinsics";methodName = "checkNotNullExpressionValue";methodSignature = "(Ljava/lang/Object;Ljava/lang/String;)V";break;case 0x001e00:  // getEncoder (Base64)className = "java/util/Base64";methodName = "getEncoder";methodSignature = "()Ljava/util/Base64$Encoder;";break;default:throw std::runtime_error("Unknown method index");}// 獲取目標類jclass targetClass = env->FindClass(className.c_str());if (targetClass == nullptr) {throw std::runtime_error("Class not found: " + className);}// 獲取方法 IDjmethodID methodID = env->GetStaticMethodID(targetClass, methodName.c_str(), methodSignature.c_str());if (methodID == nullptr) {throw std::runtime_error("Method not found: " + methodName);}// 解析方法簽名,得到參數個數和返回值類型std::vector<std::string> paramTypes;std::string returnType;parseMethodSignature(methodSignature, paramTypes, returnType);int paramCount = paramTypes.size();// 動態獲取參數uint8_t reg_list[] = {reg1, reg2};std::vector <jstring> params(paramCount);for (size_t i = 0; i < paramCount; ++i) {// 獲取寄存器中的值并轉化為 JNI 參數jvalue value = getRegisterAsJValue(reg_list[i], paramTypes[i]);params[i] = static_cast<jstring>(value.l);}// 更新程序計數器pc += 6;  // invoke-static 指令占用 6 字節// 調用靜態方法// 根據返回值類型決定調用方式if (returnType == "V") {  // void 返回值if (paramCount == 0) {env->CallStaticVoidMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {env->CallStaticVoidMethod(targetClass, methodID, params[0]);} else {env->CallStaticVoidMethod(targetClass, methodID, params[0], params[1]);}} else if (returnType == "Z") {  // boolean 返回值jboolean boolResult;if (paramCount == 0) {boolResult = env->CallStaticBooleanMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {boolResult = env->CallStaticBooleanMethod(targetClass, methodID, params[0]);} else {boolResult = env->CallStaticBooleanMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, boolResult);} else if (returnType == "B") {  // byte 返回值jbyte byteResult;if (paramCount == 0) {byteResult = env->CallStaticByteMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {byteResult = env->CallStaticByteMethod(targetClass, methodID, params[0]);} else {byteResult = env->CallStaticByteMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, byteResult);} else if (returnType == "S") {  // short 返回值jshort shortResult;if (paramCount == 0) {shortResult = env->CallStaticShortMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {shortResult = env->CallStaticShortMethod(targetClass, methodID, params[0]);} else {shortResult = env->CallStaticShortMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, shortResult);} else if (returnType == "I") {  // int 返回值jint intResult;if (paramCount == 0) {intResult = env->CallStaticIntMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {intResult = env->CallStaticIntMethod(targetClass, methodID, params[0]);} else {intResult = env->CallStaticIntMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, intResult);} else if (returnType == "J") {  // long 返回值jlong longResult;if (paramCount == 0) {longResult = env->CallStaticLongMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {longResult = env->CallStaticLongMethod(targetClass, methodID, params[0]);} else {longResult = env->CallStaticLongMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, longResult);} else if (returnType == "F") {  // float 返回值jfloat floatResult;if (paramCount == 0) {floatResult = env->CallStaticFloatMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {floatResult = env->CallStaticFloatMethod(targetClass, methodID, params[0]);} else {floatResult = env->CallStaticFloatMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, floatResult);} else if (returnType == "D") {  // double 返回值jdouble doubleResult;if (paramCount == 0) {doubleResult = env->CallStaticDoubleMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {doubleResult = env->CallStaticDoubleMethod(targetClass, methodID, params[0]);} else {doubleResult = env->CallStaticDoubleMethod(targetClass, methodID, params[0], params[1]);}// move-resulthandleMoveResultObject(env, bytecode, pc, doubleResult);} else if (returnType[0] == 'L') {  // 對象返回值jobject objResult;if (paramCount == 0) {objResult = env->CallStaticObjectMethod(targetClass, methodID);  // 無參數} else if (paramCount == 1) {objResult = env->CallStaticObjectMethod(targetClass, methodID, params[0]);} else {objResult = env->CallStaticObjectMethod(targetClass, methodID, params[0], params[1]);}// 處理返回的對象if (objResult) {if(returnType == "Ljava/lang/String;"){jstring strResult = static_cast<jstring>(objResult);handleMoveResultObject(env, bytecode, pc, strResult);}else{handleMoveResultObject(env, bytecode, pc, objResult);}}} else {throw std::runtime_error("Unsupported return type: " + returnType);}
}

3. move-result-object

move-result-object 用于從方法調用的結果中將對象類型的返回值移動到指定的寄存器中。例如:

move-result-object v0

解釋:

  • move-result-object:這條指令的作用是將最近一次方法調用的返回結果移動到指定的寄存器中。

  • v0:指定目標寄存器,返回的對象會被存儲在 v0 寄存器中。

指令結構

一個標準的 move-result-object 字節碼指令通常如下所示(2個字節):

0c <reg>操作碼 (1 字節)  | 目標寄存器 (1 字節)  

具體代碼實現

// move-result-object
template <typename T>
void handleMoveResultObject(JNIEnv *env, const uint8_t *bytecode, size_t &pc, T result) {uint8_t opcode = bytecode[pc];if (opcode == MOVE_RESULT_OBJECT_OPCODE) {uint8_t reg = bytecode[pc + 1];  // 目標寄存器setRegisterValue(reg, result);// 更新程序計數器pc += 2;  // move-result-object 指令占用 2 字節}
}

4. sget-object

sget-object 是一條靜態字段讀取指令。它用于從一個類的靜態字段中獲取一個引用類型(對象)的值,并存儲到指定的寄存器中。

例如:

sget-object v1, Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;

解釋:

  • sget-object:表示從類的靜態字段中獲取對象類型的值。

  • v1:目標寄存器,指令執行后,字段值(一個對象)會被存儲在 v1 寄存器中。

  • Lkotlin/text/Charsets;:目標類的名稱。

  • ->UTF_8:表示靜態字段 UTF_8。

  • :Ljava/nio/charset/Charset;:字段的類型描述符,表示該字段的類型是 java.nio.charset.Charset。

指令結構

一個標準的 sget-object 字節碼指令通常如下所示(4個字節):

62 <reg> <field_index>操作碼 (1 字節)  | 目標寄存器 (1 字節)  | 字段索引 (2 字節)  

具體代碼實現

// 解析和執行 sget-object 指令
void handleSgetObject(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode != SGET_OBJECT_OPCODE) {  // 檢查是否為 sget-objectthrow std::runtime_error("Unexpected opcode for sget-object");}// 解析指令uint8_t reg = bytecode[pc + 1];          // 目標寄存器uint16_t fieldIndex = (bytecode[pc + 2] << 8) | bytecode[pc + 3]; // 字段索引// 類名和方法信息std::string className;std::string fieldName;std::string fieldType;// 解析每條指令,依據方法的不同來設置類名、方法名、簽名switch (fieldIndex) {case 0x0900:  // Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;className = "kotlin/text/Charsets";fieldName = "UTF_8";fieldType = "Ljava/nio/charset/Charset;"; // 字段類型為 Charsetbreak;default:throw std::runtime_error("Unknown field index");}// 1. 獲取 Java 類jclass clazz = env->FindClass(className.c_str());if (clazz == nullptr) {LOGI("Failed to find class %s", className.c_str());return;}// 2. 獲取靜態字段的 Field IDjfieldID fieldID = env->GetStaticFieldID(clazz, fieldName.c_str(), fieldType.c_str());if (fieldID == nullptr) {LOGI("Failed to get field ID for %s", fieldName.c_str());return;}// 3. 獲取靜態字段的值jobject field = env->GetStaticObjectField(clazz, fieldID);if (field == nullptr) {LOGI("%s field is null", fieldName.c_str());return;}// 保存到目標寄存器setRegisterValue(reg, field);// 更新程序計數器pc += 4; // sget-object 指令占用 4 字節
}

5. invoke-virtual

invoke-virtual 指令會調用指定對象的實例方法。例如

invoke-virtual {v5, v1}, Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[B

解釋:

  • invoke-virtual:表示調用對象的實例方法。

  • {v5, v1}:傳遞給目標方法的參數寄存器。這里,v5 和 v1 寄存器的值會作為參數傳遞給方法。

  • Ljava/lang/String;:目標類的名稱。

  • ->getBytes:目標方法的名稱。

  • (Ljava/nio/charset/Charset;):方法的參數簽名。

  • [B:方法的返回類型簽名,表示該方法返回一個字節數組。

指令結構

一個標準的 invoke-virtual 字節碼指令通常如下所示(6個字節):

6e <reg_count> <method_index> <reg> 00操作碼 (1 字節) | 寄存器數量 (1 字節) | 方法索引 (2 字節) | 目標寄存器 (1 字節) | 填充字節,指令對齊 (1 字節)
  • 6e:操作碼,表示 invoke-static。

  • <reg_count>:寄存器數量,參數個數。

  • <method_index>:目標方法在方法表中的索引。

  • :目標寄存器,表示要將傳參存儲到的寄存器。

  • 00:填充字節,指令對齊

具體代碼實現

// invoke-virtual 指令
void handleInvokeVirtual(JNIEnv* env, const uint8_t* bytecode, size_t& pc) {// 解析指令uint8_t opcode = bytecode[pc];  // 獲取操作碼if (opcode != INVOKE_VIRTUAL_OPCODE) {  // 確保是 invoke-virtual 操作碼throw std::runtime_error("Expected invoke-virtual opcode");}// 獲取寄存器數量uint8_t regCount = (bytecode[pc + 1] >> 4) & 0xF;// 第 5 個字節表示了要使用的寄存器uint8_t reg1 = bytecode[pc + 4] & 0xF;         // 低4位表示第一個寄存器uint8_t reg2 = (bytecode[pc + 4] >> 4) & 0xF;  // 高4位表示第二個寄存器// 讀取方法索引(第 2、3、4 字節)uint32_t methodIndex = (bytecode[pc + 1] << 16) | (bytecode[pc + 2] << 8) | bytecode[pc + 3];// 類名和方法信息std::string className;std::string methodName;std::string methodSignature;// 根據 methodIndex 來解析并設置類名、方法名、簽名switch (methodIndex) {case 0x201600:  // Ljava/lang/String;->getBytes(Ljava/nio/charset/Charset;)[BclassName = "java/lang/String";methodName = "getBytes";methodSignature = "(Ljava/nio/charset/Charset;)[B";break;case 0x201b00:  // Ljava/security/MessageDigest;->digest([B)[BclassName = "java/security/MessageDigest";methodName = "digest";methodSignature = "([B)[B";break;case 0x201d00:  // Ljava/util/Base64$Encoder;->encodeToString([B)Ljava/lang/String;className = "java/util/Base64$Encoder";methodName = "encodeToString";methodSignature = "([B)Ljava/lang/String;";break;default:throw std::runtime_error("Unknown method index: " + std::to_string(methodIndex));}// 查找類和方法jclass clazz = env->FindClass(className.c_str());if (!clazz) {throw std::runtime_error("Class not found: " + className);}// 獲取方法 IDjmethodID methodID = env->GetMethodID(clazz, methodName.c_str(), methodSignature.c_str());if (!methodID) {throw std::runtime_error("Method not found: " + methodName);}// 解析方法簽名,得到參數個數和返回值類型std::vector<std::string> paramTypes;std::string returnType;parseMethodSignature(methodSignature, paramTypes, returnType);int paramCount = paramTypes.size();// 目標對象的類型std::stringstream ss;ss << "L" << className << ";";std::string classType = ss.str();// 獲取目標對象(寄存器中的第一個參數,通常是方法的目標對象)jobject targetObject = getRegisterAsJValue(reg1, classType).l;// 參數std::vector <jvalue> params(paramCount);if(paramCount > 0){params[0] = getRegisterAsJValue(reg2, paramTypes[0]);}// 更新程序計數器pc += 6;// 檢查返回值的類型,并調用適當的方法if (returnType == "V") {  // 如果沒有返回值 (void 方法)// 調用 void 方法env->CallVoidMethodA(targetObject, methodID, params.data());} else if (returnType == "[B") {  // 如果返回值是 byte 數組jbyteArray result = (jbyteArray) env->CallObjectMethodA(targetObject, methodID, params.data());// 處理返回的 byte 數組if (result) {handleMoveResultObject(env, bytecode, pc, result);}} else if (returnType[0] == 'L') {  // 如果返回值是對象jobject objResult = env->CallObjectMethodA(targetObject, methodID, params.data());// 處理返回的對象if (objResult) {if(returnType == "Ljava/lang/String;"){jstring strResult = static_cast<jstring>(objResult);handleMoveResultObject(env, bytecode, pc, strResult);}else{handleMoveResultObject(env, bytecode, pc, objResult);}}} else if (returnType == "I") {  // 如果返回值是 intjint result = env->CallIntMethodA(targetObject, methodID, params.data());// 處理返回的 inthandleMoveResultObject(env, bytecode, pc, result);} else if (returnType == "Z") {  // 如果返回值是 booleanjboolean result = env->CallBooleanMethodA(targetObject, methodID, params.data());// 處理返回的 booleanhandleMoveResultObject(env, bytecode, pc, result);} else if (returnType == "D") {  // 如果返回值是 doublejdouble result = env->CallDoubleMethodA(targetObject, methodID, params.data());// 處理返回的 doublehandleMoveResultObject(env, bytecode, pc, result);} else if (returnType == "F") {  // 如果返回值是 floatjfloat result = env->CallFloatMethodA(targetObject, methodID, params.data());// 處理返回的 floathandleMoveResultObject(env, bytecode, pc, result);} else {throw std::runtime_error("Unsupported return type in method: " + returnType);}
}

6. return-object

這條指令通常用于結束一個方法的執行,并將指定寄存器中的對象作為返回值返回給調用者。

例如:

return-object v2

解釋:

  • return-object:表示方法執行結束時,返回一個對象類型的值。

  • v2:表示返回的對象存儲在寄存器 v2 中。執行這條指令時,寄存器 v2 中的對象將作為方法的返回值。

指令結構

一個標準的 return-object 字節碼指令通常如下所示(2個字節):

11 <reg>操作碼 (1 字節)  | 目標寄存器 (1 字節)  

具體代碼實現

// return-object
void handleReturnResultObject(JNIEnv *env, const uint8_t *bytecode, size_t &pc) {uint8_t opcode = bytecode[pc];if (opcode == RETURN_OBJECT_OPCODE) {uint8_t reg = bytecode[pc + 1];  // 目標寄存器// 把目標寄存器中的值設置到 v0 寄存器setRegisterValue(0, registers[reg]);// 更新程序計數器pc += 2;}
}

注冊解析器

在 kotlin 層中定義 VMP 入口方法 execute

package com.cyrus.example.vmpclass SimpleVMP {companion object {// 加載本地庫init {System.loadLibrary("vmp-lib")}// 定義靜態方法 execute@JvmStaticexternal fun execute(bytecode: ByteArray, input: String): String}
}

在 JNI_Onload 中調用 RegisterNatives 方法動態注冊 C++ 中的 execute 方法到 com/cyrus/example/vmp/SimpleVMP

// 定義方法簽名
static JNINativeMethod gMethods[] = {{"execute", "([BLjava/lang/String;)Ljava/lang/String;", (void*)execute}
};// JNI_OnLoad 動態注冊方法
extern "C" JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {JNIEnv *env = nullptr;if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {return JNI_ERR;}jclass clazz = env->FindClass("com/cyrus/example/vmp/SimpleVMP");if (clazz == nullptr) {return JNI_ERR; // 類未找到}// 注冊所有本地方法jint result = env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0]));if (result != JNI_OK) {return JNI_ERR; // 注冊失敗}return JNI_VERSION_1_6;
}

測試

把 sign 方法的調用改為通過 VMP 執行 sign 算法計算 input 參數的加密結果。

// 參數
val input = "example"// 模擬 smali 指令的字節流
val bytecode = byteArrayOf(0x1A, 0x00, 0x4E, 0x00, // const-string v0, "input"0x71, 0x20, 0x20, 0x00, 0x05, 0x00, // invoke-static{v5, v0}, checkNotNullParameter0x1A, 0x00, 0x2C, 0x00, // const-string v0, "SHA-256"0x71, 0x10, 0x1C, 0x00, 0x00, 0x00, // invoke-static{v0}, getInstance0x0C, 0x00, // move-result-object v00x62, 0x01, 0x09, 0x00, // sget-object v1, UTF_80x6E, 0x20, 0x16, 0x00, 0x15, 0x00, // invoke-virtual{v5, v1}, getBytes0x0C, 0x01, // move-result-object v10x6E, 0x20, 0x1B, 0x00, 0x10, 0x00, // invoke-virtual{v0, v1}, digest0x0C, 0x01, // move-result-object v10x71, 0x00, 0x1E, 0x00, 0x00, 0x00, // invoke-static{}, getEncoder0x0C, 0x02, // move-result-object v20x6E, 0x20, 0x1D, 0x00, 0x12, 0x00, // invoke-virtual{v2, v1}, encodeToString0x0C, 0x02, // move-result-object v20x11, 0x02  // return-object v2
)// 通過 VMP 解析器執行指令流
val result = SimpleVMP.execute(bytecode, input)// 顯示 Toast
Toast.makeText(this, result, Toast.LENGTH_SHORT).show()

通過 VMP 執行結果如下:

word/media/image5.png

和原來算法對比結果是一樣的。

word/media/image6.png

安全性增強

  1. 指令流加密:比如使用 AES 加密指令流,在運行時解密執行。

  2. 動態加載:使用 dex 動態加載虛擬機和指令流。

  3. 多態指令集:每次保護代碼時動態生成不同的指令集,防止通過固定指令集逆向。

  4. 反調試檢測:檢測調試器附加、內存修改或運行環境,防止虛擬機被分析。

優點與局限

優點

  • 提高逆向難度:通過指令集和虛擬機隱藏關鍵邏輯。

  • 動態保護:運行時加載和執行,防止靜態分析。

局限

  • 性能開銷:解釋執行比原生代碼慢。

  • 開發成本:需要設計和實現虛擬機框架。

通過上述方法,可以實現一個基本的自定義 Android 虛擬機保護,并根據需要逐步增強安全性。

源碼

完整源碼:https://github.com/CYRUS-STUDIO/AndroidExample

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/65970.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/65970.shtml
英文地址,請注明出處:http://en.pswp.cn/web/65970.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于華為atlas的重車(滿載)空車(空載)識別

該教程主要是想摸索出華為atlas的基于ACL的推理模式。最終實現通過煤礦磅道上方的攝像頭&#xff0c;識別出車輛的重車&#xff08;滿載&#xff09;、空車&#xff08;空載&#xff09;情況。本質上是一個簡單的檢測問題。 但是整體探索過程比較坎坷&#xff0c;Tianxiaomo的…

《零基礎Go語言算法實戰》【題目 2-25】goroutine 的執行權問題

《零基礎Go語言算法實戰》 【題目 2-25】goroutine 的執行權問題 請說明以下這段代碼為什么會卡死。 package main import ( "fmt" "runtime" ) func main() { var i byte go func() { for i 0; i < 255; i { } }() fmt.Println("start&quo…

IntelliJ IDEA中Maven項目的配置、創建與導入全攻略

大家好&#xff0c;我是袁庭新。 IntelliJ IDEA是當前最流行的Java IDE&#xff08;集成開發環境&#xff09;之一&#xff0c;也是業界公認最好用的Java開發工具之一。IntelliJ IDEA支持Maven的全部功能&#xff0c;通過它我們可以很輕松地實現創建Maven項目、導入Maven項目、…

TypeScript語言的學習路線

TypeScript語言的學習路線 TypeScript&#xff08;TS&#xff09;是由Microsoft開發的一種開源編程語言&#xff0c;是JavaScript的超集&#xff0c;提供了嚴格的類型檢查和基于類的面向對象編程特性。隨著前端開發的不斷進步&#xff0c;TypeScript逐漸成為了現代前端開發的主…

計算機網絡之---靜態路由與動態路由

靜態路由 靜態路由是由網絡管理員手動配置并固定的路由方式。路由器通過靜態配置的路由條目來轉發數據包&#xff0c;而不會自動調整。它不依賴于任何路由協議。 特點&#xff1a; 手動配置&#xff1a;網絡管理員需要手動在路由器中配置每條靜態路由。不自動更新&#xff1a;…

【Rust】函數

目錄 思維導圖 1. 函數的基本概念 1.1 函數的定義 2. 參數的使用 2.1 單個參數的示例 2.2 多個參數的示例 3. 語句與表達式 3.1 語句與表達式的區別 3.2 示例 4. 帶返回值的函數 4.1 返回值的示例 4.2 返回值與表達式 5. 錯誤處理 5.1 錯誤示例 思維導圖 1. 函數…

Cython全教程2 多種定義方式

—— 本篇文章&#xff0c;主要講述Cython中的四種定義關鍵字 全教程2 多種定義方式&#xff1a; 在Cython中&#xff0c;關于定義的關鍵字有四個&#xff0c;分別是&#xff1a; cdef、def、cpdef、DEF 一、cdef定義關鍵字 顧名思義&#xff0c;cdef關鍵字定義的是一個C函數…

Web開發(一)HTML5

Web開發&#xff08;一&#xff09;HTML5 寫在前面 參考黑馬程序員前端Web教程做的筆記&#xff0c;主要是想后面自己搭建網頁玩。 這部分是前端HTML5CSS3移動web視頻教程的HTML5部分。主要涉及到HTML的基礎語法。 HTML基礎 標簽定義 HTML定義 HTML(HyperText Markup Lan…

MATLAB學習筆記目錄

MATLAB學習筆記-生成純音并保存-CSDN博客 MATLAB學習筆記-各種格式之間的轉換 - 知乎 MATLAB學習筆記-胞組&#xff08;cell array&#xff09;轉換為矩陣&#xff0c;cell2mat_matlab如何把元胞數組改為矩陣-CSDN博客MATLAB學習筆記-判斷數組、結構體、數值、字符串是否相同…

Java-數據結構-棧與隊列(常考面試題與單調棧)

在上一篇的學習中&#xff0c;我們學習了棧和隊列的基本知識&#xff0c;以及它們對應都有哪些方法&#xff0c;在什么應用場景下如何使用&#xff0c;并且還對它們進行了模擬實現&#xff0c;而其實對于棧和隊列的相關知識還遠不止于此&#xff0c;而今天我們就對棧與隊列進行…

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

JSON.stringify(res, null, 2) 是 JavaScript 中將對象轉換為 JSON 字符串的方法&#xff0c;具體說明如下&#xff1a; 參數解釋 res&#xff1a;要轉換的對象。它可以是 JavaScript 中的任意類型&#xff0c;如對象、數組、字符串、數字等。例如&#xff0c;{name: "K…

Spring 項目 基于 Tomcat容器進行部署

文章目錄 一、前置知識二、本地Idea運行Spring項目1. 將寫好的 Spring 項目先打包成 war 包2. 查看項目工件&#xff08;Artifact&#xff09;是否存在3. 配置 Tomcat3.1 添加一個本地 Tomcat 容器3.2 將項目部署到 Tomcat 4. 運行項目 三、基于 Tomcat 部署及多實例部署1. Spr…

usbredir學習

文章目錄 背景典型場景編譯usbredirparserusbredirfilterusbredirparser/usbredirproto usbredirhostusbredirect/usbredirtestclient參考 背景 usbredir 是一種用于通過網絡轉發 USB 設備流量的網絡協議。它也是一個軟件包的名稱&#xff0c;該軟件包提供了一個解析庫、一個 …

ESXI 安裝教程(3) ---?vCenter Server 安裝

不涉及復雜的操作此項可不安裝 1.鏡像加載到虛擬光盤 對應的網盤文件 2.打開文件路徑 雙擊運行文件installer.exe 3.調整安裝語言 4.點擊安裝 5. 6. 證書,有效問題導致此提示,非專業網絡管理人員,不知道如何處理,此處點是即可 證書有效開始時間是安裝時間8小時 證書有效結束…

【初識掃盲】逆概率加權

我們正在處理一個存在缺失數據的回歸模型&#xff0c;并且希望采用一種非參數的逆概率加權方法來調整估計&#xff0c;以應對這種缺失數據的情況。 首先&#xff0c;我們需要明確問題的背景。我們有樣本 { ( Y i , X i , r i ) : i 1 , … , n } \left\{\left(Y_i, \boldsym…

極客說|Azure AI Agent Service 結合 AutoGen/Semantic Kernel 構建多智能體解決?案

作者&#xff1a;盧建暉 - 微軟高級云技術布道師 「極客說」 是一檔專注 AI 時代開發者分享的專欄&#xff0c;我們邀請來自微軟以及技術社區專家&#xff0c;帶來最前沿的技術干貨與實踐經驗。在這里&#xff0c;您將看到深度教程、最佳實踐和創新解決方案。關注「極客說」&am…

【集成學習】Boosting算法詳解

文章目錄 1. 集成學習概述2. Boosting算法詳解3. Gradient Boosting算法詳解3.1 基本思想3.2 公式推導 4. Python實現 1. 集成學習概述 集成學習&#xff08;Ensemble Learning&#xff09;是一種通過結合多個模型的預測結果來提高整體預測性能的技術。相比于單個模型&#xf…

小米vela系統(基于開源nuttx內核)——如何使用信號量進行PV操作

如何使用信號量進行PV操作 前言信號量1. 信號量簡介2. NuttX中信號量的創建與使用2.1 Nuttx信號量的初始化和銷毀2.2 信號量的等待和發布 3. 信號量的實際應用&#xff1a;下載任務示例3.1 實際代碼3.2 代碼說明3.3 執行說明 4. 信號量的優勢與應用場景5. 常見應用場景&#xf…

CMake學習筆記(2)

1. 嵌套的CMake 如果項目很大&#xff0c;或者項目中有很多的源碼目錄&#xff0c;在通過CMake管理項目的時候如果只使用一個CMakeLists.txt&#xff0c;那么這個文件相對會比較復雜&#xff0c;有一種化繁為簡的方式就是給每個源碼目錄都添加一個CMakeLists.txt文件&#xff…

旅游網站設計與實現

文末附有完整項目代碼 在當今數字化時代&#xff0c;旅游網站成為人們獲取旅游信息的重要途徑。本文將詳細介紹旅游網站的設計與實現&#xff0c;讓你輕松了解其中的技術奧秘&#xff01; 一、項目背景 隨著社會經濟的發展&#xff0c;人們對精神消費愈發重視&#xff0c;旅游…