深入解析A系电商App的doCommandNative:从JNI到Frida实战

张开发
2026/4/13 23:50:12 15 分钟阅读

分享文章

深入解析A系电商App的doCommandNative:从JNI到Frida实战
1. 初识doCommandNative藏在电商App里的关键函数第一次看到doCommandNative这个函数名时我正盯着A系电商App的反编译代码发呆。作为连接Java层和Native层的桥梁这个看似普通的JNI函数背后藏着整个签名算法的核心逻辑。记得当时为了定位它我在茫茫so文件中翻找了整整三天直到在libsgmainso的导出表中发现蛛丝马迹。这个函数的特别之处在于它的动态注册机制。与静态注册不同动态注册的JNI函数不会直接暴露在导出表中而是通过RegisterNatives在运行时绑定。这就像给函数戴上了面具常规的逆向手段很难直接捕捉到它的真面目。在实际分析中我常用以下命令快速确认动态注册情况adb shell dumpsys package package_name | grep -A 20 JNI通过Frida脚本hook RegisterNatives时我发现doCommandNative接收两个关键参数一个整型命令码如70102对应x-sign生成一个Object数组用于传递动态参数。这种设计让它成为了多功能入口——就像瑞士军刀通过不同命令码切换功能模块。以下是典型的参数结构参数位置类型说明args[0]JNIEnv*JNI环境指针args[1]jobjectJava对象引用args[2]jint功能命令码如70102args[3]jobjectArray参数数组长度可变2. 动态注册破解从迷雾到清晰路径动态注册就像玩捉迷藏常规的IDA静态分析往往无功而返。记得第一次用Frida拦截RegisterNatives时控制台输出的信息让我眼前一亮Interceptor.attach(Module.findExportByName(null, RegisterNatives), { onEnter: function(args) { console.log([RegisterNatives] java_class: ${args[0]} name: ${Memory.readCString(args[1])} sig: ${Memory.readCString(args[2])} fnPtr: ${args[3]}); } });输出结果揭示了关键信息——doCommandNative的函数指针偏移量为0x1eba4。但当我兴冲冲地在IDA中跳转到这个地址时看到的却是令人困惑的机器码片段。这时候才意识到这个so文件使用了动态代码修改技术运行时才会还原真实指令。解决这个问题需要组合拳运行时Dump使用Frida的Memory.scan dump出内存中的so镜像指令修复将动态跳转如BR X11改为静态跳转B指令上下文重建通过寄存器快照恢复跳转目标地址具体操作时我常用这个脚本获取运行时寄存器状态Interceptor.attach(Module.getBaseAddress(libsgmain.so).add(0x1EC18), { onEnter: function(args) { console.log(X11 register value: this.context.x11); // 计算相对偏移x11 - module_base } });3. Frida实战穿透Java与Native的边界真正有趣的挑战在于处理跨语言参数传递。当doCommandNative的第二个参数是Object数组时直接打印只会得到无意义的地址值。经过多次踩坑我总结出可靠的类型转换方案var objArray Java.cast(args[3], Java.use([Ljava.lang.Object;)); var len Java.use(java.lang.reflect.Array).getLength(objArray); for (var i 0; i len; i) { var item Java.cast( Java.use(java.lang.reflect.Array).get(objArray, i), Java.use(java.lang.Object) ); if (item.toString() ! null) { console.log(Array[${i}]: ${item.toString()}); } }对于常见的参数类型需要特别注意这些处理细节String类型直接调用toString()可能触发异常建议先用instanceof判断基本类型数组需要区分int[]和Integer[]等包装类型自定义对象通过getClass().getName()获取完整类名在hook native层实现时参数索引容易出错。JNI规范中native方法的第一个参数实际对应args[2]前两个是JNIEnv和jobject。我曾因此浪费两小时排查一个数组越界问题——这个教训让我养成了在脚本开头打印完整参数列表的习惯。4. 对抗与突破当IDA遇上动态混淆分析libsgmainso的过程就像解九连环。这个so文件采用了多重保护导出表清理删除所有敏感函数导出项动态跳转关键代码通过寄存器间接跳转指令混淆在运行时解密真实指令针对动态跳转我的破解方法是运行时指令修补。例如遇到BR X11指令时拦截执行获取X11寄存器值计算目标地址相对偏移用Hex编辑器将BR X11替换为Bvar targetAddr this.context.x11 - Module.getBaseAddress(libsgmain.so); console.log(Patching BR X11 to B ${targetAddr.toString(16)});IDA的F5功能遇到这种代码会直接罢工。通过手动修补我最终还原出的控制流显示doCommandNative内部实际上是个巨型状态机根据命令码跳转到不同处理模块。其中70102对应的x-sign生成流程包含以下关键步骤参数校验检查数组长度≥3环境检测root/模拟器检查密钥派生基于设备指纹生成动态密钥哈希计算混合SHA256和自定义算法5. 安全攻防启示录在分析过程中最令我惊叹的是防御方设计的多层验证体系Java层动态代理拦截非法调用JNI层参数类型和范围校验Native层反调试代码自校验这些防护不是简单的技术堆砌而是形成了有机整体。比如当检测到Frida注入时不会直接崩溃而是返回看似正常实则错误的结果——这种温柔陷阱很容易让分析者误入歧途。对于安全研究者我建议建立这样的分析流程行为建模先观察正常调用链如抓包堆栈跟踪分层突破从Java层逐步深入Native层差异对比比较正常调用与hook调用的参数变化环境隔离在干净环境中验证关键发现记得有次我忽略了环境检测环节导致分析陷入死胡同。后来通过对比两台设备的日志输出才发现其中暗藏玄机——这个教训让我明白逆向工程不仅是技术活更是耐心与细心的较量。

更多文章