上周在对接某电商平台的接口时发现它的请求签名逻辑完全藏在native层Java层的SignUtils是个空壳。折腾了三天从Frida Hook到IDA Pro静态分析再到Python还原总算把整个签名算法跑通了。这篇把完整过程记录下来踩坑的地方也一并写上。一、签名验证为什么比想象中麻烦刚接触逆向那会儿我也以为抓个包改改参数就能过签名校验。实际做下来发现现在主流APP的签名验证远不是MD5加盐那么简单。常见的签名流程客户端把所有请求参数按规则排序拼接上一个只有客户端知道的密钥secret进行MD5/SHA1/SHA256哈希运算把结果作为sign参数传给服务器服务器用同样的规则计算一遍对比是否一致核心问题在于那个secret密钥。它通常被硬编码在APP的so文件里或者通过JNI调用从native层返回。抓包只能看到加密后的sign看不到原始的密钥和拼接规则。而且不少APP还做了签名时间戳验证、请求参数完整性验证、签名算法混淆甚至把签名逻辑整个放到native层用C/C实现——反编译都看不懂那种。二、整体技术方案先放一张流程图方便整体把握。是否是否抓包分析请求确定签名参数signFrida Hook Java层是否找到签名函数?Hook获取参数和结果Hook JNI调用定位native层签名函数分析拼接规则提取密钥secret还原签名算法Python实现算法测试验证验证通过?完成破解排查遗漏参数三个核心步骤定位用Frida Hook找到签名函数的位置还原分析函数逻辑提取密钥和拼接规则调用用Python实现还原后的算法生成合法签名三、Frida Hook定位签名函数这步最耗时。我在这卡了将近两天因为签名函数根本不在Java层。3.1 环境准备一台root过的安卓手机我用的是Android 9的Pixel 2兼容性比较好Frida服务端对应手机架构ARM64的话用frida-server-arm64Frida客户端pip install frida frida-tools目标APP的APK文件jadx-gui反编译工具3.2 快速定位技巧别上来就Hook所有函数效率太低。几个实用的定位方法技巧1搜索关键词用jadx打开APK全局搜sign、“signature”、“md5”、“sha1”、“sha256”。重点看返回值是String类型、参数是Map/JSONObject/多个String的函数。技巧2Hook哈希算法类签名最终都会调Java的MessageDigest类。直接Hook这个类的digest方法就能看到所有哈希运算的输入和输出。Java.perform(function(){varMessageDigestJava.use(java.security.MessageDigest);MessageDigest.digest.overload([B).implementationfunction(data){varresultthis.digest(data);varinputbytesToHex(data);varoutputbytesToHex(result);console.log([] MessageDigest.digest called);console.log( Algorithm: this.getAlgorithm());console.log( Input: input);console.log( Output: output);// 打印调用栈追踪调用来源console.log( Call stack:);console.log(Java.use(android.util.Log).getStackTraceString(Java.use(java.lang.Throwable).$new()));returnresult;};});functionbytesToHex(bytes){varhex[];for(vari0;ibytes.length;i){hex.push((0(bytes[i]0xFF).toString(16)).slice(-2));}returnhex.join();}跑这个脚本触发一次APP的网络请求所有哈希运算都会打印出来。输出和抓包sign值一致的那个就是签名函数。技巧3Hook网络请求类上面方法找不到的话可以Hook OkHttp、Retrofit这些网络库在请求发出前拦截参数看sign在哪一步被拼上去的。3.3 实战踩坑Java层SignUtils是幌子我分析的这个电商APPjadx搜sign找到了SignUtils.getSign方法publicstaticStringgetSign(MapString,Stringparams,Stringsecret){ListStringkeysnewArrayList(params.keySet());Collections.sort(keys);StringBuildersbnewStringBuilder();for(Stringkey:keys){sb.append(key).append().append(params.get(key)).append();}Stringstrsb.substring(0,sb.length()-1)secret;returnMD5Utils.md5(str).toUpperCase();}看着逻辑挺完整结果Hook这个方法——完全没被调用。这就是这APP的套路Java层放个假的签名方法做伪装真正的签名逻辑在native层。然后我用MessageDigest Hook脚本重新跑了一遍找到了真正的调用链[] MessageDigest.digest called Algorithm: MD5 Input: 6170706b65793d3132333435362674696d657374616d703d31373136373839303132333435267573657269643d3130303836616263313233646566343536 Output: 5F4DCC3B5AA765D61D8327DEB882CF99 Call stack: at com.example.app.NativeSign.getSign(Native Method) at com.example.app.HttpManager.addSignParam(HttpManager.java:123) at com.example.app.HttpManager.sendRequest(HttpManager.java:89)签名函数实际是NativeSign.getSign——一个native方法。四、算法还原找到native方法之后需要分析它的实现。两种方式静态分析和动态调试。4.1 静态分析so文件用IDA Pro打开APK里的libnative-lib.so找到Java_com_example_app_NativeSign_getSign函数。如果so被加壳了可以用Frida HookSystem.loadLibrary在so加载后dump出内存中的so文件Java.perform(function(){varSystemJava.use(java.lang.System);System.loadLibrary.implementationfunction(libName){console.log([] Loading library: libName);this.loadLibrary(libName);if(libNamenative-lib){console.log([] Dumping libnative-lib.so...);// 需要配合frida-dump工具dumpSo(libnative-lib.so);}};});dump出来之后用IDA打开分析。这个APP的native函数逻辑和Java层那个假方法基本一样区别就是密钥硬编码在了so里JNIEXPORT jstring JNICALLJava_com_example_app_NativeSign_getSign(JNIEnv*env,jclass clazz,jobject params){constchar*secret7890abcdef1234567890abcdef123456;jclass mapClassenv-GetObjectClass(params);jmethodID entrySetMethodenv-GetMethodID(mapClass,entrySet,()Ljava/util/Set;);jobject entrySetenv-CallObjectMethod(params,entrySetMethod);// 排序、拼接、MD5加密...跟Java层流程一致}4.2 动态调试验证静态分析完还需要验证。用Frida Hook这个native方法对比输入输出Java.perform(function(){varNativeSignJava.use(com.example.app.NativeSign);NativeSign.getSign.implementationfunction(params){console.log([] NativeSign.getSign called);console.log( Params: JSON.stringify(params));varresultthis.getSign(params);console.log( Result: result);returnresult;};});触发一次请求看返回值和抓包的sign值是否一致。一致就说明分析没错。五、Python还原签名算法拼接规则和密钥都拿到了直接Python实现importhashlibimporttimedefget_sign(params,secret7890abcdef1234567890abcdef123456):sorted_keyssorted(params.keys())sb[]forkeyinsorted_keys:sb.append(f{key}{params[key]})str_to_sign.join(sb)secret md5hashlib.md5()md5.update(str_to_sign.encode(utf-8))returnmd5.hexdigest().upper()if__name____main__:params{appkey:123456,timestamp:str(int(time.time()*1000)),userid:10086}signget_sign(params)print(f生成的签名:{sign})跑一下生成的签名和APP端一致就OK了。六、踩过的坑6.1 密钥是动态下发的有些APP的密钥不是硬编码在so里的而是启动时从服务器拉取存在内存中。遇到这种情况需要Hook存储密钥的变量Java.perform(function(){varNativeSignJava.use(com.example.app.NativeSign);NativeSign.$init.implementationfunction(){this.$init();console.log([] NativeSign initialized);console.log( Secret: this.secret.value);};});6.2 算法被魔改过有的APP会改MD5的初始向量、增加轮数或者在加密前后做特殊处理。这个只能逐字节对比APP实现和标准算法的差异挺费时间的。6.3 Frida被检测不少APP现在会检测Frida的端口、进程名、内存特征。Hook脚本跑了没效果大概率是被反检测了。应对方法用Frida的spawn模式启动APP隐藏Frida端口和进程名用objection工具自带部分反检测功能改Frida源码编译定制版服务端6.4 漏了签名参数最常见的低级错误。很多APP会在签名里加入隐藏参数设备信息、APP版本号等抓包时可能看不到。用Frida Hook签名函数把所有参与签名的参数打出来逐个核对。七、写在最后以上就是这个电商APP签名验证破解的完整记录。从定位到还原到调用整体流程不算复杂但中间踩的坑不少——Java层空壳、密钥动态下发、Frida检测每个都够卡半天的。本文内容仅限技术学习交流请勿用于非法用途。关于签名验证破解你遇到过哪些更棘手的情况欢迎在评论区交流。