hmacmd5算法1. 简介HMAC-MD5算法是一种基于MD5散列函数和密钥的消息认证码算法，它通过结合密钥和消息内容来生成一个长度为128位的哈希值，用于验证消息的完整性和身份；与普通的 MD5 哈希算法不同，HMAC-MD5 在计算过程中使用了一个密钥，使得攻击者无法仅凭猜测数据本身来伪造消息的认证码;如果密钥太长，它需要被压缩；如果太短，它需要被填充到特定长度；总结就是：两次加盐，两次哈希；2. 算法流程输入：消息（Message）：需要进行验证的消息数据；密钥（Key）：用于生成消息认证码的密钥；输出：HMAC-MD5值：一个128位的签名值(32个十六进制位的数据)，用于验证消息的完整性和真实性；步骤详述：步骤 1：密钥准备如果密钥长度大于MD5的块大小（64字节），则先对密钥执行一次MD5哈希运算，生成一个16字节（128位）的密钥摘要（K'），然后再进行填充；如果密钥长度小于64字节，则用零字节（0x00）填充密钥至64字节；如果密钥长度正好是64字节，则直接使用密钥；步骤2：异或0x36，也叫ipad逐字节异或得到扩展密钥1；0x36的十进制是54；这里的ipad不是一个数据，应该是与key相同长度的数组；步骤3：异或0x5c，也叫opad逐字节异或得到扩展密钥2；0x54的十进制是92；步骤5：第一次加盐扩展密钥1与明文拼接；步骤6：第一次哈希加盐的结果进行哈希；步骤7：第二次加盐扩展密钥2与第一次哈希的结果拼接；步骤8：第二次哈希将上述拼接的结果进行哈希；总结：HMAC(K, M) = H((K' ⊕ opad) || H((K' ⊕ ipad) || M))K：密钥M：明文H：哈希函数K'：扩展密钥⊕：逐字节异或opad：0x5cipad：0x36|| ：拼接算法流程简述如上，以一个简单的示例来看整体的流程；3. 完整示例以sana为明文，xiayutian为key，进行整个流程的介绍；参考公式： H((K' ⊕ opad) || H((K' ⊕ ipad) || M))3.1 密钥扩展首先进行密钥扩展，xiayutian的十六进制表示为：78 69 61 79 75 74 69 61 6e长度不够512个比特位，所以需要进行填充，填充后的结果如下：78 69 61 79 75 74 69 61 6E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 一共是55个零字节，加上原有的数据就是64个字节；3.2 与0x36异或随后密钥与0x36（ipad）逐字节异或，这里的0x36不是单独的一个数据，而是64个，毕竟要一一对应；得到异或的结果，这就对应着第一个扩展密钥，也叫扩展密钥1；4E 5F 57 4F 43 42 5F 57 58 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 3.3 第一次加盐然后拼接明文，将明文放在最后，这就是第一次加盐；明文sana的十六进制：73 61 6e 61拼接后数据（68字节）: 4E 5F 57 4F 43 42 5F 57 58 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 36 73 61 6E 61 3.4 第一次哈希将这个结果做一个md5加密，这就是第一次哈希；fa23080448b15547fe4b2a19226cf9b73.5 与0x5c异或获取扩展密钥2，也就是密钥与opad（0x5c）逐字节异或；24 35 3D 25 29 28 35 3D 32 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 3.6 第二次加盐将扩展密钥2与第一步哈希的结果拼接起来，这就是第二次加盐；24 35 3D 25 29 28 35 3D 32 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C 5C fa 23 08 04 48 b1 55 47 fe 4b 2a 19 22 6c f9 b7 3.7 第二次哈希将第二次加盐的结果进行哈希，这就是第二次哈希；3d38802f21ef45a3eb05524f504810bc做一下结果对比；结果是没问题的，主要的流程就是两次加盐两次哈希；4. 总结首先是流程：HMAC(K, M) = H((K' ⊕ opad) || H((K' ⊕ ipad) || M))两次加盐、两次哈希；也有可能是三次哈希，密钥如果超过64字节则需要先哈希再填充；算法的特征就是0x36和0x5c，也有可能是十进制的形式，54和92；魔改的点一般在于载体哈希函数，并不去魔改两次加盐两次哈希这个流程，但也说不好；5. 代码实现import hmac import hashlib def hmac_md5(key, message): # 将 key 和 message 转换为字节类型 key = bytes(key, 'utf-8') message = bytes(message, 'utf-8') # 计算 HMAC-MD5 hmac_md5_hash = hmac.new(key, message, hashlib.md5).hexdigest() return hmac_md5_hash # 示例使用 key = "xiayutian" message = "sana" result = hmac_md5(key, message) print("HMAC-MD5:", result) # 3d38802f21ef45a3eb05524f504810bc
NDK开发基础1. 基础1.1 指定平台编译apk时可以指定编译哪些平台的so，在app下的build.gradle里配置 ndk 即可，默认不写则是四个平台都编译；defaultConfig { applicationId "com.example.devicecheckfinger" minSdk 22 targetSdk 34 versionCode 1 versionName "1.0" ndk{ abiFilters 'armeabi-v7a', 'arm64-v8a' } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }1.2 可选平台查看自己的cpu支持哪些平台，可以使用Build下的一个api，如下：Log.d("sana abi support", Arrays.toString(Build.SUPPORTED_ABIS)); // [arm64-v8a, armeabi-v7a, armeabi]2. jni相关首先，JNI是Java Native Interface缩写，JNI提供了一套标准API，允许Java代码与本地编译语言（如C/C++）交互；Java调用本地代码：通过native关键字声明方法，由C/C++实现具体逻辑。本地代码调用Java：C/C++可通过JNI访问Java对象、调用Java方法；实际上ndk与jni是一回事，写的代码是c或者c++，而不是java的代码了；在生成的示例文件中，可以发现有些函数前会有 extern "C" 字样，他是什么意思？extern "C"是为了解决C++ 的重载特性，会出现名称粉碎这个特性，C++编译器会把void foo(int)编译为类似_foo_int的符号，而C编译器只会生成 _foo ，若 C++ 直接调用 C 函数，会因符号名不匹配导致链接错误，这么写就是为了消除这种特性；众所周知，jni函数前两个参数都是固定的，第一个为jnienv，第二个则是jclass或者jobject，第二个参数分别是如下情况：jclass对应的java方法是静态方法，有static修饰符；jobject对应的java方法则是实例方法；JVM虚拟机：JVM是Java虚拟机的简称，是Java字节码的运行环境，JVM标准实现是栈架构虚拟机，执行class文件，采用JIT即时编译，运行时效率不高；ART虚拟机：ART是Android Runtime，是Android系统上的运行环境，ART处理的是dex字节码文件格式；Android早期确实使用JVM（具体是Dalvik虚拟机）来运行应用，但Dalvik采用JIT即时编译，运行时效率不高；2014年安卓4.4开始引入ART作为实验选项，5.0后全面取代Dalvik；ART最大的改变是采用AOT预先编译技术，把字节码提前编译成本地机器码；so加载主要会执行的几个函数：首先是init、init_array、Jni_OnLoad；3. java反射资料：https://www.jianshu.com/p/9be58ee20dee https://www.runoob.com/java/java-reflection.html3.1 基本概念在Android开发中，反射（Reflection） 是Java语言的核心特性之一，它允许程序在运行时动态加载类、查看类的信息（比如类名、方法、字段、构造器等）、动态调用对象的方法、动态访问或修改对象的字段；或者说：JAVA反射机制是在运行状态中，对于任意一个类，都能够知道这个类的所有属性和方法；对于任意一个对象，都能够调用它的任意方法和属性；这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制；3.2 重要api与Java反射相关的类如下：类名用途Class类代表类的实体，在运行的Java应用程序中表示类和接口Field类代表类的成员变量（成员变量也称为类的属性）Method类代表类的方法Constructor类代表类的构造方法Class代表类的实体，在运行的Java应用程序中表示类和接口，在这个类中提供了很多有用的方法，这里对他们简单的分类介绍；获得类相关的方法：方法用途asSubclass(Class clazz)把传递的类的对象转换成代表其子类的对象Cast把对象转换成代表类或是接口的对象getClassLoader()获得类的加载器getClasses()返回一个数组，数组中包含该类中所有公共类和接口类的对象getDeclaredClasses()返回一个数组，数组中包含该类中所有类和接口类的对象forName(String className)根据类名返回类的对象getName()获得类的完整路径名字newInstance()创建类的实例getPackage()获得类的包getSimpleName()获得类的名字getSuperclass()获得当前类继承的父类的名字getInterfaces()获得当前类实现的类或是接口获得类中属性相关的方法：方法用途getField(String name)获得某个公有的属性对象getFields()获得所有公有的属性对象getDeclaredField(String name)获得某个属性对象getDeclaredFields()获得所有属性对象获得类中注解相关的方法：方法用途 getAnnotation(Class annotationClass)返回该类中与参数类型匹配的公有注解对象getAnnotations()返回该类所有的公有注解对象getDeclaredAnnotation(Class annotationClass)返回该类中与参数类型匹配的所有注解对象getDeclaredAnnotations()返回该类所有的注解对象获得类中构造器相关的方法：方法用途getConstructor(Class...<?> parameterTypes)获得该类中与参数类型匹配的公有构造方法getConstructors()获得该类的所有公有构造方法getDeclaredConstructor(Class...<?> parameterTypes)获得该类中与参数类型匹配的构造方法getDeclaredConstructors()获得该类所有构造方法获得类中方法相关的方法：方法用途getMethod(String name, Class...<?> parameterTypes)获得该类某个公有的方法getMethods()获得该类所有公有的方法getDeclaredMethod(String name, Class...<?> parameterTypes)获得该类某个方法getDeclaredMethods()获得该类所有方法类中其他重要的方法：方法用途isAnnotation()如果是注解类型则返回trueisAnnotationPresent(Class<? extends Annotation> annotationClass)如果是指定类型注解类型则返回trueisAnonymousClass()如果是匿名类则返回trueisArray()如果是一个数组类则返回trueisEnum()如果是枚举类则返回trueisInstance(Object obj)如果obj是该类的实例则返回trueisInterface()如果是接口类则返回trueisLocalClass()如果是局部类则返回trueisMemberClass()如果是内部类则返回trueField代表类的成员变量（成员变量也称为类的属性）：方法用途equals(Object obj)属性与obj相等则返回trueget(Object obj)获得obj中对应的属性值set(Object obj, Object value)设置obj中对应属性值Methond代表类的方法:方法用途invoke(Object obj, Object... args)传递object对象及参数调用该对象对应的方法Constructor代表类的构造方法:方法用途newInstance(Object... initargs)根据传递的参数创建类的对象setAccessible方法，用于获得权限，是一个很重要的点；4. 基础开发目标：读写SD卡文件；创建项目后需要先在静态资源里申请权限，这里分别是sd卡的读、写、操作权限：<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />读取sd卡的文件内容：extern "C" JNIEXPORT jstring JNICALL Java_com_sana_kanxuendk_MainActivity_readSDcardFile(JNIEnv *env, jobject thiz, jstring path) { jstring jstring_ret = env->NewStringUTF("null"); char * tmp = (char *)env->GetStringUTFChars(jstring_ret, 0); char* filePath = (char*)env->GetStringUTFChars(path, 0); FILE *fp; fp = fopen(filePath, "r"); if (fp == NULL){ LOGI("fp == NULL, %s", filePath); return jstring_ret; } char buff[1024]; while (fgets(buff, 1024, fp) != NULL){ LOGI("fgets :%s", buff); } env->ReleaseStringChars(jstring_ret, reinterpret_cast<const jchar *>(tmp)); jstring_ret = env->NewStringUTF(buff); return jstring_ret; }目标：练习反射，Student类如下：package com.sana.kanxuendk; import android.util.Log; public class Student { private String studentName; private int studentAge = 22; public Student(String studentName, int studentAge) { this.studentName = studentName; this.studentAge = studentAge; } public Student(String studentName) { this.studentName = studentName; } public String study(int flag) { Log.i("sanaTag", "name:" + studentName + ",age:" + studentAge + ",flag:" + flag); getAge(); return "studyRet"; } public static int calcLength(String param) { return param.length(); } private int getAge() { return studentAge; } }getDeclaredMethods：获得该类所有方法，但不包含构造方法：public static void func_reflection(){ Class clz = null; try { // 获取该类 clz = Class.forName("com.sana.kanxuendk.Student"); // getDeclaredMethods 获取该类的所有方法 但不包括构造方法 Method[] declaredMethods = clz.getDeclaredMethods(); for (Method method : declaredMethods) { Log.d("sanaTag", "method:" + method.getName()); Log.d("sanaTag", "method getReturnType:" + method.getReturnType().getSimpleName()); } } catch (ClassNotFoundException e) { throw new RuntimeException(e); } }getDeclaredConstructors：获取该类所有构造方法：// 获取该类所有构造方法 Constructor[] declaredConstructors = clz.getDeclaredConstructors(); for (Constructor constructor : declaredConstructors) { Log.d("sanaTag", "constructor:" + constructor.getName()); Log.d("sanaTag", "constructor toGenericString:" + constructor.toGenericString()); }调用方法：public static void func_reflection2() { // 获取类 Class clz = Student.class; try { // 获取方法 Method method_study = clz.getMethod("study", new Class[]{int.class}); // 获取构造方法 Constructor constructor = clz.getConstructor(new Class[]{String.class, int.class}); // 实例化 Student student = (Student) constructor.newInstance("xiayutian", 23); // 调用方法 String res = (String) method_study.invoke(student, new Object[]{666}); Log.d("sanaTag", "func_reflection2 res:" + res); } catch (Exception e) { e.printStackTrace(); } }调用private方法getAge：public static void func_reflection3() { Class clz = one.getClass(); try { // Method method_getAge = clz.getMethod("getAge", new Class[]{}); Method method_getAge = clz.getDeclaredMethod("getAge"); // 设置权限 method_getAge.setAccessible(true); int age = (int) method_getAge.invoke(one, new Object[]{}); Log.d("sanaTag", "func_reflection3 age:" + age); }catch (Exception e) { e.printStackTrace(); } }5. NDK反射NDK 通过 JNI 提供以下关键函数实现反射：FindClass()：获取 Java 类对象（如 jclass cls = env->FindClass("com/example/MyClass");）；GetMethodID() / GetFieldID()：获取方法或字段 ID（需指定方法名、签名和返回值类型）；Call<Type>Method() / Get<Type>Field()：通过 ID 调用方法或读写字段；实例1：调用study方法；extern "C" JNIEXPORT jint JNICALL Java_com_sana_kanxuendk_MainActivity_callJavaMethod(JNIEnv *env, jobject thiz, jobject student) { // 找到类 与下一句 FindClass 同等 jclass jclazz_student = env->GetObjectClass(student); jclass jclazz_student2 = env->FindClass("com/sana/kanxuendk/Student"); // 获取方法id ndk层面的获取方法 通过id去调用 jmethodID jmethod_study = env->GetMethodID(jclazz_student, "study", "(I)Ljava/lang/String;"); int flag = 34; // 调用 jmethod_study 方法 jobject jstring_ret = env->CallObjectMethod(student, jmethod_study, flag); char * tmp = (char *)env->GetStringUTFChars((jstring)jstring_ret, 0); LOGI("jstring_ret : %s", tmp); return flag; }实例2：调用static方法calcLength：extern "C" JNIEXPORT jint JNICALL Java_com_sana_kanxuendk_MainActivity_callJavaMethod2(JNIEnv *env, jobject thiz) { jclass jclazz_student2 = env->FindClass("com/sana/kanxuendk/Student"); jmethodID jmethod_study = env->GetStaticMethodID(jclazz_student2, "calcLength", "(Ljava/lang/String;)I"); jstring jstring_name = env->NewStringUTF("sana"); int length = env->CallStaticIntMethod(jclazz_student2, jmethod_study, jstring_name); LOGI("sana length : %d", length); return length; }
ARM汇编学习一参考资料：ARM汇编语言官方手册.pdf1. ARM汇编常见指令1.1 数据处理指令Ⅰ MOV数据赋值指令；mov r0, #0将 立即数 0 加载到寄存器 r0 中，也就是给r0赋值为0；Ⅱ ADD加法指令；add r0, r0, r1将 r0 + r1的值赋值给 r0；Ⅲ SUB减法指令；sub r0, r0, r1将 r0 - r1 的值赋值给r0；Ⅳ AND与操作指令；and r0, r0, #6先给r0赋值为5，然后将r0与一个立即数按位与操作，并将结果给r0，这里也可以与另一个寄存器操作；Ⅴ ORR或操作指令；orr r0, r0, #6将r0寄存器的值与6进行或操作，将结果写入r0寄存器中；Ⅵ EOR异或操作指令；eor r0, r0, #8将r0寄存器的值与8进行异或操作，并将结果写入r0寄存器中；Ⅶ BIC数据位清0指令；bic r0, r0, #0xF0000000将 R0 的高4位清零，保留低28位；假设想要将后两位置0，则写3，也就是0011，其他的以此类推；1.2 移位指令Ⅰ LSL数据左移指令；lsl r0, r0, #2左移 n 位等价于乘以2的n次方，例如，LSL R0, R0, #2 等同于 R0 = R0 * 4；@ 快速乘法运算 MOV R1, #5 @ R1 = 5 LSL R0, R1, #2 @ R0 = 5 << 2 = 20（等价于5×4）Ⅱ LSR数据右移指令；lsr r0, r0, #1立即数范围为 0–31（32位寄存器）或 0–63（64位寄存器）,右移 n 位等价于无符号数除以2的n次方;例如，LSR R0, R1, #2 等同于 R0 = R1 / 4;@ 快速无符号除法 MOV R1, #60 @ R1 = 60（二进制111100） LSR R0, R1, #2 @ R0 = 60 >> 2 = 15（二进制001111）[5](@ref)Ⅲ ROR循环右移指令；并且这里没有循环左移的指令；ror r0, r0, #1将寄存器 R0 中的二进制数据向右移动 1 位，并将最低位移出的位填充到最高位，形成循环移位；ROR R0, R0, #1 @ R0 = R0 循环右移1位 @右移时，寄存器中的每一位向右移动指定位数，最低位移出的位依次填充到最高位 MOV R0, #0x12345678 @ R0 = 0001 0010 0011 0100 0101 0110 0111 1000 ROR R0, R0, #8 @ R0 = 0111 1000 0001 0010 0011 0100 0101 0110（即 0x78123456）ror与lsr有什么区别？@ LSR（逻辑右移）： @ 将寄存器中的二进制数据向右移动指定位数，​左侧空出的高位补0，右侧移出的低位直接丢弃 LSR R0, R1, #3 @ R1右移3位（如R1=0b1000，结果R0=0b0001） @ ROR（循环右移）: @ 将寄存器中的二进制数据向右移动指定位数，右侧移出的低位依次填充到左侧高位，形成循环移位 ROR R0, R1, #1 @ R1循环右移1位（如R1=0b1010，结果R0=0b0101） @ LSR 和 ROR 是ARM汇编中两种互补的移位指令： @ 选择 LSR：当需要无符号除法、数据对齐或清除高位时 @ 选择 ROR：当需要保留所有位（如加密、循环缓冲区）或复杂位操作时Ⅳ ASR算数右移指令；将寄存器中的二进制数据向右移动指定位数，高位填充符号位（正数补0，负数补1），低位移出并丢弃；asr r0, r1, #2将 R1 的值算术右移2位，结果存入 R0（如 R1=-8，结果 R0=-2）；@ 特性 符号保留 高位填充符号位，确保有符号数的正确性（如负数右移后仍为负数）1.3 乘法指令Ⅰ MUL乘法指令；不能与常数进行计算；该指令的语法要求所有操作数必须是寄存器，而非直接数值；mul r0, r0, r1将r0寄存器的值与r1寄存器的值进行相乘，并将结果放入r0寄存器中；@ 数据类型：操作数默认视为 无符号整数（若需有符号乘法，需使用 SMULL 指令） @ 结果范围：结果为 32 位整数，若乘积超过 32 位，高位会被丢弃 @ 目标寄存器（如 R0）和源寄存器（如 R0、R1）必须为 通用寄存器（R0-R12） @ 目标寄存器不能与第一个源寄存器相同（例如 MUL R0, R0, R0 非法） 需通过临时寄存器实现Ⅱ MLA乘加指令；mla r0, r1, r2, r3将两个寄存器的值相乘，再将乘积与第三个寄存器的值相加，最终将结果存入目标寄存器;MLA R0, R1, R2, R3 @ R0 = R1×R2 + R3 @ 目标寄存器（Rd） 和 乘数寄存器（Rm、Rs）必须为通用寄存器（R0-R12） @ 目标寄存器不能与第一个乘数寄存器（Rm）相同，例如 MLA R0, R0, R1, R2 非法，需通过临时寄存器中转 @ 但是貌似也正常执行了1.4 内存读写指令Ⅰ LDR内存读取指令；将内存中的 字数据（32位） 加载到目标寄存器，支持灵活的寻址方式；ldr r0, [r1]从 R1 指向的地址加载数据到 R0，这里是将r1存储的地址的值给到r0，不是将地址给r0；@ LDR 的寻址模式灵活，覆盖多种内存访问需求 @ 基址寻址： LDR R0, [R1] @ 从 R1 指向的地址加载数据到 R0 @ 基址 + 偏移： LDR R0, [R1, #8] @ 地址 = R1 + 8（偏移为立即数） LDR R0, [R1, R2] @ 地址 = R1 + R2（偏移为寄存器值） @ 前变址寻址（更新基址）： LDR R0, [R1, #8]! @ 地址 = R1 + 8，并更新 R1 为 R1 + 8 @ 后变址寻址（操作后更新基址）： LDR R0, [R1], #8 @ 地址 = R1，操作后 R1 = R1 + 8 @ 寄存器偏移（支持移位）： LDR R0, [R1, R2, LSL #2] @ 地址 = R1 + R2 × 4（左移2位） @ 标号寻址（相对 PC 的地址）： LDR R0, Label @ 加载 Label 处的数据到 R0（Label 需在 ±4KB 范围内） ``` [3,6,8](@ref)Ⅱ STR内存写入指令，将寄存器中的值存储到内存中，是ARM汇编中核心的数据传输指令之一；str r1, [r2]将r1寄存器的地址的值存储到r2中；STR R1, [R2] @ 将R1的值存入R2指向的内存地址（32位） STRB R0, [R3, #4] @ 将R0的低8位存入R3+4的地址，并更新R3为R3+4[6,8](@ref) @ STR 支持多种寻址方式，灵活适应不同场景的内存操作 @ 基址寻址： STR R0, [R1] @ 地址 = R1 @ 基址+偏移： STR R0, [R1, #8] @ 地址 = R1 + 8（偏移量可为立即数或寄存器）[6,8](@ref) @ 前索引寻址： STR R0, [R1, #8]! @ 地址 = R1 + 8，且R1更新为R1+8[6](@ref) @ 后索引寻址： STR R0, [R1], #8 @ 地址 = R1，操作后R1 = R1 +8[6](@ref) @ 寄存器偏移： STR R0, [R1, R2, LSL #2] @ 地址 = R1 + (R2 << 2)（用于数组元素访问）[6](@ref)是可以选size的，比如 STRB 代表一个字节，还有H或者缺省，分别是16位和32位；也就是两个字节和4个字节；1.5 跳转指令Ⅰ B普通跳转指令；ARM 汇编中的 无条件/条件跳转指令，用于将程序流程跳转到指定的目标地址继续执行。其核心功能是 改变程序计数器（PC）的值，实现代码分支控制，默认无条件跳转；Ⅱ BL带链接的跳转指令；意思就是会携带当前指令的下一条指令，以便执行完目标之后继续执行应该执行的下一条指令，可以理解为函数调用；核心功能包括：跳转执行：将程序计数器（PC）指向目标地址（子程序入口），实现函数调用；保存返回地址：跳转前将下一条指令的地址（PC + 4）存入链接寄存器 LR（R14），以便子程序结束后返回到调用点；BL delay @ 调用延时函数，返回地址自动存入 LR ... @ 后续代码 delay: ... @ 子程序逻辑 BX LR @ 返回调用点Ⅲ BX带状态切换的跳转指令；Ⅳ BLX带链接、带状态切换的跳转指令；支持跳转时切换 ARM/Thumb 指令集状态（根据目标地址最低位判断）；mov pc,LR：直接修改pc指针，也可以实现跳转；LDR PC , 0X???：也是直接修改pc指针，也可以实现跳转；1.6 数据加载与存储指令Ⅰ PUSH压栈指令；写入站数据，也可叫做入栈；PUSH 是 ARM 汇编中用于将 寄存器值压入栈 的核心指令，主要功能包括：保存寄存器现场：在函数调用或中断处理前，将寄存器的值暂存到栈中，避免数据被覆盖；管理内存结构：利用栈的 后进先出（LIFO） 特性，临时存储数据（如局部变量、参数传递）；调整栈指针：压栈后自动更新栈指针（SP）的值（如 SP = SP - 4 或 SP = SP - 寄存器数量×4）；func: PUSH {R4-R6, LR} @ 保存寄存器及返回地址到栈中[3,7](@ref) ... @ 函数逻辑 POP {R4-R6, PC} @ 恢复寄存器并跳转回调用点Ⅱ POP出栈指令；弹出栈数据；func: PUSH {R4-R6, LR} @ 保存寄存器及返回地址到栈中[3,7](@ref) 从右至左 ... @ 函数逻辑 POP {R4-R6, PC} @ 恢复寄存器并跳转回调用点Ⅲ LDM连续读取数据指令；LDM（Load Multiple）是 ARM 汇编中的 批量加载指令，用于从内存中连续加载多个寄存器的值。其核心功能包括：批量数据加载：从指定内存地址开始，依次将数据加载到寄存器列表中；地址自动更新：根据寻址模式自动调整基址寄存器的值（通过后缀 ! 实现）；支持堆栈操作：可模拟栈的压入（POP）和弹出（PUSH）行为，配合 STM 实现函数调用时的现场保护；@ LDM{条件码}{模式} Rn{!}, {寄存器列表}{^} @ 使用 IA 模式（后递增） STMIA R0!, {R1, R2} @ 将 R1 和 R2 的值依次存入 R0 指向的地址，存储后 R0 += 8 LDMIA R0!, {R3, R4} @ 从 R0 指向的地址依次加载数据到 R3 和 R4，加载后 R0 += 8 @ 使用 DB 模式（前递减） STMDB R0!, {R1, R2} @ 先 R0 -= 8，再将 R1 存入 [R0]，R2 存入 [R0+4] LDMDB R0!, {R3, R4} @ 先 R0 -= 8，再从 [R0] 加载到 R3，[R0+4] 加载到 R4Ⅳ STM连续写入数据指令；STM（Store Multiple）是 ARM 汇编中的 批量存储指令，用于将多个寄存器的值批量写入内存。其核心功能包括：批量数据存储：将寄存器列表中的值依次存储到连续的内存地址；地址自动更新：根据寻址模式自动调整基址寄存器的值（通过 ! 后缀实现）；堆栈操作支持：与 LDM 配合使用，实现函数调用、中断处理等场景的 现场保护 和 内存管理；语法与寻址模式：@ STM{条件码}{模式} Rn{!}, {寄存器列表}{^} STMIA R0!, {R1, R2} @ 将 R1、R2 存入 R0 起始地址，完成后 R0 += 8 STMFD SP!, {R0-R3, LR} @ 将 R0-R3 和 LR 压入满递减栈（类似 PUSH）2. ARM寻址方式Ⅰ立即数寻址定义：操作数直接包含在指令中，以 # 符号标识，称为 立即数；ADD R0, R0, #1 @ R0 = R0 + 1（操作数1是立即数） MOV R1, #0xFF000 @ R1 = 0xFF000（16进制立即数）Ⅱ 寄存器寻址定义：操作数存储在寄存器中，指令直接通过寄存器编号访问；ADD R3, R1, R2 @ R3 = R1 + R2（操作数均为寄存器） MOV R4, R5 @ R4 = R5Ⅲ 寄存器间接寻址定义：寄存器中存储的是操作数的 内存地址，需通过 [ ] 访问该地址对应的数据；LDR R0, [R1] @ R0 = 内存地址R1处的值 STR R2, [R3] @ 将R2的值存入R3指向的内存地址也可以这么认为：R0 = *R1，相当于取指针；Ⅳ 基址变址寻址定义：通过基址寄存器（存储基地址）和偏移量（立即数或寄存器）组合计算有效地址；这个也行需要取地址的；LDR R0, [R1, #4]! @ R0 = [R1+4]，执行后R1 +=4（ARM64示例）[4](@ref) @ !表示会改变R1的值 LDR R0, [R1], #8 @ R0 = [R1]，执行后R1 +=8Ⅴ 多寄存器寻址定义：单条指令批量操作多个寄存器，常用于堆栈操作或数据块拷贝；STMIA R0!, {R1-R4} @ 将R1-R4依次存入R0起始的地址（R0自动递增） LDMFD SP!, {R0-R3} @ 从栈顶恢复R0-R3（满递减栈模式）[1,2](@ref)Ⅵ 相对寻址定义：以程序计数器（PC）的当前值为基地址，加上指令中指定的偏移量得到有效地址；BL NEXT @ 跳转到子程序NEXT（PC + 偏移量） NEXT: MOV PC, LR @ 返回主程序 @ 应用：用于函数调用和分支跳转3. ARM体系结构概述参考：ARM汇编语言官方手册.pdfARM 处理器是典型的 RISC 处理器，因为它们执行的是加载/存储体系结构；只有加载和存储指令才能访问内存，数据处理指令只操作寄存器的内容；RISC：RISC（精简指令集计算机）是一种微处理器架构，它以执行较少类型的计算机指令为特点，从而实现高速度操作；3.1 寄存器ARM 处理器拥有 37 个寄存器，包括31个通用寄存器和6个状态寄存器，这些寄存器按部分重叠组方式加以排列，提供了下列寄存器：30 个 32位通用寄存器；程序计数器 pc；应用程序状态寄存器 (APSR)/(CPSR)；保存的程序状态寄存器(SPSR)；Ⅰ 30 个 32位通用寄存器在任一时刻都存在十五个通用寄存器，即 r0、r1... r13、r14，具体取决于当前的处理器模式；r13 是堆栈指针 (SP)，在 Thumb-2 中，sp 被严格定义为堆栈指针，因此如果使用 r13，则在堆栈操作中用处不大的许多指令会产生不可预测的结果；建议不要将 sp 用作通用寄存器；在用户模式下，r14 被用作链接寄存器 (LR)，用于存储调用子例程时的返回地址；如果返回地址存储在堆栈上，则也可将 r14 用作通用寄存器；Ⅱ 程序计数器 pc程序计数器被当作 r15（或 pc）来加以访问；它在 ARM 状态下以一个字（四字节）为增量，在 Thumb 状态下则按指令的大小执行；跳转指令将目标地址加载到 pc 中，也可以使用数据操作指令来直接加载 PC；例如，若要从子例程返回，可以使用以下指令将链接寄存器复制到 PC 中：MOV pc,lr在执行期间，r15 (pc) 不包含当前执行的指令的地址，在 ARM 状态下，当前执行的指令的地址通常是 pc-8，而在 Thumb 状态下通常是 pc-4；这段话其实就是三级流水的概念，可直接跳转到 3.2 三级流水；Ⅳ 当前程序状态寄存器 (CPSR)CPSR 存放下列内容：APSR 标记、当前处理器模式、中断禁用标记；后续APSR 标记会提及，条件执行会用到；3.2 三级流水ARM三级流水线是早期ARM处理器（如ARM7系列）采用的一种指令执行机制，它将指令处理分为三个主要阶段：取指（Fetch）、译码（Decode）和执行（Execute）；这种设计通过并行处理多条指令的不同阶段来提高处理器的吞吐效率；在三级流水线中，每个时钟周期都有一条新指令进入流水线：取指阶段从内存中读取指令，译码阶段解析指令并准备操作数，执行阶段完成实际运算并将结果写回寄存器；当第一条指令处于执行阶段时，第二条指令正在译码，第三条指令正在被取出，形成流水线式的并行处理；形如上图，在执行第一条指令的时候，其实PC已经自动指向了第3条指令,或者说PC总是指向当前指令地址再加2条指令的地址；处理器处于ARM状态时，每条指令为4个字节，所以PC的值为正在执行的指令地址加2*4=8个字节，即：PC值 = 当前程序执行位置 + 8字节由于三级流水线的并行特性，程序计数器PC总是指向正在取指的指令地址；在ARM状态下（32位指令），这意味着PC=当前执行指令地址+8字节（两条指令的偏移）；在Thumb状态下（16位指令）则为PC=当前执行指令地址+4字节。这种偏移是理解ARM汇编编程的关键点之一；反过来说，当前指令的地址则是 pc-8 或者 pc-4 ；4. 条件执行demo以下示例代码为例子，简单编写汇编程序；#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> int main(int argc, char *argv[]){ char *num = argv[1]; int int_num = atoi(num); // atoi函数的作用是将字符串转换为整数 if (int_num - 10 > 0) { printf("1"); } else { printf("2"); } return 0; }主要是突出条件执行的写法，因为之前的学习并不足以解决这个需求；先简单编译一个汇编文件，就需要最简单的骨架就好了，直接main函数返回0即可；clang -target armv7a-linux-androideabi27 -E demo.c -o demo.i clang -target armv7a-linux-androideabi27 -S demo.i -o demo.s会生成一个.s文件，我们就在此基础上写汇编代码；初始代码大概是这样：main: .fnstart movw r0, #0 bx lr开始根据需求实现这个小demo，大致汇编代码如下：main: .fnstart push {lr} sub sp, sp, #8 ldr r0, [r1, #4] bl atoi mov r1, #10 cmp r0, r1 movgt r2, #0x31 movle r2, #0x32 str r2, [sp] @ str sp, [sp, #4] add r0, sp, #4 mov r0, sp bl printf add sp, sp, #8 pop {lr} movw r0, #0 bx lr将其编译成可执行文件再推到设备执行看看是否是想要的效果；clang -target armv7a-linux-androideabi27 -c demo.s -o demo.o clang -target armv7a-linux-androideabi27 demo.o -o hello得到一个hello文件，推到设备执行；adb push hello /data/local/tmp/hello adb shell chmod +x /data/local/tmp/hello adb shell /data/local/tmp/hello 123输出如下：也可以写成一个sh脚本，但是我执行出现了一些问题；clang -target armv7a-linux-androideabi27 -c demo.s -o demo.o clang -target armv7a-linux-androideabi27 demo.o -o hello adb push hello /data/local/tmp/hello adb shell chmod +x /data/local/tmp/hello adb shell /data/local/tmp/hello 1235. 调用约定关于ARM32和ARM64的调用约定大致如下：32：参数传递时，参数0-3放在R0-R3里，超过这个数字就以小端序放在栈上；函数的返回值通常以R0返回，但也有例外；64位的数据可能会使用R0-R1两个寄存器；64：参数传递时，参数0-7放在X0-X7寄存器中，额外的参数以小端序依次放在栈中；函数的返回值通常通过X0返回，大型的数据可能会通过X8寄存器返回；一般来说函数调用时会保存一些寄存器，可以理解为保护现场，在函数结尾又会恢复寄存器，这么做是为了保护上下文；看一个示例：STMFD SP!, {R11, LR} MOV R11, SP SUB SP, SP, #0x38这里有三条指令，我们分别来分析一下到底做了什么；先看第一条：STMFD SP!, {R11, LR}首先，STM是多寄存器存储指令，FD则表示指定使用慢递减堆栈类型，一般默认的也是这种类型；这里要注意，堆栈向低地址方向增长，这点对于后面理解比较重要；{R11, LR}：是要存储的寄存器列表，顺序在 ARM 中通常是固定的，一般是编号低的寄存器存储在低地址；所以这一段具体做的是：将寄存器 R11的值存储到新的内存地址，接着存储 LR寄存器，最后，将递减后的新地址写回 SP寄存器，也就是栈顶；注意：这里的感叹号是需要更新寄存器；这里的目的是保存R11和LR寄存器的值，以便执行完成后恢复；因为本函数内部可能还会调用其他函数，是有可能覆盖掉LR寄存器的，所以需要先保存；来看第二条：MOV R11, SP这个指令比较简单，将当前栈指针 SP的值复制到 R11寄存器中；它的目的呢？我们来分析一下，此时，SP指向的是刚刚保存完 R11和 LR后的新栈顶，将此时SP值设置为当前函数的新帧指针 (FP)，因为R11是FP；在函数执行过程中，R11将作为一个稳定的基址，用来访问栈帧中的局部变量和参数；也可以说这是栈底；再看第三条：SUB SP, SP, #0x38这个指令很简单，从栈指针 SP的当前值中减去十六进制数 0x38（即十进制的 56），然后将结果存回 SP；它的目的呢？这其实是在为栈开空间，通过减少 SP的值（堆栈向低地址增长），“开辟”出了一块从 [SP]到 [SP + 0x38]的未初始化的内存区域；这三条指令共同协作，搭建起了函数的“工作台”：STMFD SP!, {R11, LR}：保护现场；把“调用者的帧指针”和“回家的地址”安全地存放在栈上；MOV R11, SP：建立基地；树立一个稳定的路标（R11），之后找东西（变量）都以它为基准；SUB SP, SP, #0x38：开辟工作区；为函数自己的局部变量分配所需的空间；这个例子与调用约定其实关系不大，但也非常值得弄明白；注意堆栈平衡；
ARM可执行文件生成过程资料：https://developer.android.google.cn/ndk/guides/other_build_systems?hl=zh-cn1. 过程简述ARM可执行文件的生成过程大概是四步：预处理、编译、汇编、链接；执行完这四步后就会得到一个可执行文件，就可以在手机上运行；需要借助于clang来实现这个过程，在android ndk会有这个工具；可在如下路径找到它；F:\android_sdk\ndk\27.1.12297006\toolchains\llvm\prebuilt\windows-x86_64\bin进入此路径可以查看clang的版本；将它加入到环境变量中，即可在任意位置使用它；在进行编译之前，写一个c代码；#include <stdio.h> int main(int argc, char const *argv[]) { printf("hello sana"); return 0; }以它为例进行演示；2. 预处理-E首先看看clang的基础命令；-E Only run the preprocessor 仅运行预处理器 用法：#clang -E hello.c -o hello.i 作用：将hello.c预处理输出hello.i文件我们需要借助这个参数来进行预处理这一步，在文件目录打开终端输入以下命令；target是指定平台；clang -target armv7a-linux-androideabi27 -E sana.c -o sana.i会得到一个 .i 文件，这时候其实与平台是没关系的，它是把头文件展开之后，处理源代码中的宏定义、文件包含和条件编译指令；3. 编译-S命令如下：-S Only run preprocess and compilation steps 仅运行预处理和编译步骤clang -target armv7a-linux-androideabi27 -S sana.i -o sana.s这里会生成一个汇编文件，这里就与平台是相关的了；4. 汇编-c命令如下：-c Only run preprocess, compile, and assemble steps 仅运行预处理、编译和组装步骤 clang -target armv7a-linux-androideabi27 -c sana.s -o sana.o 他生成的其实就是ELF格式，将汇编代码转换为机器码（二进制指令），需通过链接器进一步处理；可以判断一下它的类型；这里的file命令是装了插件，可以推到手机上用file命令看，但是此时也是不能够执行的，需要执行最后一步；5. 链接-o命令：clang -target armv7a-linux-androideabi27 sana.o -o hello此时再查看文件格式：去测试一下是否是可执行的；这里可以看到是输出了我们写的那句代码，证明就没问题；另外，如需一步到位也是执行-o命令；clang -target armv7a-linux-androideabi27 sana.c -o hello
chacha20算法1. 概述ChaCha20是一种高效、安全且快速的流加密算法，由 Daniel J. Bernstein 在 2008 年基于 Salsa20算法改进而成，主要用于加密数据流；ChaCha20 将密钥（256 位）、随机数（Nonce，96 位）和计数器（Counter，32 位）经过一系列混合运算生成密钥流（Key Stream），再与明文进行按位异或得到密文ChaCha20 的解密过程与加密完全相同，因为异或操作是可逆的，类似的算法还有RC4；输入长度随意且没有填充，密文和明文长度一样；2. 与RC4的区别Chacha20和RC4都是流密码算法，但它们的加密原理有以下几个区别：密钥长度：Chacha20的密钥长度为256位（32字节） ，而RC4的密钥长度为1到256字节不等；S盒：Chacha20没有S盒，而RC4使用一个256字节的S盒来生成密钥流；Chacha20是基于置换的加密算法，它使用固定的置换来生成密钥流；加密过程：在Chacha20中，通过对输入的明文数据块进行置换和加密操作，生成密文输出；每次加密需要根据密钥和计数器生成一个新的置换和密钥流；而RC4是在密钥生成完毕后，直接用密钥流对输入的明文进行异或操作得到密文输出；区别主要就是在于密钥流的生成方式；3. 算法核心3.1 初始化构建一个固定的 4x4 矩阵（64字节）；这个矩阵由四部分填充：固定的128位魔数（16字节），ASCII编码（小端序）：0x61707865, 0x3320646e, 0x79622d32, 0x6b206574；这个是特征点；256位密钥 key（32字节）；96位 Nonce（12字节）；32位计数器 counter（4字节）；其中魔数常量是："expand 32-byte k"；它的初始排列可以看作下面的矩阵；[ 常量 ][ 常量 ][ 常量 ][ 常量 ] [ key0 ][ key1 ][ key2 ][ key3 ] [ key4 ][ key5 ][ key6 ][ key7 ] [counter][nonce0][nonce1][nonce2]每一次加密生成 64 字节的密钥流块；矩阵位置 (索引)内容长度 (32位字)说明与示例0-3固定常数4固定为字符串 "expand 32-byte k"的ASCII编码（小端序）：0x61707865, 0x3320646e, 0x79622d32, 0x6b2065744-11256位密钥8用户提供的256位（32字节）密钥，被划分为8个32位字12块计数器132位计数器，标识当前正在处理的数据块。通常初始值为1（为Poly1305认证码预留0值）13-1596位随机数3每次加密必须唯一的96位（12字节）随机数（Nonce）3.2 密钥流生成chacha20算法的重点其实就在于秘钥的扩展，通过ChaCha20的伪随机函数对初始状态进行20轮(或者说10轮)的混淆操作，生成512位(64字节)的密钥流；秘钥扩展的算法如下：# 进行 20 轮运算（10 次双轮，每次包括列轮和对角线轮） for _ in range(10): # 列轮 quarter_round(working_state, 0, 4, 8, 12) quarter_round(working_state, 1, 5, 9, 13) quarter_round(working_state, 2, 6, 10, 14) quarter_round(working_state, 3, 7, 11, 15) # 对角线轮 quarter_round(working_state, 0, 5, 10, 15) quarter_round(working_state, 1, 6, 11, 12) quarter_round(working_state, 2, 7, 8, 13) quarter_round(working_state, 3, 4, 9, 14) # 最终将原始状态与运算结果相加 for i in range(16): working_state[i] = (working_state[i] + state[i]) & 0xffffffff对 16×32bit 状态做 20 轮运算，也可以说是10轮，20轮 = 10次 column+diagonal；最终还需要将初始状态相加，也就是 out = in + 轮后状态；3.3 加密将16个32位字按小端序序列化为 64字节的密钥流块（key_stream_block），将当前64字节明文（plaintext_block）与密钥流块进行逐字节异或（XOR）；def chacha20_encrypt(key, nonce, counter, plaintext): ciphertext = bytearray() # 每个块 64 字节，若明文长度不足 64 字节，则只使用部分 keystream block_count = (len(plaintext) + 63) // 64 for i in range(block_count): keystream = chacha20_block(key, counter + i, nonce) block = plaintext[i * 64:(i + 1) * 64] # 异或运算 for j in range(len(block)): ciphertext.append(block[j] ^ keystream[j]) return bytes(ciphertext)如果明文块大于64字节，则循环上面的秘钥扩展，然后继续加密；初始密钥扩展（仅一次）；对后续每个64字节块只需要修改计数器值，将状态矩阵中的 state[12]（块计数器）递增（例如从1→2→3...）；复用其余所有状态：矩阵中的常量、密钥、Nonce部分保持不变；基于更新后的计数器值，重新执行 20轮ChaCha轮函数，生成新的密钥流块；4. 源码实现import struct def rotl(v, n): """对32位无符号整数 v 进行循环左移 n 位。""" return ((v << n) & 0xffffffff) | (v >> (32 - n)) def quarter_round(x, a, b, c, d): """ ChaCha20 的四分轮函数，对状态数组 x 的 a, b, c, d 四个位置进行操作。 """ x[a] = (x[a] + x[b]) & 0xffffffff x[d] = rotl(x[d] ^ x[a], 16) x[c] = (x[c] + x[d]) & 0xffffffff x[b] = rotl(x[b] ^ x[c], 12) x[a] = (x[a] + x[b]) & 0xffffffff x[d] = rotl(x[d] ^ x[a], 8) x[c] = (x[c] + x[d]) & 0xffffffff x[b] = rotl(x[b] ^ x[c], 7) def chacha20_block(key, counter, nonce): # ChaCha20 常量 "expand 32-byte k" constants = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574] key_words = list(struct.unpack("<8I", key)) nonce_words = list(struct.unpack("<3I", nonce)) # 构造初始状态，共16个32位整数 state = [0] * 16 state[0:4] = constants state[4:12] = key_words state[12] = counter & 0xffffffff state[13:16] = nonce_words working_state = state.copy() # 进行 20 轮运算（10 次双轮，每次包括列轮和对角线轮） for _ in range(10): # 列轮 quarter_round(working_state, 0, 4, 8, 12) quarter_round(working_state, 1, 5, 9, 13) quarter_round(working_state, 2, 6, 10, 14) quarter_round(working_state, 3, 7, 11, 15) # 对角线轮 quarter_round(working_state, 0, 5, 10, 15) quarter_round(working_state, 1, 6, 11, 12) quarter_round(working_state, 2, 7, 8, 13) quarter_round(working_state, 3, 4, 9, 14) # 最终将原始状态与运算结果相加 for i in range(16): working_state[i] = (working_state[i] + state[i]) & 0xffffffff # 将 16 个 32 位整数以小端序打包成 64 字节 return struct.pack("<16L", *working_state) def chacha20_encrypt(key, nonce, counter, plaintext): ciphertext = bytearray() # 每个块 64 字节，若明文长度不足 64 字节，则只使用部分 keystream block_count = (len(plaintext) + 63) // 64 for i in range(block_count): keystream = chacha20_block(key, counter + i, nonce) block = plaintext[i * 64:(i + 1) * 64] # 异或运算 for j in range(len(block)): ciphertext.append(block[j] ^ keystream[j]) return bytes(ciphertext) # 示例：加密和解密测试 if __name__ == '__main__': # 示例密钥（32字节）和 nonce（12字节） key = bytes.fromhex('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f') nonce = bytes.fromhex('202122232425262728292a2b') counter = 0 # 一直都是 plaintext = bytes.fromhex('30313233343536373839') print("明文:", plaintext.hex()) # 加密 ciphertext = chacha20_encrypt(key, nonce, counter, plaintext) print("密文 (hex):", ciphertext.hex()) # 解密：对密文再次使用同样的 keystream 异或即可还原明文 decrypted = chacha20_encrypt(key, nonce, counter, ciphertext) # print("解密后明文:", decrypted.hex())
unidbg文件读写1. 前言本篇分析的是自写demo，主要是为了熟悉unidbg对于文件读写的处理，会涉及到：Sharedpreference 读写；Assets 读写；文件 读写；在模拟执行之前需要手动的编写好目标应用的代码；2. demo1设计首先，了解一下Sharedpreferences的基本概念：Sharedpreferences是Android平台上常用的存储方式，用来保存应用程序的各种配置信息，其本质是一个以“键-值”对的方式保存数据的xml文件，其文件保存在/data/data/selfPackage/shared_prefs目录下；在APP刚启动时，我们新建两个Sharedpreference 文件，分别填入两个键值对，name-sana和location-china，首先先看基本的业务代码：package com.unidbg.demo1; import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.content.SharedPreferences; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; import com.unidbg.demo1.databinding.ActivityMainBinding; public class MainActivity extends AppCompatActivity { static { System.loadLibrary("demo1"); } private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 步骤1：创建SharedPreferences对象 SharedPreferences sharedPreferences1= getSharedPreferences("one", Context.MODE_PRIVATE); SharedPreferences sharedPreferences2= getSharedPreferences("two", Context.MODE_PRIVATE); // 步骤2： 实例化SharedPreferences.Editor对象 SharedPreferences.Editor editor1 = sharedPreferences1.edit(); SharedPreferences.Editor editor2 = sharedPreferences2.edit(); // 步骤3：将获取过来的值放入文件 editor1.putString("name", "xiayutian"); editor2.putString("location", "china"); // 步骤4：提交 editor1.apply(); editor2.apply(); TextView tv = findViewById(R.id.sample_text); Button btn = findViewById(R.id.btn); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String string_data = stringFromJNI(getApplicationContext()); Log.i("sanaTag", string_data); tv.setText(string_data); } }); } public native String stringFromJNI(Context applicationContext); }接下来的重点就是 stringFromJNI 这个native方法了，先不着急，看看是否有目标的文件存在了；我们的目标就是来读取它俩，stringFromJNI 的主要逻辑就是干这事，但是实现的方式选取以下两种：读取one.xml时，使用JNI去调用 java 层对SharedPreference操纵的API；读取two.xml时，使用系统调用open打开这个xml；既然Sharedpreference本质上是个xml文件，那么用native中原生的open函数去读可能会更隐蔽，可是open本质上也是通过底层系统调用open的方式实现，那我们直截了当通过系统调用open打开这个xml也是一样；完整代码如下：#include <jni.h> #include <string> #include <sys/syscall.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> __attribute__((naked)) long raw_syscall(long _number, ...) { __asm__ __volatile__ ("MOV R12,SP\r\n" "STMFD SP!,{R4-R7}\r\n" "MOV R7,R0\r\n" "MOV R0,R1\r\n" "MOV R1,R2\r\n" "MOV R2,R3\r\n" "LDMIA R12,{R3-R6}\r\n" "SVC 0\r\n" "LDMFD SP!,{R4-R7}\r\n" "mov pc,lr"); }; char *test_syscall(const char *file_path) { char *result = ""; long fd = raw_syscall(5, file_path, O_RDONLY | O_CREAT, 400); if (fd != -1) { char buffer[0x100] = {0}; raw_syscall(3, fd, buffer, 0x100); result = buffer; raw_syscall(6, fd); } return result; } extern "C" JNIEXPORT jstring JNICALL Java_com_unidbg_demo1_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */, jobject mycontext) { // 反射Context类 jclass cls_Context = env->FindClass("android/content/Context"); // 反射Context类getSharedPreferences方法 jmethodID mid_getSharedPreferences = env->GetMethodID(cls_Context, "getSharedPreferences", "(Ljava/lang/String;I)Landroid/content/SharedPreferences;"); // 获取Context类MODE_PRIVATE属性值 // 执行反射方法 jobject obj_sharedPreferences = env->CallObjectMethod(mycontext, mid_getSharedPreferences, env->NewStringUTF("one"), 0); jclass cls_SharedPreferences = env->FindClass("android/content/SharedPreferences"); //反射SharedPreferences类的getString方法 jmethodID mid_getString = env->GetMethodID(cls_SharedPreferences, "getString", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"); // 参数类型转换 jstring key_name = env->NewStringUTF("name"); // 参数类型转换 jstring default_value = env->NewStringUTF(" "); // 执行反射方法 jstring key_value1 = (jstring) env->CallObjectMethod(obj_sharedPreferences, mid_getString, key_name, default_value); const char *c_key_value1 = env->GetStringUTFChars(key_value1, 0); const char *path = "/data/data/com.unidbg.demo1/shared_prefs/two.xml"; const char *result = test_syscall(path); char dest[1000]; strcpy(dest, c_key_value1); strcat(dest, result); return env->NewStringUTF(dest); }编译的时候需要注意，只编译32位的so； defaultConfig { applicationId "com.unidbg.demo1" minSdk 21 targetSdk 34 versionCode 1 versionName "1.0" ndk{ abiFilters 'armeabi-v7a' } testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" }最后读取到的结果就是这样的：为了省事就不去切割第二部分字符串了，直接开始后模拟执行；3. 模拟执行demo1直接调用stringFromJNI方法，基本的框架如下：package com.baiLong.csdn_demo8; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.arm.context.RegisterContext; import com.github.unidbg.debugger.BreakPointCallback; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.memory.Memory; import com.github.unidbg.pointer.UnidbgPointer; import com.github.unidbg.utils.Inspector; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; public class csdn_demo8 extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; private final Module module; public csdn_demo8() { emulator = AndroidEmulatorBuilder.for32Bit().build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("src/test/java/com/baiLong/csdn_demo8/file/demo1.apk")); vm.setVerbose(true); vm.setJni(this); DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/baiLong/csdn_demo8/file/libdemo1.so"), false); module = dm.getModule(); dm.callJNI_OnLoad(emulator); } public static void main(String[] args) { csdn_demo8 demo = new csdn_demo8(); String res = demo.call(); System.out.println("res-->>" + res); } public String call() { // 898 List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数 实例方法是jobject 静态方法是jclazz 直接填0 一般用不到 DvmObject<?> context = vm.resolveClass("android/content/Context").newObject(null);// context list.add(vm.addLocalObject(context)); Number number = module.callFunction(emulator, 0x898 + 1, list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); return result; } }产生第一个报错：java.lang.UnsupportedOperationException: android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:419) at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)这正是开发中的其中一步，按照基本逻辑补好；case "android/content/Context->getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;": { String arg0 = (String) vaList.getObjectArg(0).getValue(); System.out.println("sana getSharedPreferences arg0-->> " + arg0); return vm.resolveClass("android/content/SharedPreferences").newObject(arg0); }在继续运行之前需要说明，为何这里给的是arg0，返回null行不行？假设APP在JNI中从五个SharedPreferences里读了十五个键值对，并且不同xml的键名有重复，如果每次取SharedPreferences时我们都返回空对象，那后面怎么区分a.xml和b.xml里键名都是name的数据呢？参数1是想要获取的SharedPreferences的名字，应该把它放对象里返回，这样就有了"标识性"，继续执行；java.lang.UnsupportedOperationException: android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:419) at com.baiLong.csdn_demo8.csdn_demo8.callObjectMethodV(csdn_demo8.java:68)这个方法有参数，我们打印看看；case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{ String fileName = dvmObject.getValue().toString(); String key = (String) vaList.getObjectArg(0).getValue(); String value = (String) vaList.getObjectArg(1).getValue(); System.out.println("sana getString fileName-->> " + fileName); System.out.println("sana getString key-->> " + key); System.out.println("sana getString value-->> " + value); } /* sana getString fileName-->> one sana getString key-->> name sana getString value-->> */很显然，分别是文件名和键值都是我们熟悉的，这里也可以发现，文件名是我们从上一个环境中传下来的，也是我们那么做的原因所在；返回值自然就应该是对应的值了，这里也就应该是xiayutian；case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;":{ String fileName = dvmObject.getValue().toString(); String key = (String) vaList.getObjectArg(0).getValue(); System.out.println("sana getString fileName-->> " + fileName); System.out.println("sana getString key-->> " + key); return new StringObject(vm, "xiayutian"); }这里是可以更加严谨的，做一个判断，如果读的是one这个文件再这么返回；case "android/content/SharedPreferences->getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;": { String fileName = dvmObject.getValue().toString(); String key = (String) vaList.getObjectArg(0).getValue(); if (fileName.equals("one")) { if (key.equals("name")) { return new StringObject(vm, "xiayutian"); } } }这才是正确的做法，继续运行，发现出值了；但是有个点出现了差错，我们不是还有一个文件需要读吗，为何直接完成了执行，并且unidbg并没有告诉我们任何这方面的消息，完整日志如下：[15:46:02 714] INFO [com.github.unidbg.linux.AndroidElfLoader] (AndroidElfLoader:483) - libdemo1.so load dependency libandroid.so failed JNIEnv->FindClass(android/content/Context) was called from RX@0x40000a09[libdemo1.so]0xa09 JNIEnv->GetMethodID(android/content/Context.getSharedPreferences(Ljava/lang/String;I)Landroid/content/SharedPreferences;) => 0xa0a12d1f was called from RX@0x40000a2b[libdemo1.so]0xa2b JNIEnv->NewStringUTF("one") was called from RX@0x40000aab[libdemo1.so]0xaab sana getSharedPreferences arg0-->> one JNIEnv->CallObjectMethodV(android.content.Context@4fccd51b, getSharedPreferences("one", 0x0) => android.content.SharedPreferences@60215eee) was called from RX@0x40000a65[libdemo1.so]0xa65 JNIEnv->FindClass(android/content/SharedPreferences) was called from RX@0x40000a09[libdemo1.so]0xa09 JNIEnv->GetMethodID(android/content/SharedPreferences.getString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;) => 0x2c5b4190 was called from RX@0x40000a2b[libdemo1.so]0xa2b JNIEnv->NewStringUTF("name") was called from RX@0x40000aab[libdemo1.so]0xaab JNIEnv->NewStringUTF(" ") was called from RX@0x40000aab[libdemo1.so]0xaab JNIEnv->CallObjectMethodV(android.content.SharedPreferences@60215eee, getString("name", " ") => "xiayutian") was called from RX@0x40000a65[libdemo1.so]0xa65 JNIEnv->GetStringUtfChars("xiayutian") was called from RX@0x40000ac9[libdemo1.so]0xac9 JNIEnv->NewStringUTF("xiayutian") was called from RX@0x40000aab[libdemo1.so]0xaab res-->>xiayutian这是一种非常难受的体验，若非我们提前得知都以为补完了···这里为什么没有提示呢？回忆一下我们的方式，使用的是系统调用，但我们没有打开Unidbg中系统调用的日志显示，手动打开一下吧； public static void main(String[] args) { Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG); // Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG); // Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG); // Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG); // Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG); // Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG); csdn_demo8 demo = new csdn_demo8(); String res = demo.call(); System.out.println("res-->>" + res); }在日常的样本里推荐打开全部日志，避免有所遗漏，但也会有弊端，日志会比较多；我们需要补文件访问了，这里有两种方式补文件访问，1是Unidbg提供的rootfs虚拟文件系统，2是代码方式文件重定向；我个人比较青睐io重定向的方式；package com.baiLong.csdn_demo8; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.arm.context.RegisterContext; import com.github.unidbg.debugger.BreakPointCallback; import com.github.unidbg.file.FileResult; import com.github.unidbg.file.IOResolver; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.file.ByteArrayFileIO; import com.github.unidbg.memory.Memory; import com.github.unidbg.pointer.UnidbgPointer; import com.github.unidbg.utils.Inspector; import org.apache.log4j.Level; import org.apache.log4j.Logger; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.List; // 1 实现IOResolver public class csdn_demo8 extends AbstractJni implements IOResolver { private final AndroidEmulator emulator; private final VM vm; private final Module module; @Override // 3 resolve方法中处理 public FileResult resolve(Emulator emulator, String pathname, int oflags) { if ("/data/data/com.unidbg.demo1/shared_prefs/two.xml".equals(pathname)) { return FileResult.success(new ByteArrayFileIO(oflags, pathname, "mytest".getBytes())); } return null; } public csdn_demo8() { emulator = AndroidEmulatorBuilder.for32Bit().build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("src/test/java/com/baiLong/csdn_demo8/file/demo1.apk")); vm.setVerbose(true); // 2 注意这里的处理，加上这一句 emulator.getSyscallHandler().addIOResolver(this); vm.setJni(this); DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/baiLong/csdn_demo8/file/libdemo1.so"), false); module = dm.getModule(); dm.callJNI_OnLoad(emulator); } }这里io处理有三步，请不要忘记了，继续执行；没问题了，这里想要和真机一样也是可以的，但是没必要；4. demo2设计各类加密算法大部分都有密钥的存在，非对称加密算法还有公钥私钥之分，所以加密算法运算时需要传入密钥，但是直接参数方式传递很容易被分析者意识到这是密钥，有没有办法更隐蔽一些呢？比如我们把密钥放在xml中，在native中读取它，就类似于demo1；有没有更隐蔽一些些的呢，我们可以把密钥藏在资源文件的图片里，即在so里读取资源文件里的某张图片，以它的某部分或者整体的md5结果作为密钥？这是一个更好的方案；demo2就是这个方案的简单实现——native中读取资源文件的1.jpg，并求其md5值，返回JAVA层；首先看java层代码：package com.unidbg.demo2; import androidx.appcompat.app.AppCompatActivity; import android.content.res.AssetManager; import android.os.Bundle; import android.widget.TextView; import com.unidbg.demo2.databinding.ActivityMainBinding; public class MainActivity extends AppCompatActivity { static { System.loadLibrary("demo2"); } private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); TextView tv = findViewById(R.id.sample_text); tv.setText(setNativeAssetManager(getAssets())); } public native String setNativeAssetManager(AssetManager assetManager); }native层的代码比较复杂，主要是md5代码比较多；#include <jni.h> #include <string> #include "android/asset_manager.h" #include "android/asset_manager_jni.h" #define MD5_LONG unsigned long // 分组大小 #define MD5_CBLOCK 64 // 分块个数 #define MD5_LBLOCK (MD5_CBLOCK/4) // 摘要长度（字节） #define MD5_DIGEST_LENGTH 16 #define MD32_REG_T long // 小端序 #define DATA_ORDER_IS_LITTLE_ENDIAN // 四个初始化常量 #define INIT_DATA_A (unsigned long)0x67452301L #define INIT_DATA_B (unsigned long)0xefcdab89L #define INIT_DATA_C (unsigned long)0x98badcfeL #define INIT_DATA_D (unsigned long)0x10325476L // 循环左移以及取低64位 #define ROTATE(a,n) (((a)<<(n))|(((a)&0xffffffff)>>(32-(n)))) // 大小端序互转 #define HOST_c2l(c,l) (l =(((unsigned long)(*((c)++))) ), \ l|=(((unsigned long)(*((c)++)))<< 8), \ l|=(((unsigned long)(*((c)++)))<<16), \ l|=(((unsigned long)(*((c)++)))<<24) ) #define HOST_l2c(l,c) (*((c)++)=(unsigned char)(((l) )&0xff), \ *((c)++)=(unsigned char)(((l)>> 8)&0xff), \ *((c)++)=(unsigned char)(((l)>>16)&0xff), \ *((c)++)=(unsigned char)(((l)>>24)&0xff), \ l) // 更新链接变量值 #define HASH_MAKE_STRING(c,s) do { \ unsigned long ll; \ ll=(c)->A; (void)HOST_l2c(ll,(s)); \ ll=(c)->B; (void)HOST_l2c(ll,(s)); \ ll=(c)->C; (void)HOST_l2c(ll,(s)); \ ll=(c)->D; (void)HOST_l2c(ll,(s)); \ } while (0) // 四个初始化非线性函数，或者叫逻辑函数 #define F(b,c,d) ((((c) ^ (d)) & (b)) ^ (d)) #define G(b,c,d) ((((b) ^ (c)) & (d)) ^ (c)) #define H(b,c,d) ((b) ^ (c) ^ (d)) #define I(b,c,d) (((~(d)) | (b)) ^ (c)) // F函数，每隔16步/轮 换下一个 #define R0(a,b,c,d,k,s,t) { \ a+=((k)+(t)+F((b),(c),(d))); \ a=ROTATE(a,s); \ a+=b; }; #define R1(a,b,c,d,k,s,t) { \ a+=((k)+(t)+G((b),(c),(d))); \ a=ROTATE(a,s); \ a+=b;}; #define R2(a,b,c,d,k,s,t) { \ a+=((k)+(t)+H((b),(c),(d))); \ a=ROTATE(a,s); \ a+=b; }; #define R3(a,b,c,d,k,s,t) { \ a+=((k)+(t)+I((b),(c),(d))); \ a=ROTATE(a,s); \ a+=b; }; typedef struct MD5state_st1{ MD5_LONG A,B,C,D; // ABCD MD5_LONG Nl,Nh; // 数据的bit数计数器(对2^64取余)，Nh存储高32位，Nl存储低32位。这种设计是服务于32位处理器，MD5的设计就是为了服务于32位处理器的。 MD5_LONG data[MD5_LBLOCK];//数据缓冲区 unsigned int num; }MD5_CTX; // 存放MD5算法相关信息的结构体定义 unsigned char cleanse_ctr = 0; // 初始化链接变量/幻数 int MD5_Init(MD5_CTX *c){ memset (c,0,sizeof(*c)); c->A=INIT_DATA_A; c->B=INIT_DATA_B; c->C=INIT_DATA_C; c->D=INIT_DATA_D; return 1; } // md5 一个分组中的全部运算 void md5_block_data_order(MD5_CTX *c, const void *data_, unsigned int num){ const auto *data= static_cast<const unsigned char *>(data_); unsigned MD32_REG_T A,B,C,D,l; #ifndef MD32_XARRAY unsigned MD32_REG_T XX0, XX1, XX2, XX3, XX4, XX5, XX6, XX7, XX8, XX9,XX10,XX11,XX12,XX13,XX14,XX15; # define X(i) XX##i #else MD5_LONG XX[MD5_LBLOCK]; # define X(i) XX[i] #endif A=c->A; B=c->B; C=c->C; D=c->D; // 64轮 // 前16轮需要改变分组中每个分块的 for (;num--;){ HOST_c2l(data,l); X( 0)=l; HOST_c2l(data,l); X( 1)=l; /* Round 0 */ R0(A,B,C,D,X( 0), 7,0xd76aa478L); HOST_c2l(data,l); X( 2)=l; R0(D,A,B,C,X( 1),12,0xe8c7b756L); HOST_c2l(data,l); X( 3)=l; R0(C,D,A,B,X( 2),17,0x242070dbL); HOST_c2l(data,l); X( 4)=l; R0(B,C,D,A,X( 3),22,0xc1bdceeeL); HOST_c2l(data,l); X( 5)=l; R0(A,B,C,D,X( 4), 7,0xf57c0fafL); HOST_c2l(data,l); X( 6)=l; R0(D,A,B,C,X( 5),12,0x4787c62aL); HOST_c2l(data,l); X( 7)=l; R0(C,D,A,B,X( 6),17,0xa8304613L); HOST_c2l(data,l); X( 8)=l; R0(B,C,D,A,X( 7),22,0xfd469501L); HOST_c2l(data,l); X( 9)=l; R0(A,B,C,D,X( 8), 7,0x698098d8L); HOST_c2l(data,l); X(10)=l; R0(D,A,B,C,X( 9),12,0x8b44f7afL); HOST_c2l(data,l); X(11)=l; R0(C,D,A,B,X(10),17,0xffff5bb1L); HOST_c2l(data,l); X(12)=l; R0(B,C,D,A,X(11),22,0x895cd7beL); HOST_c2l(data,l); X(13)=l; R0(A,B,C,D,X(12), 7,0x6b901122L); HOST_c2l(data,l); X(14)=l; R0(D,A,B,C,X(13),12,0xfd987193L); HOST_c2l(data,l); X(15)=l; R0(C,D,A,B,X(14),17,0xa679438eL); R0(B,C,D,A,X(15),22,0x49b40821L); /* Round 1 */ R1(A,B,C,D,X( 1), 5,0xf61e2562L); R1(D,A,B,C,X( 6), 9,0xc040b340L); R1(C,D,A,B,X(11),14,0x265e5a51L); R1(B,C,D,A,X( 0),20,0xe9b6c7aaL); R1(A,B,C,D,X( 5), 5,0xd62f105dL); R1(D,A,B,C,X(10), 9,0x02441453L); R1(C,D,A,B,X(15),14,0xd8a1e681L); R1(B,C,D,A,X( 4),20,0xe7d3fbc8L); R1(A,B,C,D,X( 9), 5,0x21e1cde6L); R1(D,A,B,C,X(14), 9,0xc33707d6L); R1(C,D,A,B,X( 3),14,0xf4d50d87L); R1(B,C,D,A,X( 8),20,0x455a14edL); R1(A,B,C,D,X(13), 5,0xa9e3e905L); R1(D,A,B,C,X( 2), 9,0xfcefa3f8L); R1(C,D,A,B,X( 7),14,0x676f02d9L); R1(B,C,D,A,X(12),20,0x8d2a4c8aL); /* Round 2 */ R2(A,B,C,D,X( 5), 4,0xfffa3942L); R2(D,A,B,C,X( 8),11,0x8771f681L); R2(C,D,A,B,X(11),16,0x6d9d6122L); R2(B,C,D,A,X(14),23,0xfde5380cL); R2(A,B,C,D,X( 1), 4,0xa4beea44L); R2(D,A,B,C,X( 4),11,0x4bdecfa9L); R2(C,D,A,B,X( 7),16,0xf6bb4b60L); R2(B,C,D,A,X(10),23,0xbebfbc70L); R2(A,B,C,D,X(13), 4,0x289b7ec6L); R2(D,A,B,C,X( 0),11,0xeaa127faL); R2(C,D,A,B,X( 3),16,0xd4ef3085L); R2(B,C,D,A,X( 6),23,0x04881d05L); R2(A,B,C,D,X( 9), 4,0xd9d4d039L); R2(D,A,B,C,X(12),11,0xe6db99e5L); R2(C,D,A,B,X(15),16,0x1fa27cf8L); R2(B,C,D,A,X( 2),23,0xc4ac5665L); /* Round 3 */ R3(A,B,C,D,X( 0), 6,0xf4292244L); R3(D,A,B,C,X( 7),10,0x432aff97L); R3(C,D,A,B,X(14),15,0xab9423a7L); R3(B,C,D,A,X( 5),21,0xfc93a039L); R3(A,B,C,D,X(12), 6,0x655b59c3L); R3(D,A,B,C,X( 3),10,0x8f0ccc92L); R3(C,D,A,B,X(10),15,0xffeff47dL); R3(B,C,D,A,X( 1),21,0x85845dd1L); R3(A,B,C,D,X( 8), 6,0x6fa87e4fL); R3(D,A,B,C,X(15),10,0xfe2ce6e0L); R3(C,D,A,B,X( 6),15,0xa3014314L); R3(B,C,D,A,X(13),21,0x4e0811a1L); R3(A,B,C,D,X( 4), 6,0xf7537e82L); R3(D,A,B,C,X(11),10,0xbd3af235L); R3(C,D,A,B,X( 2),15,0x2ad7d2bbL); R3(B,C,D,A,X( 9),21,0xeb86d391L); A = c->A += A; B = c->B += B; C = c->C += C; D = c->D += D; } } // 传入需要哈希的明文,支持多次调用 int MD5_Update(MD5_CTX *c, const void *data_, size_t len){ const unsigned char *data= static_cast<const unsigned char *>(data_); unsigned char *p; MD5_LONG l; size_t n; if (len==0) return 1; // 低位 l=(c->Nl+(((MD5_LONG)len)<<3))&0xffffffffUL; if (l < c->Nl) c->Nh++; // 高位 c->Nh+=(MD5_LONG)(len>>29); c->Nl=l; n = c->num; if (n != 0){ p=(unsigned char *)c->data; if (len >= MD5_CBLOCK || len+n >= MD5_CBLOCK){ memcpy (p+n,data,MD5_CBLOCK-n); md5_block_data_order(c,p,1); n = MD5_CBLOCK-n; data += n; len -= n; c->num = 0; memset (p,0,MD5_CBLOCK); }else{ memcpy (p+n,data,len); c->num += (unsigned int)len; return 1; } } n = len/MD5_CBLOCK; if (n > 0){ md5_block_data_order(c,data,n); n *= MD5_CBLOCK; data += n; len -= n; } if (len != 0){ p = (unsigned char *)c->data; c->num = (unsigned int)len; memcpy (p,data,len); } return 1; } // 得出最终结果 int MD5_Final(unsigned char *md, MD5_CTX *c){ unsigned char *p = (unsigned char *)c->data; size_t n = c->num; p[n] = 0x80; /* there is always room for one */ n++; if (n > (MD5_CBLOCK-8)){ memset (p+n,0,MD5_CBLOCK-n); n=0; md5_block_data_order(c,p,1); } memset (p+n,0,MD5_CBLOCK-8-n); p += MD5_CBLOCK-8; #if defined(DATA_ORDER_IS_BIG_ENDIAN) (void)HOST_l2c(c->Nh,p); (void)HOST_l2c(c->Nl,p); #elif defined(DATA_ORDER_IS_LITTLE_ENDIAN) (void)HOST_l2c(c->Nl,p); (void)HOST_l2c(c->Nh,p); #endif p -= MD5_CBLOCK; md5_block_data_order(c,p,1); c->num=0; memset (p,0,MD5_CBLOCK); #ifndef HASH_MAKE_STRING #error "HASH_MAKE_STRING must be defined!" #else HASH_MAKE_STRING(c,md); #endif return 1; } //清除加载的各种算法，包括对称算法、摘要算法以及 PBE 算法，并清除这些算法相关的哈希表的内容。 void OPENSSL_cleanse(void *ptr, size_t len){ unsigned char *p = static_cast<unsigned char *>(ptr); size_t loop = len, ctr = cleanse_ctr; while(loop--){ *(p++) = (unsigned char)ctr; ctr += (17 + ((size_t)p & 0xF)); } p= static_cast<unsigned char *>(memchr(ptr, (unsigned char) ctr, len)); if(p) ctr += (63 + (size_t)p); cleanse_ctr = (unsigned char)ctr; } extern "C" JNIEXPORT jstring JNICALL Java_com_unidbg_demo2_MainActivity_setNativeAssetManager(JNIEnv *env, jobject thiz, jobject asset_manager) { AAssetManager *nativeasset = AAssetManager_fromJava(env, asset_manager); AAsset *assetFile = AAssetManager_open(nativeasset, "1.png", AASSET_MODE_BUFFER); size_t fileLength = AAsset_getLength(assetFile); char *dataBuffer = (char *) malloc(fileLength); //read file data AAsset_read(assetFile, dataBuffer, fileLength); //the data has been copied to dataBuffer2, so , close it AAsset_close(assetFile); // 初始化MD5的上下文结构体 MD5_CTX context = {0}; MD5_Init(&context); // 传入待处理的内容以及内容的长度 MD5_Update(&context, dataBuffer, fileLength); // 收尾和输出 // 输出的缓冲区 unsigned char dest[16] = {0}; MD5_Final(dest, &context); // 结果转成十六进制字符串 int i = 0; char szMd5[33] = {0}; for(i=0; i<16; i++){ sprintf(szMd5, "%s%02x", szMd5, dest[i]); } //free malloc free(dataBuffer); // 传回Java世界 return env->NewStringUTF(szMd5); }需要创建一个assets目录，在main文件夹下右键即可创建；将图片放进去即可，运行应用；得到目标图片的md5值，我们的目标就是模拟执行它；5. 模拟执行demo2基本的框架如下：package com.baiLong.csdn_demo8; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.file.FileResult; import com.github.unidbg.file.IOResolver; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.file.ByteArrayFileIO; import com.github.unidbg.memory.Memory; import org.apache.log4j.Level; import org.apache.log4j.Logger; import java.io.File; import java.util.ArrayList; import java.util.List; public class csdn_demo8_2 extends AbstractJni{ private final AndroidEmulator emulator; private final VM vm; private final Module module; public csdn_demo8_2() { emulator = AndroidEmulatorBuilder.for32Bit().build(); Memory memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("src/test/java/com/baiLong/csdn_demo8/file/demo2.apk")); vm.setVerbose(true); vm.setJni(this); DalvikModule dm = vm.loadLibrary(new File("src/test/java/com/baiLong/csdn_demo8/file/libdemo2.so"), false); module = dm.getModule(); emulator.traceCode(); dm.callJNI_OnLoad(emulator); } public static void main(String[] args) { Logger.getLogger("com.github.unidbg.linux.ARM32SyscallHandler").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.unix.UnixSyscallHandler").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.AbstractEmulator").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm.DalvikVM").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm.BaseVM").setLevel(Level.DEBUG); Logger.getLogger("com.github.unidbg.linux.android.dvm").setLevel(Level.DEBUG); csdn_demo8_2 demo = new csdn_demo8_2(); String res = demo.call(); System.out.println("res-->>" + res); } public String call() { // 1D30 List<Object> list = new ArrayList<>(10); list.add(vm.getJNIEnv()); // 第一个参数是env list.add(0); // 第二个参数，实例方法是jobject，静态方法是jclazz，直接填0，一般用不到。 DvmObject<?> assetManager = vm.resolveClass("android/content/res/AssetManager").newObject(null);// context list.add(vm.addLocalObject(assetManager)); Number number = module.callFunction(emulator, 0x1D30 + 1, list.toArray()); String result = vm.getObject(number.intValue()).getValue().toString(); return result; } }速速执行；不出所料报大错了，我们去报错的位置看看，启动ida；AAssetManager_fromJava函数哪来的？为什么报错？这需要我们思考两个问题；首先来回忆一下我们做了什么，在native层读取资源文件不比demo1的简单，demo1直接open系统调用就行；我们去查一下这个 AAssetManager_fromJava 函数的相关信息，它的作用不难分析，与资源文件有关，它是由libandroid.so这个系统SO实现，但是我们并没有加载这个so；不过好在，可以使用虚拟模块实现；// 添加这个就可以 new AndroidModule(emulator,vm).register(memory); // 添加这个就可以在模拟执行的so加载前注册这个虚拟模块即可；非常简单，直接就执行出了结果；但这里可以引出几个问题；为什么Unidbg不内置支持所有系统SO的加载？如果一个SO的依赖SO里包含Unidbg尚未支持的系统SO，那该怎么办？unidbg出现一个我不认识的报错，我应该怎样去溯源问题？先讨论第一个问题，一部分原因是大部分SO中主要的依赖项，就是Unidbg已经支持的这些，即已经够用了；把Android系统中全部SO都成功加载进Unidbg虚拟内存中，既是很大的工作量，又会占用过多内存；另一个更主要的原因是，比如libandroid.so，其依赖SO实在太多了，想顺利加载整个SO确确实实是个苦差事；再看第二个问题，如果SO的依赖项中有Unidbg不支持的系统SO，怎么办？首先，Unidbg会给予提示，其实在最开始的日志就会有体现，只是容易被忽略；其次，尽管SO加载了Unidbg不支持的SO，但有可能我们的目标函数并没有使用到这个系统SO，这种情况下就不用理会，当作不存在就行；如果目标用到了这个so，就有两种处理办法：Patch/Hook 这个不支持的SO所使用的函数；使用Unidbg VirtualModule；第一个很好理解，第二种方法就是我们前面使用的方式；它本质上也是Hook，只不过实现了SO中少数几个函数罢了；碰巧有我们的目标，所以执行也就顺理成章；再看问题三，unidbg出现一个我不认识的报错，我应该怎样去溯源问题？我们的例子就很说明了这个问题，若有不理解的报错，打开tracecode看看哪里停下来，基本上都会找到对应的问题所在，再根据具体问题去patch或者hook；6. 总结致谢：文章参考于白龙大佬分享在csdn的优秀文章，我仅做复现并记录下此文章；文章中所有观点均来自于原文；By：下雨天 2025.10.17；逆向交流+vx：HeiYuKuaiDou23；
补系统调用(一)之得物apk：得物版本：4.94.0目标方法：com.shizhuang.stone.main.SzSdk.lf目标方法实现：libszstone.so方法实现如下：public static native byte[] lf(String str, int i2, int i3);1. 基本框架基本的jni环境如下，这里不讨论；package com.kanxue2w.sana; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject; import com.github.unidbg.memory.Memory; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; import java.util.ArrayList; import java.util.List; public class dewu extends AbstractJni { public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public dewu() { emulator = AndroidEmulatorBuilder .for32Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName("com.shizhuang.duapp") .build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/kanxue2w/du/du.apk")); vm.setJni(this); // vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary("szstone", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public void call_lf() { List<Object> list = new ArrayList<>(10); // jnienv list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(new StringObject(vm, "awt0bapt/data/user/0/com.shizhuang.duapp/filesbav4.94.0bavn486bcndewubc"))); list.add(1); list.add(0); Number number = module.callFunction(emulator, 0x302fd, list.toArray()); String res = (String) vm.getObject(number.intValue()).getValue(); System.out.println("call_lf-->> " + res); } @Override public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) { switch (signature) { case "android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;": { // System.out.println("<<<--- dvmClass --->>>"+dvmClass); return dvmClass.newObject(null); } case "android/provider/Settings$Secure->getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;": { System.out.println("getString方法 dvmClass -->>" + dvmClass); System.out.println("getString方法 参数2 -->>" + varArg.getObjectArg(1).getValue()); return new StringObject(vm, "8drd14lf"); } } return super.callStaticObjectMethod(vm, dvmClass, signature, varArg); } @Override public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) { switch (signature) { case "android/app/ActivityThread->getApplication()Landroid/app/Application;": { return vm.resolveClass("android/app/Application").newObject(null); } case "android/app/Application->getPackageManager()Landroid/content/pm/PackageManager;": { return vm.resolveClass("android/content/pm/PackageManager").newObject(null); } case "android/app/Application->getContentResolver()Landroid/content/ContentResolver;": { return vm.resolveClass("android/content/ContentResolver").newObject(null); } } return super.callObjectMethod(vm, dvmObject, signature, varArg); } public static void main(String[] args) { dewu du = new dewu(); du.call_lf(); } }2. clock_gettime依据上述的代码骨架，尝试继续运行，会出现以下报错；[17:10:10 070] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=263, svcNumber=0x0, PC=RX@0x40117b88[libc.so]0x40b88, LR=RX@0x400f0ef3[libc.so]0x19ef3, syscall=null java.lang.UnsupportedOperationException: clk_id=2 at com.github.unidbg.linux.ARM32SyscallHandler.clock_gettime(ARM32SyscallHandler.java:1760) at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:411) at com.github.unidbg.arm.backend.Unicorn2Backend$11.hook(Unicorn2Backend.java:343) at com.github.unidbg.arm.backend.unicorn.Unicorn$NewHook.onInterrupt(Unicorn.java:109)首先分析这个错误是什么意思？一般来说，svcNumber等于0即代表系统调用，NR则是调用号，并且结合报错路径结合判断，它是一个系统调用的异常，去看看什么调用；很显然，它也做了提示，这里也可以去异常位置看看；这里证明，不只是一个结果，其实 Unidbg 对 clock_gettime 做了模拟实现，只是并没有做到完美的形态，它做到了一些分支，可以去看看；那么需要知道这个系统调用具体是在做什么吧，问问ai；// clock_gettime 是 Linux 系统中的一个高精度时间获取系统调用，用于从指定类型的时钟源中获取当前时间戳，支持纳秒级精度； // 函数原型与参数： #include <time.h> int clock_gettime(clockid_t clk_id, struct timespec *tp); //参数说明： // clk_id：时钟类型标识符，决定时间来源（见下文详解）。 // tp：指向 struct timespec 的指针，用于存储获取的时间值。 // 该结构体包含： // tv_sec：自 Unix Epoch（1970年1月1日00:00:00 UTC）的秒数。 // tv_nsec：纳秒级精度的时间（范围：0~999,999,999）。 // 返回值：成功返回 0，失败返回 -1 并设置 errno。 // timespec 结构体 struct timespec { long tv_sec; // 秒时间戳 long tv_nsec; // 余下的纳秒时间戳 };如果想获取毫秒级时间戳，可以像下面这样写：long long gettime3(){ struct timespec t = {0, 0}; clock_gettime(CLOCK_REALTIME, &t); return (t.tv_sec*1000) + (t.tv_nsec/1000000); }参数1是时钟类型标识符，在此是2，大致有以下的选择；#define CLOCK_REALTIME 0 //当前的实际时间，是自 1970年1月1日（UNIX 时间纪元）以来经过的秒数，受到系统时钟调整的影响 #define CLOCK_MONOTONIC 1 //自系统启动以来经过的时间 #define CLOCK_PROCESS_CPUTIME_ID 2 //当前进程消耗的 CPU 时间 #define CLOCK_THREAD_CPUTIME_ID 3 //当前线程消耗的 CPU 时间 #define CLOCK_MONOTONIC_RAW 4 //类似于CLOCK_MONOTONIC，但是它不会受到任何系统时间调整或时钟源变化的影响 #define CLOCK_REALTIME_COARSE 5 //类似于 CLOCK_REALTIME，但是提供较低精度的时间 #define CLOCK_MONOTONIC_COARSE 6 //类似于 CLOCK_MONOTONIC，但提供较低精度的时间 #define CLOCK_BOOTTIME 7 //系统启动以来经过的时间，包含了系统休眠时间 #define CLOCK_REALTIME_ALARM 8 //当前时间，但以时间报警机制的精度为准 #define CLOCK_BOOTTIME_ALARM 9 //系统启动以来的时间，类似于 CLOCK_BOOTTIME，但它同样考虑了定时器或闹钟机制的精度。 #define CLOCK_SGI_CYCLE 10 //SGI（Silicon Graphics）的专有时钟 #define CLOCK_TAI 11 //国际原子时简而言之，时钟可以分为 真实时间、CPU 时间、开机时间、从不确定的某个时间点开始计时 这四大类；Unidbg、Qiling、ExAndroidNativeEmu 等模拟器，其实都是对它有所实现，但又没有实现每一个时钟；CLOCK_REALTIME这个时钟，也就是真实时间，来看看各个工具如何实现：# ExAndroidNativeEmu 通过 Python 内置 API 获得了秒级时间戳，然后赋给 tv_sec，tv_nsec 则是置空 if clk_id == CLOCK_REALTIME: # Its time represents seconds and nanoseconds since the Epoch. clock_real = calendar.timegm(time.gmtime()) mu.mem_write(tp_ptr + 0, int(clock_real).to_bytes(self.__ptr_sz, byteorder='little')) mu.mem_write(tp_ptr + self.__ptr_sz, int(0).to_bytes(self.__ptr_sz, byteorder='little')) return 0 # 这么做不太好，意味着获取返回的真实时间其精度只有秒，和语义有一定偏差 # qiling 则是完全没有管其他类型的时钟，只当成真实时间去处理 def ql_syscall_clock_gettime(ql, clock_gettime_clock_id, clock_gettime_timespec, *args, **kw): ql.log.info(f"clock_gettime(clock_id={clock_gettime_clock_id}, tp={hex(clock_gettime_timespec)})") now = datetime.now().timestamp() tv_sec = floor(now) tv_nsec = floor((now - floor(now)) * 1e6) tp = timespec(tv_sec= tv_sec, tv_nsec=tv_nsec) ql.mem.write(clock_gettime_timespec, bytes(tp)) ql.log.debug(f"timespec(tv_sec={tv_sec}, tv_nsec={tv_nsec})") return 0 # 相比 ExAndroidNativeEmu，它更完善一些，对 tv_nsec 也做了赋值 # Unidbg 在CLOCK_REALTIME时钟下，通过 Java 标准库获取了毫秒级时间戳，把秒的部分赋给 tv_sec，再毫秒部分乘 1000000 赋给 tv_nsec。和 Qiling 的逻辑完全一致 long offset = clk_id == CLOCK_REALTIME ? System.currentTimeMillis() * 1000000L : System.nanoTime() - nanoTime;简而言之，这几款工具都做了一定程度上的模拟，但都不齐全不完善，当然不止是上面的这种情况，那么这里的问题是clk_id=2的情况，2 对应于 CLOCK_PROCESS_CPUTIME_ID 即进程的 CPU 时间，这里我们也破罐子破摔，并入 CLOCK_THREAD_CPUTIME_ID；这是龙哥的原话，那么我们也这么来，因为3，即：#define CLOCK_PROCESS_CPUTIME_ID 2 //当前进程消耗的 CPU 时间 #define CLOCK_THREAD_CPUTIME_ID 3 //当前线程消耗的 CPU 时间它俩是可以类似的，我们就按这个方案来处理，所以这里应该在源码里修改吗？当然不选择这样做，这是一种不优雅的做法，和补jni环境的时候类似，我们自实现系统调用处理器，新建一个类来做处理；package com.kanxue2w.sana.dewu; import com.github.unidbg.Emulator; import com.github.unidbg.arm.backend.Backend; import com.github.unidbg.linux.ARM32SyscallHandler; import com.github.unidbg.memory.SvcMemory; import com.github.unidbg.pointer.UnidbgPointer; import com.sun.jna.Pointer; import unicorn.ArmConst; public class deWuSyscallHandler extends ARM32SyscallHandler { public deWuSyscallHandler(SvcMemory svcMemory) { super(svcMemory); } protected int clock_gettime(Backend backend, Emulator<?> emulator) { int clk_id = backend.reg_read(ArmConst.UC_ARM_REG_R0).intValue(); Pointer tp = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R1); if (clk_id == 2) { tp.setInt(0, 0); tp.setInt(4, 1); return 0; } return super.clock_gettime(backend, emulator); } }这里依据上面的逻辑，完全模仿着unidbg的处理就行了，在我们之前的代码需要使用这个系统调用，换成如下写法：package com.kanxue2w.sana.dewu; import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Emulator; import com.github.unidbg.Module; import com.github.unidbg.arm.backend.Unicorn2Factory; import com.github.unidbg.file.FileResult; import com.github.unidbg.file.IOResolver; import com.github.unidbg.file.linux.AndroidFileIO; import com.github.unidbg.linux.android.AndroidARMEmulator; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.linux.android.dvm.*; import com.github.unidbg.linux.file.SimpleFileIO; import com.github.unidbg.memory.Memory; import com.github.unidbg.memory.SvcMemory; import com.github.unidbg.unix.UnixSyscallHandler; import com.github.unidbg.virtualmodule.android.AndroidModule; import java.io.File; import java.util.ArrayList; import java.util.List; public class dewu extends AbstractJni implements IOResolver { @Override public FileResult resolve(Emulator emulator, String pathname, int oflags) { System.out.println("sana凑崎纱夏 file open:" + pathname); return null; } public static AndroidEmulator emulator; public static Memory memory; public static VM vm; public static Module module; public dewu() { // 重点start // 创建模拟器实例 AndroidEmulatorBuilder 的参数表示是否是 64 位的 AndroidEmulatorBuilder builder = new AndroidEmulatorBuilder(false) { @Override public AndroidEmulator build() { return new AndroidARMEmulator(processName, rootDir, backendFactories) { @Override protected UnixSyscallHandler<AndroidFileIO> createSyscallHandler(SvcMemory svcMemory) { return new deWuSyscallHandler(svcMemory); } }; } }; // 重点end emulator = builder // .for32Bit() .addBackendFactory(new Unicorn2Factory(true)) .setProcessName("com.shizhuang.duapp") .build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); vm = emulator.createDalvikVM(new File("apks/kanxue2w/du/du.apk")); vm.setJni(this); vm.setVerbose(true); new AndroidModule(emulator, vm).register(memory); DalvikModule dm = vm.loadLibrary("szstone", true); dm.callJNI_OnLoad(emulator); module = dm.getModule(); } public void call_lf() { List<Object> list = new ArrayList<>(10); // jnienv list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(new StringObject(vm, "awt0bapt/data/user/0/com.shizhuang.duapp/filesbav4.94.0bavn486bcndewubc"))); list.add(1); list.add(0); Number number = module.callFunction(emulator, 0x302fd, list.toArray()); String res = (String) vm.getObject(number.intValue()).getValue(); System.out.println("call_lf-->> " + res); } @Override public DvmObject<?> callStaticObjectMethod(BaseVM vm, DvmClass dvmClass, String signature, VarArg varArg) { switch (signature) { case "android/app/ActivityThread->currentActivityThread()Landroid/app/ActivityThread;": { return dvmClass.newObject(null); } case "android/provider/Settings$Secure->getString(Landroid/content/ContentResolver;Ljava/lang/String;)Ljava/lang/String;": { System.out.println("getString方法 dvmClass -->>" + dvmClass); System.out.println("getString方法 参数2 -->>" + varArg.getObjectArg(1).getValue()); return new StringObject(vm, "8drd14lf"); } } return super.callStaticObjectMethod(vm, dvmClass, signature, varArg); } @Override public DvmObject<?> callObjectMethod(BaseVM vm, DvmObject<?> dvmObject, String signature, VarArg varArg) { switch (signature) { case "android/app/ActivityThread->getApplication()Landroid/app/Application;": { return vm.resolveClass("android/app/Application").newObject(null); } case "android/app/Application->getPackageManager()Landroid/content/pm/PackageManager;": { return vm.resolveClass("android/content/pm/PackageManager").newObject(null); } case "android/app/Application->getContentResolver()Landroid/content/ContentResolver;": { return vm.resolveClass("android/content/ContentResolver").newObject(null); } } return super.callObjectMethod(vm, dvmObject, signature, varArg); } public static void main(String[] args) { dewu du = new dewu(); du.call_lf(); } }运行看看会有什么情况；[16:59:52 968] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1911) - openat dirfd=-100, pathname=/proc/sys/kernel/random/boot_id, oflags=0x20000, mode=0 [16:59:52 973] INFO [com.github.unidbg.linux.AndroidSyscallHandler] (AndroidSyscallHandler:456) - Return default pipe pair. [16:59:52 973] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=190, svcNumber=0x0, PC=RX@0x40118b5c[libc.so]0x41b5c, LR=RX@0x401065cb[libc.so]0x2f5cb, syscall=null [16:59:52 974] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=358, svcNumber=0x0, PC=RX@0x40118db0[libc.so]0x41db0, LR=RX@0x40106635[libc.so]0x2f635, syscall=null java.lang.AbstractMethodError: com.github.unidbg.linux.file.PipedWriteFileIO at com.github.unidbg.file.AbstractFileIO.dup2(AbstractFileIO.java:160) at com.github.unidbg.linux.ARM32SyscallHandler.dup3(ARM32SyscallHandler.java:2124) at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:501) at com.github.unidbg.arm.backend.Unicorn2Backend$11.hook(Unicorn2Backend.java:343) at com.github.unidbg.arm.backend.unicorn.Unicorn$NewHook.onInterrupt(Unicorn.java:109)3. popen新的报错非常多，首先关键信息主要有以下内容：[16:59:52 973] INFO [com.github.unidbg.linux.AndroidSyscallHandler] (AndroidSyscallHandler:456) - Return default pipe pair. [16:59:52 973] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=190, svcNumber=0x0, PC=RX@0x40118b5c[libc.so]0x41b5c, LR=RX@0x401065cb[libc.so]0x2f5cb, syscall=null [16:59:52 974] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=358, svcNumber=0x0, PC=RX@0x40118db0[libc.so]0x41db0, LR=RX@0x40106635[libc.so]0x2f635, syscall=null java.lang.AbstractMethodError: com.github.unidbg.linux.file.PipedWriteFileIO先看系统调用，这里有两处；NR=190 vfork NR=358 dup3 popen内部会调用vfork和dup3 这种错误：com.github.unidbg.linux.file.PipedWriteFileIO 直接到popen去处理龙哥原话：为什么这里有多个系统调用会WARN？事实上这是库函数popen惹的祸，只要读者看到com.github.unidbg.linux.file.PipedWriteFileIO报错，那么就应该直接转入处理popen的逻辑；这就是典型的popen问题，我们这里要了解三个小知识：基本语义和用法？它是什么意思？Unidbg 为什么会在这个库函数上出错？Unidbg 里如何处理这个问题？首先，popen 在 Android Native 上是一个高频函数，远非getrusage这类函数可比，设备信息收集、环境检测都可以用到它；函数原型如下：FILE *popen(const char *command, const char *type);其次它是什么意思？popen 是 C 标准库中的一个函数（属于 文件操作接口，而非直接的系统调用），用于创建一个管道（pipe）并启动子进程执行指定的命令。其核心功能是 在程序中调用外部命令并与其输入/输出交互，类似于在终端中执行命令并捕获结果；popen 以创建管道的方式启动一个进程, 并调用 shell，像下面这样使用，比如这里在作用上等价于 adb shell 里输入getprop ro.build.id；std::string cmd = "getprop ro.build.id"; char value[PROP_VALUE_MAX] = {0}; FILE* file = popen(cmd.c_str(), "r"); fread(value, PROP_VALUE_MAX, 1, file); pclose(file);其次为什么 Unidbg 会在这个库函数上出错，popen 在底层原理上是创建了一个新进程，然后通过管道去做通信，返回数据；但 Unidbg 在多进程相关的系统调用处理上十分不完善，无法支撑popen正常的执行它的底层逻辑；最后是 Unidbg 里如何处理好 popen，作者提供过一种好思路，就是越过创建多线程的相关逻辑，只处理好管道的返回值，来让它的功能基本满足；下面做演示，这可以当成固定处理模式，遇到了就这么做就行；第一步是用 Dobby 或任意 Hook 工具，在库函数的层面 Hook 获取到 popen 的 command，放到 emulator 的全局变量里，方便后续形成对应。我这里使用的是 xhook；public void hookPopen(){ IxHook xHook = XHookImpl.getInstance(emulator); xHook.register("libszstone.so", "popen", new ReplaceCallback() { @Override public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) { RegisterContext registerContext = emulator.getContext(); String command = registerContext.getPointerArg(0).getString(0); emulator.set("command", command); System.out.println("sana command-->>"+command); return HookStatus.RET(emulator, originFunction); } }, true); // 使其生效 xHook.refresh(); }看看运行结果是什么，然后再根据提示去补；我们在第一个自实现类里实现pipe2方法，重写 pipe2，根据当前的 command 返回从 adb shell 获取到的值，以及出现 popen 内部会使用的 vfork 和 wait4 系统调用，并依次在代码里进行解释；package com.kanxue2w.sana.dewu; import com.github.unidbg.Emulator; import com.github.unidbg.arm.backend.Backend; import com.github.unidbg.arm.context.EditableArm32RegisterContext; import com.github.unidbg.linux.ARM32SyscallHandler; import com.github.unidbg.linux.file.ByteArrayFileIO; import com.github.unidbg.linux.file.DumpFileIO; import com.github.unidbg.memory.SvcMemory; import com.github.unidbg.pointer.UnidbgPointer; import com.sun.jna.Pointer; import unicorn.ArmConst; import java.util.concurrent.ThreadLocalRandom; public class deWuSyscallHandler extends ARM32SyscallHandler { public deWuSyscallHandler(SvcMemory svcMemory) { super(svcMemory); } @Override protected boolean handleUnknownSyscall(Emulator<?> emulator, int NR) { switch (NR) { // 仿照源码写 case 190: vfork(emulator); return true; case 114: wait4(emulator); return true; } return super.handleUnknownSyscall(emulator, NR); } protected int clock_gettime(Backend backend, Emulator<?> emulator) { int clk_id = backend.reg_read(ArmConst.UC_ARM_REG_R0).intValue(); Pointer tp = UnidbgPointer.register(emulator, ArmConst.UC_ARM_REG_R1); if (clk_id == 2) { tp.setInt(0, 0); tp.setInt(4, 1); return 0; } return super.clock_gettime(backend, emulator); } @Override protected int pipe2(Emulator<?> emulator) { EditableArm32RegisterContext context = (EditableArm32RegisterContext) emulator.getContext(); Pointer pipefd = context.getPointerArg(0); int write = getMinFd(); this.fdMap.put(write, new DumpFileIO(write)); int read = getMinFd(); String command = emulator.get("command"); System.out.println("sana pipe2 command-->>" + command); // stdout中写入popen command 应该返回的结果 String stdout = "\n"; switch (command) { // 这里根据command来判断 给它返回shell里的结果 case "stat /data": { stdout = " File: /data\n" + " Size: 4096 Blocks: 16 IO Blocks: 512 directory\n" + "Device: 10305h/66309d Inode: 2 Links: 53\n" + "Access: (0771/drwxrwx--x) Uid: ( 1000/ system) Gid: ( 1000/ system)\n" + "Access: 1970-01-20 05:08:00.000000000 +0800\n" + "Modify: 2024-11-19 21:13:13.707334006 +0800\n" + "Change: 1970-05-22 00:15:12.503333433 +0800"; } } this.fdMap.put(read, new ByteArrayFileIO(0, "pipe2_read_side", stdout.getBytes())); pipefd.setInt(0, read); pipefd.setInt(4, write); context.setR0(0); return 0; } // 补足相应的系统调用报错 根据调用号查看 private void vfork(Emulator<?> emulator) { EditableArm32RegisterContext context = emulator.getContext(); int childPid = emulator.getPid() + ThreadLocalRandom.current().nextInt(256); int r0 = childPid; System.out.println("vfork pid-->>" + r0); context.setR0(r0); } // 补足相应的系统调用报错 根据调用号查看 private void wait4(Emulator<?> emulator) { return; } }尝试运行，看看会有什么反应；大概截图一点，可以看到，还有多个case的情况，需要补齐，这里不管了，因为下一个异常已经出来了；另外，包括文件访问的处理也并没有做；又是系统调用，依旧去查看，// 这是文件访问 [16:24:07 447] INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1122) - stat64 pathname=/data/data/com.microvirt.launcher, LR=RX@0x400024c9[libszstone.so]0x24c9 [16:24:07 456] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=54, svcNumber=0x0, PC=RX@0x40118aa8[libc.so]0x41aa8, LR=RX@0x4011e7f1[libc.so]0x477f1, syscall=null java.lang.AbstractMethodError: com.github.unidbg.linux.file.TcpSocket: request=0x8927, argp=0xbffff470 at com.github.unidbg.file.AbstractFileIO.ioctl(AbstractFileIO.java:57) at com.github.unidbg.linux.ARM32SyscallHandler.ioctl(ARM32SyscallHandler.java:2045) at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:210) at com.github.unidbg.arm.backend.Unicorn2Backend$11.hook(Unicorn2Backend.java:343) at com.github.unidbg.arm.backend.unicorn.Unicorn$NewHook.onInterrupt(Unicorn.java:109)4. ioctlhttps://segmentfault.com/a/1190000005138358[16:24:07 456] WARN [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:534) - handleInterrupt intno=2, NR=54, svcNumber=0x0, PC=RX@0x40118aa8[libc.so]0x41aa8, LR=RX@0x4011e7f1[libc.so]0x477f1, syscall=null java.lang.AbstractMethodError: com.github.unidbg.linux.file.TcpSocket: request=0x8927, argp=0xbffff470 at com.github.unidbg.file.AbstractFileIO.ioctl(AbstractFileIO.java:57) at com.github.unidbg.linux.ARM32SyscallHandler.ioctl(ARM32SyscallHandler.java:2045) at com.github.unidbg.linux.ARM32SyscallHandler.hook(ARM32SyscallHandler.java:210)NR=54 对应 ioctl ，在 Android Native 里，它常用来获取 MAC 地址。它的参数 request用于指令操作类型，对于各种各样的文件和需求，有不同的request，Unidbg 首先预处理了其中的一部分，但这里报错的 0x8927 尚未处理；一个最简单的处理办法就是来到报错点com.github.unidbg.file.AbstractFileIO.ioctl(AbstractFileIO.java:64)，添加判断request=0x8927 的情况，让其返回0；@Override public int ioctl(Emulator<?> emulator, long request, long argp) { // add ioctl start // if (log.isTraceEnabled()) { // emulator.attach().debug(); // } if (request == 0x8927){ return 0; } // add ioctl end throw new AbstractMethodError(getClass().getName() + ": request=0x" + Long.toHexString(request) + ", argp=0x" + Long.toHexString(argp)); }根据龙哥的方式也可以：一个最简单的处理办法就是来到报错点com.github.unidbg.file.AbstractFileIO.ioctl(AbstractFileIO.java:64)，将抛出异常改为直接返回 -1，即函数调用失败之意；摆烂可耻但有用；@Override public int ioctl(Emulator<?> emulator, long request, long argp) { if (log.isTraceEnabled()) { emulator.attach().debug(); } return -1; }
快手去花指令1. 简述学习过程的记录，仅代表个人浅显理解，有错误烦请纠正；Ⅰ 什么是花指令？花指令是由开发者设计，通过某种方式插入程序，用于阻碍反汇编器和代码分析者对程序控制流的认识的汇编代码片段；它是由开发者设计的，希望使反汇编出错，让破解者无法清楚正确的理解程序逻辑的一种技术；2. 确认是否有花一般来说，插花都会在比较重要的函数做，或者说重要的函数被插花的概率更大，所以我们在做这一步的时候可以优先查看JNI_OnLoad这种函数来看是否被插花；这算是比较明显的特征，或者看上面的导航条，出现了大段的红色，以及对应的机器码部分也是红色，这就属于未被识别为函数的特征，所以可以确认这个样本存在花指令；3. 修复JNI_OnLoad2.1 手动修复样本存在大量的花指令，所以不可能一个一个单独修复，肯定是需要借助ida脚本来做这件事，但在批量处理之前我们可以先看一下单个的处理，先看看对应的汇编代码；.text:00000000000462C4 STP X0, X1, [SP,#var_20]! .text:00000000000462C8 STP X2, X30, [SP,#0x20+var_10] .text:00000000000462CC ADR X1, dword_462EC .text:00000000000462D0 SUBS X1, X1, #0xC .text:00000000000462D4 MOV X0, X1 .text:00000000000462D8 ADDS X0, X0, #0x3C ; '<' .text:00000000000462DC STR X0, [SP,#0x20+var_8] .text:00000000000462E0 LDP X2, X9, [SP,#0x20+var_10] .text:00000000000462E4 LDP X0, X1, [SP+0x20+var_20],#0x20 .text:00000000000462E8 BR X9可以看到最后它是跳转到了X9寄存器的地址，而ida其实已经识别到这个地址了，或者说他计算出了这个地址，就是JUMPOUT位置的值，0x4631，那么这个值在哪里？可以看到，它们隔的其实其实不远，只是中间有一些数据，我们按下D可以将他们转换一下；这里其实ida已经将该跳转的地方识别为了函数，只是它没有创建，因为它处在数据里面，ida无法百分百确定它是代码段，所以选择了较为稳妥的方案，将选择权交给用户，那我们可以尝试按下P将它创建为一个函数，这里注意光标位置放置准确；可以发现成功被创建成了一个函数，按下tab即可看伪c；此时点击JNI_OnLoad函数里的地址，它也会顺利的跳到这个新创建的sub_4631C函数处；2.2 keypatch修复接下来需要去分析这些汇编代码具体都有什么含义，依次来看，以jni_onload这个函数为例，这里可以按下h将一些数据转换成十六进制，方便我们分析，先看完整的汇编代码：.text:00000000000462C4 ; jint JNI_OnLoad(JavaVM *vm, void *reserved) .text:00000000000462C4 EXPORT JNI_OnLoad .text:00000000000462C4 JNI_OnLoad ; DATA XREF: LOAD:0000000000003AD0↑o .text:00000000000462C4 .text:00000000000462C4 var_20 = -0x20 .text:00000000000462C4 var_18 = -0x18 .text:00000000000462C4 var_10 = -0x10 .text:00000000000462C4 var_8 = -8 .text:00000000000462C4 .text:00000000000462C4 E0 07 BE A9 STP X0, X1, [SP,#-0x20]! .text:00000000000462C8 E2 7B 01 A9 STP X2, X30, [SP,#0x10] .text:00000000000462CC 01 01 00 10 ADR X1, 0x462EC .text:00000000000462D0 21 30 00 F1 SUBS X1, X1, #0xC .text:00000000000462D4 E0 03 01 AA MOV X0, X1 .text:00000000000462D8 00 F0 00 B1 ADDS X0, X0, #0x3C ; '<' .text:00000000000462DC E0 0F 00 F9 STR X0, [SP,#0x18] .text:00000000000462E0 E2 27 41 A9 LDP X2, X9, [SP,#0x10] .text:00000000000462E4 E0 07 C2 A8 LDP X0, X1, [SP],#0x20 .text:00000000000462E8 20 01 1F D6 BR X9 .text:00000000000462E8 ; End of function JNI_OnLoad先看第一条指令；这里可以按下h将一些数据转换成十六进制或者十进制都行，后续注意进制即可；E0 07 BE A9 STP X0, X1, [SP,#-0x20]! @ 感叹号代表前缀索引模式 这里的作用是将X0、X1这两个寄存器依次存入栈顶 此时的栈顶应当是SP - 32的位置 @ 并更新栈指针为 SP - 32 大概就是做了这两件事情再看第二条指令：E2 7B 01 A9 STP X2, X30, [SP,#0x10] @ 这里是偏移寻址 直接sp加上16即可 将寄存器 X2 和 X30（LR）的值依次存储到栈顶偏移 16 字节的位置第三条指令：01 01 00 10 ADR X1, 0x462EC @ 这里可以简单的理解为 X1 = 0x462EC adr其实是一个地址加载伪指令 第四条指令：21 30 00 F1 SUBS X1, X1, #0xC @ 这是一个减法指令 作用：X1 = X1 - 0xC = 0x462E0 根据上条代码得到第五条指令，后续就接着分析到结束了；E0 03 01 AA MOV X0, X1 @ 将x1的值赋值给x0 X0 = X1 = 0x462E000 D0 00 B1 ADDS X0, X0, #0x3C ; '<' @ 加法指令 X0 = X0 + 0X34 = 0x462E0 + 0x3C = 0x4631CE0 0F 00 F9 STR X0, [SP,#0x18] @ 这里也是偏移寻址 将X0放在SP+24的位置 并且这里是经过重新赋值的新X0E2 27 41 A9 LDP X2, X9, [SP,#0x10] @ 依旧是偏移寻址 作用是从内存中连续加载两个寄存器的值 它的加载顺序如下： @ X2 ← [SP + 16] @ X9 ← [SP + 24]（每个寄存器占 8 字节） @ 这条指令得到的结果就是X2不变 依靠上面的逻辑可以知道SP + 16的寄存器正是X2 所以X2是不变的 @ 而X9的结果就是上面的新X0E0 07 C2 A8 LDP X0, X1, [SP],#0x20 @ 后索引寻址 LDP是 ARM64 架构中的双寄存器加载指令 作用是从内存中连续加载两个寄存器的值 并更新栈指针 @ 该指令的核心功能是 恢复栈空间 加载完成后 SP 向高地址方向移动 32 字节 释放之前分配的栈空间 @ （例如函数调用时通过 STP 指令分配的栈空间） STP X0, X1, [SP, #-32]! ; 保存 X0/X1 到栈顶，SP 减少 32 字节 ... LDP X0, X1, [SP], #32 ; 恢复 X0/X1，SP 增加 32 字节 @ 这里实际上就是将栈顶存着的X0、X1恢复到原先的值20 01 1F D6 BR X9 @ 跳转到X9的位置 也就是 BR 0x4631C关于上述的分析其实也是有借助于ai的，所以并不是标准答案，仅仅只是我的拙见；指令一共也就这几条，不会有什么难度，而且重要的指令也就两条运算而已，不熟悉的朋友可以多多借助ai来帮助分析；将这十条汇编分析完之后我们来总结一下寄存器与栈的使用情况，首先是寄存器，一共用到了X0、X1、X2、X30、X9 这五个寄存器，而真正参与计算的只有X1、X2、X9这三个寄存器，其余两个的值是没有变过的，这里主要起到一个混淆的作用，X0和X1寄存器经过计算得到0x4631C这个值，而X9只是用来承接这个值而已；再就是栈，第一个作用就是为花指令打掩护，前面我们提到过，花指令是人为的，目标是干扰分析者或者反汇编工具，这里借助栈来实现这种隐晦的赋值方案来反击分析者，否则会很轻易的知道具体是跳转到哪里去了，也就失去了花指令的意义；第二个作用就是保护上下文，我们知道，有些寄存器是易失的，有些寄存器是非易失的，区别就在于，在函数调用时，是否需要保存它的值，在用完之后再将它恢复回去，这里就很显然，X0、X1就是易失的，所以最后才需要将栈上的值恢复回去，也就是这个作用；通过手动推演的方式我们已经了解了这个花指令，然后需要去进行patch，来抵消花指令对我们分析的困扰；他有这几个要求，首先将实际的功能patch到整个程序的首行，在这里也就是B 0x4631C，这个跳转也就是它的实际作用，再就是将其余行全部nop掉，防止他干扰我们的分析以及对反汇编工具的干扰；这里可以选择自带的patch也可以选择keypatch工具，我这里选择使用keypacth，他比较方便；在首行代码处右键选择keypatch；这个工具没有的话需要自行安装，相关的依赖也请百度后安装；选择keypatch后，随后根据出现的选项选择第一个patcher，将assembly这里改成B 0x4631C，随后点击patch，后续的框可以取消掉；这里需要注意位置是首行，不用patch错了；随后同样的操作，选择剩余的所有代码，改为nop；此时反编译依旧是JUMPOUT，此时将光标放在第一行汇编上，选择左上角的edit，找到Functions选项，然后选择delete function，此时这段代码会变红，代表ida无法识别这段代码，然后继续将光标放在首行，按下p键或者去delete的地方找create function将其转换为函数；然后再次按下tab即可发现反编译没问题了；跳转一下；这样JNI_OnLoad就修复完成了，其实手动更加的灵活，只是第二种方式可以明白它的原理且更加适应复杂场景，后续批量处理的时候也需要依靠刚刚的分析；4. 批量修复4.1 idapython代码提示首先我们需要一个开发脚本的环境，ida里显然是不太舒适的，我这里选择pycharm来进行脚本开发，如下配置即可；4.2 修复首先我们需要明确一点的是，究竟有多少处花指令，这里我们去ida搜索一下特征即可确定，那么哪一个算是特征？我们任意的去找一些红色的位置，看看它们的汇编代码分别是什么样的；大部分都是这种结构，那么我们就可以以他为基准来搜索特征，这里需要注意，尽量不要依靠反汇编的汇编字符串来做特征，这样会有未被反汇编器识别的结果被错过，这样会干扰分析，所以最好是通过机器码来匹配；我们选择搜索机器码，且建议搜索两条，一条的话可能会存在误判；搜索的条件是：E0 07 BE A9 E2 7B 01 A9，结果有123条；在开始编写之前我们需要明确一件事，我们应该如何批量修改；首先需要跟我们进行的操作一样，找到匹配的特征项，其次再是统一处理；接下来开始编写ida脚本，这个我也不是特别熟悉，参考了很多大佬的脚本，在结尾会进行感谢；首先看看特征匹配的代码；from keystone import * import ida_bytes import idaapi import idc import ida_nalt def binSearch(start, patternStr): matches = [] pattern = ida_bytes.compiled_binpat_vec_t() ida_bytes.parse_binpat_str(pattern, 0x0, patternStr, 16, ida_nalt.BPU_2B) # ida_nalt.BPU_2B 表示使用2字节的搜索单位 while True: # 从 start 地址到 idc.BADADDR（表示无效地址）的范围内搜索 pattern模式 1 表示只搜索第一个匹配项 addr, _ = ida_bytes.bin_search3(start, idc.BADADDR, pattern, 1) if addr == idc.BADADDR: # bad address break else: matches.append(addr) start = addr + 1 return matches # 特征 matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9") print(matches) print(f"共有{len(matches)}个匹配项")这个当然不是在ida里运行的，需要去ida里跑，选择file 下的 Script command即可运行；将脚本粘贴上去后即可运行，这里提一句我用的是ida 9.0，不同的版本ida的api会不太一样，所以有报错的话需要自行摸索；个数和我们在ida搜索匹配到的数量是对的上的，接下来就需要确认修复方案，我们手动用keypatch的时候是在找它的最终跳转地址吧，那我们对每一条都需要这么做，找到他最终跳转的地址后patch即可；回到之前的汇编分析，我们发现了实际上它最终的地址计算依靠的就是三条指令，一条获取到一个给定的地址，然后减去一个值，再次加上一个值，就能得到它最终需要跳转的地址了；def getJumpAddress(addr): A = idc.get_operand_value(addr + 8, 1) # get_operand_value 需要地址识别为代码 B = idc.get_operand_value(addr + 12, 2) C = idc.get_operand_value(addr + 20, 2) return A - B + C # 获取跳转的地址关于这段代码解释如下：addr是当前地址，+8则是第三行，也就是获取第一个操作数，这里我暂且这么说，后面的1可以理解为它的位置吧，后续两行代码是一样的道理，能理解就行，最终计算出跳转的实际地址；随后需要的步骤就是上面提及的nop，这个已经在 2.2 位置提及过，脚本如下：import keystone # pip install keystone-engine from keystone import * import ida_bytes import idc import ida_nalt # https://python.docs.hex-rays.com/ # https://python.docs.hex-rays.com/ida_bytes def binSearch(start, patternStr): matches = [] pattern = ida_bytes.compiled_binpat_vec_t() ida_bytes.parse_binpat_str(pattern, 0x0, patternStr, 16, ida_nalt.BPU_2B) # ida_nalt.BPU_2B 表示使用2字节的搜索单位 while True: # 从 start 地址到 idc.BADADDR（表示无效地址）的范围内搜索 pattern模式 1 表示只搜索第一个匹配项 addr, _ = ida_bytes.bin_search3(start, idc.BADADDR, pattern, 1) if addr == idc.BADADDR: # bad address break else: matches.append(addr) start = addr + 1 return matches def getJumpAddress(addr): A = idc.get_operand_value(addr + 8, 1) # get_operand_value 需要地址识别为代码 B = idc.get_operand_value(addr + 12, 2) C = idc.get_operand_value(addr + 20, 2) return A - B + C # 获取跳转的地址 def makeInsn(addr): # 尝试在给定的地址 addr 上创建一条指令 if idc.create_insn(addr) == 0: # 如果在地址 addr 上没有成功创建指令，则删除该地址上的所有项（扩展项） idc.del_items(addr, idc.DELIT_EXPAND) # 重新尝试在地址 addr 上创建一条指令 idc.create_insn(addr) # 等待自动操作完成 idc.auto_wait() def generate(code, addr): ks = Ks(keystone.KS_ARCH_ARM64, keystone.KS_MODE_LITTLE_ENDIAN) # 参数2是地址，很多指令是地址相关的，比如 B 指令，如果地址无关直接传 0 即可，比如 nop encoding, _ = ks.asm(code, addr) return encoding # 1 搜索匹配 matches = binSearch(0, "E0 07 BE A9 E2 7B 01 A9") for addr in matches: # 2 计算跳转地址 makeInsn(addr) # 给定的地址 addr 上创建一条指令 targetAddr = getJumpAddress(addr) print(hex(targetAddr)) # 3 patch code = f"B {hex(targetAddr)}" bCode = generate(code, addr) nopCode = generate("nop", 0) ida_bytes.patch_bytes(addr, bytes(bCode)) ida_bytes.patch_bytes(addr + 4, bytes(nopCode) * 9) print("patch compelete")patch完成之后记得选择edit下的 patch program应用到整个文件；完成后退出ida，此时要选择不保存数据库；随后重新打开so文件，加载后即可发现花指令被去除了；导航条已经没有红色的部分了，JNI_OnLoad函数也是正确的反编译了；至此去除花指令结束；5. 总结感谢龙哥、季冬、如画等大佬的文章，受益良多，ida脚本主要也来源于此；By：下雨天 2025.5.2逆向交流+vx：HeiYuKuaiDou23
JNI层检测1. Findclass的anti新建一个项目，写一个简单的findclass程序；extern "C" JNIEXPORT jstring JNICALL Java_com_sana_antifindclass_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { std::string hello = "Hello from C++"; jclass MainActivity = env->FindClass("com/sana/antifindclass/MainActivity2"); bool exception = env->ExceptionCheck(); if (exception) { hello.append("Exception!!"); env->ExceptionClear(); }else { hello.append("No Exception"); } return env->NewStringUTF(hello.c_str()); }build一下，在真机和unidbg分别跑起来这个方法；真机：Unidbg：可以很明显的发现，走了不同的分支；这里的原理是通过找一个不存在的类，以此来产生不同的差异；来看看产生这个差异的原因，去看unidbg如何实现fiadclass的，位置：DalvikVM64.javaPointer _FindClass = svcMemory.registerSvc(new Arm64Svc() { @Override public long handle(Emulator<?> emulator) { RegisterContext context = emulator.getContext(); Pointer env = context.getPointerArg(0); Pointer className = context.getPointerArg(1); String name = className.getString(0); boolean notFound = notFoundClassSet.contains(name); if (verbose) { if (notFound) { System.out.printf("JNIEnv->FindNoClass(%s) was called from %s%n", name, context.getLRPointer()); } else { System.out.printf("JNIEnv->FindClass(%s) was called from %s%n", name, context.getLRPointer()); } } if (notFound) { throwable = resolveClass("java/lang/NoClassDefFoundError").newObject(name); return 0; } DvmClass dvmClass = resolveClass(name); long hash = dvmClass.hashCode() & 0xffffffffL; if (log.isDebugEnabled()) { log.debug("FindClass env=" + env + ", className=" + name + ", hash=0x" + Long.toHexString(hash)); } return hash; } });有几个关键点，由于unidbg对于java层是没有感知的，所以我们看不到关于类存不存在的代码，我们发现，不管怎样，他都会执行到DvmClass dvmClass = resolveClass(name)，这就是产生差异的根本原因；当然也有其他转机，这里有一个notFoundClassSet，他是一个集合，目的就是规避掉这种检测，使用方式如下：private void stringFromJNI() { vm.addNotFoundClass("com/sana/antifindclass/MainActivity2"); DvmObject<?> obj = vm.resolveClass("com/sana/antifindclass/MainActivity").newObject(null); DvmObject<?> dvmObject = obj.callJniMethodObject(emulator, "stringFromJNI()Ljava/lang/String;"); Object value = dvmObject.getValue(); System.out.println(value); }再次运行，即可发现输出与真机一致了；Find native function Java_com_sana_antifindclass_MainActivity_stringFromJNI => RX@0x400244e0[libantifindclass.so]0x244e0 JNIEnv->FindNoClass(com/sana/antifindclass/MainActivity2) was called from RX@0x400246dc[libantifindclass.so]0x246dc JNIEnv->NewStringUTF("Hello from C++Exception!!") was called from RX@0x40024768[libantifindclass.so]0x24768 Hello from C++Exception!!还输出了 FindNoClass 这个信息，也算是规避掉了这个检测；2. Methodid的anti新建一个项目，创建两个java类，一个Person，一个ZhangSan；public class Person { public String getName() { return "Sana"; } public int getAge() { return 18; } } public class ZhangSan extends Person { @Override public String getName() { return "张三"; } @Override public int getAge() { return 23; } }随后在MainActivity里进行调用，如下：public class MainActivity extends AppCompatActivity { // Used to load the 'antimethodid' library on application startup. static { System.loadLibrary("antimethodid"); } private ActivityMainBinding binding; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); ZhangSan zhangSan = new ZhangSan(); TextView tv = binding.sampleText; tv.setText(stringFromJNI(zhangSan)); } public native String stringFromJNI(ZhangSan zhangSan); }stringFromJNI方法的定义如下：extern "C" JNIEXPORT jstring JNICALL Java_com_sana_antimethodid_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */, jobject person) { jclass Person = env->FindClass("com/sana/antimethodid/Person"); jmethodID getName = env->GetMethodID(Person, "getName", "()Ljava/lang/String;"); jobject name = env->CallObjectMethod(person, getName); char* name_str = (char*) env->GetStringUTFChars((jstring) name, nullptr); return env->NewStringUTF(name_str); }真机运行：Unidbg运行：这是典型的methodid问题，先看看源码怎么回事，分析分析出现这个问题的原因是什么？首先，我们的调用是这样的：取到Person类，拿到它的方法getName，传进来的是zhangsan类，所以我们最终调用的是张三类下的getName方法，实际上我们是继承的Person类，所以是能找到被重写的这个方法的；依旧回顾到那个话题，unidbg对java环境是没有感知的，他并不知道这组继承关系，说了这么多，看看它是怎么处理的；@Override public long handle(Emulator<?> emulator) { RegisterContext context = emulator.getContext(); UnidbgPointer object = context.getPointerArg(1); UnidbgPointer jmethodID = context.getPointerArg(2); UnidbgPointer va_list = context.getPointerArg(3); if (log.isDebugEnabled()) { log.debug("CallObjectMethodV object=" + object + ", jmethodID=" + jmethodID + ", va_list=" + va_list + ", lr=" + context.getLRPointer()); } DvmObject<?> dvmObject = getObject(object.toIntPeer()); DvmClass dvmClass = dvmObject == null ? null : dvmObject.getObjectType(); DvmMethod dvmMethod = dvmClass == null ? null : dvmClass.getMethod(jmethodID.toIntPeer()); if (dvmMethod == null) { throw new BackendException("dvmObject=" + dvmObject + ", dvmClass=" + dvmClass + ", jmethodID=" + jmethodID); } else { VaList vaList = new VaList64(emulator, DalvikVM64.this, va_list, dvmMethod); DvmObject<?> obj = dvmMethod.callObjectMethodV(dvmObject, vaList); if (verbose || verboseMethodOperation) { System.out.printf("JNIEnv->CallObjectMethodV(%s, %s(%s) => %s) was called from %s%n", dvmObject, dvmMethod.methodName, vaList.formatArgs(), obj, context.getLRPointer()); } return addLocalObject(obj); } }final DvmMethod getMethod(int hash) { DvmMethod method = methodMap.get(hash); if (method == null && superClass != null) { method = superClass.getMethod(hash); } if (method == null) { for (DvmClass interfaceClass : interfaceClasses) { method = interfaceClass.getMethod(hash); if (method != null) { break; } } } return method; }我们发现方法是怎么找的？他是在一个methodMap里取的，那么methodMap在哪里赋值的？int getMethodID(String methodName, String args) { String signature = getClassName() + "->" + methodName + args; int hash = signature.hashCode(); if (log.isDebugEnabled()) { log.debug("getMethodID signature=" + signature + ", hash=0x" + Long.toHexString(hash)); } if (vm.jni == null || vm.jni.acceptMethod(this, signature, false)) { if (!methodMap.containsKey(hash)) { methodMap.put(hash, new DvmMethod(this, methodName, args, false)); } return hash; } else { return 0; } }我们调试一下，看看是否是我们demo那样；确实是将getName这个方法放进去了，这里需要解释，unidbg中，每个类都有一个methodMap，执行时去对应的methodMap里取即可，这里就能够发现端倪了；我们的getName方法放在了Person这个类的methodMap里了，那zhangsan这个类去找肯定就是空了，也就找不到这个方法；可以发现方法是null，这里的解决之法也就在这个截图里，他下面有一句判断，superClass，也就是父类，同样可以得到getMethod，这里也符合我们出现问题的根本原因，unidbg不知道这个继承关系，所以我们需要做的就是告诉他这组继承关系；DvmObject<?> obj2 = vm.resolveClass("com/sana/antimethodid/ZhangSan", vm.resolveClass("com/sana/antimethodid/Person")).newObject(null);这就是一组继承关系的声明，再次运行发现没有methodid问题了；这里是anti点之一，引用龙哥的话，大部分新手看到这个错误是没有能力解决的，因此龙哥认为算是一种Anti-Unidbg 的手段，接下来看另一个anti点；将上面的环境补完，其实就是返回一个字符串，那么正常他的逻辑，应该返回Person类下的getName方法，按照我们常规的逻辑肯定是这样，所以我们补上；正常来说确实是这样，但是不要忘了，真机不是这样的，它的输出应该是张三才对，这里就是另一个需要注意的点，这里返回的结果可能是错的，而我们对于这个错误却可能毫不知情，解决办法就是打印出对应的dvmObject，看看究竟应该是哪个对象；@Override public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList) { switch (signature){ case "com/sana/antimethodid/Person->getName()Ljava/lang/String;":{ System.out.println(dvmObject.toString()); return new StringObject(vm, "Sana"); } } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }查看输出；com.sana.antimethodid.ZhangSan@7006c658 ··· Sana所以我们应该返回ZhangSan类下的值；return new StringObject(vm, "张三");这个算是第二个anti点；3. 未实现jni方法找一些未实现的方法，如：这种调用了就会直接抛异常，也是可以作为一个anti的点；还有一个字节对齐相关，暂时没有实现；