SO逆向实战:Unidbg补环境与魔改MD5算法还原

SO逆向实战:Unidbg补环境与魔改MD5算法还原 1. Unidbg模拟执行环境搭建逆向分析Android应用的SO文件时Unidbg是最常用的工具之一。它能在PC端模拟Android Native代码的执行环境不需要真机或模拟器就能运行SO文件。我最近在分析一个社交应用的签名算法时就遇到了需要补JAVA环境的情况。首先需要准备基础环境。建议使用IntelliJ IDEA创建Maven项目在pom.xml中添加Unidbg依赖dependency groupIdcom.github.zhkl0228/groupId artifactIdunidbg-android/artifactId version0.9.4/version /dependency搭建模拟器的代码骨架如下public class CryptoAnalysis { private final AndroidEmulator emulator; private final VM vm; private final Module module; CryptoAnalysis() { // 创建32位模拟器 emulator AndroidEmulatorBuilder.for32Bit() .setProcessName(com.example.app) .build(); // 设置系统库解析器 Memory memory emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); // 创建Android虚拟机 vm emulator.createDalvikVM(); // 加载目标SO文件 DalvikModule dm vm.loadLibrary(new File(libnet_crypto.so), true); module dm.getModule(); // 设置JNI处理类 vm.setJni(this); vm.setVerbose(true); dm.callJNI_OnLoad(emulator); } }这里有几个关键点需要注意32位/64位选择要与SO架构匹配AndroidResolver的API级别要适配目标APPloadLibrary的第二个参数控制是否执行init函数2. 补JAVA环境实战技巧当SO文件调用了JAVA层方法时Unidbg会抛出AbstractJni.java相关的异常。这时就需要我们补全对应的JAVA环境。我总结了一套高效的补环境方法2.1 动态追踪调用链首先让程序跑起来观察报错信息。比如常见的Context获取UnsupportedOperationException: com/izuiyou/common/base/BaseApplication-getAppContext()Landroid/content/Context;这时需要在继承的AbstractJni类中重写对应方法Override public DvmObject? callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList) { switch (signature) { case com/izuiyou/common/base/BaseApplication-getAppContext()Landroid/content/Context;: return vm.resolveClass(android/content/Context).newObject(null); } return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList); }2.2 关键数据回填策略补环境时可以采用最小满足原则 - 先返回空对象等程序抛出具体需求时再完善。比如处理类名获取Override public DvmObject? callObjectMethodV(BaseVM vm, DvmObject? dvmObject, String signature, VaList vaList) { switch (signature) { case java/lang/Class-getSimpleName()Ljava/lang/String;: return new StringObject(vm, AppController); } return super.callObjectMethodV(vm, dvmObject, signature, vaList); }实际项目中这些值需要通过动态分析获取。可以使用Frida脚本Java.perform(() { let Application Java.use(com.izuiyou.common.base.BaseApplication); let context Application.getAppContext(); console.log(context.getClass().getSimpleName()); });3. 魔改MD5算法分析当遇到32位固定长度的哈希值时首先要怀疑MD5算法。但标准MD5的结果通常与样本不一致这时需要考虑算法魔改。3.1 识别算法特征使用FindHash等工具扫描SO文件可以快速定位哈希函数。关键特征包括初始化向量IV4个32位整数64元素的常量数组SV循环左移操作四轮主循环结构在IDA中查看疑似函数如果发现类似下面的代码基本可以确定是MD5变种v3 _byteswap_ulong(0x67552301); v4 _byteswap_ulong(0xEDCDAB89); v5 _byteswap_ulong(0x98BADEFE); v6 _byteswap_ulong(0x16325476);3.2 动态Hook验证使用Unidbg的Hook功能验证算法public void hookMD5() { IHookZz hookZz HookZz.getInstance(emulator); hookZz.wrap(module.base 0x65540 1, new WrapCallbackHookZzArm32RegisterContext() { Override public void preCall(Emulator? emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { // 打印输入参数 Pointer input ctx.getR0Pointer(); int length ctx.getR1Int(); Pointer output ctx.getR2Pointer(); Inspector.inspect(input.getByteArray(0, length), MD5 Input); } Override public void postCall(Emulator? emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) { // 打印输出结果 Pointer output ctx.getR2Pointer(); Inspector.inspect(output.getByteArray(0, 16), MD5 Output); } }); }3.3 算法还原实现根据Hook结果还原Python实现def custom_md5(data): # 魔改IV A 0x67552301 B 0xEDCDAB89 C 0x98BADEFE D 0x16325476 # 标准MD5常量 T [0xd76aa478, 0xe8c7b756, ..., 0xeb86d391] # 标准MD5流程 def F(X,Y,Z): return (X Y) | ((~X) Z) def G(X,Y,Z): return (X Z) | (Y (~Z)) def H(X,Y,Z): return X ^ Y ^ Z def I(X,Y,Z): return Y ^ (X | (~Z)) # 填充和分块 orig_len len(data) data b\x80 data b\x00 * ((56 - (orig_len 1) % 64) % 64) data (orig_len * 8).to_bytes(8, little) # 处理每个512bit块 for i in range(0, len(data), 64): a, b, c, d A, B, C, D block data[i:i64] # 四轮主循环 # 第一轮FF变换 # 第二轮GG变换 # 第三轮HH变换 # 第四轮II变换 A (A a) 0xFFFFFFFF B (B b) 0xFFFFFFFF C (C c) 0xFFFFFFFF D (D d) 0xFFFFFFFF return struct.pack(4I, A, B, C, D).hex()4. 完整调用链分析在实际项目中签名算法往往不是孤立存在的。完整的分析流程包括4.1 参数预处理通常输入参数会经过以下处理URL编码转换参数排序拼接添加时间戳/随机数二次哈希处理可以使用Unidbg的trace功能记录完整调用过程// 启用指令级trace emulator.getBackend().hook_add_new( new CodeHook() { Override public void hook(Backend backend, long address, int size, Object user) { System.out.printf(Executing at 0x%x\n, address); } }, module.base, module.base module.size, null);4.2 交叉验证为确保还原正确需要多组测试用例验证输入标准MD5样本输出还原结果123202cb962ac59075b...4a6f8d7c2...4a6f8d7c2...abc900150983cd24fb0...e12f5c8d3...e12f5c8d3...4.3 性能优化当算法非常复杂时可以考虑只模拟关键函数缓存中间结果使用JNI加速// 直接调用Native函数 public native byte[] encrypt(byte[] input); // 在JNI中实现算法 extern C JNIEXPORT jbyteArray JNICALL Java_com_analysis_CryptoModule_encrypt(JNIEnv *env, jobject, jbyteArray input) { // 调用还原的算法 return result; }在实际分析过程中我遇到过各种奇葩的魔改方式有的修改了填充规则有的调整了循环次数还有的甚至把MD5和SHA1混合使用。关键是要耐心跟踪每一步的数据变化同时善用动态调试工具验证猜想。