
1. 逆向分析前的准备工作在开始逆向分析京东sign加密算法之前我们需要做好充分的准备工作。首先需要准备一台root过的安卓手机或者模拟器我推荐使用Genymotion模拟器因为它对xposed框架的支持比较好。其次需要安装以下工具jadx用于反编译apk文件查看java层代码IDA Pro用于分析so文件中的native代码Frida动态hook工具可以实时修改和监控函数调用Charles抓包工具用于捕获网络请求我建议先使用Charles抓取几个京东APP的请求观察sign参数的特征。从抓包结果来看sign通常是一个32位的字符串看起来像是MD5或者SHA1之类的哈希值。但是直接对参数进行哈希计算并不能得到相同的结果说明京东在哈希之前还做了其他处理。2. 定位sign生成的关键代码2.1 使用jadx分析apk用jadx打开京东apk后我们可以通过搜索关键词来定位sign生成的代码。根据经验sign通常会在网络请求的拦截器或者工具类中生成。我们可以搜索以下关键词signsignatureencryptgetSign在京东apk中我们发现了一个名为BitmapkitUtils的类里面有一个getSignFromJni方法这个方法看起来很有嫌疑。它的参数包括context、业务类型字符串、json数据和一些设备信息返回值就是我们要找的sign。public class BitmapkitUtils { public static native String getSignFromJni(Context context, String str, String str2, String str3, String str4, String str5); }2.2 追踪native方法实现getSignFromJni是一个native方法说明实际的加密逻辑是在so文件中实现的。我们需要找到对应的so文件。通过分析apk的lib目录发现libjdbitmapkit.so这个文件最有可能是我们要找的。用IDA Pro打开这个so文件我们需要找到getSignFromJni对应的native函数。可以通过查看JNI_OnLoad函数或者直接搜索getSignFromJni字符串来定位。找到函数后我们可以开始分析它的实现逻辑。3. 动态分析加密过程3.1 使用Frida进行hook静态分析只能看到代码的结构要真正理解加密过程我们需要进行动态分析。这里我们使用Frida来hook关键函数。首先hook Java层的getSignFromJni方法Java.perform(function() { var BitmapkitUtils Java.use(com.jingdong.common.utils.BitmapkitUtils); BitmapkitUtils.getSignFromJni.implementation function(context, str1, str2, str3, str4, str5) { console.log(getSignFromJni called with params:); console.log(str1: str1); console.log(str2: str2); console.log(str3: str3); console.log(str4: str4); console.log(str5: str5); var result this.getSignFromJni(context, str1, str2, str3, str4, str5); console.log(getSignFromJni result: result); return result; }; });通过hook我们发现str2参数是一个json字符串包含了请求的各种参数。str3看起来像是一个固定值可能是某种密钥或者盐值。3.2 hook native层函数接下来我们需要hook so文件中的native函数。首先找到getSignFromJni对应的native函数然后hook它的调用Interceptor.attach(Module.findExportByName(libjdbitmapkit.so, Java_com_jingdong_common_utils_BitmapkitUtils_getSignFromJni), { onEnter: function(args) { console.log(native getSignFromJni called); // 打印参数 var env Java.vm.getEnv(); var str1 env.getStringUtfChars(args[2], null).readCString(); var str2 env.getStringUtfChars(args[3], null).readCString(); var str3 env.getStringUtfChars(args[4], null).readCString(); console.log(str1: str1); console.log(str2: str2); console.log(str3: str3); }, onLeave: function(retval) { console.log(native getSignFromJni return:); console.log(Java.vm.getEnv().getStringUtfChars(retval, null).readCString()); } });通过hook我们发现native函数内部还会调用其他函数进行实际的计算。我们需要继续深入分析这些函数。4. 分析加密算法实现4.1 跟踪加密流程通过IDA Pro的交叉引用功能我们可以跟踪getSignFromJni函数的调用流程。发现它最终会调用一个sub_xxxx的函数进行实际的加密计算。这个函数内部又调用了MD5相关的函数。有趣的是在调用MD5之前数据会经过一个自定义的加密函数处理。这个函数会对数据进行异或和加减操作使用了一个固定的密钥表和密钥值。这可能是京东为了防止直接MD5被破解而增加的混淆层。4.2 复现加密算法通过分析我们可以将加密过程总结为以下几个步骤将输入参数按照特定格式拼接使用自定义算法对拼接后的字符串进行加密对加密结果进行MD5计算对MD5结果进行二次处理以下是加密算法的C语言实现#include stdio.h #include string.h #include openssl/md5.h void jd_encrypt(char* input, int length) { const char* key 80306f4370b39fd5630ad0529f77adb6; unsigned char table[16] {0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0xF, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A}; for(int i 0; i length; i) { int key_index i 7; int table_index i 0xF; unsigned char tmp (table[table_index] (input[i] ^ key[key_index] ^ table[table_index])) ^ table[table_index]; input[i] key[key_index] ^ tmp; } } void calculate_jd_sign(char* input, char* output) { // 第一步自定义加密 int length strlen(input); jd_encrypt(input, length); // 第二步MD5计算 unsigned char md5_result[MD5_DIGEST_LENGTH]; MD5((unsigned char*)input, length, md5_result); // 第三步格式化输出 for(int i 0; i MD5_DIGEST_LENGTH; i) { sprintf(output i*2, %02x, md5_result[i]); } output[MD5_DIGEST_LENGTH*2] \0; } int main() { char input[] wareBusiness{\skuId\:\100000000001\}; char output[33]; calculate_jd_sign(input, output); printf(JD Sign: %s\n, output); return 0; }5. 验证和优化5.1 验证算法正确性为了验证我们的算法是否正确我们可以使用Frida在APP运行时捕获输入参数和输出结果然后用我们的算法计算同样的输入比较结果是否一致。我在测试中发现对于某些特殊字符算法结果会有差异这说明京东可能在处理特殊字符时有额外的规则。5.2 处理边界情况经过多次测试我发现以下边界情况需要特别注意当参数值为null时京东会将其转换为空字符串布尔值true/false会转换为字符串true/false浮点数会保留一定位数的小数参数名会按照字母顺序排序修正后的参数处理代码如下def prepare_params(params): prepared {} for key in sorted(params.keys()): value params[key] if value is None: prepared[key] elif isinstance(value, bool): prepared[key] true if value else false elif isinstance(value, float): prepared[key] {:.6f}.format(value) else: prepared[key] str(value) return prepared6. 完整实现方案6.1 Python实现基于以上分析我们可以用Python实现完整的sign生成算法import hashlib import json def jd_encrypt(data): key 80306f4370b39fd5630ad0529f77adb6 table [0x37, 0x92, 0x44, 0x68, 0xA5, 0x3D, 0xCC, 0x7F, 0xBB, 0x0F, 0xD9, 0x88, 0xEE, 0x9A, 0xE9, 0x5A] data_bytes bytearray(data.encode(utf-8)) for i in range(len(data_bytes)): key_index i 7 table_index i 0xF tmp (table[table_index] (data_bytes[i] ^ ord(key[key_index]) ^ table[table_index])) ^ table[table_index] data_bytes[i] ord(key[key_index]) ^ tmp return bytes(data_bytes) def generate_jd_sign(params): # 准备参数 prepared_params prepare_params(params) param_str json.dumps(prepared_params, separators(,, :), ensure_asciiFalse) # 加密 encrypted jd_encrypt(param_str) # 计算MD5 md5 hashlib.md5() md5.update(encrypted) return md5.hexdigest()6.2 使用示例params { skuId: 100000000001, cityId: 72, latitude: 39.90469, longitude: 116.40717, isFromOpenApp: True } sign generate_jd_sign(params) print(Generated Sign:, sign)7. 实际应用中的注意事项在实际使用中我发现京东的sign算法会不定期更新所以这个方案可能需要根据情况进行调整。以下是一些使用建议定期验证每隔一段时间验证算法是否仍然有效异常处理当sign无效时要有备用方案获取新的算法性能优化对于高频请求可以缓存计算结果参数更新注意京东APP更新后参数可能发生变化我在实际项目中实现了一个自动更新机制当检测到sign失效时会自动触发重新分析流程。这个机制大大提高了系统的稳定性。