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

Unidbg实战:逆向分析SO库中的魔改MD5签名算法

1. 项目概述:当逆向工程遇上算法魔改

在移动安全与协议分析领域,SO(Shared Object,即Linux/Android的动态链接库)逆向分析是绕不开的核心技能。很多时候,我们面对的不是标准的、开源的算法,而是经过开发者精心“魔改”后的版本,它们被封装在SO库中,作为应用安全防护的最后一道防线。最近,我在分析一个移动端应用的签名协议时,就遇到了一个典型的场景:核心签名算法是一个魔改的MD5,它被编译进了SO库,并且这个SO库对运行环境(如Java上下文、系统属性)有强依赖。直接静态分析IDA看汇编固然可以,但效率低下;想动态调试又发现应用有反调试保护。这时候,一个强大的工具链组合就派上了用场:Unidbg + 主动补环境 + 算法还原

这个组合拳的威力在于,它允许我们在一个模拟的、完全可控的环境中,“驯服”那个原本桀骜不驯的SO库。Unidbg是一个基于Unicorn引擎的Android模拟执行框架,它不依赖于真机或模拟器,可以直接在PC上调用SO的导出函数。而“补环境”则是关键中的关键,因为很多SO在运行时会通过JNI调用Java层方法获取设备信息、应用上下文等,如果这些调用得不到正确响应,SO就会崩溃或返回错误结果。最后,通过Hook等技术手段,我们可以窥探到魔改算法的内部运算过程,从而将其还原为标准算法或理解其修改逻辑。本次实战,我们就来完整走通这个流程,目标是一个经过魔改的MD5签名函数。

2. 核心思路与工具选型解析

2.1 为什么选择Unidbg而非真机动态调试?

面对一个需要复杂环境交互的SO,很多人的第一反应可能是上真机或模拟器,用Frida、IDA进行动态调试。这当然是一种方法,但存在几个显著痛点:

  1. 环境依赖复杂:应用可能依赖特定的系统版本、厂商ROM特性,甚至需要登录特定账号才能触发目标函数,搭建调试环境成本高。
  2. 反调试对抗:现代应用普遍具备反调试能力,如检测Tracepid、调试端口,增加调试难度和时间成本。
  3. 执行效率与自动化:动态调试过程交互性强,但不利于快速、批量地测试输入输出,进行算法分析。

Unidbg的优势恰恰解决了这些问题:

  • 沙盒化与可控性:它提供了一个纯粹的、与宿主系统隔离的沙盒环境。你可以精确控制给SO“看到”的Java虚拟机、系统属性、文件系统,这对于“补环境”来说是天生的优势。
  • 绕过反调试:由于不是真正的进程调试,许多基于ptrace或/proc/self/status的反调试手段自然失效。
  • 可编程与自动化:所有操作都可以用Java/Python代码编写,便于构造测试用例、批量执行、记录中间结果,非常适合算法还原这种需要反复尝试和分析的工作。

2.2 “补环境”的本质与常见类型

“补环境”这个词听起来有点玄乎,其实本质就是实现SO库通过JNI接口预期调用的那些Java/Native函数。SO在执行时,并不是孤立的,它经常会:

  • 调用JNI函数(如FindClass,GetMethodID,CallObjectMethod来访问Java对象,获取如Build.SERIALTelephonyManager.getDeviceId()Context.getPackageName()等信息。
  • 调用系统Libc函数(如strlen,memcmp,time或Android专属函数(如__system_property_get)。
  • 访问特定文件或内存映射

如果这些调用在Unidbg的虚拟环境中得不到实现,就会抛出异常,导致执行中断。补环境就是为这些调用提供合理的、符合预期的返回值。常见的补环境类型包括:

  • Java层补环境:实现JNIEnvJavaVM相关的函数,返回模拟的Java类、对象和方法。例如,当SO调用android/os/Build->SERIAL的getter方法时,我们需要返回一个模拟的字符串,如“模拟器序列号”。
  • Native层补环境:Hook或实现SO导入的Libc等系统库函数。例如,实现一个gettimeofday函数返回可控的时间戳。
  • 内存与文件访问补环境:处理SO对/proc/self/maps/dev/__properties__等特殊文件的访问。

2.3 魔改算法还原的一般路径

我们的最终目标是理解或还原那个魔改的MD5。通过Unidbg执行并配合补环境让SO跑起来后,我们可以通过以下路径进行还原:

  1. 黑盒测试:首先确保函数能正常执行,输入不同的明文,收集其输出的密文(签名)。通过大量输入输出对,初步判断算法是否具备MD5的特征(如128位输出)。
  2. 关键点Hook:在Unidbg中,我们可以方便地Hook函数。重点Hook标准MD5算法中的关键函数或魔改可能发生的点,如:
    • MD5_Init,MD5_Update,MD5_Final等标准函数。
    • 内存操作函数memcpy,memset,观察数据流转。
    • 常量表访问,魔改MD5常从修改标准的64个常量K值或循环左移位数开始。
  3. 数据流追踪:记录下每次Hook时函数的参数、返回值以及相关内存区域的内容。对比标准MD5算法的中间状态,差异点就是魔改发生的地方。
  4. 静态分析辅助:将动态执行得到的信息(如函数偏移、常量值)与IDA静态分析的结果对照,可以更快地定位到魔改代码的具体位置。

3. 实战准备:搭建Unidbg测试框架

3.1 环境搭建与项目初始化

我们使用Java作为开发语言。首先,你需要一个Java开发环境(JDK 8或11)和Maven。创建一个标准的Maven项目,在pom.xml中添加Unidbg的依赖。建议使用我撰写时较为稳定的版本。

<dependencies> <dependency> <groupId>com.github.zhkl0228</groupId> <artifactId>unidbg</artifactId> <version>0.9.4</version> <!-- 请检查GitHub获取最新版本 --> </dependency> </dependencies>

创建一个主类,例如ModMD5Emulator。核心是初始化一个Android模拟器实例,并加载目标SO文件。

import com.github.unidbg.AndroidEmulator; import com.github.unidbg.Module; import com.github.unidbg.linux.android.AndroidEmulatorBuilder; import com.github.unidbg.linux.android.AndroidResolver; import com.github.unidbg.memory.Memory; import java.io.File; public class ModMD5Emulator { private final AndroidEmulator emulator; private final Memory memory; private final Module module; public ModMD5Emulator() { // 1. 创建模拟器实例,通常选择ARM32或ARM64,根据SO架构决定 emulator = AndroidEmulatorBuilder.for32Bit().build(); // 2. 获取内存接口 memory = emulator.getMemory(); // 3. 设置库解析器,用于自动加载SO依赖的系统库(如libc, libdl) memory.setLibraryResolver(new AndroidResolver(23)); // API Level 23 // 4. 加载目标SO库 module = emulator.loadLibrary(new File("target.so")); } public static void main(String[] args) { ModMD5Emulator emu = new ModMD5Emulator(); // 后续补环境和函数调用将在这里进行 } }

3.2 定位目标函数与初步调用

加载SO后,我们需要找到要调用的魔改MD5函数。通常有两种方式:

  • 导出函数:如果函数是JNI导出(即Java_开头),可以通过module.findSymbol查找。
  • 偏移地址:通过IDA静态分析,找到函数的偏移地址(File Offset + Base Address)。

假设我们通过分析,知道目标函数签名是jstring Java_com_example_app_SignUtil_signMD5(JNIEnv* env, jobject thiz, jstring input)。在Unidbg中,我们需要封装参数进行调用。这里以直接调用偏移地址为例:

public void callSignMD5(String input) { // 假设我们通过IDA分析,得到函数偏移是0x1234,SO加载基址是module.base long funcAddr = module.base + 0x1234; // 准备参数。在JNI中,第一个参数是JNIEnv*,第二个是jobject this,第三个是jstring input。 // Unidbg提供了便捷的封装。这里我们创建一个模拟的JNI环境。 // 注意:在真正调用前,我们必须先“补”好JNI环境相关的函数,否则调用会失败。 emulator.getBackend().showRegs(); // 调试用,打印寄存器 // 使用Unidbg的NativeCall封装调用更为安全,这里演示直接通过emulator.eFunc调用 // 但由于涉及JNI字符串转换,实际操作更复杂。更常见的做法是先补好JNI环境, // 然后直接调用SO的JNI导出函数,让Unidbg去处理JNI转换。 System.out.println("函数地址: 0x" + Long.toHexString(funcAddr)); }

在能够成功调用之前,我们面临的最大障碍就是SO对JNI环境的依赖。因此,下一步的核心就是系统性地补环境

4. 系统性补环境实战:从崩溃到稳定执行

4.1 识别缺失的环境:分析崩溃日志

直接运行上面的代码,十有八九会崩溃。Unidbg会抛出异常,并打印出调用栈和最后失败的指令。这是最宝贵的信息。例如,你可能会看到:

Exception in thread "main" com.github.unidbg.arm.backend.BackendException: ... at ... (Unidbg) Memory map: ... >>> 00000000: 00000000 00000000 00000000 00000000 ................ >>> 00000010: 00000000 00000000 00000000 00000000 ................ >>> 00000020: 00000000 00000000 00000000 00000000 ................ >>> 00000030: 00000000 00000000 00000000 00000000 ................ >>> 00000040: 00000000 00000000 00000000 00000000 ................ >>> 00000050: 00000000 00000000 00000000 00000000 ................ >>> 00000060: 00000000 00000000 00000000 00000000 ................ >>> 00000070: 00000000 00000000 00000000 00000000 ................ >>> 00000080: 00000000 00000000 00000000 00000000 ................ >>> 00000090: 00000000 00000000 00000000 00000000 ................ >>> 000000a0: 00000000 00000000 00000000 00000000 ................ >>> 000000b0: 00000000 00000000 00000000 00000000 ................ >>> 000000c0: 00000000 00000000 00000000 00000000 ................ >>> 000000d0: 00000000 00000000 00000000 00000000 ................ >>> 000000e0: 00000000 00000000 00000000 00000000 ................ >>> 000000f0: 00000000 00000000 00000000 00000000 ................ >>> 00000100: 00000000 00000000 00000000 00000000 ................ >>> 00000110: 00000000 00000000 00000000 00000000 ................ >>> 00000120: 00000000 00000000 00000000 00000000 ................ >>> 00000130: 00000000 00000000 00000000 00000000 ................ >>> 00000140: 00000000 00000000 00000000 00000000 ................ >>> 00000150: 00000000 00000000 00000000 00000000 ................ >>> 00000160: 00000000 00000000 00000000 00000000 ................ >>> 00000170: 00000000 00000000 00000000 00000000 ................ >>> 00000180: 00000000 00000000 00000000 00000000 ................ >>> 00000190: 00000000 00000000 00000000 00000000 ................ >>> 000001a0: 00000000 00000000 00000000 00000000 ................ >>> 000001b0: 00000000 00000000 00000000 00000000 ................ >>> 000001c0: 00000000 00000000 00000000 00000000 ................ >>> 000001d0: 00000000 00000000 00000000 00000000 ................ >>> 000001e0: 00000000 00000000 00000000 00000000 ................ >>> 000001f0: 00000000 00000000 00000000 00000000 ................ CPU: ARM R0=00000000 R1=00000000 R2=00000000 R3=00000000 R4=00000000 R5=00000000 R6=00000000 R7=00000000 R8=00000000 R9=00000000 R10=00000000 R11=00000000 R12=00000000 SP=00000000 LR=00000000 PC=00000000

PC(程序计数器)为0,这通常意味着代码尝试跳转到一个空指针函数。结合调用栈,往往能发现是某个JNI函数(如FindClass)没有实现。我们需要开启Unidbg的详细日志。

// 在初始化模拟器后添加 emulator.getBackend().enableVerbose(); // 打印每条执行的指令(慎用,输出极多) // 更推荐使用日志级别控制 Logger.getLogger("com.github.unidbg").setLevel(Level.INFO); // 或 DEBUG

运行后,观察日志中JNI相关的FindClassGetMethodIDCallVoidMethod等调用。缺失的调用就是需要补的第一个点。

4.2 实现基础的JNI环境补全

Unidbg提供了IOModuleVM来模拟Android环境,但针对具体的SO,我们需要更精细的控制。通常,我们会创建一个类来实现JniMethod接口,并注册到模拟器中。

一个最基础的补环境示例是处理android/os/Build类的信息获取:

public class BasicJni implements JniMethod { private final AndroidEmulator emulator; public BasicJni(AndroidEmulator emulator) { this.emulator = emulator; } @Override public void call(Emulator<?> emulator, long javaClass, long methodId, Object... args) { // args[0] 通常是 JNIEnv* 指针,args[1] 是 jclass 或 jobject // 通过 methodId 或方法签名来判断具体调用哪个方法 // 这里需要将 methodId 与通过 Unidbg API 获取的 methodId 进行比较 // 由于比较繁琐,实践中常用一种“惰性”补法:先让SO调用失败,根据日志中的方法签名来针对性实现。 } // 更实用的方法:直接使用Unidbg的补环境模块 public void patchBuildClass() { DalvikModule dm = emulator.getDalvikModule(); // 获取Dalvik模块 // 找到android/os/Build类 DvmClass buildClass = dm.resolveClass("android/os/Build"); // 为它的字段设置返回值 buildClass.setStaticObjectField("SERIAL", new StringObject(emulator.getDalvikVM(), "自定义序列号")); buildClass.setStaticObjectField("MODEL", new StringObject(emulator.getDalvikVM(), "Unidbg Device")); buildClass.setStaticObjectField("BRAND", new StringObject(emulator.getDalvikVM(), "Unidbg")); buildClass.setStaticObjectField("MANUFACTURER", new StringObject(emulator.getDalvikVM(), "Unidbg Inc.")); // ... 补全其他常用字段,如 BOARD, DEVICE, HARDWARE, PRODUCT, TAGS, TYPE, USER } }

在初始化模拟器并加载SO后,调用patchBuildClass()方法。这相当于告诉SO,当它查询Build.SERIAL时,返回我们设定的值,而不是崩溃。

4.3 处理系统属性与文件访问

除了Java类,SO经常通过__system_property_get函数读取系统属性,或访问/proc/cpuinfo/system/build.prop等文件。我们需要拦截这些调用。

对于系统属性,可以实现一个IRegisterNative来处理:

emulator.getMemory().addHookListener(new HookListener() { @Override public void hook(Backend backend, long address, int size, Object user) { if (address == module.findSymbolByName("__system_property_get")) { // 获取函数参数 Pointer namePtr = emulator.getPointer(emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R0)); Pointer valuePtr = emulator.getPointer(emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_R1)); String propName = namePtr.getString(0); String propValue = getSystemProperty(propName); // 自定义函数,返回模拟的属性值 if (propValue != null) { valuePtr.write(0, propValue.getBytes(), 0, propValue.length()); valuePtr.setByte(propValue.length(), (byte) 0); // 字符串结尾 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_R0, propValue.length()); // 返回值 emulator.getBackend().reg_write(ArmConst.UC_ARM_REG_PC, emulator.getBackend().reg_read(ArmConst.UC_ARM_REG_LR)); // 跳过原函数执行,直接返回 } // 如果不知道的属性,可以选择不处理,让原函数执行(如果存在的话) } } }); private String getSystemProperty(String name) { Map<String, String> props = new HashMap<>(); props.put("ro.product.model", "Unidbg Phone"); props.put("ro.build.version.sdk", "23"); props.put("ro.serialno", "unidbg123456"); // ... 添加更多常见属性 return props.get(name); }

对于文件访问,可以通过实现FileSystem接口或Hook文件打开/读函数(如open,read)来返回模拟的内容。

4.4 补环境的心得与技巧

  1. 由简入繁,逐步逼近:不要试图一次性补全所有环境。先让SO加载不崩溃,然后让目标函数被调用起来(哪怕参数不对),再根据新的崩溃日志补充下一个缺失项。这是一个迭代过程。
  2. 善用日志与Hook:Unidbg的日志和Hook功能是补环境的眼睛。在关键函数(如FindClass,CallStaticObjectMethod)处下Hook,打印出参数和返回值,能清晰看到SO的执行路径和期望值。
  3. 返回值模拟要合理:模拟的返回值(如设备ID、时间戳)要符合格式和逻辑。例如,时间戳要递增,设备ID要像一个真实的IMEI或序列号。有时SO会校验返回值的格式或长度。
  4. 注意线程与上下文:有些JNI调用可能发生在特定的线程或需要特定的Java上下文(Context)。在Unidbg中,你需要确保模拟的JNI环境 (JNIEnv*) 和 jobject 是有效的。使用emulator.getDalvikVM()创建和管理这些对象。
  5. 利用现有轮子:GitHub上有一些开源的Unidbg补环境项目或代码片段,针对特定厂商(如某里、某讯)的SO有现成的补环境方案。参考这些代码可以节省大量时间,但需要理解其原理以适应自己的目标SO。

5. 魔改MD5算法的动态分析与还原

5.1 确保函数可调用并验证基础功能

在补环境到一定程度,SO不再崩溃,并且我们能成功调用到目标签名函数后,第一步是验证它的基本功能。我们构造几个简单的输入,看看输出是否稳定、是否符合MD5的128位(32字符十六进制)特征。

public String callSignFunc(String input) { // 假设我们已经通过补环境,可以通过JNI方式正常调用函数 // 这里演示一个简化的调用流程。实际中可能需要通过DvmObject封装 DalvikModule dm = emulator.getDalvikModule(); // 找到目标类和方法 DvmClass signClass = dm.resolveClass("com/example/app/SignUtil"); // 获取方法ID(需要知道方法签名,例如 (Ljava/lang/String;)Ljava/lang/String;) Number methodId = signClass.getStaticMethodId("signMD5", "(Ljava/lang/String;)Ljava/lang/String;"); // 调用静态方法 StringObject result = signClass.callStaticJniMethodObject(emulator, methodId, new StringObject(emulator.getDalvikVM(), input)); return result.getValue(); } // 测试 System.out.println(callSignFunc("hello")); System.out.println(callSignFunc("world")); System.out.println(callSignFunc("")); System.out.println(callSignFunc("a".repeat(100)));

观察输出。如果都是32位十六进制字符串,且对相同输入输出恒定,那么函数基本可用。接下来,我们可以与标准MD5对比。用Python或在线工具计算相同输入的标准MD5,如果结果不同,则确认是魔改MD5。

5.2 Hook关键函数,洞察算法内部

标准MD5算法的实现,无论是C版本还是Java版本,其核心流程是固定的:初始化缓冲区(A,B,C,D)-> 处理数据块(64字节一组)-> 应用四轮循环(每轮16次操作,使用不同的非线性函数F,G,H,I)-> 最终拼接输出。

魔改通常发生在:

  1. 初始化向量(IV):修改了初始的A、B、C、D四个常量(标准是 0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476)。
  2. 常量K表:修改了64个常量K[i]。
  3. 循环左移位数s:修改了每轮操作中循环左移的位数。
  4. 填充规则:修改了数据末尾的填充方式(标准是bit 1 + 若干个0 + 数据长度的64位表示)。
  5. 附加操作:在标准流程前后添加了额外的变换,如对输入先做一次Base64,或对输出再做一次HMAC。

我们的策略是Hook标准MD5函数(如果SO使用了系统或自定义的MD5函数),或者直接Hook内存操作和运算指令密集的代码区域。

使用Unidbg的Hook功能:

import com.github.unidbg.hook.HookContext; import com.github.unidbg.hook.ReplaceCallback; import com.github.unidbg.hook.InterceptCallback; import com.github.unidbg.hook.IHook; // 假设通过静态分析,我们怀疑SO内部调用了一个名为 `native_md5_transform` 的函数,地址在 0x5678 long targetFuncAddr = module.base + 0x5678; emulator.getBackend().hook_add_new(new CodeHook() { @Override public void hook(Backend backend, long address, int size, Object user) { // 每次执行到该地址时触发 if (address == targetFuncAddr) { System.out.println("[Hook] 进入疑似MD5变换函数"); // 打印寄存器状态,查看输入数据指针和状态缓冲区指针 long dataPtr = backend.reg_read(ArmConst.UC_ARM_REG_R0); // 假设第一个参数是数据块 long statePtr = backend.reg_read(ArmConst.UC_ARM_REG_R1); // 假设第二个参数是状态数组(A,B,C,D) System.out.printf("数据块地址: 0x%x, 状态地址: 0x%x\n", dataPtr, statePtr); // 读取状态值 Memory memory = emulator.getMemory(); int A = memory.pointer(statePtr).getInt(0); int B = memory.pointer(statePtr).getInt(4); int C = memory.pointer(statePtr).getInt(8); int D = memory.pointer(statePtr).getInt(12); System.out.printf("当前状态: A=0x%08x, B=0x%08x, C=0x%08x, D=0x%08x\n", A, B, C, D); // 读取一部分输入数据 byte[] inputBlock = memory.pointer(dataPtr).getByteArray(0, 64); System.out.print("输入数据块 (前16字节): "); for(int i=0; i<16; i++) { System.out.printf("%02x ", inputBlock[i]); } System.out.println(); } } }, targetFuncAddr, targetFuncAddr, null); // 范围Hook,从开始到结束

运行测试代码,调用签名函数。Hook代码会在每次执行到目标函数时打印出中间状态。对比标准MD5在相同输入和初始状态下的第一次变换输出,就能立即看出差异。

5.3 对比分析与算法还原

收集了足够的中间状态数据后,就可以开始对比分析了。你需要一个标准MD5的实现作为参考(可以用Python的hashlib.md5并修改源码打印中间状态,或者找一个带调试信息的C实现)。

  1. 对比初始化向量:查看Hook打印的第一次进入变换函数时的A、B、C、D值,与标准值对比。
  2. 对比常量K:在变换函数内部,会使用K[i]。你可以Hook内存读取指令,或者静态分析SO中K表存储的位置(通常是一个静态数组),在Unidbg中dump出该内存区域的值。
    // 假设通过IDA发现K表位于 .rodata 段,偏移 0x3000 long kTableAddr = module.base + 0x3000; byte[] kTable = emulator.getMemory().pointer(kTableAddr).getByteArray(0, 64*4); // 64个int // 打印并与标准K表对比
  3. 对比循环左移s:观察变换函数中的移位指令,或者通过动态跟踪寄存器的移位操作来分析。
  4. 对比填充:Hook输入字符串被处理成数据块的过程。标准MD5是先对输入做长度填充,然后分块处理。魔改可能修改了填充字符或长度编码的格式。

通过以上对比,魔改点就基本无处遁形了。还原工作就是记录下所有不同的参数(IV, K, s),然后用这些参数实现一个自定义的MD5算法。你可以选择:

  • Patch SO:直接修改SO文件中的常量值,将其改回标准值,这样就能使用标准库验证。
  • 独立实现:用Python/Java/C++按照魔改逻辑重新实现算法,用于后续的协议模拟。

5.4 实操心得:效率与准确性权衡

  • 静态动态结合:不要只依赖动态Hook。先用IDA等工具静态分析SO,找到疑似MD5初始化、更新、结束的函数,以及常量表、移位表的位置。这能让你在Hook时有的放矢,事半功倍。
  • 聚焦差异点:魔改算法通常只改几个关键点。重点关注IV、K表和第一轮运算。如果这些和标准一样,再去看填充和附加操作。
  • 编写自动化对比脚本:手动对比数据效率低且易错。可以写一个脚本,将Unidbg Hook到的中间状态(A,B,C,D)与标准算法在相同输入下的中间状态进行逐轮对比,自动标出差异。
  • 注意字节序:ARM架构可能是小端序(Little-Endian)。从内存中dump出的int或long,可能需要做字节序转换后再与标准值(通常是大端序表示)对比。
  • 耐心与迭代:算法还原是个精细活,可能需要多次调整Hook点、补充环境、重新测试。保持耐心,每次迭代解决一个问题。

6. 常见问题排查与解决方案实录

在Unidbg补环境和算法还原过程中,你会遇到各种各样的坑。下面记录一些典型问题及解决思路。

问题现象可能原因排查方法与解决方案
加载SO时崩溃SO依赖的其他库未找到;SO初始化代码中有严重环境检查。1. 使用memory.setLibraryResolver确保所有依赖库都被解析(即使返回空实现)。
2. 开启enableVerbose日志,看崩溃在哪个系统调用或函数。
3. 尝试先不调用任何函数,只加载SO,看是否在JNI_OnLoad或初始化段(.init_array)就崩溃,针对性补环境。
调用JNI函数时崩溃(PC=0)所需的Java类、方法或字段未找到(FindClass/GetMethodID返回NULL)。1. 在JNIEnv->FindClass被调用时下Hook,打印类名。
2. 在BasicJni或类似补环境类中,为这些类创建模拟的DvmClass并注册。
3. 注意类名格式(如android/content/Context而不是android.content.Context)。
函数调用后返回乱码或固定值补环境不完整,导致函数执行路径提前返回或使用了错误数据;函数本身有反模拟检测。1. 检查所有GetFieldID/CallMethod的返回值是否合理。
2. Hook更多系统调用(如time,rand),看是否因获取不到随机数或时间而走默认分支。
3. 检查SO中是否有基于cpuidgettimeofday差值等反模拟代码,并Hook返回合理值。
Hook点从未被触发Hook的地址不对;函数被内联或代码动态生成。1. 确认函数地址:静态分析地址 + SO加载基址。用IDA确认函数范围。
2. 尝试Hook函数入口附近的指令地址。
3. 如果函数是Thumb指令集(ARM),地址可能需要+1。在Unidbg中,可以通过module.findSymbol返回的地址判断(通常Thumb模式地址最低位为1)。
算法还原时,中间状态与标准MD5对不上Hook点不是核心变换函数;魔改发生在更早的预处理或更晚的后处理阶段。1. 回溯:在调用签名函数前,Hook所有memcpy,strlen等,看输入数据是否被修改(如增加前缀、后缀)。
2. 顺推:在签名函数返回后,Hook输出缓冲区,看结果是否被二次处理(如异或、Base64)。
3. 扩大Hook范围:不要只Hook一个函数,用范围Hook覆盖整个疑似算法区域,观察数据流。
Unidbg执行速度极慢指令级Hook过多;模拟的代码量巨大。1. 减少不必要的enableVerbose和过于频繁的Hook回调。
2. 将Hook从指令级改为函数级(hook_add_new带范围)。
3. 对于已知的、稳定的补环境点,用addHook或直接修改内存(Patch)代替运行时Hook。
4. 考虑只Hook关键函数,而不是每条指令。
内存访问错误(如读取0x0地址)指针未初始化或为空,可能是因为某个JNI调用返回了NULL,但后续代码未检查。1. 检查导致该指针产生的上游JNI调用,确保其返回有效的模拟对象或地址。
2. 在内存访问指令处下Hook,在崩溃前拦截,并手动设置一个有效的内存区域和值。

提示:补环境是一个“猫鼠游戏”。有时SO会故意调用一些不常见或废弃的API,或者检查返回值是否“太假”(如所有设备信息都是“unknown”)。你需要让模拟环境看起来更真实,例如,让Build.SERIAL返回一个符合特定品牌设备格式的随机字符串,让System.currentTimeMillis()返回一个合理递增的时间戳。

最后,当你的Unidbg脚本能够稳定地输出与目标应用一致的签名,并且你完全理解了魔改MD5的每一个步骤时,这次逆向实战才算圆满成功。整个过程不仅锻炼了逆向分析能力,更深化了对密码学算法实现、系统底层交互的理解。将这些经验沉淀下来,未来再遇到更复杂的VMP(虚拟机保护)或OLLVM混淆的SO时,你也能有一套清晰的应对思路。

http://www.jsqmd.com/news/1279547/

相关文章:

  • 物料需求波动大、生产计划变化频繁?一文教你用工厂ERP系统MRP精确计算生产物料!
  • Unity YAML解析器:自动化批量修改Prefab与场景文件的利器
  • 树莓派上使用arduino-cli开发ESP32-C3:从环境配置到项目实战
  • 2026 西安 PVC 塑胶地板、橡胶地板行业盘点:工装采购痛点与一站式解决方案 - 国麟测评
  • JizhiCMS 1.6.7前台SQL注入漏洞深度剖析与实战利用
  • flutter_tts多平台适配攻略:Android、iOS、Web与桌面端实现详解
  • ZigbeeTLc按钮功能详解:4种操作模式让设备控制更简单
  • 大麦网自动化抢票架构解析:从API逆向到请求模拟的技术实现方案
  • 架构师必备:分布式锁方案选型
  • 3大Flipper Zero固件编译方案对比:从入门到精通的全流程指南
  • 二分查找算法原理、实现与优化全解析
  • AI代码审查安全吗?从Claude Code看人机协同安全防线构建
  • 并查集原理、优化与应用实战指南
  • 微服务多环境架构设计实战:5套环境2个参数搞定部署(从混乱到体系化)
  • 校招投递多少家公司合适?别用海投数量掩盖低匹配度
  • LDAP与NoSQL注入攻击:超越SQL的Web安全新威胁
  • 释放游戏潜能:Wand-Enhancer 的本地化增强方案
  • 基于行空板与GStreamer的低延迟图传系统实现与优化
  • 2026本溪持证防水补漏商家权威TOP3榜单 卫生间厨房外墙屋面天花板漏水检测靠谱师傅维修指南 - 宅安选房屋修缮
  • NVIDIA Profile Inspector终极指南:3步快速优化显卡性能,提升游戏体验50%
  • Caddy WAF测试方法论:从单元测试到ELK日志分析的完整流程
  • 千笔与万方智搜AI:学术写作工具深度对比与应用技巧
  • AI入门五大核心技能:Python数据处理与机器学习实战
  • 苹果触控板Windows驱动终极指南:5分钟实现原生级触控体验
  • 零成本搭建私有知识库:Dify整合DeepSeek实现本地RAG方案
  • IMU与GPS数据融合的卡尔曼滤波实现与优化
  • 解决tldr-python-client常见问题:网络错误、缓存失效与平台兼容
  • 大统一逻辑链7.0(GULP7.0):演化的涌现(草稿)
  • 2026巴中持证防水补漏商家权威TOP3榜单 卫生间厨房外墙屋面天花板漏水检测靠谱师傅维修指南 - 宅安选房屋修缮
  • 智慧树学习效率革命:三招告别手动刷课的智能解决方案