当前位置: 首页 > news >正文

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

相关文章:

详解如何自定义 Android Dex VMP 保护壳

版权归作者所有&#xff0c;如有转发&#xff0c;请注明文章出处&#xff1a;https://cyrus-studio.github.io/blog/ 前言 Android Dex VMP&#xff08;Virtual Machine Protection&#xff0c;虚拟机保护&#xff09;壳是一种常见的应用保护技术&#xff0c;主要用于保护 And…...

PHP中的魔术函数

PHP 魔术函数是在某些情况下会自动调用的特殊函数&#xff0c;它们以双下划线 __ 开头&#xff0c;以下是对常见 PHP 魔术函数的详细介绍&#xff1a; ### 构造函数和析构函数 - **__construct()**&#xff1a; - 这是一个构造函数&#xff0c;在创建类的新对象时会自动调…...

excel 判断某个单元格的日期,如果超过3天,则在另一个单元格显示超过三天的公式

excel 判断某个单元格的日期&#xff0c;如果超过3天&#xff0c;则在另一个单元格显示超过三天的公式&#xff0c;公式如下&#xff1a; IF(DATEDIF(C627,TODAY(),"d")<4,"3天以内","超过三天") IF(D627"超过3天","文件赶紧…...

TCP 序列和确认号说明 | seq 和 ack 号计算方法

注&#xff1a;本文为 “TCP 序列” 相关文章合辑。 英文引文机翻未校。 TCP Sequence and Acknowledgement Numbers Explained TCP 序列和确认编号说明 TCP Sequence (seq) and Acknowledgement (ack) numbers help enable ordered reliable data transfer for TCP streams…...

【Linux】Mysql部署步骤

一、JDK安装配置 在home目录下执行命令&#xff1a;mkdir Jdk 1.将JDK 上传至该文件夹&#xff0c;有些终端工具可以直接上传文件&#xff0c;比如&#xff1a;MobaXterm 可以看到安装包已经上传上来了 2.直接安装 命令&#xff1a;rpm -ivh jdk-8u311-linux-x64.rpm 3.安装成…...

【算法】枚举

枚举 普通枚举1.铺地毯2.回文日期3.扫雷 二进制枚举1.子集2.费解的开关3.Even Parity 顾名思义&#xff0c;就是把所有情况全都罗列出来&#xff0c;然后找出符合题目要求的那一个。因此&#xff0c;枚举是一种纯暴力的算法。一般情况下&#xff0c;枚举策略都是会超时的。此时…...

【C++】构造函数与析构函数

写在前面 构造函数与析构函数都是属于类的默认成员函数&#xff01; 默认成员函数是程序猿不显示声明定义&#xff0c;编译器会中生成。 构造函数和析构函数的知识需要建立在有初步类与对象的基础之上的&#xff0c;关于类与对象不才在前面笔记中有详细的介绍&#xff1a;点我…...

力扣刷题汇总

动态规划 1 . 最大子序和 (Maximum Subarray Sum) Leetcode 53. 最大子数组和 经典dp 问题描述&#xff1a;给定一个整数数组&#xff0c;求其中和最大的连续子数组的和。 状态定义&#xff1a;dp[i] 表示以第 i 个元素结尾的最大子序和。 2 . 最长公共子序列 (Longest Commo…...

Ansible自动化运维:基础与实践

在当今的IT运维领域&#xff0c;Ansible作为一款强大的自动化运维工具&#xff0c;正发挥着日益重要的作用。本文将详细介绍Ansible的相关知识&#xff0c;包括其作用、特点、安装配置以及常用模块的使用方法&#xff0c;旨在帮助读者快速上手并熟练运用Ansible进行自动化运维工…...

微信小程序在使用页面栈保存页面信息时,如何避免数据丢失?

微信小程序在使用页面栈保存页面信息时避免数据丢失的方法&#xff1a; 一、使用全局变量存储关键数据&#xff1a; 定义一个全局变量&#xff0c;例如在 app.js 中&#xff0c;用于存储页面的重要信息。在页面的 onHide 或 onUnload 生命周期中&#xff0c;将需要保存的数据…...

我国无人机新增实名登记110.3 万架,累计完成飞行2666万小时

据央视新闻从中国民航局了解到&#xff0c;2024 年我国全年新增通航企业 145 家、通用机场 26 个&#xff0c;颁发无人驾驶航空器型号合格证 6 个、新增实名登记无人机 110.3 万架&#xff0c;无人机运营单位总数超过 2 万家&#xff0c;累计完成无人机飞行 2666 万小时&#x…...

vue3+vite+ts+router4+Pinia+Axios+sass 从0到1搭建

1、使用vite构建项目 npm create vitelatest 填写项目名的时候不能大写 2、跑起来之后配置下 import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path // https://vite.dev/config/ export default defineConfig({plugins: [vue…...

C语言:-三子棋游戏代码:分支-循环-数组-函数集合

思路分析&#xff1a; 1、写菜单 2、菜单之后进入游戏的操作 3、写函数 实现游戏 3.1、初始化棋盘函数&#xff0c;使数组元素都为空格 3.2、打印棋盘 棋盘的大概样子 3.3、玩家出棋 3.3.1、限制玩家要下的坐标位置 3.3.2、判断玩家要下的位置是否由棋子 3.4、电脑出棋 3.4.1、…...

前端调试遇到的无限debugger的原理与绕过

背景 debugger 是 JavaScript 中定义的一个专门用于断点调试的关键字,只要遇到它,JavaScript 的执行便会在此处中断,进入调试模式。有了 debugger 这个关键字,我们就可以非常方便地对 JavaScript 代码进行调试,比如使用 JavaScript Hook 时,我们可以加入 debugger 关键字…...

Java负载均衡

Java中的负载均衡原理是指通过合理分配网络请求或计算任务的方式&#xff0c;将工作负载分配到多个服务器、处理单元或服务实例上&#xff0c;从而提高系统的性能、可扩展性和可用性。负载均衡不仅可以分散请求压力&#xff0c;还能增强系统的容错能力&#xff0c;避免单点故障…...

spark,读取和写入同一张表问题

读取a表&#xff0c;写入a表 1.写入的是分区表&#xff0c;不报错 2.读取上来之后&#xff0c;创建为临时视图temp&#xff0c;然后先写入a表&#xff0c;再使用temp&#xff0c;就会报错 解决办法&#xff1a;可以先使用temp&#xff0c;再写入a表 3.写入的不是分区表&…...

用gpg和sha256验证ubuntu.iso

链接 https://ubuntu.com/tutorials/how-to-verify-ubuntuhttps://releases.ubuntu.com/jammy/ 本文是2的简明版 sha256sum介绍 sha256sum -c SHA256SUMS 2>&1这段脚本的作用是验证文件的 SHA-256 校验和。具体来说&#xff0c;命令的各个部分含义如下&#xff1a; …...

HIVE技术

本文章基于黑马免费资料编写。 hive介绍 简介 hive架构 hive需要启动的配置 执行元数据库初始化命令 使用hive必须启动的服务 ./schematool -initSchema -dbType mysql -verbos启动 Hive 创建一个 hive 的日志文件夹 mkdir /export/server/hive/logs启动元数据管理服务 n…...

我的世界-与门、或门、非门等基本门电路实现

一、红石比较器 (1) 红石比较器结构 红石比较器有前端单火把、后端双火把以及两个侧端 其中后端和侧端是输入信号,前端是输出信号 (2) 红石比较器的两种模式 比较模式 前端火把未点亮时处于比较模式 侧端>后端 → 0 当任一侧端强度大于后端强度时,输出…...

GPU 硬件原理架构(一)

这张费米管线架构图能看懂了&#xff0c;整个GPU的架构基本就熟了。市面上有很多GPU厂家&#xff0c;他们产品的架构各不相同&#xff0c;但是核心往往差不多&#xff0c;整明白一了个基本上就可以触类旁通了。下面这张图信息量很大&#xff0c;可以结合博客GPU 英伟达GPU架构回…...

[Qt]窗口-QMainWindow类-QMenuBar、QToolBar、QStatusBar、QDockWidget控件

目录 1.QMainWindow类介绍 2.菜单栏-QMenuBar控件 创建菜单栏 添加菜单和菜单选项 triggered信号 设置快捷键 添加分割线 添加图标 使用案例 3.工具栏-QToolBar控件 使用介绍 设置停靠位置 设置浮动属性 设置移动属性 使用案例 4.状态栏-QStatusBar控件 状…...

Linux命令行工具-使用方法

参考资料 Linux网络命令&#xff1a;网络工具socat详解-CSDN博客 arm-linux-gnueabihf、aarch64-linux-gnu等ARM交叉编译GCC的区别_aarch64-elf-gcc aarch64-linux-gnu-CSDN博客 解决Linux内核问题实用技巧之-dev/mem的新玩法-腾讯云开发者社区-腾讯云 热爱学习地派大星-CS…...

HTML中如何保留字符串的空白符和换行符号的效果

有个字符串 储值门店{{thing3.DATA}}\n储值卡号{{character_string1.DATA}}\n储值金额{{amount4.DATA}}\n当前余额{{amount5.DATA}}\n储值时间{{time2.DATA}} &#xff0c; HTML中想要保留 \n的换行效果的有下面3种方法&#xff1a; 1、style 中 设置 white-space: pre-lin…...

ASP.NET Core WebApi接口IP限流实践技术指南

在当今的Web开发中&#xff0c;接口的安全性和稳定性至关重要。面对恶意请求或频繁访问&#xff0c;我们需要采取有效的措施来保护我们的WebApi接口。IP限流是一种常见的技术手段&#xff0c;通过对来自同一IP地址的请求进行频率控制&#xff0c;可以有效地防止恶意攻击和过度消…...

SparkSQL数据模型综合实践

文章目录 1. 实战概述2. 实战步骤2.1 创建数据集2.2 创建数据模型对象2.2.1 创建常量2.2.2 创建加载数据方法2.2.3 创建过滤年龄方法2.2.4 创建平均薪水方法2.2.5 创建主方法2.2.6 查看完整代码 2.3 运行程序&#xff0c;查看结果 3. 实战小结 1. 实战概述 在本次实战中&#…...

C++实现设计模式---外观模式 (Facade)

外观模式 (Facade) 外观模式 是一种结构型设计模式&#xff0c;为子系统中的一组接口提供一个一致的界面。外观模式定义了一个更高层次的接口&#xff0c;使得子系统更容易使用。 意图 简化复杂子系统的接口。为客户端提供一个统一的入口&#xff0c;屏蔽子系统的内部细节。 …...

计算机网络 (43)万维网WWW

前言 万维网&#xff08;World Wide Web&#xff0c;WWW&#xff09;是Internet上集文本、声音、动画、视频等多种媒体信息于一身的信息服务系统。 一、基本概念与组成 定义&#xff1a;万维网是一个分布式、联机式的信息存储空间&#xff0c;通过超文本链接的方式将分散的信息…...

C# 获取PDF文档中的字体信息(字体名、大小、颜色、样式等

在设计和出版行业中&#xff0c;字体的选择和使用对最终作品的质量有着重要影响。然而&#xff0c;有时我们可能会遇到包含未知字体的PDF文件&#xff0c;这使得我们无法准确地复制或修改文档。获取PDF中的字体信息可以解决这个问题&#xff0c;让我们能够更好地处理这些文件。…...

Docker Desktop 中安装 MySQL 并开启远程访问的详细教程

是在 Docker Desktop 中安装 MySQL 并开启远程访问的详细教程&#xff1a; 一、安装 MySQL 容器 拉取 MySQL 镜像&#xff1a; docker pull mysql:latest这将从 Docker Hub 上拉取最新版本的 MySQL 镜像。如果你想使用特定版本的 MySQL&#xff0c;可以将 latest 替换为具体…...

沸点 | 聚焦嬴图Cloud V2.1:具备水平可扩展性+深度计算的云原生嬴图动力站!

近日&#xff0c;嬴图正式推出嬴图Cloud V2.1&#xff0c;此次发布专注于提供无与伦比的用户体验&#xff0c;包括具有水平可扩展性的嬴图Powerhouse的一键部署、具有灵活定制功能的管理控制台、VPC / 专用链接等&#xff0c;旨在满足用户不断变化需求的各项前沿功能&#xff0…...

西门子【Library of Basic Controls (LBC)基本控制库”(LBC) 提供基本控制功能】

AF架构中使用的库 文章目录 Table of contents Legal information ..............................................................................................................................2 1 Introduction ................................................…...

EMQX集群搭建

集群搭建 通过使用 EMQX 集群&#xff0c;您可以在一个或多个节点发生故障时仍然保持集群运行&#xff0c;从而享受到容错和高可用性的好处。 尽管没有严格的上限&#xff0c;但建议在 EMQX 开源版中将集群大小限制为三个节点。仅使用核心类型节点时&#xff0c;较小的集群规模…...

【Flink系列】10. Flink SQL

10. Flink SQL Table API和SQL是最上层的API&#xff0c;在Flink中这两种API被集成在一起&#xff0c;SQL执行的对象也是Flink中的表&#xff08;Table&#xff09;&#xff0c;所以我们一般会认为它们是一体的。Flink是批流统一的处理框架&#xff0c;无论是批处理&#xff08…...

Java安全—SPEL表达式XXESSTI模板注入JDBCMyBatis注入

前言 之前我们讲过SpringBoot中的MyBatis注入和模板注入的原理&#xff0c;那么今天我们就讲一下利用以及发现。 这里推荐两个专门研究java漏洞的靶场&#xff0c;本次也是根据这两个靶场来分析代码&#xff0c;两个靶场都是差不多的。 https://github.com/bewhale/JavaSec …...

TCP 连接状态标识 | SYN, FIN, ACK, PSH, RST, URG

注&#xff1a;本文为“TCP 连接状态标识”相关文章合辑。 TCP 的状态&#xff1a;SYN, FIN, ACK, PSH, RST, URG 简介及 ACK 确认机制 llzhang_fly 于 2020-09-19 05:25:26 发布 1、TCP 的状态 FLAGS 字段状态 在 TCP 层&#xff0c;有个 FLAGS 字段&#xff0c;这个字段有…...

OSPF的LSA的学习研究

OSPF常见1、2、3、4、5、7类LSA的研究 1、拓扑如图&#xff0c;按照地址表配置&#xff0c;激活OSPF划分相关区域并宣告相关网段 2、1类LSA&#xff0c;每台运行了OSPF的路由器都会产生&#xff0c;描述了路由器的直连接口状况和cost 可以看到R1产生了一条router lsa&#xff0…...

C# OpenCV机器视觉:转速测量

在一个看似平常却又暗藏神秘能量的日子里&#xff0c;阿杰正在他那充满科技感的实验室里&#xff0c;对着一堆奇奇怪怪的仪器发呆。突然&#xff0c;手机铃声如一道凌厉的剑气划破寂静&#xff0c;原来是工厂的赵厂长打来的紧急电话&#xff1a;“阿杰啊&#xff0c;咱们工厂新…...

wireshark 网络分析工具

✍作者&#xff1a;柒烨带你飞 &#x1f4aa;格言&#xff1a;生活的情况越艰难&#xff0c;我越感到自己更坚强&#xff1b;我这个人走得很慢&#xff0c;但我从不后退。 &#x1f4dc;系列专栏&#xff1a;网络安全从菜鸟到飞鸟的逆袭 目录 一、网络截获数据包的基础1、以太网…...

XXL-JOB 加入 GitCode:推动分布式任务调度进阶发展

在当今企业数字化转型加速的时代背景下&#xff0c;任务调度在保障系统高效运行方面的关键作用日益凸显。XXL-JOB 正式加入 GitCode&#xff0c;成为 G-Star 优秀毕业项目&#xff0c;为分布式任务调度领域带来了新的契机与活力&#xff0c;助力企业应对复杂多变的业务需求。 X…...

Java Web开发进阶——WebSocket与实时通信

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议&#xff0c;广泛应用于需要实时数据交换的应用程序中。它能够实现服务器与客户端之间的双向通信&#xff0c;避免了传统 HTTP 请求/响应的延迟。结合 Spring Boot&#xff0c;开发实时通信应用变得更加高效与简便。 1. …...

解决“无法定位程序输入点 av_buffer_create 于动态链接库 XXX\Obsidian.exe 上”问题

解决“无法定位程序输入点 av_buffer_create 于动态链接库 XXX\Obsidian.exe 上”问题 问题描述 本人在使用zotero中的zotero one&#xff08;青柠学术插件&#xff09;的时候&#xff0c;使用插件跳转obsidian中的对应笔记&#xff0c;出现上图情况。&#xff08;错误中提到的…...

晨辉面试抽签和评分管理系统之十:如何搭建自己的数据库服务器,使用本软件的网络版

晨辉面试抽签和评分管理系统&#xff08;下载地址:www.chenhuisoft.cn&#xff09;是公务员招录面试、教师资格考试面试、企业招录面试等各类面试通用的考生编排、考生入场抽签、候考室倒计时管理、面试考官抽签、面试评分记录和成绩核算的面试全流程信息化管理软件。提供了考生…...

分布式数据存储基础与HDFS操作实践(副本)

以下为作者本人撰写的报告&#xff0c;步骤略有繁琐&#xff0c;不建议作为参考内容&#xff0c;可以适当浏览&#xff0c;进一步理解。 一、实验目的 1、理解分布式文件系统的基本概念和工作原理。 2、掌握Hadoop分布式文件系统&#xff08;HDFS&#xff09;的基本操作。 …...

Rust:指针 `*T` 和引用 `T`的区别

在 Rust 编程语言中&#xff0c;*T 和 &T 是两种不同类型的指针&#xff0c;它们各自代表了不同的内存访问方式和所有权模型。 *T&#xff08;原始指针或裸指针&#xff09;&#xff1a; *T 是一个原始指针&#xff08;也称为裸指针或裸引用&#xff09;&#xff0c;它可以…...

【2025最新版】PCL点云处理算法汇总(C++长期更新版)

博客长期更新&#xff0c;最近一次更新时间为&#xff1a;2025年1月17日。 pcl::copyPointCloud(*cloud, indicesY, *cloud_yboundary);目录 配库常用数据免费下载链接一、点云滤波1、常用滤波器2、采样滤波3、裁剪滤波 二、KD树与八叉树1、KD树2、八叉树 三、点云配准粗配准精…...

换了城市ip属地会变吗?为什么换了城市IP属地不变

当我们跨越城市的界限&#xff0c;从一个地方迁移到另一个地方时&#xff0c;许多日常使用的网络服务和应用程序都会感知到这种变化&#xff0c;其中一个显著的现象就是IP属地的变化。IP属地&#xff0c;即IP地址所在的地理位置信息&#xff0c;它通常与互联网服务提供商&#…...

mysql 如何快速删除表数据

在数据库管理中, 经常会遇到需要删除大量数据的情况. 对于 MySQL 数据库而言, 如何高效快速地删除数据是一个值得深入探讨的问题. 本文将详细介绍几种在 MySQL 中快速删除数据的方法及相关注意事项. delete 语句 delete 语句可以删除符合条件的指定数据, 但是在删除大量数据…...

Windows安装Jenkins——及修改主目录、配置简体中文、修改插件源

一、简介 Jenkinshttps://www.jenkins.io/zh/ Jenkins是开源CI&CD软件领导者, 提供超过1000个插件来支持构建、部署、自动化, 满足任何项目的需要。 二、Windows安装配置Jenkins2.479 2.1、J...

【机器学习:二十二、机器学习项目开发的技巧】

机器学习项目开发的技巧 机器学习项目的开发不仅仅依赖于算法的选择和模型的调优&#xff0c;还需要良好的项目管理技巧和方法论。以下是机器学习项目开发中的关键技巧&#xff1a; 明确需求&#xff1a;在项目启动之前&#xff0c;明确问题定义和业务目标。例如&#xff0c;…...

用python实战excel和word自动化

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 python实现excel和word自动化--批量处理 前言--需求快要期末了需要&#xff0c;提交一个年级的学生成绩数据&#xff0c;也就是几百份。当前我们收集了一份excel表格&#xf…...