实战解析:基于unidbg的APP逆向与关键算法模拟执行

张开发
2026/4/11 0:32:13 15 分钟阅读

分享文章

实战解析:基于unidbg的APP逆向与关键算法模拟执行
1. 为什么需要unidbg进行APP逆向分析当你尝试分析一个移动应用的核心算法时最头疼的问题是什么我猜90%的开发者都会说无法直接运行和调试so文件中的native代码。传统的逆向方法要么需要真机环境要么要处理复杂的交叉编译问题而unidbg的出现完美解决了这个痛点。我在分析某社交平台的X-Argus签名算法时发现它的核心逻辑被封装在libutility.so中。这个so文件包含了超过20个加密函数如果按照传统方式我需要搭建完整的Android逆向环境处理各种反调试机制编写繁琐的JNI调用代码而使用unidbg后整个过程变得异常简单。它就像一个Java版的QEMU可以直接在PC上模拟执行ARM指令集。实测下来从零开始搭建环境到成功调用目标函数整个过程不超过2小时。2. 实战环境搭建与工具准备2.1 基础环境配置先来看我的开发环境配置清单JDK 1.8必须用这个版本高版本会有兼容性问题IntelliJ IDEA社区版完全够用Android Studio仅用于提取apk中的so文件Python 3.7辅助脚本编写安装unidbg只需要在pom.xml中添加dependency groupIdcom.github.zhkl0228/groupId artifactIdunidbg/artifactId version0.9.4/version /dependency2.2 必备辅助工具这些工具是我逆向过程中高频使用的Jadx反编译apk查看Java层逻辑Frida动态Hook关键函数IDA Pro静态分析so文件Charles抓包分析网络请求010 Editor分析二进制数据结构特别提醒遇到加固的应用时需要先用frida_dump脱壳。我常用的脱壳脚本是Interceptor.attach(Module.findExportByName(libart.so, _ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_), { onLeave: function(retval) { console.log(Dump dex start); var dex_file_size Memory.readU32(ptr(retval).add(32)); var dex_file_ptr Memory.readPointer(ptr(retval).add(24)); var dex_file_buffer ptr(dex_file_ptr).readByteArray(dex_file_size); ... } });3. 从抓包到定位关键算法3.1 网络请求特征分析以某电商APP为例其请求头包含多个加密参数X-Gorgon: 840420dc000093169b8d6fc8ef69b91569f74aa48edd98e2e178 X-Khronos: 1745889238 X-Argus: SzxcOpmeBIp1CDgK9rKJjkucHDkHTQF4QEdpfXDxokONKt5t/TnsnJuu1tXWSrGZYME1I626Y32Z6pFgFoj3uNC4NoIK9vqcO4DsHpKpgPvIbv0T/heNUtTRPJl7zaE44j9TEtiJxa4BnbTFM7048vLJ3zGsh3yLoxwTIrLxmi3qRFh8qa5QU19qayfd/Pa2mQhNoDYu9yJsKshJChsjB3wxlgw195D9gTBp6WY8R9Hw通过对比多个请求发现X-Khronos是简单的时间戳X-Gorgon长度固定为40字节X-Argus每次都会变化且长度不固定3.2 Hook定位算法位置使用Frida Hook Java层的网络请求代码Java.perform(function() { var OkHttpClient Java.use(okhttp3.OkHttpClient); OkHttpClient.newCall.implementation function(request) { var headers request.headers(); for (var i 0; i headers.size(); i) { console.log(headers.name(i) : headers.value(i)); } return this.newCall(request); }; });通过堆栈回溯发现加密逻辑在Native层at com.xxx.security.SecurityHelper.getArgus(Native Method) at com.xxx.network.a.a(SourceFile:134) at com.xxx.network.b.a(SourceFile:89)4. unidbg模拟执行核心流程4.1 加载so文件创建unidbg实例的模板代码public class XArgusEmulator extends AbstractJni { private final AndroidEmulator emulator; private final VM vm; public XArgusEmulator() { emulator AndroidEmulatorBuilder.for32Bit() .setProcessName(com.xxx) .build(); vm emulator.createDalvikVM(); } public void loadSo() { DalvikModule dm vm.loadLibrary(utility, false); dm.callJNI_OnLoad(emulator); } }常见问题处理SO加载失败检查依赖的其它so是否缺失JNI_OnLoad崩溃可能需要注册特定的JNI方法段错误通常是CPU架构设置错误4.2 函数符号定位在IDA中分析导出函数00018F24 T _Z15generateArgus1P7_JNIEnvP8_jobjectP8_jstring 00019088 T _Z15generateArgus2P7_JNIEnvP8_jobjectS1_对应的unidbg调用代码public String getArgus(String input) { ListObject list new ArrayList(); list.add(vm.getJNIEnv()); list.add(0); list.add(vm.addLocalObject(new StringObject(vm, input))); Number result emulator.eFunc(DynarmicModule.load(emulator, utility).findSymbolByName(_Z15generateArgus1P7_JNIEnvP8_jobjectP8_jstring), list.toArray())[0]; return vm.getObject(result.intValue()).getValue().toString(); }4.3 参数传递技巧遇到结构体参数时需要手动构造内存布局。比如遇到如下结构typedef struct { int version; char* deviceId; long timestamp; } ArgusParams;对应的Java处理代码MemoryBlock block emulator.getMemory().malloc(32, true); UnicornPointer pointer block.getPointer(); pointer.setInt(0, 0x1001); // version pointer.setPointer(4, vm.addLocalObject(new StringObject(vm, abcdef123456)).getPointer()); // deviceId pointer.setLong(8, System.currentTimeMillis() / 1000); // timestamp5. 常见问题与调试技巧5.1 内存访问异常处理当遇到SIGSEGV错误时可以通过注册异常回调来定位问题emulator.getBackend().hook_add_new(new CodeHook() { Override public void hook(Backend backend, long address, int size, Object user) { if (address 0xdeadbeef) { // 崩溃地址 System.out.println(Crash at Long.toHexString(address)); emulator.attach().debug(); } } }, 0, 0, null);5.2 JNI函数补全遇到未实现的JNI函数时需要手动补充。例如处理GetStringUTFCharsOverride public long GetStringUTFChars(long env, long jstring, long isCopy) { StringObject str vm.getObject(jstring); MemoryBlock block emulator.getMemory().malloc(str.getValue().length() 1, true); block.getPointer().write(str.getValue().getBytes()); return block.getPointer().peer; }5.3 性能优化建议当模拟执行速度过慢时启用Dynarmic后端加速缓存频繁调用的函数结果减少不必要的内存分配实测对比优化方式执行100次耗时(ms)原始模式4850启用Dynarmic620启用缓存1206. 完整案例X-Gorgon算法还原以某短视频平台的X-Gorgon为例完整还原流程定位算法入口public static native String generateGorgon(byte[] paramArrayOfByte);分析输入输出输入请求URL的MD5值输出40位16进制字符串unidbg调用代码public String calculateGorgon(byte[] input) { ByteArray ba new ByteArray(vm, input); ListObject args Arrays.asList( vm.getJNIEnv(), 0, ba.getObject() ); Number result emulator.eFunc(module.findSymbolByName(Java_com_xxx_security_GorgonHelper_generateGorgon), args.toArray())[0]; return vm.getObject(result.intValue()).getValue().toString(); }关键发现算法内部使用了AES-ECB模式加密密钥通过设备指纹动态生成包含时间戳校验机制7. 进阶技巧与经验分享在逆向某金融APP时遇到了高级反调试手段。这里分享我的破解过程检测ptraceso文件会检查TracerPidif(fgets(line, sizeof(line), fp) ! NULL) { if(strstr(line, TracerPid:) ! NULL) { pid atoi(line[10]); if(pid ! 0) exit(0); } }解决方案在unidbg中hook文件读取操作emulator.getSyscallHandler().addIOResolver(new SyscallHandler.IOResolver() { Override public int resolve(Emulator? emulator, String pathname, int oflags) { if (pathname.contains(/status)) { return -1; // 返回错误 } return 0; } });指令混淆使用O-LLVM混淆了关键代码 应对方案在unidbg中单步执行观察寄存器变化通过内存断点定位关键数据使用Capstone引擎反汇编可疑片段动态加载so文件被分割加密存储 解决方法Hook dlopen和dlsym函数在内存解密后dump完整soemulator.getSyscallHandler().addIOResolver(new SyscallHandler.IOResolver() { Override public int open(Emulator? emulator, String pathname, int flags) { if (pathname.contains(libencrypted.so)) { byte[] decrypted decrypt(emulator, pathname); MemoryBlock block emulator.getMemory().malloc(decrypted.length, true); block.getPointer().write(decrypted); return block.getPointer().peer; } return 0; } });

更多文章