1. 快手签名算法逆向分析入门第一次接触快手签名逆向时我也被那两个神秘的参数搞懵了。__nstokensig和sig就像两道加密锁牢牢守护着快手的接口安全。记得当时用Fiddler抓包看到请求里这两个长长的字符串第一反应就是这玩意儿肯定有规律逆向分析最有趣的地方在于它就像侦探破案一样需要层层推理。我们先从最基础的抓包开始用Fiddler或者Charles都能看到快手极速版以1.2.2.8版本为例的每个请求都会携带这两个签名参数。有趣的是它们一个在Java层生成一个在Native层计算形成了双重保护机制。工欲善其事必先利其器逆向分析需要准备好这些工具静态分析Jadx-GUI反编译APK、IDA Pro分析SO文件动态调试Frida/XposedHook关键函数、unidbg模拟执行SO抓包工具Fiddler/Charles捕获网络请求辅助工具Android Studio调试日志、JEB可选的反编译器2. __nstokensig签名算法解析2.1 算法定位过程用Jadx打开快手极速版APK直接搜索__nstokensig字符串很快就能定位到关键代码。这里有个小技巧不要只看字符串出现的位置还要注意它所在的类和方法名。通常签名相关的代码会放在network、security或者utils这样的包名下。我找到的代码路径是com.kuaishou.android.security.internal.a.e - a(String str, String str2)这个方法接收两个参数返回的就是我们需要的__nstokensig值。通过Xposed Hook这个方法可以观察到str参数实际上是sig的值就是另一个签名str2参数固定为kuaishou.api_client_salt2.2 算法实现细节跟入代码后发现处理逻辑很清晰将两个字符串简单拼接sig salt用SHA-256计算哈希值将二进制哈希结果转为十六进制字符串这里有个容易踩坑的地方哈希结果的字节转十六进制时快手使用了自定义的转换方法代码中的b()方法。它不是直接调用现成的库而是手动实现了一个查表法转换。如果直接用Java标准库的Hex转换得到的字符串会不一样完整的算法实现如下public String generateNsTokenSig(String sig, String salt) { String combined sig salt; byte[] hash sha256(combined.getBytes()); return bytesToHex(hash); } private byte[] sha256(byte[] input) { MessageDigest digest MessageDigest.getInstance(SHA-256); return digest.digest(input); } private String bytesToHex(byte[] bytes) { char[] hexChars new char[bytes.length * 2]; final char[] hexArray 0123456789abcdef.toCharArray(); for (int i 0; i bytes.length; i) { int v bytes[i] 0xFF; hexChars[i * 2] hexArray[v 4]; hexChars[i * 2 1] hexArray[v 0x0F]; } return new String(hexChars); }2.3 关键参数获取最麻烦的是salt值的获取。通过动态调试发现这个值只在登录接口的响应中返回一次后续请求都不会再传输这个值客户端会将其保存在内存中重复使用所以如果要完整复现签名过程必须先模拟登录流程获取到salt值。这也是为什么直接抓包后续请求时看不到这个关键参数的原因。3. sig签名算法深度剖析3.1 Native层算法定位sig比__nstokensig复杂得多因为它藏在SO文件里。在Jadx中搜索sig相关代码最终会定位到一个native方法声明public static native byte[] a(Context context, byte[] bArr, int i);这个方法的三个参数很有意思Context对象传入Android上下文byte[]数据实际要签名的原始数据int标志位疑似算法版本号用IDA Pro分析libkwai.so文件可以找到对应的Native函数。不过逆向SO需要一定的汇编基础新手可能会觉得吃力。这时候unidbg就派上用场了——它可以直接模拟执行SO文件省去了逆向算法的麻烦。3.2 签名数据预处理通过Hook发现第二个byte[]参数其实是由URL参数和POST参数合并而成。具体处理流程从URL中提取查询参数问号后的部分获取POST Body中的参数合并两个参数列表按字母序排序所有参数拼接成单个字符串后转为byte[]关键代码实现public byte[] prepareSigData(String url, String postBody) { // 提取URL参数 String query url.substring(url.indexOf(?) 1); String[] urlParams query.split(); // 提取POST参数 String[] postParams postBody.split(); // 合并并排序 ListString allParams new ArrayList(); Collections.addAll(allParams, urlParams); Collections.addAll(allParams, postParams); Collections.sort(allParams); // 拼接字符串 String combined String.join(, allParams); return combined.getBytes(StandardCharsets.UTF_8); }3.3 完整签名流程实际项目中我推荐两种方案处理sig签名Xposed直接调用Hook原始方法获取参数后调用原so方法unidbg模拟执行加载so文件构造参数后调用native方法第一种方案最简单但需要root环境。第二种更通用但需要处理so初始化等问题。以unidbg为例核心代码如下public class KwaiSig { private final AndroidEmulator emulator; private final VM vm; public KwaiSig() { emulator new AndroidEmulator(); vm emulator.createDalvikVM(); vm.loadLibrary(kwai, true); } public byte[] generateSig(byte[] data) { DvmObject? context vm.resolveClass(android/content/Context).newObject(null); Number result vm.callJniMethod(emulator, com/kuaishou/android/security/internal/a/CPU.a(Landroid/content/Context;[BI)[B, context, vm.addLocalObject(new ByteArray(vm, data)), 0); return vm.getObject(result.intValue()).getValue(); } }4. 逆向工程中的实用技巧4.1 动态Hook的注意事项使用Xposed或Frida Hook时有几个常见问题需要注意多线程环境签名方法可能在网络线程调用Hook代码要线程安全参数类型转换遇到byte[]等特殊类型时要正确处理数据转换性能影响高频调用的方法不要打印过多日志会影响APP运行一个实用的Frida Hook脚本示例Java.perform(function() { let targetClass Java.use(com.kuaishou.android.security.internal.a.CPU); targetClass.a.implementation function(context, data, flag) { console.log(Sig input: JSON.stringify(data)); let result this.a(context, data, flag); console.log(Sig output: result); return result; }; });4.2 算法还原的验证方法验证自己实现的签名是否正确可以采用交叉验证法用官方APP发起请求抓取签名值用自己实现的算法计算相同参数的签名比对两个签名是否一致为了提高效率可以搭建自动化测试框架使用MITMProxy捕获真实请求自动提取请求参数调用本地签名算法验证4.3 应对签名升级的策略快手和其他APP一样会定期更新签名算法我总结了几条应对经验版本快照保留各个版本的APK和SO文件差异对比用Beyond Compare等工具比对不同版本的代码差异特征监控监控签长度、字符分布等特征变化自动化警报当签名验证失败时自动触发分析流程记得有一次快手更新后sig算法从v1升级到v2主要变化是增加了一个新的so文件签名前对输入数据做了额外的HMAC处理结果使用了Base64编码而不是纯十六进制这种时候之前的hook代码和unidbg环境就能快速派上用场省去了从头开始分析的时间。
快手Android端__nstokensig与sig签名算法逆向实战解析
1. 快手签名算法逆向分析入门第一次接触快手签名逆向时我也被那两个神秘的参数搞懵了。__nstokensig和sig就像两道加密锁牢牢守护着快手的接口安全。记得当时用Fiddler抓包看到请求里这两个长长的字符串第一反应就是这玩意儿肯定有规律逆向分析最有趣的地方在于它就像侦探破案一样需要层层推理。我们先从最基础的抓包开始用Fiddler或者Charles都能看到快手极速版以1.2.2.8版本为例的每个请求都会携带这两个签名参数。有趣的是它们一个在Java层生成一个在Native层计算形成了双重保护机制。工欲善其事必先利其器逆向分析需要准备好这些工具静态分析Jadx-GUI反编译APK、IDA Pro分析SO文件动态调试Frida/XposedHook关键函数、unidbg模拟执行SO抓包工具Fiddler/Charles捕获网络请求辅助工具Android Studio调试日志、JEB可选的反编译器2. __nstokensig签名算法解析2.1 算法定位过程用Jadx打开快手极速版APK直接搜索__nstokensig字符串很快就能定位到关键代码。这里有个小技巧不要只看字符串出现的位置还要注意它所在的类和方法名。通常签名相关的代码会放在network、security或者utils这样的包名下。我找到的代码路径是com.kuaishou.android.security.internal.a.e - a(String str, String str2)这个方法接收两个参数返回的就是我们需要的__nstokensig值。通过Xposed Hook这个方法可以观察到str参数实际上是sig的值就是另一个签名str2参数固定为kuaishou.api_client_salt2.2 算法实现细节跟入代码后发现处理逻辑很清晰将两个字符串简单拼接sig salt用SHA-256计算哈希值将二进制哈希结果转为十六进制字符串这里有个容易踩坑的地方哈希结果的字节转十六进制时快手使用了自定义的转换方法代码中的b()方法。它不是直接调用现成的库而是手动实现了一个查表法转换。如果直接用Java标准库的Hex转换得到的字符串会不一样完整的算法实现如下public String generateNsTokenSig(String sig, String salt) { String combined sig salt; byte[] hash sha256(combined.getBytes()); return bytesToHex(hash); } private byte[] sha256(byte[] input) { MessageDigest digest MessageDigest.getInstance(SHA-256); return digest.digest(input); } private String bytesToHex(byte[] bytes) { char[] hexChars new char[bytes.length * 2]; final char[] hexArray 0123456789abcdef.toCharArray(); for (int i 0; i bytes.length; i) { int v bytes[i] 0xFF; hexChars[i * 2] hexArray[v 4]; hexChars[i * 2 1] hexArray[v 0x0F]; } return new String(hexChars); }2.3 关键参数获取最麻烦的是salt值的获取。通过动态调试发现这个值只在登录接口的响应中返回一次后续请求都不会再传输这个值客户端会将其保存在内存中重复使用所以如果要完整复现签名过程必须先模拟登录流程获取到salt值。这也是为什么直接抓包后续请求时看不到这个关键参数的原因。3. sig签名算法深度剖析3.1 Native层算法定位sig比__nstokensig复杂得多因为它藏在SO文件里。在Jadx中搜索sig相关代码最终会定位到一个native方法声明public static native byte[] a(Context context, byte[] bArr, int i);这个方法的三个参数很有意思Context对象传入Android上下文byte[]数据实际要签名的原始数据int标志位疑似算法版本号用IDA Pro分析libkwai.so文件可以找到对应的Native函数。不过逆向SO需要一定的汇编基础新手可能会觉得吃力。这时候unidbg就派上用场了——它可以直接模拟执行SO文件省去了逆向算法的麻烦。3.2 签名数据预处理通过Hook发现第二个byte[]参数其实是由URL参数和POST参数合并而成。具体处理流程从URL中提取查询参数问号后的部分获取POST Body中的参数合并两个参数列表按字母序排序所有参数拼接成单个字符串后转为byte[]关键代码实现public byte[] prepareSigData(String url, String postBody) { // 提取URL参数 String query url.substring(url.indexOf(?) 1); String[] urlParams query.split(); // 提取POST参数 String[] postParams postBody.split(); // 合并并排序 ListString allParams new ArrayList(); Collections.addAll(allParams, urlParams); Collections.addAll(allParams, postParams); Collections.sort(allParams); // 拼接字符串 String combined String.join(, allParams); return combined.getBytes(StandardCharsets.UTF_8); }3.3 完整签名流程实际项目中我推荐两种方案处理sig签名Xposed直接调用Hook原始方法获取参数后调用原so方法unidbg模拟执行加载so文件构造参数后调用native方法第一种方案最简单但需要root环境。第二种更通用但需要处理so初始化等问题。以unidbg为例核心代码如下public class KwaiSig { private final AndroidEmulator emulator; private final VM vm; public KwaiSig() { emulator new AndroidEmulator(); vm emulator.createDalvikVM(); vm.loadLibrary(kwai, true); } public byte[] generateSig(byte[] data) { DvmObject? context vm.resolveClass(android/content/Context).newObject(null); Number result vm.callJniMethod(emulator, com/kuaishou/android/security/internal/a/CPU.a(Landroid/content/Context;[BI)[B, context, vm.addLocalObject(new ByteArray(vm, data)), 0); return vm.getObject(result.intValue()).getValue(); } }4. 逆向工程中的实用技巧4.1 动态Hook的注意事项使用Xposed或Frida Hook时有几个常见问题需要注意多线程环境签名方法可能在网络线程调用Hook代码要线程安全参数类型转换遇到byte[]等特殊类型时要正确处理数据转换性能影响高频调用的方法不要打印过多日志会影响APP运行一个实用的Frida Hook脚本示例Java.perform(function() { let targetClass Java.use(com.kuaishou.android.security.internal.a.CPU); targetClass.a.implementation function(context, data, flag) { console.log(Sig input: JSON.stringify(data)); let result this.a(context, data, flag); console.log(Sig output: result); return result; }; });4.2 算法还原的验证方法验证自己实现的签名是否正确可以采用交叉验证法用官方APP发起请求抓取签名值用自己实现的算法计算相同参数的签名比对两个签名是否一致为了提高效率可以搭建自动化测试框架使用MITMProxy捕获真实请求自动提取请求参数调用本地签名算法验证4.3 应对签名升级的策略快手和其他APP一样会定期更新签名算法我总结了几条应对经验版本快照保留各个版本的APK和SO文件差异对比用Beyond Compare等工具比对不同版本的代码差异特征监控监控签长度、字符分布等特征变化自动化警报当签名验证失败时自动触发分析流程记得有一次快手更新后sig算法从v1升级到v2主要变化是增加了一个新的so文件签名前对输入数据做了额外的HMAC处理结果使用了Base64编码而不是纯十六进制这种时候之前的hook代码和unidbg环境就能快速派上用场省去了从头开始分析的时间。